Oliver Eyton-Williams e118dda13a
fix: order imports and remove circular dependencies (#41824)
* fix: remove circular dependency

redux depended on templates/Challenges/redux and vice versa.  This
meant that import order mattered and confusing bugs could arise.

(cherry picked from commit 7d67a4e70922bbb3051f2f9982dcc69e240d43dc)

* feat: require imports to be in alphabetical order

Import order generally does not matter, but there are edge cases
(circular  imports and css imports, for example) where changing order
changes behaviour

(cherry picked from commit b8d1393a91ec6e068caf8e8498a5c95df68c2b2c)

* chore: order imports

* fix: lift up challenge description + title comps

This brings the classic Show closer to the others as they
now all create the description and title components

* fix: remove donation-saga/index circular import

(cherry picked from commit 51a44ca668a700786d2744feffeae4fdba5fd207)

* refactor: extract action-types from settings

(cherry picked from commit 25e26124d691c84a0d0827d41dafb761c686fadd)

* fix: lint errors

* feat: prevent useless renames
2021-08-02 08:39:40 -05:00

289 lines
7.2 KiB
TypeScript

import { existsSync } from 'fs';
import fs from 'fs/promises';
import path from 'path';
import { prompt } from 'inquirer';
import { format } from 'prettier';
import { blockNameify } from '../../utils/block-nameify';
import { createStepFile } from './utils.js';
const superBlocks = [
'responsive-web-design',
'javascript-algorithms-and-data-structures',
'front-end-libraries',
'data-visualization',
'apis-and-microservices',
'quality-assurance',
'scientific-computing-with-python',
'data-analysis-with-python',
'information-security',
'machine-learning-with-python',
'coding-interview-prep'
] as const;
type SuperBlock = typeof superBlocks[number];
const helpCategories = ['HTML-CSS', 'JavaScript', 'Python'] as const;
type BlockInfo = {
title: string;
intro: string[];
};
type SuperBlockInfo = {
blocks: Record<string, BlockInfo>;
};
type IntroJson = Record<SuperBlock, SuperBlockInfo>;
type Meta = {
name: string;
isUpcomingChange: boolean;
dashedName: string;
order: number;
time: string;
template: string;
required: string[];
superBlock: string;
superOrder: number;
isBeta: boolean;
challengeOrder: string[][];
};
async function createProject(
superBlock: SuperBlock,
block: string,
helpCategory: string,
order: number,
title?: string
) {
if (!title) {
title = blockNameify(block);
} else if (title !== blockNameify(block)) {
updateBlockNames(block, title).catch(reason => {
throw reason;
});
}
updateIntroJson(superBlock, block, title).catch(reason => {
throw reason;
});
updateHelpCategoryMap(block, helpCategory).catch(reason => {
throw reason;
});
const challengeId = await createFirstChallenge(superBlock, block).catch(
reason => {
throw reason;
}
);
createMetaJson(superBlock, block, title, order, challengeId).catch(reason => {
throw reason;
});
// TODO: remove once we stop relying on markdown in the client.
createIntroMD(superBlock, block, title).catch(reason => {
throw reason;
});
}
async function updateIntroJson(
superBlock: SuperBlock,
block: string,
title: string
) {
const introJsonPath = path.resolve(
__dirname,
'../../client/i18n/locales/english/intro.json'
);
const newIntro = await parseJson<IntroJson>(introJsonPath);
newIntro[superBlock].blocks[block] = {
title,
intro: ['', '']
};
fs.writeFile(
introJsonPath,
format(JSON.stringify(newIntro), { parser: 'json' })
).catch(reason => {
throw reason;
});
}
async function updateHelpCategoryMap(block: string, helpCategory: string) {
const helpCategoryPath = path.resolve(
__dirname,
'../../client/utils/help-category-map.json'
);
const helpMap = await parseJson<Record<string, string>>(helpCategoryPath);
helpMap[block] = helpCategory;
fs.writeFile(
helpCategoryPath,
format(JSON.stringify(helpMap), { parser: 'json' })
).catch(reason => {
throw reason;
});
}
async function updateBlockNames(block: string, title: string) {
const blockNamesPath = path.resolve(
__dirname,
'../../utils/preformatted-block-names.json'
);
const blockNames = await parseJson<Record<string, string>>(blockNamesPath);
blockNames[block] = title;
fs.writeFile(
blockNamesPath,
format(JSON.stringify(blockNames), { parser: 'json' })
).catch(reason => {
throw reason;
});
}
async function createMetaJson(
superBlock: SuperBlock,
block: string,
title: string,
order: number,
challengeId: string
) {
const metaDir = path.resolve(__dirname, '../../curriculum/challenges/_meta');
const newMeta = await parseJson<Meta>('./base-meta.json');
newMeta.name = title;
newMeta.dashedName = block;
newMeta.order = order;
newMeta.superOrder = superBlocks.indexOf(superBlock) + 1;
newMeta.superBlock = superBlock;
newMeta.challengeOrder = [[challengeId, 'Part 1']];
const newMetaDir = path.resolve(metaDir, block);
if (!existsSync(newMetaDir)) {
await fs.mkdir(newMetaDir);
}
fs.writeFile(
path.resolve(metaDir, `${block}/meta.json`),
format(JSON.stringify(newMeta), { parser: 'json' })
).catch(reason => {
throw reason;
});
}
async function createIntroMD(superBlock: string, block: string, title: string) {
const introMD = `---
title: Introduction to the ${title}
block: ${block}
superBlock: Responsive Web Design
isBeta: true
---
## Introduction to the ${title}
This is a test for the new project-based curriculum.`;
const dirPath = path.resolve(
__dirname,
`../../client/src/pages/learn/${superBlock}/${block}/`
);
const filePath = path.resolve(dirPath, 'index.md');
if (!existsSync(dirPath)) {
await fs.mkdir(dirPath);
}
fs.writeFile(filePath, introMD, { encoding: 'utf8' }).catch(reason => {
throw reason;
});
}
async function createFirstChallenge(
superBlock: SuperBlock,
block: string
): Promise<string> {
const superBlockId = (superBlocks.indexOf(superBlock) + 1)
.toString()
.padStart(2, '0');
const newChallengeDir = path.resolve(
__dirname,
`../../curriculum/challenges/english/${superBlockId}-${superBlock}/${block}`
);
if (!existsSync(newChallengeDir)) {
await fs.mkdir(newChallengeDir);
}
// TODO: would be nice if the extension made sense for the challenge, but, at
// least until react I think they're all going to be html anyway.
const challengeSeeds = {
indexhtml: {
contents: '',
ext: 'html',
editableRegionBoundaries: [0, 2]
}
};
// including trailing slash for compatibility with createStepFile
return createStepFile({
projectPath: newChallengeDir + '/',
stepNum: 1,
challengeSeeds,
stepBetween: false
});
}
function parseJson<JsonSchema>(filePath: string) {
return fs
.readFile(filePath, { encoding: 'utf8' })
.then(result => JSON.parse(result) as JsonSchema)
.catch(reason => {
throw reason;
});
}
prompt([
{
name: 'superBlock',
message: 'Which certification does this belong to?',
default: 'responsive-web-design',
type: 'list',
choices: superBlocks
},
{
name: 'block',
message: 'What is the short name (in kebab-case) for this project?',
validate: (block: string) => {
if (!block.length) {
return 'please enter a short name';
}
if (/[^a-z0-9\-]/.test(block)) {
return 'please use alphanumerical characters and kebab case';
}
return true;
},
filter: (block: string) => {
return block.toLowerCase();
}
},
{
name: 'title',
default: ({ block }: { block: string }) => blockNameify(block)
},
{
name: 'helpCategory',
message: 'Choose a help category',
default: 'HTML-CSS',
type: 'list',
choices: helpCategories
},
{
name: 'order',
message: 'Which position does this appear in the certificate?',
default: 42,
validate: (order: string) => {
return parseInt(order, 10) > 0
? true
: 'Order must be an number greater than zero.';
},
filter: (order: string) => {
return parseInt(order, 10);
}
}
])
.then(({ superBlock, block, title, helpCategory, order }) =>
createProject(superBlock, block, helpCategory, order, title)
)
.then(() =>
console.log(
'All set. Now use npm run clean:client in the root and it should be good to go.'
)
)
.catch(console.error);