From 108d2460e7a19bc884552980ac91e67430c1ddea Mon Sep 17 00:00:00 2001 From: Oliver Eyton-Williams Date: Mon, 7 Mar 2022 06:21:33 +0100 Subject: [PATCH] fix(tools): use correct superblock paths (#45285) * fix: use array of choices * fix: capture traces from fs promise errors * fix: use helper to get superblock subpath --- .../create-project.ts | 112 +++++++++--------- .../challenge-helper-scripts/fs-utils.test.ts | 15 +++ tools/challenge-helper-scripts/fs-utils.ts | 21 ++++ 3 files changed, 90 insertions(+), 58 deletions(-) create mode 100644 tools/challenge-helper-scripts/fs-utils.test.ts create mode 100644 tools/challenge-helper-scripts/fs-utils.ts diff --git a/tools/challenge-helper-scripts/create-project.ts b/tools/challenge-helper-scripts/create-project.ts index 8ef6d564b70..38ac73048e2 100644 --- a/tools/challenge-helper-scripts/create-project.ts +++ b/tools/challenge-helper-scripts/create-project.ts @@ -8,6 +8,7 @@ import ObjectID from 'bson-objectid'; import { SuperBlocks } from '../../config/certification-settings'; import { blockNameify } from '../../utils/block-nameify'; import { createStepFile } from './utils'; +import { getSuperBlockSubPath } from './fs-utils'; const helpCategories = ['HTML-CSS', 'JavaScript', 'Python'] as const; @@ -54,29 +55,15 @@ async function createProject( if (!title) { title = blockNameify(block); } else if (title !== blockNameify(block)) { - updateBlockNames(block, title).catch(reason => { - throw reason; - }); + void updateBlockNames(block, title); } - updateIntroJson(superBlock, block, title).catch(reason => { - throw reason; - }); - updateHelpCategoryMap(block, helpCategory).catch(reason => { - throw reason; - }); + void updateIntroJson(superBlock, block, title); + void updateHelpCategoryMap(block, helpCategory); - const challengeId = await createFirstChallenge(superBlock, block).catch( - reason => { - throw reason; - } - ); - createMetaJson(superBlock, block, title, order, challengeId).catch(reason => { - throw reason; - }); + const challengeId = await createFirstChallenge(superBlock, block); + void createMetaJson(superBlock, block, title, order, challengeId); // TODO: remove once we stop relying on markdown in the client. - createIntroMD(superBlock, block, title).catch(reason => { - throw reason; - }); + void createIntroMD(superBlock, block, title); } async function updateIntroJson( @@ -93,12 +80,11 @@ async function updateIntroJson( title, intro: ['', ''] }; - fs.writeFile( + void withTrace( + fs.writeFile, introJsonPath, format(JSON.stringify(newIntro), { parser: 'json' }) - ).catch(reason => { - throw reason; - }); + ); } async function updateHelpCategoryMap(block: string, helpCategory: string) { @@ -108,12 +94,11 @@ async function updateHelpCategoryMap(block: string, helpCategory: string) { ); const helpMap = await parseJson>(helpCategoryPath); helpMap[block] = helpCategory; - fs.writeFile( + void withTrace( + fs.writeFile, helpCategoryPath, format(JSON.stringify(helpMap), { parser: 'json' }) - ).catch(reason => { - throw reason; - }); + ); } async function updateBlockNames(block: string, title: string) { @@ -123,12 +108,11 @@ async function updateBlockNames(block: string, title: string) { ); const blockNames = await parseJson>(blockNamesPath); blockNames[block] = title; - fs.writeFile( + void withTrace( + fs.writeFile, blockNamesPath, format(JSON.stringify(blockNames), { parser: 'json' }) - ).catch(reason => { - throw reason; - }); + ); } async function createMetaJson( @@ -148,14 +132,14 @@ async function createMetaJson( newMeta.challengeOrder = [[challengeId.toString(), 'Step 1']]; const newMetaDir = path.resolve(metaDir, block); if (!existsSync(newMetaDir)) { - await fs.mkdir(newMetaDir); + await withTrace(fs.mkdir, newMetaDir); } - fs.writeFile( + + void withTrace( + 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) { @@ -176,26 +160,22 @@ This is a test for the new project-based curriculum. ); const filePath = path.resolve(dirPath, 'index.md'); if (!existsSync(dirPath)) { - await fs.mkdir(dirPath); + await withTrace(fs.mkdir, dirPath); } - fs.writeFile(filePath, introMD, { encoding: 'utf8' }).catch(reason => { - throw reason; - }); + void withTrace(fs.writeFile, filePath, introMD, { encoding: 'utf8' }); } async function createFirstChallenge( superBlock: SuperBlocks, block: string ): Promise { - const superBlockId = (Object.values(SuperBlocks).indexOf(superBlock) + 1) - .toString() - .padStart(2, '0'); + const superBlockSubPath = getSuperBlockSubPath(superBlock); const newChallengeDir = path.resolve( __dirname, - `../../curriculum/challenges/english/${superBlockId}-${superBlock}/${block}` + `../../curriculum/challenges/english/${superBlockSubPath}/${block}` ); if (!existsSync(newChallengeDir)) { - await fs.mkdir(newChallengeDir); + await withTrace(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. @@ -215,21 +195,32 @@ async function createFirstChallenge( } function parseJson(filePath: string) { - return fs - .readFile(filePath, { encoding: 'utf8' }) - .then(result => JSON.parse(result) as JsonSchema) - .catch(reason => { - throw reason; - }); + return withTrace(fs.readFile, filePath, 'utf8').then( + // unfortunately, withTrace does not correctly infer that the third argument + // is a string, so it uses the (path, options?) overload and we have to cast + // result to string. + result => JSON.parse(result as string) as JsonSchema + ); } -prompt([ +// fs Promise functions return errors, but no stack trace. This adds back in +// the stack trace. +function withTrace( + fn: (...x: Args) => Promise, + ...args: Args +): Promise { + return fn(...args).catch((reason: Error) => { + throw Error(reason.message); + }); +} + +void prompt([ { name: 'superBlock', message: 'Which certification does this belong to?', default: SuperBlocks.RespWebDesign, type: 'list', - choices: SuperBlocks + choices: Object.values(SuperBlocks) }, { name: 'block', @@ -273,12 +264,17 @@ prompt([ } ]) .then( - ({ superBlock, block, title, helpCategory, order }: CreateProjectArgs) => - createProject(superBlock, block, helpCategory, order, title) + async ({ + superBlock, + block, + title, + helpCategory, + order + }: CreateProjectArgs) => + await 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); + ); diff --git a/tools/challenge-helper-scripts/fs-utils.test.ts b/tools/challenge-helper-scripts/fs-utils.test.ts new file mode 100644 index 00000000000..efbd484ebfa --- /dev/null +++ b/tools/challenge-helper-scripts/fs-utils.test.ts @@ -0,0 +1,15 @@ +import fs from 'fs/promises'; +import path from 'path'; +import { SuperBlocks } from '../../config/certification-settings'; +import { getSuperBlockSubPath } from './fs-utils'; + +describe('getSuperBlockSubPath', () => { + it('should return sub-paths that exist', async () => { + const subPaths = Object.values(SuperBlocks).map(getSuperBlockSubPath); + const paths = subPaths.map(sub => + path.resolve(__dirname, '../../curriculum/challenges/english', sub) + ); + + await Promise.all(paths.map(path => fs.access(path))); + }); +}); diff --git a/tools/challenge-helper-scripts/fs-utils.ts b/tools/challenge-helper-scripts/fs-utils.ts new file mode 100644 index 00000000000..1c5403cb976 --- /dev/null +++ b/tools/challenge-helper-scripts/fs-utils.ts @@ -0,0 +1,21 @@ +import { SuperBlocks } from '../../config/certification-settings'; + +export function getSuperBlockSubPath(superBlock: SuperBlocks): string { + const pathMap = { + [SuperBlocks.RespWebDesign]: '01-responsive-web-design', + [SuperBlocks.JsAlgoDataStruct]: + '02-javascript-algorithms-and-data-structures', + [SuperBlocks.FrontEndDevLibs]: '03-front-end-development-libraries', + [SuperBlocks.DataVis]: '04-data-visualization', + [SuperBlocks.BackEndDevApis]: '05-back-end-development-and-apis', + [SuperBlocks.QualityAssurance]: '06-quality-assurance', + [SuperBlocks.SciCompPy]: '07-scientific-computing-with-python', + [SuperBlocks.DataAnalysisPy]: '08-data-analysis-with-python', + [SuperBlocks.InfoSec]: '09-information-security', + [SuperBlocks.CodingInterviewPrep]: '10-coding-interview-prep', + [SuperBlocks.MachineLearningPy]: '11-machine-learning-with-python', + [SuperBlocks.RelationalDb]: '13-relational-databases', + [SuperBlocks.RespWebDesignNew]: '14-responsive-web-design-22' + }; + return pathMap[superBlock]; +}