fix(tools): make create-challenge-helper prioritize full stack curriculum (#59644)

Co-authored-by: Oliver Eyton-Williams <ojeytonwilliams@gmail.com>
This commit is contained in:
Anna
2025-09-11 11:34:04 -04:00
committed by GitHub
parent 18b7cd8acc
commit 392f7f805e
3 changed files with 254 additions and 34 deletions
+223 -31
View File
@@ -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<SuperBlocks, string>
)[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<IntroJson>(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<ObjectID> {
async function createFirstChallenge(block: string): Promise<ObjectID> {
const { blockContentDir } = getContentConfig('english') as {
blockContentDir: string;
};
@@ -138,6 +222,26 @@ async function createFirstChallenge(
});
}
async function createQuizChallenge(
block: string,
title: string,
questionCount: number
): Promise<ObjectID> {
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<JsonSchema>(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(
@@ -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:
@@ -1,5 +1,4 @@
import path from 'path';
import {
getBlockStructure,
writeBlockStructure