* 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
289 lines
7.2 KiB
TypeScript
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);
|