mirror of
https://github.com/freeCodeCamp/freeCodeCamp.git
synced 2026-05-28 18:26:54 +00:00
fix(challenge-helper-scripts): make create-language-block support chapter-based structure (#62268)
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import fs from 'fs/promises';
|
||||
import { existsSync } from 'fs';
|
||||
import path from 'path';
|
||||
import { prompt } from 'inquirer';
|
||||
import { format } from 'prettier';
|
||||
@@ -6,17 +7,23 @@ import ObjectID from 'bson-objectid';
|
||||
|
||||
import {
|
||||
SuperBlocks,
|
||||
languageSuperBlocks
|
||||
languageSuperBlocks,
|
||||
chapterBasedSuperBlocks
|
||||
} from '../../shared/config/curriculum';
|
||||
import { BlockLayouts, BlockTypes } from '../../shared/config/blocks';
|
||||
import {
|
||||
getContentConfig,
|
||||
writeBlockStructure
|
||||
writeBlockStructure,
|
||||
getSuperblockStructure
|
||||
} from '../../curriculum/file-handler';
|
||||
import { superBlockToFilename } from '../../curriculum/build-curriculum';
|
||||
import { getBaseMeta } from './helpers/get-base-meta';
|
||||
import { createIntroMD } from './helpers/create-intro';
|
||||
import { createDialogueFile, validateBlockName } from './utils';
|
||||
import { updateSimpleSuperblockStructure } from './helpers/create-project';
|
||||
import { createDialogueFile, createQuizFile, validateBlockName } from './utils';
|
||||
import {
|
||||
updateSimpleSuperblockStructure,
|
||||
updateChapterModuleSuperblockStructure
|
||||
} from './helpers/create-project';
|
||||
|
||||
const helpCategories = ['English'] as const;
|
||||
|
||||
@@ -36,25 +43,70 @@ interface CreateBlockArgs {
|
||||
block: string;
|
||||
helpCategory: string;
|
||||
title?: string;
|
||||
chapter?: string;
|
||||
module?: string;
|
||||
position?: number;
|
||||
blockType?: string;
|
||||
blockLayout?: string;
|
||||
questionCount?: number;
|
||||
}
|
||||
|
||||
async function createLanguageBlock(
|
||||
superBlock: SuperBlocks,
|
||||
block: string,
|
||||
helpCategory: string,
|
||||
title?: string
|
||||
title?: string,
|
||||
chapter?: string,
|
||||
module?: string,
|
||||
position?: number,
|
||||
blockType?: string,
|
||||
blockLayout?: string,
|
||||
questionCount?: number
|
||||
) {
|
||||
if (!title) {
|
||||
title = block;
|
||||
}
|
||||
await updateIntroJson(superBlock, block, title);
|
||||
|
||||
const challengeId = await createDialogueChallenge(superBlock, block);
|
||||
await createMetaJson(block, title, helpCategory, challengeId);
|
||||
let challengeId: ObjectID;
|
||||
|
||||
if (blockType === BlockTypes.quiz) {
|
||||
challengeId = await createQuizChallenge(block, title, questionCount!);
|
||||
blockLayout = BlockLayouts.Link;
|
||||
} else {
|
||||
challengeId = await createDialogueChallenge(superBlock, block);
|
||||
}
|
||||
|
||||
await createMetaJson(
|
||||
block,
|
||||
title,
|
||||
helpCategory,
|
||||
challengeId,
|
||||
blockType,
|
||||
blockLayout
|
||||
);
|
||||
|
||||
const superblockFilename = (
|
||||
superBlockToFilename as Record<SuperBlocks, string>
|
||||
)[superBlock];
|
||||
void updateSimpleSuperblockStructure(block, {}, superblockFilename);
|
||||
|
||||
if (chapterBasedSuperBlocks.includes(superBlock)) {
|
||||
if (!chapter || !module || typeof position === 'undefined') {
|
||||
throw Error(
|
||||
'Missing one of the following arguments: chapter, module, position'
|
||||
);
|
||||
}
|
||||
|
||||
void updateChapterModuleSuperblockStructure(
|
||||
block,
|
||||
// Convert human-friendly (1-based) position to 0-based index for insertion.
|
||||
{ order: position - 1, chapter, module },
|
||||
superblockFilename
|
||||
);
|
||||
} else {
|
||||
void updateSimpleSuperblockStructure(block, {}, superblockFilename);
|
||||
}
|
||||
|
||||
// TODO: remove once we stop relying on markdown in the client.
|
||||
await createIntroMD(superBlock, block, title);
|
||||
}
|
||||
@@ -84,15 +136,31 @@ async function createMetaJson(
|
||||
block: string,
|
||||
title: string,
|
||||
helpCategory: string,
|
||||
challengeId: ObjectID
|
||||
challengeId: ObjectID,
|
||||
blockType?: string,
|
||||
blockLayout?: string
|
||||
) {
|
||||
const newMeta = getBaseMeta('Language');
|
||||
newMeta.name = title;
|
||||
newMeta.dashedName = block;
|
||||
newMeta.helpCategory = helpCategory;
|
||||
|
||||
if (blockType) {
|
||||
newMeta.blockType = blockType;
|
||||
}
|
||||
if (blockLayout) {
|
||||
newMeta.blockLayout = blockLayout;
|
||||
}
|
||||
|
||||
const challengeTitle =
|
||||
blockType === BlockTypes.quiz ? title : "Dialogue 1: I'm Tom";
|
||||
|
||||
newMeta.challengeOrder = [
|
||||
// eslint-disable-next-line @typescript-eslint/no-base-to-string
|
||||
{ id: challengeId.toString(), title: "Dialogue 1: I'm Tom" }
|
||||
{
|
||||
// eslint-disable-next-line @typescript-eslint/no-base-to-string
|
||||
id: challengeId.toString(),
|
||||
title: challengeTitle
|
||||
}
|
||||
];
|
||||
|
||||
await writeBlockStructure(block, newMeta);
|
||||
@@ -114,6 +182,26 @@ async function createDialogueChallenge(
|
||||
});
|
||||
}
|
||||
|
||||
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
|
||||
@@ -160,11 +248,117 @@ void prompt([
|
||||
default: 'English',
|
||||
type: 'list',
|
||||
choices: helpCategories
|
||||
},
|
||||
{
|
||||
name: 'blockType',
|
||||
message: 'Choose a block type',
|
||||
default: BlockTypes.learn,
|
||||
type: 'list',
|
||||
choices: Object.values(BlockTypes),
|
||||
when: (answers: CreateBlockArgs) =>
|
||||
chapterBasedSuperBlocks.includes(answers.superBlock)
|
||||
},
|
||||
{
|
||||
name: 'blockLayout',
|
||||
message: 'Choose a block layout',
|
||||
default: BlockLayouts.DialogueGrid,
|
||||
type: 'list',
|
||||
choices: Object.values(BlockLayouts),
|
||||
when: (answers: CreateBlockArgs) =>
|
||||
chapterBasedSuperBlocks.includes(answers.superBlock) &&
|
||||
answers.blockType !== BlockTypes.quiz
|
||||
},
|
||||
{
|
||||
name: 'questionCount',
|
||||
message: 'Choose a question count',
|
||||
default: 20,
|
||||
type: 'list',
|
||||
choices: [10, 20],
|
||||
when: (answers: CreateBlockArgs) => answers.blockType === BlockTypes.quiz
|
||||
},
|
||||
{
|
||||
name: 'chapter',
|
||||
message: 'What chapter should this language block go in?',
|
||||
type: 'list',
|
||||
choices: (answers: CreateBlockArgs) => {
|
||||
const superblockFilename = (
|
||||
superBlockToFilename as Record<SuperBlocks, string>
|
||||
)[answers.superBlock];
|
||||
const structure = getSuperblockStructure(superblockFilename) as {
|
||||
chapters: {
|
||||
dashedName: string;
|
||||
modules: { dashedName: string; blocks: string[] }[];
|
||||
}[];
|
||||
};
|
||||
return structure.chapters.map(chapter => chapter.dashedName);
|
||||
},
|
||||
when: (answers: CreateBlockArgs) =>
|
||||
chapterBasedSuperBlocks.includes(answers.superBlock)
|
||||
},
|
||||
{
|
||||
name: 'module',
|
||||
message: 'What module should this language block go in?',
|
||||
type: 'list',
|
||||
choices: (answers: CreateBlockArgs) => {
|
||||
const superblockFilename = (
|
||||
superBlockToFilename as Record<SuperBlocks, string>
|
||||
)[answers.superBlock];
|
||||
const structure = getSuperblockStructure(superblockFilename) as {
|
||||
chapters: {
|
||||
dashedName: string;
|
||||
modules: { dashedName: string; blocks: string[] }[];
|
||||
}[];
|
||||
};
|
||||
return (
|
||||
structure.chapters
|
||||
.find(chapter => chapter.dashedName === answers.chapter)
|
||||
?.modules.map(module => module.dashedName) ?? []
|
||||
);
|
||||
},
|
||||
when: (answers: CreateBlockArgs) =>
|
||||
chapterBasedSuperBlocks.includes(answers.superBlock)
|
||||
},
|
||||
{
|
||||
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: CreateBlockArgs) =>
|
||||
chapterBasedSuperBlocks.includes(answers.superBlock),
|
||||
filter: (position: string) => {
|
||||
return parseInt(position, 10);
|
||||
}
|
||||
}
|
||||
])
|
||||
.then(
|
||||
async ({ superBlock, block, helpCategory, title }: CreateBlockArgs) =>
|
||||
await createLanguageBlock(superBlock, block, helpCategory, title)
|
||||
async ({
|
||||
superBlock,
|
||||
block,
|
||||
helpCategory,
|
||||
title,
|
||||
chapter,
|
||||
module,
|
||||
position,
|
||||
blockType,
|
||||
blockLayout,
|
||||
questionCount
|
||||
}: CreateBlockArgs) =>
|
||||
await createLanguageBlock(
|
||||
superBlock,
|
||||
block,
|
||||
helpCategory,
|
||||
title,
|
||||
chapter,
|
||||
module,
|
||||
position,
|
||||
blockType,
|
||||
blockLayout,
|
||||
questionCount
|
||||
)
|
||||
)
|
||||
.then(() =>
|
||||
console.log(
|
||||
|
||||
Reference in New Issue
Block a user