mirror of
https://github.com/freeCodeCamp/freeCodeCamp.git
synced 2026-05-28 18:26:54 +00:00
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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user