mirror of
https://github.com/freeCodeCamp/freeCodeCamp.git
synced 2026-02-21 11:04:47 -05:00
feat(tools): create challenge helper script for quiz files (#59523)
This commit is contained in:
@@ -40,6 +40,7 @@
|
||||
"clean:server": "rm -rf ./api-server/lib",
|
||||
"create:shared": "tsc -p shared",
|
||||
"create-new-project": "cd ./tools/challenge-helper-scripts/ && pnpm run create-project",
|
||||
"create-new-quiz": "cd ./tools/challenge-helper-scripts/ && pnpm run create-quiz",
|
||||
"predevelop": "npm-run-all -p create:shared -s build:curriculum",
|
||||
"develop": "npm-run-all -p develop:*",
|
||||
"develop:client": "cd ./client && pnpm run develop",
|
||||
|
||||
224
tools/challenge-helper-scripts/create-quiz.ts
Normal file
224
tools/challenge-helper-scripts/create-quiz.ts
Normal file
@@ -0,0 +1,224 @@
|
||||
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 { SuperBlocks } from '../../shared/config/curriculum';
|
||||
import { createQuizFile, validateBlockName } from './utils';
|
||||
import { getSuperBlockSubPath } from './fs-utils';
|
||||
import { Meta } from './helpers/project-metadata';
|
||||
|
||||
const helpCategories = [
|
||||
'HTML-CSS',
|
||||
'JavaScript',
|
||||
'Backend Development',
|
||||
'Python'
|
||||
] as const;
|
||||
|
||||
type BlockInfo = {
|
||||
title: string;
|
||||
intro: string[];
|
||||
};
|
||||
|
||||
type SuperBlockInfo = {
|
||||
blocks: Record<string, BlockInfo>;
|
||||
};
|
||||
|
||||
type IntroJson = Record<SuperBlocks, SuperBlockInfo>;
|
||||
|
||||
interface CreateQuizArgs {
|
||||
superBlock: SuperBlocks;
|
||||
block: string;
|
||||
helpCategory: string;
|
||||
title?: string;
|
||||
questionCount: number;
|
||||
}
|
||||
|
||||
async function createQuiz(
|
||||
superBlock: SuperBlocks,
|
||||
block: string,
|
||||
helpCategory: string,
|
||||
questionCount: number,
|
||||
title?: string
|
||||
) {
|
||||
if (!title) {
|
||||
title = block;
|
||||
}
|
||||
void updateIntroJson(superBlock, block, title);
|
||||
|
||||
const challengeId = await createQuizChallenge(
|
||||
superBlock,
|
||||
block,
|
||||
title,
|
||||
questionCount
|
||||
);
|
||||
void createMetaJson(superBlock, block, title, helpCategory, challengeId);
|
||||
// TODO: remove once we stop relying on markdown in the client.
|
||||
void createIntroMD(superBlock, block, title);
|
||||
}
|
||||
|
||||
async function updateIntroJson(
|
||||
superBlock: SuperBlocks,
|
||||
block: string,
|
||||
title: string
|
||||
) {
|
||||
const introJsonPath = path.resolve(
|
||||
__dirname,
|
||||
'../../client/i18n/locales/english/intro.json'
|
||||
);
|
||||
const newIntro = await parseJson<IntroJson>(introJsonPath);
|
||||
newIntro[superBlock].blocks[block] = {
|
||||
title,
|
||||
intro: ['', '']
|
||||
};
|
||||
void withTrace(
|
||||
fs.writeFile,
|
||||
introJsonPath,
|
||||
await format(JSON.stringify(newIntro), { parser: 'json' })
|
||||
);
|
||||
}
|
||||
|
||||
async function createMetaJson(
|
||||
superBlock: SuperBlocks,
|
||||
block: string,
|
||||
title: string,
|
||||
helpCategory: string,
|
||||
challengeId: ObjectID
|
||||
) {
|
||||
const metaDir = path.resolve(__dirname, '../../curriculum/challenges/_meta');
|
||||
const newMeta = await parseJson<Meta>('./quiz-meta.json');
|
||||
newMeta.name = title;
|
||||
newMeta.dashedName = block;
|
||||
newMeta.helpCategory = helpCategory;
|
||||
newMeta.superBlock = superBlock;
|
||||
// eslint-disable-next-line @typescript-eslint/no-base-to-string
|
||||
newMeta.challengeOrder = [{ id: challengeId.toString(), title: title }];
|
||||
const newMetaDir = path.resolve(metaDir, block);
|
||||
if (!existsSync(newMetaDir)) {
|
||||
await withTrace(fs.mkdir, newMetaDir);
|
||||
}
|
||||
|
||||
void withTrace(
|
||||
fs.writeFile,
|
||||
path.resolve(metaDir, `${block}/meta.json`),
|
||||
await format(JSON.stringify(newMeta), { parser: 'json' })
|
||||
);
|
||||
}
|
||||
|
||||
async function createIntroMD(superBlock: string, block: string, title: string) {
|
||||
const introMD = `---
|
||||
title: Introduction to the ${title}
|
||||
block: ${block}
|
||||
superBlock: ${superBlock}
|
||||
---
|
||||
|
||||
## Introduction to the ${title}
|
||||
|
||||
This page is for the ${title}
|
||||
`;
|
||||
const dirPath = path.resolve(
|
||||
__dirname,
|
||||
`../../client/src/pages/learn/${superBlock}/${block}/`
|
||||
);
|
||||
const filePath = path.resolve(dirPath, 'index.md');
|
||||
if (!existsSync(dirPath)) {
|
||||
await withTrace(fs.mkdir, dirPath);
|
||||
}
|
||||
void withTrace(fs.writeFile, filePath, introMD, { encoding: 'utf8' });
|
||||
}
|
||||
|
||||
async function createQuizChallenge(
|
||||
superBlock: SuperBlocks,
|
||||
block: string,
|
||||
title: string,
|
||||
questionCount: number
|
||||
): Promise<ObjectID> {
|
||||
const superBlockSubPath = getSuperBlockSubPath(superBlock);
|
||||
const newChallengeDir = path.resolve(
|
||||
__dirname,
|
||||
`../../curriculum/challenges/english/${superBlockSubPath}/${block}`
|
||||
);
|
||||
if (!existsSync(newChallengeDir)) {
|
||||
await withTrace(fs.mkdir, newChallengeDir);
|
||||
}
|
||||
return createQuizFile({
|
||||
challengeType: '8',
|
||||
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
|
||||
// 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
|
||||
);
|
||||
}
|
||||
|
||||
// fs Promise functions return errors, but no stack trace. This adds back in
|
||||
// the stack trace.
|
||||
function withTrace<Args extends unknown[], Result>(
|
||||
fn: (...x: Args) => Promise<Result>,
|
||||
...args: Args
|
||||
): Promise<Result> {
|
||||
return fn(...args).catch((reason: Error) => {
|
||||
throw Error(reason.message);
|
||||
});
|
||||
}
|
||||
|
||||
void prompt([
|
||||
{
|
||||
name: 'superBlock',
|
||||
message: 'Which certification does this belong to?',
|
||||
default: SuperBlocks.FullStackDeveloper,
|
||||
type: 'list',
|
||||
choices: Object.values(SuperBlocks)
|
||||
},
|
||||
{
|
||||
name: 'block',
|
||||
message: 'What is the dashed name (in kebab-case) for this quiz?',
|
||||
validate: validateBlockName,
|
||||
filter: (block: string) => {
|
||||
return block.toLowerCase().trim();
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'title',
|
||||
default: ({ block }: { block: string }) => block
|
||||
},
|
||||
{
|
||||
name: 'helpCategory',
|
||||
message: 'Choose a help category',
|
||||
default: 'HTML-CSS',
|
||||
type: 'list',
|
||||
choices: helpCategories
|
||||
},
|
||||
{
|
||||
name: 'questionCount',
|
||||
message: 'Should this quiz have either ten or twenty questions?',
|
||||
default: 20,
|
||||
type: 'list',
|
||||
choices: [20, 10]
|
||||
}
|
||||
])
|
||||
.then(
|
||||
async ({
|
||||
superBlock,
|
||||
block,
|
||||
title,
|
||||
helpCategory,
|
||||
questionCount
|
||||
}: CreateQuizArgs) =>
|
||||
await createQuiz(superBlock, block, helpCategory, questionCount, title)
|
||||
)
|
||||
.then(() =>
|
||||
console.log(
|
||||
'All set. Now use pnpm run clean:client in the root and it should be good to go.'
|
||||
)
|
||||
);
|
||||
@@ -10,6 +10,7 @@ interface ChallengeOptions {
|
||||
title: string;
|
||||
dashedName: string;
|
||||
challengeType: string;
|
||||
questionCount?: number;
|
||||
}
|
||||
|
||||
const buildFrontMatter = ({
|
||||
@@ -76,13 +77,13 @@ const getQuizChallengeTemplate = (
|
||||
|
||||
# --description--
|
||||
|
||||
To pass the quiz, you must correctly answer at least 18 of the 20 questions below.
|
||||
To pass the quiz, you must correctly answer at least ${options.questionCount! == 20 ? '18' : '9'} of the ${options.questionCount!.toString()} questions below.
|
||||
|
||||
# --quizzes--
|
||||
|
||||
## --quiz--
|
||||
|
||||
### --question--
|
||||
${`### --question--
|
||||
|
||||
#### --text--
|
||||
|
||||
@@ -104,6 +105,7 @@ Placeholder distractor 3
|
||||
|
||||
Placeholder answer
|
||||
|
||||
`.repeat(options.questionCount! - 1)}
|
||||
### --question--
|
||||
|
||||
#### --text--
|
||||
@@ -125,403 +127,6 @@ Placeholder distractor 3
|
||||
#### --answer--
|
||||
|
||||
Placeholder answer
|
||||
|
||||
### --question--
|
||||
|
||||
#### --text--
|
||||
|
||||
Placeholder question
|
||||
|
||||
#### --distractors--
|
||||
|
||||
Placeholder distractor 1
|
||||
|
||||
---
|
||||
|
||||
Placeholder distractor 2
|
||||
|
||||
---
|
||||
|
||||
Placeholder distractor 3
|
||||
|
||||
#### --answer--
|
||||
|
||||
Placeholder answer
|
||||
|
||||
### --question--
|
||||
|
||||
#### --text--
|
||||
|
||||
Placeholder question
|
||||
|
||||
#### --distractors--
|
||||
|
||||
Placeholder distractor 1
|
||||
|
||||
---
|
||||
|
||||
Placeholder distractor 2
|
||||
|
||||
---
|
||||
|
||||
Placeholder distractor 3
|
||||
|
||||
#### --answer--
|
||||
|
||||
Placeholder answer
|
||||
|
||||
### --question--
|
||||
|
||||
#### --text--
|
||||
|
||||
Placeholder question
|
||||
|
||||
#### --distractors--
|
||||
|
||||
Placeholder distractor 1
|
||||
|
||||
---
|
||||
|
||||
Placeholder distractor 2
|
||||
|
||||
---
|
||||
|
||||
Placeholder distractor 3
|
||||
|
||||
#### --answer--
|
||||
|
||||
Placeholder answer
|
||||
|
||||
### --question--
|
||||
|
||||
#### --text--
|
||||
|
||||
Placeholder question
|
||||
|
||||
#### --distractors--
|
||||
|
||||
Placeholder distractor 1
|
||||
|
||||
---
|
||||
|
||||
Placeholder distractor 2
|
||||
|
||||
---
|
||||
|
||||
Placeholder distractor 3
|
||||
|
||||
#### --answer--
|
||||
|
||||
Placeholder answer
|
||||
|
||||
### --question--
|
||||
|
||||
#### --text--
|
||||
|
||||
Placeholder question
|
||||
|
||||
#### --distractors--
|
||||
|
||||
Placeholder distractor 1
|
||||
|
||||
---
|
||||
|
||||
Placeholder distractor 2
|
||||
|
||||
---
|
||||
|
||||
Placeholder distractor 3
|
||||
|
||||
#### --answer--
|
||||
|
||||
Placeholder answer
|
||||
|
||||
### --question--
|
||||
|
||||
#### --text--
|
||||
|
||||
Placeholder question
|
||||
|
||||
#### --distractors--
|
||||
|
||||
Placeholder distractor 1
|
||||
|
||||
---
|
||||
|
||||
Placeholder distractor 2
|
||||
|
||||
---
|
||||
|
||||
Placeholder distractor 3
|
||||
|
||||
#### --answer--
|
||||
|
||||
Placeholder answer
|
||||
|
||||
### --question--
|
||||
|
||||
#### --text--
|
||||
|
||||
Placeholder question
|
||||
|
||||
#### --distractors--
|
||||
|
||||
Placeholder distractor 1
|
||||
|
||||
---
|
||||
|
||||
Placeholder distractor 2
|
||||
|
||||
---
|
||||
|
||||
Placeholder distractor 3
|
||||
|
||||
#### --answer--
|
||||
|
||||
Placeholder answer
|
||||
|
||||
### --question--
|
||||
|
||||
#### --text--
|
||||
|
||||
Placeholder question
|
||||
|
||||
#### --distractors--
|
||||
|
||||
Placeholder distractor 1
|
||||
|
||||
---
|
||||
|
||||
Placeholder distractor 2
|
||||
|
||||
---
|
||||
|
||||
Placeholder distractor 3
|
||||
|
||||
#### --answer--
|
||||
|
||||
Placeholder answer
|
||||
|
||||
### --question--
|
||||
|
||||
#### --text--
|
||||
|
||||
Placeholder question
|
||||
|
||||
#### --distractors--
|
||||
|
||||
Placeholder distractor 1
|
||||
|
||||
---
|
||||
|
||||
Placeholder distractor 2
|
||||
|
||||
---
|
||||
|
||||
Placeholder distractor 3
|
||||
|
||||
#### --answer--
|
||||
|
||||
Placeholder answer
|
||||
|
||||
### --question--
|
||||
|
||||
#### --text--
|
||||
|
||||
Placeholder question
|
||||
|
||||
#### --distractors--
|
||||
|
||||
Placeholder distractor 1
|
||||
|
||||
---
|
||||
|
||||
Placeholder distractor 2
|
||||
|
||||
---
|
||||
|
||||
Placeholder distractor 3
|
||||
|
||||
#### --answer--
|
||||
|
||||
Placeholder answer
|
||||
|
||||
### --question--
|
||||
|
||||
#### --text--
|
||||
|
||||
Placeholder question
|
||||
|
||||
#### --distractors--
|
||||
|
||||
Placeholder distractor 1
|
||||
|
||||
---
|
||||
|
||||
Placeholder distractor 2
|
||||
|
||||
---
|
||||
|
||||
Placeholder distractor 3
|
||||
|
||||
#### --answer--
|
||||
|
||||
Placeholder answer
|
||||
|
||||
### --question--
|
||||
|
||||
#### --text--
|
||||
|
||||
Placeholder question
|
||||
|
||||
#### --distractors--
|
||||
|
||||
Placeholder distractor 1
|
||||
|
||||
---
|
||||
|
||||
Placeholder distractor 2
|
||||
|
||||
---
|
||||
|
||||
Placeholder distractor 3
|
||||
|
||||
#### --answer--
|
||||
|
||||
Placeholder answer
|
||||
|
||||
### --question--
|
||||
|
||||
#### --text--
|
||||
|
||||
Placeholder question
|
||||
|
||||
#### --distractors--
|
||||
|
||||
Placeholder distractor 1
|
||||
|
||||
---
|
||||
|
||||
Placeholder distractor 2
|
||||
|
||||
---
|
||||
|
||||
Placeholder distractor 3
|
||||
|
||||
#### --answer--
|
||||
|
||||
Placeholder answer
|
||||
|
||||
### --question--
|
||||
|
||||
#### --text--
|
||||
|
||||
Placeholder question
|
||||
|
||||
#### --distractors--
|
||||
|
||||
Placeholder distractor 1
|
||||
|
||||
---
|
||||
|
||||
Placeholder distractor 2
|
||||
|
||||
---
|
||||
|
||||
Placeholder distractor 3
|
||||
|
||||
#### --answer--
|
||||
|
||||
Placeholder answer
|
||||
|
||||
### --question--
|
||||
|
||||
#### --text--
|
||||
|
||||
Placeholder question
|
||||
|
||||
#### --distractors--
|
||||
|
||||
Placeholder distractor 1
|
||||
|
||||
---
|
||||
|
||||
Placeholder distractor 2
|
||||
|
||||
---
|
||||
|
||||
Placeholder distractor 3
|
||||
|
||||
#### --answer--
|
||||
|
||||
Placeholder answer
|
||||
|
||||
### --question--
|
||||
|
||||
#### --text--
|
||||
|
||||
Placeholder question
|
||||
|
||||
#### --distractors--
|
||||
|
||||
Placeholder distractor 1
|
||||
|
||||
---
|
||||
|
||||
Placeholder distractor 2
|
||||
|
||||
---
|
||||
|
||||
Placeholder distractor 3
|
||||
|
||||
#### --answer--
|
||||
|
||||
Placeholder answer
|
||||
|
||||
### --question--
|
||||
|
||||
#### --text--
|
||||
|
||||
Placeholder question
|
||||
|
||||
#### --distractors--
|
||||
|
||||
Placeholder distractor 1
|
||||
|
||||
---
|
||||
|
||||
Placeholder distractor 2
|
||||
|
||||
---
|
||||
|
||||
Placeholder distractor 3
|
||||
|
||||
#### --answer--
|
||||
|
||||
Placeholder answer
|
||||
|
||||
### --question--
|
||||
|
||||
#### --text--
|
||||
|
||||
Placeholder question
|
||||
|
||||
#### --distractors--
|
||||
|
||||
Placeholder distractor 1
|
||||
|
||||
---
|
||||
|
||||
Placeholder distractor 2
|
||||
|
||||
---
|
||||
|
||||
Placeholder distractor 3
|
||||
|
||||
#### --answer--
|
||||
|
||||
Placeholder answer
|
||||
|
||||
`;
|
||||
|
||||
const getVideoChallengeTemplate = (
|
||||
|
||||
@@ -5,6 +5,8 @@ import { getProjectName, getProjectPath } from './get-project-info';
|
||||
|
||||
export type Meta = {
|
||||
name: string;
|
||||
blockLayout: string;
|
||||
blockType: string;
|
||||
isUpcomingChange: boolean;
|
||||
dashedName: string;
|
||||
helpCategory: string;
|
||||
|
||||
@@ -20,7 +20,8 @@
|
||||
"main": "utils.js",
|
||||
"scripts": {
|
||||
"test": "mocha --delay --reporter progress --bail",
|
||||
"create-project": "tsx create-project"
|
||||
"create-project": "tsx create-project",
|
||||
"create-quiz": "tsx create-quiz"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/glob": "^8.0.1",
|
||||
|
||||
14
tools/challenge-helper-scripts/quiz-meta.json
Normal file
14
tools/challenge-helper-scripts/quiz-meta.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"name": "",
|
||||
"blockType": "quiz",
|
||||
"blockLayout": "link",
|
||||
"isUpcomingChange": true,
|
||||
"dashedName": "",
|
||||
"superBlock": "",
|
||||
"challengeOrder": [
|
||||
{
|
||||
"id": "",
|
||||
"title": ""
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
isTaskChallenge,
|
||||
getTaskNumberFromTitle
|
||||
} from './helpers/task-helpers';
|
||||
import { getTemplate } from './helpers/get-challenge-template';
|
||||
|
||||
interface Options {
|
||||
stepNum: number;
|
||||
@@ -19,6 +20,14 @@ interface Options {
|
||||
isFirstChallenge?: boolean;
|
||||
}
|
||||
|
||||
interface QuizOptions {
|
||||
challengeType: string;
|
||||
projectPath?: string;
|
||||
title: string;
|
||||
dashedName: string;
|
||||
questionCount: number;
|
||||
}
|
||||
|
||||
const createStepFile = ({
|
||||
stepNum,
|
||||
challengeType,
|
||||
@@ -50,6 +59,28 @@ const createChallengeFile = (
|
||||
fs.writeFileSync(`${path}${filename}.md`, template);
|
||||
};
|
||||
|
||||
const createQuizFile = ({
|
||||
challengeType,
|
||||
projectPath = getProjectPath(),
|
||||
title,
|
||||
dashedName,
|
||||
questionCount
|
||||
}: QuizOptions): ObjectID => {
|
||||
const challengeId = new ObjectID();
|
||||
const template = getTemplate(challengeType);
|
||||
|
||||
const quizText = template({
|
||||
challengeId,
|
||||
challengeType,
|
||||
title,
|
||||
dashedName,
|
||||
questionCount
|
||||
});
|
||||
// eslint-disable-next-line @typescript-eslint/no-base-to-string
|
||||
fs.writeFileSync(`${projectPath}${challengeId.toString()}.md`, quizText);
|
||||
return challengeId;
|
||||
};
|
||||
|
||||
interface InsertOptions {
|
||||
stepNum: number;
|
||||
stepId: ObjectID;
|
||||
@@ -225,5 +256,6 @@ export {
|
||||
insertStepIntoMeta,
|
||||
deleteChallengeFromMeta,
|
||||
deleteStepFromMeta,
|
||||
validateBlockName
|
||||
validateBlockName,
|
||||
createQuizFile
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user