From a374c2fade9bfa3ea8cbbe066c62fe6fe7f9564b Mon Sep 17 00:00:00 2001 From: Naomi Carrigan Date: Tue, 11 Jul 2023 10:32:25 -0700 Subject: [PATCH] feat: challenge helpers for non-step-based challenges (#50769) Co-authored-by: Oliver Eyton-Williams Co-authored-by: Tom <20648924+moT01@users.noreply.github.com> --- curriculum/package.json | 4 + docs/how-to-work-on-coding-challenges.md | 66 ++++++ .../create-next-challenge.ts | 25 +++ .../delete-challenge.ts | 38 ++++ .../helpers/get-challenge-order.test.ts | 110 ++++++++++ .../helpers/get-challenge-order.ts | 23 +++ .../helpers/get-challenge-template.ts | 195 ++++++++++++++++++ .../helpers/get-file-name.test.ts | 67 ++++++ .../helpers/get-file-name.ts | 18 ++ .../helpers/new-challenge-prompts.ts | 45 ++++ .../insert-challenge.ts | 46 +++++ tools/challenge-helper-scripts/repair-meta.ts | 22 +- .../update-challenge-order.ts | 55 +++++ tools/challenge-helper-scripts/utils.test.ts | 28 ++- tools/challenge-helper-scripts/utils.ts | 9 + 15 files changed, 734 insertions(+), 17 deletions(-) create mode 100644 tools/challenge-helper-scripts/create-next-challenge.ts create mode 100644 tools/challenge-helper-scripts/delete-challenge.ts create mode 100644 tools/challenge-helper-scripts/helpers/get-challenge-order.test.ts create mode 100644 tools/challenge-helper-scripts/helpers/get-challenge-order.ts create mode 100644 tools/challenge-helper-scripts/helpers/get-challenge-template.ts create mode 100644 tools/challenge-helper-scripts/helpers/get-file-name.test.ts create mode 100644 tools/challenge-helper-scripts/helpers/get-file-name.ts create mode 100644 tools/challenge-helper-scripts/helpers/new-challenge-prompts.ts create mode 100644 tools/challenge-helper-scripts/insert-challenge.ts create mode 100644 tools/challenge-helper-scripts/update-challenge-order.ts diff --git a/curriculum/package.json b/curriculum/package.json index 08300358114..812f0782831 100644 --- a/curriculum/package.json +++ b/curriculum/package.json @@ -20,11 +20,15 @@ "scripts": { "build": "ts-node --project ../tsconfig.json ../tools/scripts/build/build-curriculum", "create-empty-steps": "cross-env CALLING_DIR=$INIT_CWD ts-node --project ../tsconfig.json ../tools/challenge-helper-scripts/create-empty-steps", + "create-next-challenge": "cross-env CALLING_DIR=$INIT_CWD ts-node --project ../tsconfig.json ../tools/challenge-helper-scripts/create-next-challenge", "create-next-step": "cross-env CALLING_DIR=$INIT_CWD ts-node --project ../tsconfig.json ../tools/challenge-helper-scripts/create-next-step", + "insert-challenge": "cross-env CALLING_DIR=$INIT_CWD ts-node --project ../tsconfig.json ../tools/challenge-helper-scripts/insert-challenge", "insert-step": "cross-env CALLING_DIR=$INIT_CWD ts-node --project ../tsconfig.json ../tools/challenge-helper-scripts/insert-step", "delete-step": "cross-env CALLING_DIR=$INIT_CWD ts-node --project ../tsconfig.json ../tools/challenge-helper-scripts/delete-step", + "delete-challenge": "cross-env CALLING_DIR=$INIT_CWD ts-node --project ../tsconfig.json ../tools/challenge-helper-scripts/delete-challenge", "lint": "ts-node --project ../tsconfig.json lint-localized", "repair-meta": "cross-env CALLING_DIR=$INIT_CWD ts-node --project ../tsconfig.json ../tools/challenge-helper-scripts/repair-meta", + "update-challenge-order": "cross-env CALLING_DIR=$INIT_CWD ts-node --project ../tsconfig.json ../tools/challenge-helper-scripts/update-challenge-order", "update-step-titles": "cross-env CALLING_DIR=$INIT_CWD ts-node --project ../tsconfig.json ../tools/challenge-helper-scripts/update-step-titles", "test": "ts-node ./node_modules/mocha/bin/mocha.js --delay --exit --reporter progress --bail", "test:full-output": "cross-env FULL_OUTPUT=true ts-node ./node_modules/mocha/bin/mocha.js --delay --reporter progress" diff --git a/docs/how-to-work-on-coding-challenges.md b/docs/how-to-work-on-coding-challenges.md index b42c976fb70..61fc6bbe423 100644 --- a/docs/how-to-work-on-coding-challenges.md +++ b/docs/how-to-work-on-coding-challenges.md @@ -500,3 +500,69 @@ Creating and Editing Challenges: 1. [Challenge types](https://github.com/freeCodeCamp/freeCodeCamp/blob/main/client/utils/challenge-types.js#L1-L13) - what the numeric challenge type values mean (enum). 2. [Contributing to FreeCodeCamp - Writing ES6 Challenge Tests](https://www.youtube.com/watch?v=iOdD84OSfAE#t=2h49m55s) - a video following [Ethan Arrowood](https://twitter.com/ArrowoodTech) as he contributes to the old version of the curriculum. + +## Helper Scripts + +> [!NOTE] +> If you are working with the step-based challenges, refer to the [Work on Practice Projects](how-to-work-on-practice-projects.md) section. + +There are a few helper scripts that can be used to manage the challenges in a block. Note that these commands should all be run in the block directory. For example: + +```bash +cd curriculum/challenges/english/02-javascript-algorithms-and-data-structures/basic-algorithm-scripting +``` + +### Add New Challenge + +To add a new challenge at the end of a block, call the script: + +```bash +pnpm run create-next-challenge +``` + +This will prompt you for the challenge information and create the challenge file, updating the `meta.json` file with the new challenge information. + +### Delete a Challenge + +To delete a challenge, call the script: + +```bash +pnpm run delete-challenge +``` + +This will prompt you to select which challenge should be deleted, then delete the file and update the `meta.json` file to remove the challenge from the order. + +### Insert a Challenge + +To insert a challenge before an existing challenge, call the script: + +```bash +pnpm run insert-challenge +``` + +This will prompt you for the challenge information, then for the challenge to insert before. For example, if your choices are: + +```bash +a +b +c +``` + +And you choose `b`, your new order will be: + +```bash +a +new challenge +b +c +``` + +### Update Challenge Order + +If you need to manually re-order the challenges, call the script: + +```bash +pnpm run update-challenge-order +``` + +This will take you through an interactive process to select the order of the challenges. diff --git a/tools/challenge-helper-scripts/create-next-challenge.ts b/tools/challenge-helper-scripts/create-next-challenge.ts new file mode 100644 index 00000000000..d0dd6745a6e --- /dev/null +++ b/tools/challenge-helper-scripts/create-next-challenge.ts @@ -0,0 +1,25 @@ +import ObjectID from 'bson-objectid'; +import { challengeTypeToTemplate } from './helpers/get-challenge-template'; +import { newChallengePrompts } from './helpers/new-challenge-prompts'; +import { getProjectPath } from './helpers/get-project-info'; +import { getMetaData, updateMetaData } from './helpers/project-metadata'; +import { createChallengeFile } from './utils'; + +const createNextChallenge = async () => { + const path = getProjectPath(); + + const options = await newChallengePrompts(); + const templateGenerator = challengeTypeToTemplate[options.challengeType]; + if (!templateGenerator) { + return; + } + const challengeId = new ObjectID(); + const template = templateGenerator({ ...options, challengeId }); + createChallengeFile(options.dashedName, template, path); + + const meta = getMetaData(); + meta.challengeOrder.push([challengeId.toString(), options.title]); + updateMetaData(meta); +}; + +void createNextChallenge(); diff --git a/tools/challenge-helper-scripts/delete-challenge.ts b/tools/challenge-helper-scripts/delete-challenge.ts new file mode 100644 index 00000000000..a456e2194f2 --- /dev/null +++ b/tools/challenge-helper-scripts/delete-challenge.ts @@ -0,0 +1,38 @@ +import { unlink } from 'fs/promises'; +import { prompt } from 'inquirer'; +import { getProjectPath } from './helpers/get-project-info'; +import { getMetaData, updateMetaData } from './helpers/project-metadata'; +import { getChallengeOrderFromMeta } from './helpers/get-challenge-order'; +import { getFileName } from './helpers/get-file-name'; + +const deleteChallenge = async () => { + const path = getProjectPath(); + + const challenges = getChallengeOrderFromMeta(); + + const challengeToDelete = (await prompt({ + name: 'id', + message: 'Which challenge should be deleted?', + type: 'list', + choices: challenges.map(([id, title]) => ({ + name: title, + value: id + })) + })) as { id: string }; + const indexToDelete = challenges.findIndex( + ([id]) => id === challengeToDelete.id + ); + const fileToDelete = await getFileName(challengeToDelete.id); + + if (!fileToDelete) { + throw new Error(`File not found for challenge ${challengeToDelete.id}`); + } + + await unlink(`${path}${fileToDelete}`); + + const meta = getMetaData(); + meta.challengeOrder.splice(indexToDelete, 1); + updateMetaData(meta); +}; + +void deleteChallenge(); diff --git a/tools/challenge-helper-scripts/helpers/get-challenge-order.test.ts b/tools/challenge-helper-scripts/helpers/get-challenge-order.test.ts new file mode 100644 index 00000000000..258319928e7 --- /dev/null +++ b/tools/challenge-helper-scripts/helpers/get-challenge-order.test.ts @@ -0,0 +1,110 @@ +import { join } from 'path'; + +import mock from 'mock-fs'; +import { + getChallengeOrderFromFileTree, + getChallengeOrderFromMeta +} from './get-challenge-order'; + +describe('getChallengeOrderFromMeta helper', () => { + beforeEach(() => { + mock({ + curriculum: { + challenges: { + english: { + superblock: { + 'mock-project': { + 'this-is-a-challenge.md': + '---\nid: 1\ntitle: This is a Challenge\n---', + 'what-a-cool-thing.md': + '---\nid: 100\ntitle: What a Cool Thing\n---', + 'i-dunno.md': '---\nid: 2\ntitle: I Dunno\n---' + } + } + }, + _meta: { + 'mock-project': { + 'meta.json': `{ + "id": "mock-id", + "challengeOrder": [["1","This title is wrong"], ["2","I Dunno"], ["100","What a Cool Thing"]]} + ` + } + } + } + } + }); + }); + + it('should load the file order', () => { + process.env.CALLING_DIR = join( + process.cwd(), + 'curriculum', + 'challenges', + 'english', + 'superblock', + 'mock-project' + ); + const challengeOrder = getChallengeOrderFromMeta(); + expect(challengeOrder).toEqual([ + ['1', 'This title is wrong'], + ['2', 'I Dunno'], + ['100', 'What a Cool Thing'] + ]); + }); + + afterEach(() => { + mock.restore(); + delete process.env.CALLING_DIR; + }); +}); + +describe('getChallengeOrderFromFileTree helper', () => { + beforeEach(() => { + mock({ + curriculum: { + challenges: { + english: { + superblock: { + 'mock-project': { + 'step-001.md': '---\nid: 1\ntitle: Step 1\n---', + 'step-002.md': '---\nid: 100\ntitle: Step 2\n---', + 'step-003.md': '---\nid: 2\ntitle: Step 3\n---' + } + } + }, + _meta: { + 'mock-project': { + 'meta.json': `{ + "id": "mock-id", + "challengeOrder": [["1","step1"], ["100","step2"], ["2","step3"]]} + ` + } + } + } + } + }); + }); + + it('should load the file order', async () => { + expect.assertions(1); + process.env.CALLING_DIR = join( + process.cwd(), + 'curriculum', + 'challenges', + 'english', + 'superblock', + 'mock-project' + ); + const challengeOrder = await getChallengeOrderFromFileTree(); + expect(challengeOrder).toEqual([ + [1, 'Step 1'], + [100, 'Step 2'], + [2, 'Step 3'] + ]); + }); + + afterEach(() => { + mock.restore(); + delete process.env.CALLING_DIR; + }); +}); diff --git a/tools/challenge-helper-scripts/helpers/get-challenge-order.ts b/tools/challenge-helper-scripts/helpers/get-challenge-order.ts new file mode 100644 index 00000000000..c4be75da84b --- /dev/null +++ b/tools/challenge-helper-scripts/helpers/get-challenge-order.ts @@ -0,0 +1,23 @@ +import { readdir } from 'fs/promises'; +import { join } from 'path'; + +import matter from 'gray-matter'; + +import { getProjectPath } from './get-project-info'; +import { getMetaData } from './project-metadata'; + +export const getChallengeOrderFromFileTree = async (): Promise< + [string, string][] +> => { + const path = getProjectPath(); + const fileList = await readdir(path); + const challengeOrder = fileList + .map(file => matter.read(join(path, file))) + .map(({ data }) => [data.id, data.title] as [string, string]); + return challengeOrder; +}; + +export const getChallengeOrderFromMeta = (): [string, string][] => { + const meta = getMetaData(); + return meta.challengeOrder.map(el => [el[0], el[1]]); +}; diff --git a/tools/challenge-helper-scripts/helpers/get-challenge-template.ts b/tools/challenge-helper-scripts/helpers/get-challenge-template.ts new file mode 100644 index 00000000000..9de1a655952 --- /dev/null +++ b/tools/challenge-helper-scripts/helpers/get-challenge-template.ts @@ -0,0 +1,195 @@ +import ObjectID from 'bson-objectid'; + +export interface ChallengeOptions { + challengeId: ObjectID; + title: string; + dashedName: string; + challengeType: string; +} + +const buildFrontMatter = ({ + challengeId, + title, + dashedName, + challengeType +}: ChallengeOptions) => `--- +id: ${challengeId.toString()} +title: ${title} +challengeType: ${challengeType} +dashedName: ${dashedName} +---`; + +const buildFrontMatterWithVideo = ({ + challengeId, + title, + dashedName, + challengeType +}: ChallengeOptions) => `--- +id: ${challengeId.toString()} +videoId: ADD YOUR VIDEO ID HERE!!! +title: ${title} +challengeType: ${challengeType} +dashedName: ${dashedName} +---`; + +export const getLegacyChallengeTemplate = ( + options: ChallengeOptions +): string => `${buildFrontMatter(options)} + +# --description-- + +${options.title} description. + +# --instructions-- + +${options.title} instructions. + +# --hints-- + +Test 1 + +\`\`\`js + +\`\`\` + +# --seed-- + +\`\`\`js + +\`\`\` + +# --solutions-- + +\`\`\`js + +\`\`\` +`; + +export const getQuizChallengeTemplate = ( + options: ChallengeOptions +): string => `${buildFrontMatter(options)} + +# --description-- + +${options.title} description. + +# --question-- + +## --text-- + +${options.title} question? + +## --answers-- + +Answer 1 + +--- + +Answer 2 + +--- + +Answer 3 + +## --video-solution-- + +1 +`; + +export const getVideoChallengeTemplate = ( + options: ChallengeOptions +): string => `${buildFrontMatterWithVideo(options)} + +# --description-- + +${options.title} description. + +# --question-- + +## --text-- + +${options.title} question? + +## --answers-- + +Answer 1 + +--- + +Answer 2 + +--- + +Answer 3 + +## --video-solution-- + +1 +`; + +export const getAssignmentChallengeTemplate = ( + options: ChallengeOptions +): string => `${buildFrontMatter(options)} + +# --description-- + +${options.title} description. + +# --question-- + +## --assignment-- + +${options.title} assignment! + +## --text-- + +${options.title} question? + +## --answers-- + +Answer 1 + +--- + +Answer 2 + +--- + +Answer 3 + +## --video-solution-- + +1 +`; + +/** + * This should be kept in parity with the challengeTypes in the + * client. + * + * Keys are explicitly marked null so we know the challenge type itself + * exists, and can expand this to use the correct template later on. + */ +export const challengeTypeToTemplate: { + [key: string]: null | ((opts: ChallengeOptions) => string); +} = { + 0: getLegacyChallengeTemplate, + 1: getLegacyChallengeTemplate, + 2: null, + 3: getLegacyChallengeTemplate, + 4: getLegacyChallengeTemplate, + 5: getLegacyChallengeTemplate, + 6: getLegacyChallengeTemplate, + 7: null, + 8: getQuizChallengeTemplate, + 9: null, + 10: null, + 11: getVideoChallengeTemplate, + 12: null, + 13: null, + 14: null, + 15: getAssignmentChallengeTemplate, + 16: null, + 17: null, + 18: null, + 19: null +}; diff --git a/tools/challenge-helper-scripts/helpers/get-file-name.test.ts b/tools/challenge-helper-scripts/helpers/get-file-name.test.ts new file mode 100644 index 00000000000..5ea66702f34 --- /dev/null +++ b/tools/challenge-helper-scripts/helpers/get-file-name.test.ts @@ -0,0 +1,67 @@ +import { join } from 'path'; + +import mock from 'mock-fs'; +import { getFileName } from './get-file-name'; + +describe('getFileName helper', () => { + beforeEach(() => { + mock({ + curriculum: { + challenges: { + english: { + superblock: { + 'mock-project': { + 'this-is-a-challenge.md': + '---\nid: 1\ntitle: This is a Challenge\n---', + 'what-a-cool-thing.md': + '---\nid: 100\ntitle: What a Cool Thing\n---', + 'i-dunno.md': '---\nid: 2\ntitle: I Dunno\n---' + } + } + }, + _meta: { + 'mock-project': { + 'meta.json': `{ + "id": "mock-id", + "challengeOrder": [["1","This title is wrong"], ["2","I Dunno"], ["100","What a Cool Thing"]]} + ` + } + } + } + } + }); + }); + + it('should return the file name if found', async () => { + expect.assertions(1); + process.env.CALLING_DIR = join( + process.cwd(), + 'curriculum', + 'challenges', + 'english', + 'superblock', + 'mock-project' + ); + const fileName = await getFileName('1'); + expect(fileName).toEqual('this-is-a-challenge.md'); + }); + + it('should return null if not found', async () => { + expect.assertions(1); + process.env.CALLING_DIR = join( + process.cwd(), + 'curriculum', + 'challenges', + 'english', + 'superblock', + 'mock-project' + ); + const fileName = await getFileName('42'); + expect(fileName).toBeNull(); + }); + + afterEach(() => { + mock.restore(); + delete process.env.CALLING_DIR; + }); +}); diff --git a/tools/challenge-helper-scripts/helpers/get-file-name.ts b/tools/challenge-helper-scripts/helpers/get-file-name.ts new file mode 100644 index 00000000000..eaaf96adcce --- /dev/null +++ b/tools/challenge-helper-scripts/helpers/get-file-name.ts @@ -0,0 +1,18 @@ +import { readdir } from 'fs/promises'; +import matter from 'gray-matter'; +import { getProjectPath } from './get-project-info'; + +export const getFileName = async (id: string): Promise => { + const path = getProjectPath(); + const files = await readdir(path); + for (const file of files) { + if (!file.endsWith('.md')) { + continue; + } + const frontMatter = matter.read(`${path}${file}`); + if (String(frontMatter.data.id) === id) { + return file; + } + } + return null; +}; diff --git a/tools/challenge-helper-scripts/helpers/new-challenge-prompts.ts b/tools/challenge-helper-scripts/helpers/new-challenge-prompts.ts new file mode 100644 index 00000000000..b1581e7bbe2 --- /dev/null +++ b/tools/challenge-helper-scripts/helpers/new-challenge-prompts.ts @@ -0,0 +1,45 @@ +import { prompt } from 'inquirer'; +import { challengeTypes } from '../../../client/utils/challenge-types'; + +export const newChallengePrompts = async (): Promise<{ + title: string; + dashedName: string; + challengeType: string; +}> => { + const dashedName = await prompt<{ value: string }>({ + name: 'value', + message: 'What is the short name (in kebab-case) for this challenge?', + validate: (block: string) => { + if (!block.length) { + return 'please enter a short name'; + } + if (/[^a-z0-9-]/.test(block)) { + return 'please use alphanumerical characters and kebab case'; + } + return true; + }, + filter: (block: string) => { + return block.toLowerCase(); + } + }); + const title = await prompt<{ value: string }>({ + name: 'value', + message: 'What is the title of this challenge?', + default: (title: { value: string }) => title.value + }); + const challengeType = await prompt<{ value: string }>({ + name: 'value', + message: 'What type of challenge is this?', + type: 'list', + choices: Object.entries(challengeTypes).map(([key, value]) => ({ + name: key, + value + })) + }); + + return { + title: title.value, + dashedName: dashedName.value, + challengeType: challengeType.value + }; +}; diff --git a/tools/challenge-helper-scripts/insert-challenge.ts b/tools/challenge-helper-scripts/insert-challenge.ts new file mode 100644 index 00000000000..bb08394b6a4 --- /dev/null +++ b/tools/challenge-helper-scripts/insert-challenge.ts @@ -0,0 +1,46 @@ +import ObjectID from 'bson-objectid'; +import { prompt } from 'inquirer'; +import { challengeTypeToTemplate } from './helpers/get-challenge-template'; +import { newChallengePrompts } from './helpers/new-challenge-prompts'; +import { getProjectPath } from './helpers/get-project-info'; +import { getMetaData, updateMetaData } from './helpers/project-metadata'; +import { createChallengeFile } from './utils'; +import { getChallengeOrderFromMeta } from './helpers/get-challenge-order'; + +const insertChallenge = async () => { + const path = getProjectPath(); + + const options = await newChallengePrompts(); + + const challenges = getChallengeOrderFromMeta(); + + const challengeAfter = await prompt<{ id: string }>({ + name: 'id', + message: 'Which challenge should come AFTER this new one?', + type: 'list', + choices: challenges.map(([id, title]) => ({ + name: title, + value: id + })) + }); + const indexToInsert = challenges.findIndex( + ([id]) => id === challengeAfter.id + ); + + const templateGenerator = challengeTypeToTemplate[options.challengeType]; + if (!templateGenerator) { + return; + } + const challengeId = new ObjectID(); + const template = templateGenerator({ ...options, challengeId }); + createChallengeFile(options.dashedName, template, path); + + const meta = getMetaData(); + meta.challengeOrder.splice(indexToInsert, 0, [ + challengeId.toString(), + options.title + ]); + updateMetaData(meta); +}; + +void insertChallenge(); diff --git a/tools/challenge-helper-scripts/repair-meta.ts b/tools/challenge-helper-scripts/repair-meta.ts index 6ddec68f4f7..886e5309f7f 100644 --- a/tools/challenge-helper-scripts/repair-meta.ts +++ b/tools/challenge-helper-scripts/repair-meta.ts @@ -1,31 +1,21 @@ -import { readdir } from 'fs/promises'; -import { join } from 'path'; - -import * as matter from 'gray-matter'; - -import { getProjectPath } from './helpers/get-project-info'; import { getMetaData, updateMetaData } from './helpers/project-metadata'; +import { getChallengeOrderFromFileTree } from './helpers/get-challenge-order'; const sortByStepNum = (a: string, b: string) => parseInt(a.split('-')[1]) - parseInt(b.split('-')[1]); const repairMeta = async () => { - const path = getProjectPath(); - const fileList = await readdir(path); - // [id, title] - const challengeOrder = fileList - .map(file => matter.read(join(path, file))) - .sort((a, b) => - sortByStepNum(a.data.dashedName as string, b.data.dashedName as string) - ) - .map(({ data }) => [data.id, data.title] as [string, string]); + const challengeOrder = await getChallengeOrderFromFileTree(); if (!challengeOrder.every(([, step]) => /Step \d+/.test(step))) { throw new Error( 'You can only run this command on project-based blocks with step files.' ); } + const sortedChallengeOrder = challengeOrder.sort((a, b) => + sortByStepNum(a[1], b[1]) + ); const meta = getMetaData(); - meta.challengeOrder = challengeOrder; + meta.challengeOrder = sortedChallengeOrder; updateMetaData(meta); }; diff --git a/tools/challenge-helper-scripts/update-challenge-order.ts b/tools/challenge-helper-scripts/update-challenge-order.ts new file mode 100644 index 00000000000..78d89b09b2f --- /dev/null +++ b/tools/challenge-helper-scripts/update-challenge-order.ts @@ -0,0 +1,55 @@ +import { prompt } from 'inquirer'; + +import { getMetaData, updateMetaData } from './helpers/project-metadata'; +import { getChallengeOrderFromMeta } from './helpers/get-challenge-order'; + +const updateChallengeOrder = async () => { + const oldChallengeOrder = getChallengeOrderFromMeta(); + console.log('Current challenge order is: '); + console.table(oldChallengeOrder.map(([_id, title]) => ({ title }))); + + const newChallengeOrder: [string, string][] = []; + + while (oldChallengeOrder.length) { + const nextChallenge = (await prompt({ + name: 'id', + message: newChallengeOrder.length + ? `What challenge comes after ${ + newChallengeOrder[newChallengeOrder.length - 1][1] + }?` + : 'What is the first challenge?', + type: 'list', + choices: oldChallengeOrder.map(([id, title]) => ({ + name: title, + value: id + })) + })) as { id: string }; + const nextChallengeIndex = oldChallengeOrder.findIndex( + ([id]) => id === nextChallenge.id + ); + const targetChallenge = oldChallengeOrder[nextChallengeIndex]; + oldChallengeOrder.splice(nextChallengeIndex, 1); + newChallengeOrder.push(targetChallenge); + } + + console.log('New challenge order is: '); + console.table(newChallengeOrder.map(([_id, title]) => ({ title }))); + + const confirm = await prompt({ + name: 'correct', + message: 'Is this correct?', + type: 'confirm', + default: false + }); + + if (!confirm.correct) { + console.error('Aborting.'); + return; + } + + const meta = getMetaData(); + meta.challengeOrder = newChallengeOrder; + updateMetaData(meta); +}; + +void (async () => await updateChallengeOrder())(); diff --git a/tools/challenge-helper-scripts/utils.test.ts b/tools/challenge-helper-scripts/utils.test.ts index e17ae79d723..b3c0cf17450 100644 --- a/tools/challenge-helper-scripts/utils.test.ts +++ b/tools/challenge-helper-scripts/utils.test.ts @@ -22,7 +22,12 @@ jest.mock('./helpers/get-step-template', () => { const mockChallengeId = '60d35cf3fe32df2ce8e31b03'; import { getStepTemplate } from './helpers/get-step-template'; -import { createStepFile, insertStepIntoMeta, updateStepTitles } from './utils'; +import { + createChallengeFile, + createStepFile, + insertStepIntoMeta, + updateStepTitles +} from './utils'; describe('Challenge utils helper scripts', () => { describe('createStepFile util', () => { @@ -57,6 +62,27 @@ describe('Challenge utils helper scripts', () => { }); }); + describe('createChallengeFile util', () => { + it('should create the challenge', () => { + mock({ + 'project/': { + 'fake-challenge.md': 'Lorem ipsum...', + 'so-many-fakes.md': 'Lorem ipsum...' + } + }); + + createChallengeFile('hi', 'pretend this is a template', 'project/'); + // - Should write a file with a given name and template + const files = glob.sync(`project/*.md`); + + expect(files).toEqual([ + `project/fake-challenge.md`, + `project/hi.md`, + `project/so-many-fakes.md` + ]); + }); + }); + describe('insertStepIntoMeta util', () => { it('should update the meta with a new file id and name', () => { mock({ diff --git a/tools/challenge-helper-scripts/utils.ts b/tools/challenge-helper-scripts/utils.ts index 879db8a1d4a..a45c5c24367 100644 --- a/tools/challenge-helper-scripts/utils.ts +++ b/tools/challenge-helper-scripts/utils.ts @@ -31,6 +31,14 @@ const createStepFile = ({ return challengeId; }; +const createChallengeFile = ( + title: string, + template: string, + path = getProjectPath() +): void => { + fs.writeFileSync(`${path}${title}.md`, template); +}; + interface InsertOptions { stepNum: number; stepId: ObjectID; @@ -98,6 +106,7 @@ const getChallengeSeeds = ( export { createStepFile, + createChallengeFile, updateStepTitles, getChallengeSeeds, insertStepIntoMeta,