From 392f7f805eb48f44ab6bb995a2363f47c6395336 Mon Sep 17 00:00:00 2001 From: Anna Date: Thu, 11 Sep 2025 11:34:04 -0400 Subject: [PATCH] fix(tools): make create-challenge-helper prioritize full stack curriculum (#59644) Co-authored-by: Oliver Eyton-Williams --- .../create-project.ts | 254 +++++++++++++++--- .../helpers/get-base-meta.ts | 33 ++- .../helpers/project-metadata.ts | 1 - 3 files changed, 254 insertions(+), 34 deletions(-) diff --git a/tools/challenge-helper-scripts/create-project.ts b/tools/challenge-helper-scripts/create-project.ts index 66f73f57993..fc91c428e67 100644 --- a/tools/challenge-helper-scripts/create-project.ts +++ b/tools/challenge-helper-scripts/create-project.ts @@ -1,19 +1,25 @@ +import { existsSync } from 'fs'; import fs from 'fs/promises'; import path from 'path'; import { prompt } from 'inquirer'; import { format } from 'prettier'; import ObjectID from 'bson-objectid'; +import fullStackData from '../../curriculum/structure/superblocks/full-stack-developer.json'; import { SuperBlocks } from '../../shared/config/curriculum'; +import { BlockLayouts, BlockTypes } from '../../shared/config/blocks'; import { getContentConfig, writeBlockStructure } from '../../curriculum/file-handler'; import { superBlockToFilename } from '../../curriculum/build-curriculum'; -import { createStepFile, validateBlockName } from './utils'; +import { createQuizFile, createStepFile, validateBlockName } from './utils'; import { getBaseMeta } from './helpers/get-base-meta'; import { createIntroMD } from './helpers/create-intro'; -import { updateSimpleSuperblockStructure } from './helpers/create-project'; +import { + updateChapterModuleSuperblockStructure, + updateSimpleSuperblockStructure +} from './helpers/create-project'; const helpCategories = [ 'HTML-CSS', @@ -41,34 +47,103 @@ interface CreateProjectArgs { superBlock: SuperBlocks; block: string; helpCategory: string; - order: number; + blockType?: string; + blockLayout?: string; + questionCount?: number; + order?: number; + chapter?: string; + position?: number; + module?: string; title?: string; } -async function createProject( - superBlock: SuperBlocks, - block: string, - helpCategory: string, - order: number, - title?: string -) { - if (!title) { - title = block; +async function createProject(projectArgs: CreateProjectArgs) { + if (!projectArgs.title) { + projectArgs.title = projectArgs.block; } - void updateIntroJson(superBlock, block, title); - const challengeId = await createFirstChallenge(superBlock, block); - void createMetaJson(block, title, helpCategory, challengeId); + const order = projectArgs.order; + const chapter = projectArgs.chapter; + const module = projectArgs.module; + const position = projectArgs.position; + const superblockFilename = ( superBlockToFilename as Record - )[superBlock]; - // TODO: handle full-stack-developer (createProjects needs calling with a - // chapter and module name as well) - if (superBlock !== SuperBlocks.FullStackDeveloper) { - void updateSimpleSuperblockStructure(block, { order }, superblockFilename); + )[projectArgs.superBlock]; + + if (projectArgs.superBlock === SuperBlocks.FullStackDeveloper) { + if (!chapter || !module || typeof position == 'undefined') { + throw Error( + 'Missing one of the following arguments: chapter, module, position' + ); + } + void updateChapterModuleSuperblockStructure( + projectArgs.block, + { order: position, chapter, module }, + superblockFilename + ); + } else { + if (typeof order == 'undefined') { + throw Error('Missing argument: order'); + } + void updateSimpleSuperblockStructure( + projectArgs.block, + { order }, + superblockFilename + ); } - // TODO: remove once we stop relying on markdown in the client. - void createIntroMD(superBlock, block, title); + + void updateIntroJson( + projectArgs.superBlock, + projectArgs.block, + projectArgs.title + ); + + if (projectArgs.blockType === BlockTypes.quiz) { + if (projectArgs.questionCount == null) { + throw new Error( + 'Property `questionCount` is null when creating new Quiz Challenge' + ); + } + const challengeId = await createQuizChallenge( + projectArgs.block, + projectArgs.title, + projectArgs.questionCount + ); + void createMetaJson( + projectArgs.superBlock, + projectArgs.block, + projectArgs.title, + projectArgs.helpCategory, + challengeId + ); + } else { + const challengeId = await createFirstChallenge(projectArgs.block); + void createMetaJson( + projectArgs.superBlock, + projectArgs.block, + projectArgs.title, + projectArgs.helpCategory, + challengeId, + projectArgs.order, + projectArgs.blockType, + projectArgs.blockLayout + ); + // TODO: remove once we stop relying on markdown in the client. + } + + if ( + (projectArgs.superBlock === SuperBlocks.FullStackDeveloper && + projectArgs.blockType) == null + ) { + throw new Error('Missing argument: blockType when updating intro markdown'); + } + + void createIntroMD( + projectArgs.superBlock, + projectArgs.block, + projectArgs.title + ); } async function updateIntroJson( @@ -83,7 +158,7 @@ async function updateIntroJson( const newIntro = await parseJson(introJsonPath); newIntro[superBlock].blocks[block] = { title, - intro: ['', ''] + intro: [title, ''] }; void withTrace( fs.writeFile, @@ -93,12 +168,24 @@ async function updateIntroJson( } async function createMetaJson( + superBlock: SuperBlocks, block: string, title: string, helpCategory: string, - challengeId: ObjectID + challengeId: ObjectID, + order?: number, + blockType?: string, + blockLayout?: string ) { - const newMeta = getBaseMeta('Step'); + let newMeta; + if (superBlock === SuperBlocks.FullStackDeveloper) { + newMeta = getBaseMeta('FullStack'); + newMeta.blockType = blockType; + newMeta.blockLayout = blockLayout; + } else { + newMeta = getBaseMeta('Step'); + newMeta.order = order; + } newMeta.name = title; newMeta.dashedName = block; newMeta.helpCategory = helpCategory; @@ -108,10 +195,7 @@ async function createMetaJson( await writeBlockStructure(block, newMeta); } -async function createFirstChallenge( - superBlock: SuperBlocks, - block: string -): Promise { +async function createFirstChallenge(block: string): Promise { const { blockContentDir } = getContentConfig('english') as { blockContentDir: string; }; @@ -138,6 +222,26 @@ async function createFirstChallenge( }); } +async function createQuizChallenge( + block: string, + title: string, + questionCount: number +): Promise { + const newChallengeDir = path.resolve( + __dirname, + `../../curriculum/challenges/english/${block}` + ); + if (!existsSync(newChallengeDir)) { + await withTrace(fs.mkdir, newChallengeDir); + } + return createQuizFile({ + projectPath: newChallengeDir + '/', + title: title, + dashedName: block, + questionCount: questionCount + }); +} + function parseJson(filePath: string) { return withTrace(fs.readFile, filePath, 'utf8').then( // unfortunately, withTrace does not correctly infer that the third argument @@ -162,7 +266,7 @@ void prompt([ { name: 'superBlock', message: 'Which certification does this belong to?', - default: SuperBlocks.RespWebDesign, + default: SuperBlocks.FullStackDeveloper, type: 'list', choices: Object.values(SuperBlocks) }, @@ -185,6 +289,74 @@ void prompt([ type: 'list', choices: helpCategories }, + { + name: 'blockType', + message: 'Choose a block type', + default: BlockTypes.lab, + type: 'list', + choices: Object.values(BlockTypes), + when: (answers: CreateProjectArgs) => + answers.superBlock === SuperBlocks.FullStackDeveloper + }, + { + name: 'blockLayout', + message: 'Choose a block layout', + + default: (answers: { blockType: BlockTypes }) => + answers.blockType == BlockTypes.quiz + ? BlockLayouts.Link + : BlockLayouts.ChallengeList, + type: 'list', + choices: Object.values(BlockLayouts), + when: (answers: CreateProjectArgs) => + answers.superBlock === SuperBlocks.FullStackDeveloper + }, + { + name: 'questionCount', + message: 'Choose a question count', + default: 20, + type: 'list', + choices: [10, 20], + when: (answers: CreateProjectArgs) => answers.blockType === BlockTypes.quiz + }, + { + name: 'chapter', + message: + 'What chapter in full-stack.json should this full stack project go in?', + default: 'html', + type: 'list', + choices: fullStackData.chapters.map(x => x.dashedName), + when: (answers: CreateProjectArgs) => + answers.superBlock === SuperBlocks.FullStackDeveloper + }, + { + name: 'module', + message: + 'What module in full-stack.json should this full stack project go in?', + default: 'html', + type: 'list', + choices: (answers: CreateProjectArgs) => + fullStackData.chapters + .find(x => x.dashedName === answers.chapter) + ?.modules.map(x => x.dashedName), + when: (answers: CreateProjectArgs) => + answers.superBlock === SuperBlocks.FullStackDeveloper + }, + { + name: 'position', + message: 'At which position does this appear in the module?', + default: 1, + validate: (position: string) => { + return parseInt(position, 10) > 0 + ? true + : 'Position must be an number greater than zero.'; + }, + when: (answers: CreateProjectArgs) => + answers.superBlock === SuperBlocks.FullStackDeveloper, + filter: (position: string) => { + return parseInt(position, 10); + } + }, { name: 'order', message: 'Which position does this appear in the certificate?', @@ -194,6 +366,8 @@ void prompt([ ? true : 'Order must be an number greater than zero.'; }, + when: (answers: CreateProjectArgs) => + answers.superBlock !== SuperBlocks.FullStackDeveloper, filter: (order: string) => { return parseInt(order, 10); } @@ -205,9 +379,27 @@ void prompt([ block, title, helpCategory, + blockType, + blockLayout, + questionCount, + chapter, + module, + position, order }: CreateProjectArgs) => - await createProject(superBlock, block, helpCategory, order, title) + await createProject({ + superBlock, + block, + helpCategory, + blockType, + blockLayout, + questionCount, + title, + chapter, + module, + position, + order + }) ) .then(() => console.log( diff --git a/tools/challenge-helper-scripts/helpers/get-base-meta.ts b/tools/challenge-helper-scripts/helpers/get-base-meta.ts index 0354b7f2741..3270acde620 100644 --- a/tools/challenge-helper-scripts/helpers/get-base-meta.ts +++ b/tools/challenge-helper-scripts/helpers/get-base-meta.ts @@ -1,5 +1,23 @@ -const baseMeta = { +interface Meta { + name: string; + isUpcomingChange: boolean; + dashedName: string; + superBlock: string; + helpCategory: string; + challengeOrder: Array<{ + id: string; + title: string; + }>; + usesMultifileEditor?: boolean; + hasEditableBoundaries?: boolean; + blockType?: string; + blockLayout?: string; + order?: number; +} + +const baseMeta: Meta = { name: '', + superBlock: '', isUpcomingChange: true, dashedName: '', helpCategory: '', @@ -18,6 +36,13 @@ const stepMeta = { hasEditableBoundaries: true }; +const fullStackStepMeta = { + ...baseMeta, + blockType: '', + blockLayout: '', + usesMultifileEditor: true +}; + const quizMeta = { ...baseMeta, blockType: 'quiz', @@ -29,12 +54,16 @@ const languageMeta = { blockLayout: 'dialogue-grid' }; -export const getBaseMeta = (projectType: 'Step' | 'Quiz' | 'Language') => { +export const getBaseMeta = ( + projectType: 'Step' | 'Quiz' | 'Language' | 'FullStack' +): Meta => { switch (projectType) { case 'Step': return stepMeta; case 'Quiz': return quizMeta; + case 'FullStack': + return fullStackStepMeta; case 'Language': return languageMeta; default: diff --git a/tools/challenge-helper-scripts/helpers/project-metadata.ts b/tools/challenge-helper-scripts/helpers/project-metadata.ts index 92cd067b45e..41e8fb7b5d9 100644 --- a/tools/challenge-helper-scripts/helpers/project-metadata.ts +++ b/tools/challenge-helper-scripts/helpers/project-metadata.ts @@ -1,5 +1,4 @@ import path from 'path'; - import { getBlockStructure, writeBlockStructure