fix(tools): curriculum command line helpers (#61831)

This commit is contained in:
Oliver Eyton-Williams
2025-09-02 16:03:28 +02:00
committed by GitHub
parent c58ba56eeb
commit 10c565828e
40 changed files with 773 additions and 1010 deletions

View File

@@ -9,6 +9,7 @@ const {
getContentDir,
getBlockCreator
} = require('../../curriculum/build-curriculum');
const { getBlockStructure } = require('../../curriculum/file-handler');
const { curriculumLocale } = envData;
@@ -23,7 +24,7 @@ exports.replaceChallengeNode = () => {
const filename = path.basename(filePath);
console.log(`Replacing challenge node for ${filePath}`);
const meta = blockCreator.getMetaForBlock(block);
const meta = getBlockStructure(block);
return await blockCreator.createChallenge({
filename,

View File

@@ -1,6 +1,5 @@
const fs = require('fs');
const path = require('path');
const assert = require('assert');
const { isEmpty } = require('lodash');
const debug = require('debug')('fcc:build-curriculum');
@@ -13,14 +12,13 @@ const {
const { buildCertification } = require('./build-certification');
const { applyFilters } = require('./utils');
const CURRICULUM_DIR = __dirname;
const I18N_CURRICULUM_DIR = path.resolve(
CURRICULUM_DIR,
'i18n-curriculum',
'curriculum'
);
const STRUCTURE_DIR = path.resolve(CURRICULUM_DIR, 'structure');
const {
getContentDir,
getLanguageConfig,
getCurriculumStructure,
getBlockStructure,
getSuperblockStructure
} = require('./file-handler');
/**
* Creates a BlockCreator instance for a specific language with appropriate configuration
@@ -35,7 +33,6 @@ const STRUCTURE_DIR = path.resolve(CURRICULUM_DIR, 'structure');
const getBlockCreator = (lang, skipValidation, opts) => {
const {
blockContentDir,
blockStructureDir,
i18nBlockContentDir,
dictionariesDir,
i18nDictionariesDir
@@ -47,7 +44,6 @@ const getBlockCreator = (lang, skipValidation, opts) => {
return new BlockCreator({
lang,
blockContentDir,
blockStructureDir,
i18nBlockContentDir,
commentTranslations: createCommentMap(
dictionariesDir,
@@ -194,84 +190,12 @@ const superBlockNames = {
'dev-playground': 'dev-playground'
};
/**
* Gets language-specific configuration paths for curriculum content
* @param {string} lang - The language code (e.g., 'english', 'spanish', etc.)
* @param {Object} [options] - Optional configuration object with directory overrides
* @param {string} [options.baseDir] - Base directory for curriculum content (defaults to CURRICULUM_DIR)
* @param {string} [options.i18nBaseDir] - Base directory for i18n content (defaults to I18N_CURRICULUM_DIR)
* @param {string} [options.structureDir] - Directory for curriculum structure (defaults to STRUCTURE_DIR)
* @returns {Object} Object containing all relevant directory paths for the language
* @throws {AssertionError} When required i18n directories don't exist for non-English languages
*/
function getLanguageConfig(
lang,
{ baseDir, i18nBaseDir, structureDir } = {
baseDir: CURRICULUM_DIR,
i18nBaseDir: I18N_CURRICULUM_DIR,
structureDir: STRUCTURE_DIR
}
) {
const contentDir = path.resolve(baseDir, 'challenges', 'english');
const i18nContentDir = path.resolve(i18nBaseDir, 'challenges', lang);
const blockContentDir = path.resolve(contentDir, 'blocks');
const i18nBlockContentDir = path.resolve(i18nContentDir, 'blocks');
const blockStructureDir = path.resolve(structureDir, 'blocks');
const dictionariesDir = path.resolve(baseDir, 'dictionaries');
const i18nDictionariesDir = path.resolve(i18nBaseDir, 'dictionaries');
if (lang !== 'english') {
assert(
fs.existsSync(i18nContentDir),
`i18n content directory does not exist: ${i18nContentDir}`
);
assert(
fs.existsSync(i18nBlockContentDir),
`i18n block content directory does not exist: ${i18nBlockContentDir}`
);
assert(
fs.existsSync(i18nDictionariesDir),
`i18n dictionaries directory does not exist: ${i18nDictionariesDir}`
);
}
debug(`Using content directory: ${contentDir}`);
debug(`Using i18n content directory: ${i18nContentDir}`);
debug(`Using block content directory: ${blockContentDir}`);
debug(`Using i18n block content directory: ${i18nBlockContentDir}`);
debug(`Using dictionaries directory: ${dictionariesDir}`);
debug(`Using i18n dictionaries directory: ${i18nDictionariesDir}`);
return {
contentDir,
i18nContentDir,
blockContentDir,
i18nBlockContentDir,
blockStructureDir,
dictionariesDir,
i18nDictionariesDir
};
}
/**
* Gets the appropriate content directory path for a given language
* @param {string} lang - The language code (e.g., 'english', 'spanish', etc.)
* @returns {string} Path to the content directory for the specified language
*/
function getContentDir(lang) {
const { contentDir, i18nContentDir } = getLanguageConfig(lang);
return lang === 'english' ? contentDir : i18nContentDir;
}
const getCurriculumStructure = () => {
const curriculumPath = path.resolve(STRUCTURE_DIR, 'curriculum.json');
if (!fs.existsSync(curriculumPath)) {
throw new Error(`Curriculum file not found: ${curriculumPath}`);
}
return JSON.parse(fs.readFileSync(curriculumPath, 'utf8'));
};
const superBlockToFilename = Object.entries(superBlockNames).reduce(
(map, entry) => {
return { ...map, [entry[1]]: entry[0] };
},
{}
);
/**
* Builds an array of superblock structures from a curriculum object
@@ -304,26 +228,6 @@ function addSuperblockStructure(superblocks) {
return superblockStructures;
}
function getSuperblockStructure(superblock) {
const superblockPath = path.resolve(
STRUCTURE_DIR,
'superblocks',
`${superblock}.json`
);
return JSON.parse(fs.readFileSync(superblockPath, 'utf8'));
}
function getBlockStructure(block) {
const blockPath = path.resolve(STRUCTURE_DIR, 'blocks', `${block}.json`);
try {
return JSON.parse(fs.readFileSync(blockPath, 'utf8'));
} catch {
console.warn('block missing', block);
}
}
function addBlockStructure(
superblocks,
_getBlockStructure = getBlockStructure
@@ -383,5 +287,6 @@ module.exports = {
getBlockCreator,
getBlockStructure,
getSuperblockStructure,
createCommentMap
createCommentMap,
superBlockToFilename
};

View File

@@ -226,8 +226,6 @@ class BlockCreator {
/**
* @param {object} options - Options object
* @param {string} options.blockContentDir - Directory containing block content files
* @param {string} options.blockStructureDir - Directory containing block structure files (meta
* .json)
* @param {string} options.i18nBlockContentDir - Directory containing i18n block content files
* @param {string} options.lang - Language code for the block content
* @param {object} options.commentTranslations - Translations for comments in challenges
@@ -238,14 +236,12 @@ class BlockCreator {
*/
constructor({
blockContentDir,
blockStructureDir,
i18nBlockContentDir,
lang,
commentTranslations,
skipValidation
}) {
this.blockContentDir = blockContentDir;
this.blockStructureDir = blockStructureDir;
this.i18nBlockContentDir = i18nBlockContentDir;
this.lang = lang;
this.commentTranslations = commentTranslations;
@@ -309,29 +305,6 @@ class BlockCreator {
);
}
/**
* Gets meta information for a block from its JSON file
* @param {string} blockName - Name of the block
* @returns {object} The meta information object for the block
* @throws {Error} If meta file is not found
*/
getMetaForBlock(blockName) {
// Read meta.json for this block
const metaPath = path.resolve(this.blockStructureDir, `${blockName}.json`);
if (!fs.existsSync(metaPath)) {
throw new Error(
`Meta file not found for block ${blockName}: ${metaPath}`
);
}
// Not all "meta information" can be found in the meta.json.
const rawMeta = JSON.parse(fs.readFileSync(metaPath, 'utf8'));
debug(
`Meta file indicates ${rawMeta.challengeOrder.length} challenges should exist`
);
return rawMeta;
}
async processBlock(block, { superBlock, order }) {
const blockName = block.dashedName;
debug(`Processing block ${blockName} in superblock ${superBlock}`);

199
curriculum/file-handler.js Normal file
View File

@@ -0,0 +1,199 @@
const path = require('node:path');
const assert = require('node:assert');
const fs = require('node:fs');
const fsP = require('node:fs/promises');
// const prettier = require('prettier');
const debug = require('debug')('fcc:file-handler');
const CURRICULUM_DIR = __dirname;
const I18N_CURRICULUM_DIR = path.resolve(
CURRICULUM_DIR,
'i18n-curriculum',
'curriculum'
);
const STRUCTURE_DIR = path.resolve(CURRICULUM_DIR, 'structure');
const BLOCK_STRUCTURE_DIR = path.resolve(STRUCTURE_DIR, 'blocks');
/**
* Gets language-specific configuration paths for curriculum content
* @param {string} lang - The language code (e.g., 'english', 'spanish', etc.)
* @param {Object} [options] - Optional configuration object with directory overrides
* @param {string} [options.baseDir] - Base directory for curriculum content (defaults to CURRICULUM_DIR)
* @param {string} [options.i18nBaseDir] - Base directory for i18n content (defaults to I18N_CURRICULUM_DIR)
* @param {string} [options.structureDir] - Directory for curriculum structure (defaults to STRUCTURE_DIR)
* @returns {Object} Object containing all relevant directory paths for the language
* @throws {AssertionError} When required i18n directories don't exist for non-English languages
*/
function getContentConfig(
lang,
{ baseDir, i18nBaseDir } = {
baseDir: CURRICULUM_DIR,
i18nBaseDir: I18N_CURRICULUM_DIR
}
) {
const contentDir = path.resolve(baseDir, 'challenges', 'english');
const i18nContentDir = path.resolve(i18nBaseDir, 'challenges', lang);
const blockContentDir = path.resolve(contentDir, 'blocks');
const i18nBlockContentDir = path.resolve(i18nContentDir, 'blocks');
const dictionariesDir = path.resolve(baseDir, 'dictionaries');
const i18nDictionariesDir = path.resolve(i18nBaseDir, 'dictionaries');
if (lang !== 'english') {
assert(
fs.existsSync(i18nContentDir),
`i18n content directory does not exist: ${i18nContentDir}`
);
assert(
fs.existsSync(i18nBlockContentDir),
`i18n block content directory does not exist: ${i18nBlockContentDir}`
);
assert(
fs.existsSync(i18nDictionariesDir),
`i18n dictionaries directory does not exist: ${i18nDictionariesDir}`
);
}
debug(`Using content directory: ${contentDir}`);
debug(`Using i18n content directory: ${i18nContentDir}`);
debug(`Using block content directory: ${blockContentDir}`);
debug(`Using i18n block content directory: ${i18nBlockContentDir}`);
debug(`Using dictionaries directory: ${dictionariesDir}`);
debug(`Using i18n dictionaries directory: ${i18nDictionariesDir}`);
return {
contentDir,
i18nContentDir,
blockContentDir,
i18nBlockContentDir,
dictionariesDir,
i18nDictionariesDir
};
}
/**
* Gets the appropriate content directory path for a given language
* @param {string} lang - The language code (e.g., 'english', 'spanish', etc.)
* @returns {string} Path to the content directory for the specified language
*/
function getContentDir(lang) {
const { contentDir, i18nContentDir } = getContentConfig(lang);
return lang === 'english' ? contentDir : i18nContentDir;
}
function getCurriculumStructure() {
const curriculumPath = path.resolve(STRUCTURE_DIR, 'curriculum.json');
if (!fs.existsSync(curriculumPath)) {
throw new Error(`Curriculum file not found: ${curriculumPath}`);
}
return JSON.parse(fs.readFileSync(curriculumPath, 'utf8'));
}
function getBlockStructurePath(block) {
return path.resolve(BLOCK_STRUCTURE_DIR, `${block}.json`);
}
function getBlockStructure(block) {
return JSON.parse(fs.readFileSync(getBlockStructurePath(block), 'utf8'));
}
async function writeBlockStructure(block, structure) {
// TODO: format with prettier (jest, at least this version, is not compatible
// with prettier)
const content = JSON.stringify(structure);
await fsP.writeFile(getBlockStructurePath(block), content, 'utf8');
}
async function writeSuperblockStructure(superblock, structure) {
const content = JSON.stringify(structure);
await fsP.writeFile(getSuperblockStructurePath(superblock), content);
}
function getSuperblockStructure(superblockFilename) {
const superblockPath = getSuperblockStructurePath(superblockFilename);
if (!fs.existsSync(superblockPath)) {
throw Error(`Superblock file not found: ${superblockPath}`);
}
return JSON.parse(fs.readFileSync(superblockPath, 'utf8'));
}
function getSuperblockStructurePath(superblockFilename) {
return path.resolve(
STRUCTURE_DIR,
'superblocks',
`${superblockFilename}.json`
);
}
/**
* Gets language-specific configuration paths for curriculum content
* @param {string} lang - The language code (e.g., 'english', 'spanish', etc.)
* @param {Object} [options] - Optional configuration object with directory overrides
* @param {string} [options.baseDir] - Base directory for curriculum content (defaults to CURRICULUM_DIR)
* @param {string} [options.i18nBaseDir] - Base directory for i18n content (defaults to I18N_CURRICULUM_DIR)
* @param {string} [options.structureDir] - Directory for curriculum structure (defaults to STRUCTURE_DIR)
* @returns {Object} Object containing all relevant directory paths for the language
* @throws {AssertionError} When required i18n directories don't exist for non-English languages
*/
function getLanguageConfig(
lang,
{ baseDir, i18nBaseDir, structureDir } = {
baseDir: CURRICULUM_DIR,
i18nBaseDir: I18N_CURRICULUM_DIR,
structureDir: STRUCTURE_DIR
}
) {
const contentDir = path.resolve(baseDir, 'challenges', 'english');
const i18nContentDir = path.resolve(i18nBaseDir, 'challenges', lang);
const blockContentDir = path.resolve(contentDir, 'blocks');
const i18nBlockContentDir = path.resolve(i18nContentDir, 'blocks');
const blockStructureDir = path.resolve(structureDir, 'blocks');
const dictionariesDir = path.resolve(baseDir, 'dictionaries');
const i18nDictionariesDir = path.resolve(i18nBaseDir, 'dictionaries');
if (lang !== 'english') {
assert(
fs.existsSync(i18nContentDir),
`i18n content directory does not exist: ${i18nContentDir}`
);
assert(
fs.existsSync(i18nBlockContentDir),
`i18n block content directory does not exist: ${i18nBlockContentDir}`
);
assert(
fs.existsSync(i18nDictionariesDir),
`i18n dictionaries directory does not exist: ${i18nDictionariesDir}`
);
}
debug(`Using content directory: ${contentDir}`);
debug(`Using i18n content directory: ${i18nContentDir}`);
debug(`Using block content directory: ${blockContentDir}`);
debug(`Using i18n block content directory: ${i18nBlockContentDir}`);
debug(`Using dictionaries directory: ${dictionariesDir}`);
debug(`Using i18n dictionaries directory: ${i18nDictionariesDir}`);
return {
contentDir,
i18nContentDir,
blockContentDir,
i18nBlockContentDir,
blockStructureDir,
dictionariesDir,
i18nDictionariesDir
};
}
exports.getContentConfig = getContentConfig;
exports.getContentDir = getContentDir;
exports.getBlockStructure = getBlockStructure;
exports.getSuperblockStructure = getSuperblockStructure;
exports.getCurriculumStructure = getCurriculumStructure;
exports.writeBlockStructure = writeBlockStructure;
exports.writeSuperblockStructure = writeSuperblockStructure;
exports.getLanguageConfig = getLanguageConfig;

View File

@@ -32,7 +32,6 @@
"delete-challenge": "CALLING_DIR=$INIT_CWD tsx --tsconfig ../tsconfig.json ../tools/challenge-helper-scripts/delete-challenge",
"delete-task": "CALLING_DIR=$INIT_CWD tsx --tsconfig ../tsconfig.json ../tools/challenge-helper-scripts/delete-task",
"lint": "tsx --tsconfig ../tsconfig.json lint-localized",
"repair-meta": "CALLING_DIR=$INIT_CWD tsx --tsconfig ../tsconfig.json ../tools/challenge-helper-scripts/repair-meta",
"reorder-tasks": "CALLING_DIR=$INIT_CWD tsx --tsconfig ../tsconfig.json ../tools/challenge-helper-scripts/reorder-tasks",
"update-challenge-order": "CALLING_DIR=$INIT_CWD tsx --tsconfig ../tsconfig.json ../tools/challenge-helper-scripts/update-challenge-order",
"update-step-titles": "CALLING_DIR=$INIT_CWD tsx --tsconfig ../tsconfig.json ../tools/challenge-helper-scripts/update-step-titles",

View File

@@ -37,9 +37,10 @@ const {
prefixDoctype,
helperVersion
} = require('../../client/src/templates/Challenges/utils/frame');
const { STRUCTURE_DIR, getBlockCreator } = require('../build-curriculum');
const { curriculumSchemaValidator } = require('../schema/curriculum-schema');
const { validateMetaSchema } = require('../schema/meta-schema');
const { getBlockStructure } = require('../file-handler');
const ChallengeTitles = require('./utils/challenge-titles');
const MongoIds = require('./utils/mongo-ids');
const createPseudoWorker = require('./utils/pseudo-worker');
@@ -168,10 +169,7 @@ async function setup() {
// we can skip them.
// TODO: omit certifications from the list of challenges
if (dashedBlockName && !meta[dashedBlockName]) {
meta[dashedBlockName] = await getBlockCreator(lang).getMetaForBlock(
dashedBlockName,
STRUCTURE_DIR
);
meta[dashedBlockName] = getBlockStructure(dashedBlockName);
const result = validateMetaSchema(meta[dashedBlockName]);
if (result.error) {

View File

@@ -35,41 +35,6 @@ export enum SuperBlocks {
DevPlayground = 'dev-playground'
}
// Note that this object is used to create folderToSuperBlockMap object
export const superBlockToFolderMap = {
[SuperBlocks.RespWebDesign]: '01-responsive-web-design',
[SuperBlocks.JsAlgoDataStruct]:
'02-javascript-algorithms-and-data-structures',
[SuperBlocks.FrontEndDevLibs]: '03-front-end-development-libraries',
[SuperBlocks.DataVis]: '04-data-visualization',
[SuperBlocks.BackEndDevApis]: '05-back-end-development-and-apis',
[SuperBlocks.QualityAssurance]: '06-quality-assurance',
[SuperBlocks.SciCompPy]: '07-scientific-computing-with-python',
[SuperBlocks.DataAnalysisPy]: '08-data-analysis-with-python',
[SuperBlocks.InfoSec]: '09-information-security',
[SuperBlocks.CodingInterviewPrep]: '10-coding-interview-prep',
[SuperBlocks.MachineLearningPy]: '11-machine-learning-with-python',
[SuperBlocks.RelationalDb]: '13-relational-databases',
[SuperBlocks.RespWebDesignNew]: '14-responsive-web-design-22',
[SuperBlocks.JsAlgoDataStructNew]:
'15-javascript-algorithms-and-data-structures-22',
[SuperBlocks.TheOdinProject]: '16-the-odin-project',
[SuperBlocks.CollegeAlgebraPy]: '17-college-algebra-with-python',
[SuperBlocks.ProjectEuler]: '18-project-euler',
[SuperBlocks.FoundationalCSharp]: '19-foundational-c-sharp-with-microsoft',
[SuperBlocks.A2English]: '21-a2-english-for-developers',
[SuperBlocks.RosettaCode]: '22-rosetta-code',
[SuperBlocks.PythonForEverybody]: '23-python-for-everybody',
[SuperBlocks.B1English]: '24-b1-english-for-developers',
[SuperBlocks.FullStackDeveloper]: '25-front-end-development',
[SuperBlocks.A2Spanish]: '26-a2-professional-spanish',
[SuperBlocks.A2Chinese]: '27-a2-professional-chinese',
[SuperBlocks.BasicHtml]: '28-basic-html',
[SuperBlocks.SemanticHtml]: '29-semantic-html',
[SuperBlocks.A1Chinese]: '30-a1-professional-chinese',
[SuperBlocks.DevPlayground]: '99-dev-playground'
};
export const languageSuperBlocks = [
SuperBlocks.A2English,
SuperBlocks.B1English,

View File

@@ -11,8 +11,7 @@ import { availableLangs } from '../../shared/config/i18n';
import { getChallengesForLang } from '../../curriculum/get-challenges';
import {
SuperBlocks,
getAuditedSuperBlocks,
superBlockToFolderMap
getAuditedSuperBlocks
} from '../../shared/config/curriculum';
// TODO: re-organise the types to a common 'types' folder that can be shared
@@ -91,15 +90,17 @@ void (async () => {
'challenges',
language
);
const auditedFiles = englishFilePaths.filter(file =>
certs.some(
cert =>
// we're not ready to audit the new curriculum yet
(cert !== SuperBlocks.JsAlgoDataStructNew ||
process.env.SHOW_UPCOMING_CHANGES === 'true') &&
file.startsWith(superBlockToFolderMap[cert])
)
);
// TODO: decide if we need to audit files at all.
const auditedFiles = englishFilePaths;
// const auditedFiles = englishFilePaths.filter(file =>
// certs.some(
// cert =>
// // we're not ready to audit the new curriculum yet
// (cert !== SuperBlocks.JsAlgoDataStructNew ||
// process.env.SHOW_UPCOMING_CHANGES === 'true') &&
// file.startsWith(superBlockToFolderMap[cert])
// )
// );
const noMissingFiles = await auditChallengeFiles(auditedFiles, {
langCurriculumDirectory
});

View File

@@ -1,9 +1,6 @@
import fs from 'fs';
import { SuperBlocks } from '../../shared/config/curriculum';
import { challengeTypes } from '../../shared/config/challenge-types';
import { getProjectPath } from './helpers/get-project-info';
import { getMetaData, updateMetaData } from './helpers/project-metadata';
import { getChallengeOrderFromFileTree } from './helpers/get-challenge-order';
import { getMetaData } from './helpers/project-metadata';
import {
createStepFile,
deleteStepFromMeta,
@@ -12,7 +9,7 @@ import {
updateStepTitles
} from './utils';
function deleteStep(stepNum: number): void {
async function deleteStep(stepNum: number): Promise<void> {
if (stepNum < 1) {
throw Error('Step not deleted. Step num must be a number greater than 0.');
}
@@ -27,13 +24,13 @@ function deleteStep(stepNum: number): void {
const stepId = challengeOrder[stepNum - 1].id;
fs.unlinkSync(`${getProjectPath()}${stepId}.md`);
deleteStepFromMeta({ stepNum });
await deleteStepFromMeta({ stepNum });
updateStepTitles();
console.log(`Successfully deleted step #${stepNum}`);
}
function insertStep(stepNum: number): void {
async function insertStep(stepNum: number): Promise<void> {
if (stepNum < 1) {
throw Error('Step not inserted. New step number must be greater than 0.');
}
@@ -45,11 +42,6 @@ function insertStep(stepNum: number): void {
challengeOrder.length + 2
}.`
);
const challengeType = [SuperBlocks.SciCompPy].includes(
getMetaData().superBlock
)
? challengeTypes.python
: challengeTypes.html;
const challengeSeeds =
stepNum > 1
@@ -60,16 +52,15 @@ function insertStep(stepNum: number): void {
const stepId = createStepFile({
stepNum,
challengeType,
challengeSeeds
});
insertStepIntoMeta({ stepNum, stepId });
await insertStepIntoMeta({ stepNum, stepId });
updateStepTitles();
console.log(`Successfully inserted new step #${stepNum}`);
}
function createEmptySteps(num: number): void {
async function createEmptySteps(num: number): Promise<void> {
if (num < 1 || num > 1000) {
throw Error(
`No steps created. arg 'num' must be between 1 and 1000 inclusive`
@@ -77,35 +68,11 @@ function createEmptySteps(num: number): void {
}
const nextStepNum = getMetaData().challengeOrder.length + 1;
const challengeType = [SuperBlocks.SciCompPy].includes(
getMetaData().superBlock
)
? challengeTypes.python
: challengeTypes.html;
for (let stepNum = nextStepNum; stepNum < nextStepNum + num; stepNum++) {
const stepId = createStepFile({ stepNum, challengeType });
insertStepIntoMeta({ stepNum, stepId });
const stepId = createStepFile({ stepNum });
await insertStepIntoMeta({ stepNum, stepId });
}
console.log(`Successfully added ${num} steps`);
}
const repairMeta = async () => {
const sortByStepNum = (a: string, b: string) =>
parseInt(a.split(' ')[1]) - parseInt(b.split(' ')[1]);
const challengeOrder = await getChallengeOrderFromFileTree();
if (!challengeOrder.every(({ title }) => /Step \d+/.test(title))) {
throw new Error(
'You can only run this command on project-based blocks with step files.'
);
}
const sortedChallengeOrder = challengeOrder.sort((a, b) =>
sortByStepNum(a.title, b.title)
);
const meta = getMetaData();
meta.challengeOrder = sortedChallengeOrder;
updateMetaData(meta);
};
export { deleteStep, insertStep, createEmptySteps, repairMeta };
export { deleteStep, insertStep, createEmptySteps };

View File

@@ -1,6 +1,4 @@
import { getArgValue } from './helpers/get-arg-value';
import { createEmptySteps } from './commands';
import { validateMetaData } from './helpers/project-metadata';
validateMetaData();
createEmptySteps(getArgValue(process.argv));
void createEmptySteps(getArgValue(process.argv));

View File

@@ -1,4 +1,3 @@
import { existsSync } from 'fs';
import fs from 'fs/promises';
import path from 'path';
import { prompt } from 'inquirer';
@@ -7,12 +6,17 @@ import ObjectID from 'bson-objectid';
import {
SuperBlocks,
languageSuperBlocks,
superBlockToFolderMap
languageSuperBlocks
} from '../../shared/config/curriculum';
import { createDialogueFile, validateBlockName } from './utils';
import {
getContentConfig,
writeBlockStructure
} 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';
const helpCategories = ['English'] as const;
@@ -46,7 +50,11 @@ async function createLanguageBlock(
await updateIntroJson(superBlock, block, title);
const challengeId = await createDialogueChallenge(superBlock, block);
await createMetaJson(superBlock, block, title, helpCategory, challengeId);
await createMetaJson(block, title, helpCategory, challengeId);
const superblockFilename = (
superBlockToFilename as Record<SuperBlocks, string>
)[superBlock];
void updateSimpleSuperblockStructure(block, { order: 0 }, superblockFilename);
// TODO: remove once we stop relying on markdown in the client.
await createIntroMD(superBlock, block, title);
}
@@ -73,46 +81,34 @@ async function updateIntroJson(
}
async function createMetaJson(
superBlock: SuperBlocks,
block: string,
title: string,
helpCategory: string,
challengeId: ObjectID
) {
const metaDir = path.resolve(__dirname, '../../curriculum/challenges/_meta');
const newMeta = getBaseMeta('Language');
newMeta.name = title;
newMeta.dashedName = block;
newMeta.helpCategory = helpCategory;
newMeta.superBlock = superBlock;
newMeta.challengeOrder = [
// eslint-disable-next-line @typescript-eslint/no-base-to-string
{ id: challengeId.toString(), title: "Dialogue 1: I'm Tom" }
];
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' })
);
await writeBlockStructure(block, newMeta);
}
async function createDialogueChallenge(
superBlock: SuperBlocks,
block: string
): Promise<ObjectID> {
const superBlockSubPath = superBlockToFolderMap[superBlock];
const newChallengeDir = path.resolve(
__dirname,
`../../curriculum/challenges/english/${superBlockSubPath}/${block}`
);
if (!existsSync(newChallengeDir)) {
await withTrace(fs.mkdir, newChallengeDir);
}
const { blockContentDir } = getContentConfig('english') as {
blockContentDir: string;
};
const newChallengeDir = path.resolve(blockContentDir, block);
await fs.mkdir(newChallengeDir, { recursive: true });
return createDialogueFile({
projectPath: newChallengeDir + '/'
});

View File

@@ -22,7 +22,7 @@ const createNextChallenge = async () => {
id: challengeId.toString(),
title: options.title
});
updateMetaData(meta);
await updateMetaData(meta);
};
void createNextChallenge();

View File

@@ -1,6 +1,4 @@
import { getLastStep } from './helpers/get-last-step-file-number';
import { insertStep } from './commands';
import { validateMetaData } from './helpers/project-metadata';
validateMetaData();
insertStep(getLastStep().stepNum + 1);
void insertStep(getLastStep().stepNum + 1);

View File

@@ -2,11 +2,7 @@ import ObjectID from 'bson-objectid';
import { getTemplate } from './helpers/get-challenge-template';
import { newTaskPrompts } from './helpers/new-task-prompts';
import { getProjectPath } from './helpers/get-project-info';
import {
getMetaData,
updateMetaData,
validateMetaData
} from './helpers/project-metadata';
import { getMetaData, updateMetaData } from './helpers/project-metadata';
import {
createChallengeFile,
updateTaskMeta,
@@ -14,8 +10,6 @@ import {
} from './utils';
const createNextTask = async () => {
validateMetaData();
const { challengeType } = await newTaskPrompts();
// Placeholder title, to be replaced by updateTaskMarkdownFiles
@@ -40,10 +34,10 @@ const createNextTask = async () => {
id: challengeIdString,
title: options.title
});
updateMetaData(meta);
await updateMetaData(meta);
console.log(`Finished inserting task into 'meta.json' file.`);
updateTaskMeta();
await updateTaskMeta();
console.log("Finished updating tasks in 'meta.json'.");
updateTaskMarkdownFiles();

View File

@@ -1,17 +1,19 @@
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 {
SuperBlocks,
superBlockToFolderMap
} from '../../shared/config/curriculum';
getContentConfig,
writeBlockStructure
} from '../../curriculum/file-handler';
import { superBlockToFilename } from '../../curriculum/build-curriculum';
import { createStepFile, validateBlockName } from './utils';
import { getBaseMeta } from './helpers/get-base-meta';
import { createIntroMD } from './helpers/create-intro';
import { updateSimpleSuperblockStructure } from './helpers/create-project';
const helpCategories = [
'HTML-CSS',
@@ -56,14 +58,15 @@ async function createProject(
void updateIntroJson(superBlock, block, title);
const challengeId = await createFirstChallenge(superBlock, block);
void createMetaJson(
superBlock,
block,
title,
helpCategory,
order,
challengeId
);
void createMetaJson(block, title, helpCategory, challengeId);
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);
}
// TODO: remove once we stop relying on markdown in the client.
void createIntroMD(superBlock, block, title);
}
@@ -90,46 +93,31 @@ async function updateIntroJson(
}
async function createMetaJson(
superBlock: SuperBlocks,
block: string,
title: string,
helpCategory: string,
order: number,
challengeId: ObjectID
) {
const metaDir = path.resolve(__dirname, '../../curriculum/challenges/_meta');
const newMeta = getBaseMeta('Step');
newMeta.name = title;
newMeta.dashedName = block;
newMeta.helpCategory = helpCategory;
newMeta.order = order;
newMeta.superBlock = superBlock;
// eslint-disable-next-line @typescript-eslint/no-base-to-string
newMeta.challengeOrder = [{ id: challengeId.toString(), title: 'Step 1' }];
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' })
);
await writeBlockStructure(block, newMeta);
}
async function createFirstChallenge(
superBlock: SuperBlocks,
block: string
): Promise<ObjectID> {
const superBlockSubPath = superBlockToFolderMap[superBlock];
const newChallengeDir = path.resolve(
__dirname,
`../../curriculum/challenges/english/${superBlockSubPath}/${block}`
);
if (!existsSync(newChallengeDir)) {
await withTrace(fs.mkdir, newChallengeDir);
}
const { blockContentDir } = getContentConfig('english') as {
blockContentDir: string;
};
const newChallengeDir = path.resolve(blockContentDir, block);
await fs.mkdir(newChallengeDir, { recursive: true });
// TODO: would be nice if the extension made sense for the challenge, but, at
// least until react I think they're all going to be html anyway.

View File

@@ -1,17 +1,19 @@
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 {
SuperBlocks,
superBlockToFolderMap
} from '../../shared/config/curriculum';
getContentConfig,
writeBlockStructure
} from '../../curriculum/file-handler';
import { superBlockToFilename } from '../../curriculum/build-curriculum';
import { createQuizFile, validateBlockName } from './utils';
import { getBaseMeta } from './helpers/get-base-meta';
import { createIntroMD } from './helpers/create-intro';
import { updateSimpleSuperblockStructure } from './helpers/create-project';
const helpCategories = [
'HTML-CSS',
@@ -57,7 +59,11 @@ async function createQuiz(
title,
questionCount
);
await createMetaJson(superBlock, block, title, helpCategory, challengeId);
await createMetaJson(block, title, helpCategory, challengeId);
const superblockFilename = (
superBlockToFilename as Record<SuperBlocks, string>
)[superBlock];
void updateSimpleSuperblockStructure(block, { order: 0 }, superblockFilename);
// TODO: remove once we stop relying on markdown in the client.
await createIntroMD(superBlock, block, title);
}
@@ -84,30 +90,19 @@ async function updateIntroJson(
}
async function createMetaJson(
superBlock: SuperBlocks,
block: string,
title: string,
helpCategory: string,
challengeId: ObjectID
) {
const metaDir = path.resolve(__dirname, '../../curriculum/challenges/_meta');
const newMeta = getBaseMeta('Quiz');
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' })
);
await writeBlockStructure(block, newMeta);
}
async function createQuizChallenge(
@@ -116,14 +111,13 @@ async function createQuizChallenge(
title: string,
questionCount: number
): Promise<ObjectID> {
const superBlockSubPath = superBlockToFolderMap[superBlock];
const newChallengeDir = path.resolve(
__dirname,
`../../curriculum/challenges/english/${superBlockSubPath}/${block}`
);
if (!existsSync(newChallengeDir)) {
await withTrace(fs.mkdir, newChallengeDir);
}
const { blockContentDir } = getContentConfig('english') as {
blockContentDir: string;
};
const newChallengeDir = path.resolve(blockContentDir, block);
await fs.mkdir(newChallengeDir, { recursive: true });
return createQuizFile({
projectPath: newChallengeDir + '/',
title: title,

View File

@@ -7,9 +7,14 @@
* you want that.
*/
import ObjectID from 'bson-objectid';
import {
getBlockStructure,
writeBlockStructure
} from '../../curriculum/file-handler';
import { createChallengeFile } from './utils';
import { getProjectPath } from './helpers/get-project-info';
import { getMetaData, updateMetaData } from './helpers/project-metadata';
import { getBlock, type Meta } from './helpers/project-metadata';
// eslint-disable-next-line @typescript-eslint/no-base-to-string
const challengeId = new ObjectID().toString();
@@ -141,16 +146,18 @@ Watch the video
const path = getProjectPath();
if (
!/freeCodeCamp\/curriculum\/challenges\/english\/[^/]+\/[^/]+\/$/.test(path)
!/freeCodeCamp\/curriculum\/challenges\/english\/blocks\/[^/]+\/$/.test(path)
) {
throw Error(`
You cannot run this script from anywhere other than a block folder of the English curriculum.
In the terminal, go to the block folder where you want to create this challenge first.
For example: 'freeCodeCamp/curriculum/challenges/english/21-a2-english-for-developers/learn-greetings-in-your-first-day-at-the-office/'
For example: 'freeCodeCamp/curriculum/challenges/english/blocks/learn-greetings-in-your-first-day-at-the-office/'
`);
}
const meta = getMetaData();
const block = getBlock(path);
const meta = getBlockStructure(block) as Meta;
if (meta.challengeOrder.some(c => c.title === title)) {
throw Error(`
A challenge with the title ${title} already exists in this block.
@@ -162,8 +169,6 @@ meta.challengeOrder.push({
title
});
// write the meta.json file
updateMetaData(meta);
void writeBlockStructure(block, meta);
// write the challenge file, the first argument is the filename
createChallengeFile(challengeId, template, path);

View File

@@ -2,13 +2,12 @@ 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 challenges = getMetaData().challengeOrder;
const challengeToDelete = (await prompt({
name: 'id',
@@ -32,7 +31,7 @@ const deleteChallenge = async () => {
const meta = getMetaData();
meta.challengeOrder.splice(indexToDelete, 1);
updateMetaData(meta);
await updateMetaData(meta);
};
void deleteChallenge();

View File

@@ -1,6 +1,4 @@
import { deleteStep } from './commands';
import { getArgValue } from './helpers/get-arg-value';
import { validateMetaData } from './helpers/project-metadata';
validateMetaData();
deleteStep(getArgValue(process.argv));
void deleteStep(getArgValue(process.argv));

View File

@@ -1,21 +1,18 @@
import { unlink } from 'fs/promises';
import { prompt } from 'inquirer';
import { getProjectPath } from './helpers/get-project-info';
import { getChallengeOrderFromMeta } from './helpers/get-challenge-order';
import { getFileName } from './helpers/get-file-name';
import { validateMetaData } from './helpers/project-metadata';
import {
deleteChallengeFromMeta,
updateTaskMarkdownFiles,
updateTaskMeta
} from './utils';
import { isTaskChallenge } from './helpers/task-helpers';
import { getMetaData } from './helpers/project-metadata';
const deleteTask = async () => {
validateMetaData();
const path = getProjectPath();
const challenges = getChallengeOrderFromMeta();
const challenges = getMetaData().challengeOrder;
const challengeToDelete = (await prompt({
name: 'id',
@@ -39,11 +36,11 @@ const deleteTask = async () => {
await unlink(`${path}${fileToDelete}`);
console.log(`Finished deleting file: '${fileToDelete}'.`);
deleteChallengeFromMeta(indexToDelete);
await deleteChallengeFromMeta(indexToDelete);
console.log(`Finished removing challenge from 'meta.json'.`);
if (isTaskChallenge(challenges[indexToDelete].title)) {
updateTaskMeta();
await updateTaskMeta();
console.log("Finished updating tasks in 'meta.json'.");
updateTaskMarkdownFiles();

View File

@@ -0,0 +1,186 @@
import {
getSuperblockStructure,
writeSuperblockStructure
} from '../../../curriculum/file-handler';
import {
updateChapterModuleSuperblockStructure,
updateSimpleSuperblockStructure
} from './create-project';
jest.mock('../../../curriculum/file-handler');
const mockGetSuperblockStructure =
getSuperblockStructure as jest.MockedFunction<typeof getSuperblockStructure>;
const mockWriteSuperblockStructure =
writeSuperblockStructure as jest.MockedFunction<
typeof writeSuperblockStructure
>;
const incompleteSimpleChapterModuleSuperblock = {
chapters: [
{
dashedName: 'chapter1',
modules: [
{
dashedName: 'module1c1',
blocks: ['block1', 'block3']
}
]
}
]
};
const simpleChapterModuleSuperblock = {
chapters: [
{
dashedName: 'chapter1',
modules: [
{
dashedName: 'module1c1',
blocks: ['block1', 'block2', 'block3']
}
]
}
]
};
describe('updateSimpleSuperblockStructure', () => {
beforeEach(() => {
jest.clearAllMocks();
});
it('should insert the block into the blocks array at the expected position', async () => {
const existingBlocks = ['block1', 'block2', 'block4'];
const superblockFilename = 'test-superblock';
const newBlock = 'block3';
const order = 2;
mockGetSuperblockStructure.mockReturnValue({
blocks: existingBlocks
});
await updateSimpleSuperblockStructure(
newBlock,
{ order },
superblockFilename
);
expect(mockGetSuperblockStructure).toHaveBeenCalledWith(superblockFilename);
expect(mockWriteSuperblockStructure).toHaveBeenCalledWith(
superblockFilename,
{
blocks: ['block1', 'block2', 'block3', 'block4']
}
);
});
});
describe('updateChapterModuleSuperblockStructure', () => {
beforeEach(() => {
jest.clearAllMocks();
});
it('should insert the block correctly when there is only one chapter and one module', async () => {
const superblockFilename = 'test-superblock';
const newBlock = 'block2';
const position = {
order: 1,
chapter: 'chapter1',
module: 'module1c1'
};
mockGetSuperblockStructure.mockReturnValue(
incompleteSimpleChapterModuleSuperblock
);
await updateChapterModuleSuperblockStructure(
newBlock,
position,
superblockFilename
);
expect(mockGetSuperblockStructure).toHaveBeenCalledWith(superblockFilename);
expect(mockWriteSuperblockStructure).toHaveBeenCalledWith(
superblockFilename,
simpleChapterModuleSuperblock
);
});
it('should create a module if it does not exist', async () => {
const superblockFilename = 'test-superblock';
const newBlock = 'block2';
const position = {
order: 0,
chapter: 'chapter1',
module: 'module2c1'
};
mockGetSuperblockStructure.mockReturnValue(
incompleteSimpleChapterModuleSuperblock
);
await updateChapterModuleSuperblockStructure(
newBlock,
position,
superblockFilename
);
expect(mockGetSuperblockStructure).toHaveBeenCalledWith(superblockFilename);
expect(mockWriteSuperblockStructure).toHaveBeenCalledWith(
superblockFilename,
{
chapters: [
{
dashedName: 'chapter1',
modules: [
{
dashedName: 'module1c1',
blocks: ['block1', 'block3']
},
{
dashedName: 'module2c1',
blocks: ['block2']
}
]
}
]
}
);
});
it('should create a chapter and module if they do not exist', async () => {
const superblockFilename = 'test-superblock';
const newBlock = 'block1m2c2';
const position = {
order: 0,
chapter: 'chapter2',
module: 'module1c2'
};
mockGetSuperblockStructure.mockReturnValue({ chapters: [] });
await updateChapterModuleSuperblockStructure(
newBlock,
position,
superblockFilename
);
expect(mockGetSuperblockStructure).toHaveBeenCalledWith(superblockFilename);
expect(mockWriteSuperblockStructure).toHaveBeenCalledWith(
superblockFilename,
{
chapters: [
{
dashedName: 'chapter2',
modules: [
{
dashedName: 'module1c2',
blocks: ['block1m2c2']
}
]
}
]
}
);
});
});

View File

@@ -0,0 +1,94 @@
// TODO: this belongs in create-project, but we can't test that (since it uses
// prettier) until we migrate to vitest
import {
getSuperblockStructure,
writeSuperblockStructure
} from '../../../curriculum/file-handler';
import { insertInto } from './utils';
export async function updateSimpleSuperblockStructure(
block: string,
position: { order: number },
superblockFilename: string
) {
const existing = getSuperblockStructure(superblockFilename) as {
blocks: string[];
};
const updated = {
blocks: insertInto(existing.blocks, position.order, block)
};
await writeSuperblockStructure(superblockFilename, updated);
}
function createNewChapter(chapter: string, module: string, block: string) {
return {
dashedName: chapter,
modules: [
{
dashedName: module,
blocks: [block]
}
]
};
}
function createNewModule(module: string, block: string) {
return {
dashedName: module,
blocks: [block]
};
}
type ChapterModuleSuperblockStructure = {
chapters: {
dashedName: string;
modules: {
dashedName: string;
blocks: string[];
}[];
}[];
};
export async function updateChapterModuleSuperblockStructure(
block: string,
position: { order: number; chapter: string; module: string },
superblockFilename: string
) {
const existing = getSuperblockStructure(
superblockFilename
) as ChapterModuleSuperblockStructure;
const modifiedChapter = existing.chapters.find(
chapter => chapter.dashedName === position.chapter
);
const modifiedModule = modifiedChapter?.modules.find(
module => module.dashedName === position.module
);
const updatedModule = modifiedModule
? {
...modifiedModule,
blocks: insertInto(modifiedModule.blocks, position.order, block)
}
: createNewModule(position.module, block);
const updatedChapter = modifiedChapter
? {
...modifiedChapter,
modules: modifiedModule
? modifiedChapter.modules.map(module =>
module === modifiedModule ? updatedModule : module
)
: [...modifiedChapter.modules, updatedModule]
}
: createNewChapter(position.chapter, position.module, block);
const updated = {
chapters: modifiedChapter
? existing.chapters.map(chapter =>
chapter === modifiedChapter ? updatedChapter : chapter
)
: [...existing.chapters, updatedChapter]
};
await writeSuperblockStructure(superblockFilename, updated);
}

View File

@@ -2,9 +2,8 @@ const baseMeta = {
name: '',
isUpcomingChange: true,
dashedName: '',
superBlock: '',
order: 42,
helpCategory: '',
blockLayout: 'legacy-challenge-list',
challengeOrder: [
{
id: '',

View File

@@ -1,111 +0,0 @@
import fs from 'fs';
import { join } from 'path';
import {
getChallengeOrderFromFileTree,
getChallengeOrderFromMeta
} from './get-challenge-order';
const basePath = join(
process.cwd(),
'__fixtures__' + process.env.JEST_WORKER_ID
);
const commonPath = join(basePath, 'curriculum', 'challenges');
const block = 'project-get-challenge-order';
const metaPath = join(commonPath, '_meta', block);
const superBlockPath = join(
commonPath,
'english',
'superblock-get-challenge-order'
);
const projectPath = join(superBlockPath, block);
describe('get-challenge-order helper', () => {
beforeEach(() => {
fs.mkdirSync(superBlockPath, { recursive: true });
fs.mkdirSync(projectPath, { recursive: true });
fs.mkdirSync(metaPath, { recursive: true });
});
describe('getChallengeOrderFromMeta helper', () => {
beforeEach(() => {
fs.writeFileSync(
join(projectPath, 'this-is-a-challenge.md'),
'---\nid: 1\ntitle: This is a Challenge\n---',
'utf-8'
);
fs.writeFileSync(
join(projectPath, 'what-a-cool-thing.md'),
'---\nid: 100\ntitle: What a Cool Thing\n---',
'utf-8'
);
fs.writeFileSync(
join(projectPath, 'i-dunno.md'),
'---\nid: 2\ntitle: I Dunno\n---'
);
fs.writeFileSync(
join(metaPath, 'meta.json'),
`{
"id": "mock-id",
"challengeOrder": [{"id": "1", "title": "This title is wrong"}, {"id": "2", "title": "I Dunno"}, {"id": "100", "title": "What a Cool Thing"}]}`,
'utf-8'
);
});
it('should load the file order', () => {
process.env.CALLING_DIR = projectPath;
const challengeOrder = getChallengeOrderFromMeta();
expect(challengeOrder).toEqual([
{ id: '1', title: 'This title is wrong' },
{ id: '2', title: 'I Dunno' },
{ id: '100', title: 'What a Cool Thing' }
]);
});
});
describe('getChallengeOrderFromFileTree helper', () => {
beforeEach(() => {
fs.writeFileSync(
join(projectPath, 'step-001.md'),
'---\nid: a8d97bd4c764e91f9d2bda01\ntitle: Step 1\n---',
'utf-8'
);
fs.writeFileSync(
join(projectPath, 'step-002.md'),
'---\nid: a6b0bb188d873cb2c8729495\ntitle: Step 2\n---',
'utf-8'
);
fs.writeFileSync(
join(projectPath, 'step-003.md'),
'---\nid: a5de63ebea8dbee56860f4f2\ntitle: Step 3\n---'
);
fs.writeFileSync(
join(metaPath, 'meta.json'),
`{
"id": "mock-id",
"challengeOrder": [{"id": "a8d97bd4c764e91f9d2bda01", "title": "Step 1"}, {"id": "a6b0bb188d873cb2c8729495", "title": "Step 3"}, {"id": "a5de63ebea8dbee56860f4f2", "title": "Step 2"}]}`,
'utf-8'
);
});
it('should load the file order', async () => {
expect.assertions(1);
process.env.CALLING_DIR = projectPath;
const challengeOrder = await getChallengeOrderFromFileTree();
expect(challengeOrder).toEqual([
{ id: 'a8d97bd4c764e91f9d2bda01', title: 'Step 1' },
{ id: 'a6b0bb188d873cb2c8729495', title: 'Step 2' },
{ id: 'a5de63ebea8dbee56860f4f2', title: 'Step 3' }
]);
});
});
afterEach(() => {
delete process.env.CALLING_DIR;
try {
fs.rmSync(basePath, { recursive: true });
} catch (err) {
console.log(err);
console.log('Could not remove fixtures folder.');
}
});
});

View File

@@ -1,34 +0,0 @@
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<
{ id: string; title: string }[]
> => {
const path = getProjectPath();
const fileList = await readdir(path);
const challengeOrder = fileList
.map(file => {
return matter.read(join(path, file));
})
.map(({ data }) => ({
id: data.id as string,
title: data.title as string
}));
return challengeOrder;
};
export const getChallengeOrderFromMeta = (): {
id: string;
title: string;
}[] => {
const meta = getMetaData();
return meta.challengeOrder.map(({ id, title }) => ({
id,
title
}));
};

View File

@@ -24,7 +24,7 @@ type StepOptions = {
challengeId: ObjectID;
challengeSeeds: Record<string, ChallengeSeed>;
stepNum: number;
challengeType: number;
challengeType?: number;
isFirstChallenge?: boolean;
};
@@ -79,7 +79,7 @@ demoType: onClick`
`---
id: ${challengeId.toString()}
title: Step ${stepNum}
challengeType: ${challengeType}
challengeType: ${challengeType ?? 'placeholder'}
dashedName: step-${stepNum}${demoString}
---

View File

@@ -1,202 +1,18 @@
import fs from 'fs';
import { join } from 'path';
import {
getMetaData,
getProjectMetaPath,
validateMetaData
} from './project-metadata';
import { getBlockStructure } from '../../../curriculum/file-handler';
import { getMetaData } from './project-metadata';
const basePath = join(
process.cwd(),
'__fixtures__' + process.env.JEST_WORKER_ID
);
const commonPath = join(basePath, 'curriculum', 'challenges');
jest.mock('../../../curriculum/file-handler');
const block = 'project-project-metadata';
const metaPath = join(commonPath, '_meta', block);
const superBlockPath = join(
commonPath,
'english',
'superblock-project-metadata'
);
const projectPath = join(superBlockPath, block);
const commonPath = join('curriculum', 'challenges', 'blocks');
const block = 'block-name';
describe('project-metadata helper', () => {
beforeEach(() => {
fs.mkdirSync(superBlockPath, { recursive: true });
fs.mkdirSync(projectPath, { recursive: true });
fs.mkdirSync(metaPath, { recursive: true });
});
describe('getProjectMetaPath helper', () => {
it('should return the meta path', () => {
const expected = join(metaPath, 'meta.json');
process.env.CALLING_DIR = projectPath;
expect(getProjectMetaPath()).toEqual(expected);
});
});
describe('getMetaData helper', () => {
beforeEach(() => {
fs.writeFileSync(
join(projectPath, 'step-001.md'),
'Lorem ipsum...',
'utf-8'
);
fs.writeFileSync(
join(projectPath, 'step-002.md'),
'Lorem ipsum...',
'utf-8'
);
fs.writeFileSync(
join(projectPath, 'step-003.md'),
'Lorem ipsum...',
'utf-8'
);
fs.writeFileSync(
join(metaPath, 'meta.json'),
`{
"id": "mock-id",
"challengeOrder": [{"id": "1", "title": "Step 1"}, {"id": "2", "title": "Step 2"}, {"id": "1", "title": "Step 3"}]}`,
'utf-8'
);
it('should call getBlockStructure with the correct path', () => {
process.env.CALLING_DIR = join(commonPath, block);
getMetaData();
expect(getBlockStructure).toHaveBeenCalledWith(block);
});
it('should process requested file', () => {
const expected = {
id: 'mock-id',
challengeOrder: [
{ id: '1', title: 'Step 1' },
{ id: '2', title: 'Step 2' },
{ id: '1', title: 'Step 3' }
]
};
process.env.CALLING_DIR = projectPath;
expect(getMetaData()).toEqual(expected);
});
it('should throw if file is not found', () => {
process.env.CALLING_DIR =
'curriculum/challenges/english/superblock/mick-priject';
const errorPath = join(
'curriculum',
'challenges',
'_meta',
'mick-priject',
'meta.json'
);
expect(() => {
getMetaData();
}).toThrowError(
new Error(`ENOENT: no such file or directory, open '${errorPath}'`)
);
});
});
describe('validateMetaData helper', () => {
it('should throw if a stepfile is missing', () => {
fs.writeFileSync(
join(projectPath, 'step-001.md'),
`---
id: id-1
title: Step 2
challengeType: a
dashedName: step-2
---
`,
'utf-8'
);
fs.writeFileSync(
join(projectPath, 'step-003.md'),
`---
id: id-3
title: Step 3
challengeType: c
dashedName: step-3
---
`,
'utf-8'
);
fs.writeFileSync(
join(metaPath, 'meta.json'),
`{
"id": "mock-id",
"challengeOrder": [{"id": "1", "title": "Step 1"}, {"id": "2", "title": "Step 2"}, {"id": "1", "title": "Step 3"}]}`,
'utf-8'
);
process.env.CALLING_DIR = projectPath;
expect(() => validateMetaData()).toThrow(
`The file
${projectPath}/1.md
does not exist, but is required by the challengeOrder of
${metaPath}/meta.json
To fix this, you can rename the file containing id: 1 to 1.md
If there is no file for this id, then either the challengeOrder needs to be updated, or the file needs to be created.
`
);
});
it('should throw if a step is present in the project, but not the meta', () => {
fs.writeFileSync(
join(projectPath, '1.md'),
`---
id: id-1
title: Step 2
challengeType: a
dashedName: step-2
---
`,
'utf-8'
);
fs.writeFileSync(
join(projectPath, '2.md'),
`---
id: id-2
title: Step 1
challengeType: b
dashedName: step-1
---
`,
'utf-8'
);
fs.writeFileSync(
join(projectPath, '3.md'),
`---
id: id-3
title: Step 3
challengeType: c
dashedName: step-3
---
`,
'utf-8'
);
fs.writeFileSync(
join(metaPath, 'meta.json'),
`{
"id": "mock-id",
"challengeOrder": [{"id": "1", "title": "Step 1"}, {"id": "2", "title": "Step 2"}, {"id": "1", "title": "Step 3"}]}`,
'utf-8'
);
process.env.CALLING_DIR = projectPath;
expect(() => validateMetaData()).toThrow(
`File ${projectPath}/3.md should be in the meta.json's challengeOrder`
);
});
});
afterEach(() => {
delete process.env.CALLING_DIR;
try {
fs.rmSync(basePath, { recursive: true });
} catch (err) {
console.log(err);
console.log('Could not remove fixtures folder.');
}
});
});

View File

@@ -1,7 +1,10 @@
import fs from 'fs';
import path from 'path';
import glob from 'glob';
import { getProjectName, getProjectPath } from './get-project-info';
import {
getBlockStructure,
writeBlockStructure
} from '../../../curriculum/file-handler';
import { getProjectPath } from './get-project-info';
export type Meta = {
name: string;
@@ -10,65 +13,24 @@ export type Meta = {
isUpcomingChange: boolean;
dashedName: string;
helpCategory: string;
order: number;
time: string;
template: string;
required: string[];
superBlock: string;
challengeOrder: { id: string; title: string }[];
};
function getMetaData(): Meta {
const metaData = fs.readFileSync(getProjectMetaPath(), 'utf-8');
return JSON.parse(metaData) as Meta;
function getMetaData() {
const block = getBlock(getProjectPath());
return getBlockStructure(block) as Meta;
}
function updateMetaData(newMetaData: Record<string, unknown>): void {
fs.writeFileSync(getProjectMetaPath(), JSON.stringify(newMetaData, null, 2));
function getBlock(filePath: string) {
return path.basename(filePath);
}
function getProjectMetaPath(): string {
return path.join(
getProjectPath(),
'../../..',
'_meta',
getProjectName(),
'meta.json'
);
async function updateMetaData(newMetaData: Record<string, unknown>) {
const block = getBlock(getProjectPath());
await writeBlockStructure(block, newMetaData);
}
// This (and everything else) should be async, but it's fast enough
// for the moment.
function validateMetaData(): void {
const { challengeOrder } = getMetaData();
// each step in the challengeOrder should correspond to a file
challengeOrder.forEach(({ id }) => {
const filePath = `${getProjectPath()}${id}.md`;
try {
fs.accessSync(filePath);
} catch (_e) {
throw new Error(
`The file
${filePath}
does not exist, but is required by the challengeOrder of
${getProjectMetaPath()}
To fix this, you can rename the file containing id: ${id} to ${id}.md
If there is no file for this id, then either the challengeOrder needs to be updated, or the file needs to be created.
`
);
}
});
// each file should have a corresponding step in the challengeOrder
glob.sync(`${getProjectPath()}/*.md`).forEach(file => {
const id = path.basename(file, '.md');
if (!challengeOrder.find(({ id: stepId }) => stepId === id))
throw new Error(
`File ${file} should be in the meta.json's challengeOrder`
);
});
}
export { getMetaData, updateMetaData, getProjectMetaPath, validateMetaData };
export { getMetaData, updateMetaData, getBlock };

View File

@@ -0,0 +1,36 @@
import { insertInto } from './utils';
describe('insertInto', () => {
it('should not modify the original array', () => {
const arr = [1, 2, 3];
const result = insertInto(arr, 1, 99);
expect(arr).toEqual([1, 2, 3]);
expect(result).not.toBe(arr);
});
it('should insert at the end if the index is larger than the original array', () => {
const arr = [1, 2, 3];
const result = insertInto(arr, 10, 99);
expect(result).toEqual([1, 2, 3, 99]);
});
it('should insert at the beginning if the index is <= 0', () => {
const arr = [1, 2, 3];
const result = insertInto(arr, 0, 99);
expect(result).toEqual([99, 1, 2, 3]);
const resultNeg = insertInto(arr, -5, 99);
expect(resultNeg).toEqual([99, 1, 2, 3]);
});
it('should insert at the correct index', () => {
const arr = [1, 2, 3];
const result = insertInto(arr, 1, 99);
expect(result).toEqual([1, 99, 2, 3]);
});
it('should work with empty arrays', () => {
const arr: number[] = [];
const result = insertInto(arr, 0, 99);
expect(result).toEqual([99]);
});
});

View File

@@ -0,0 +1,8 @@
export function insertInto<T>(arr: T[], index: number, elem: T): T[] {
if (index >= arr.length) return [...arr, elem];
if (index <= 0) return [elem, ...arr];
return arr.flatMap((x, id) => {
return id === index ? [elem, x] : x;
});
}

View File

@@ -5,14 +5,13 @@ 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 challenges = getMetaData().challengeOrder;
const challengeAfter = await prompt<{ id: string }>({
name: 'id',
@@ -38,7 +37,7 @@ const insertChallenge = async () => {
id: challengeId.toString(),
title: options.title
});
updateMetaData(meta);
await updateMetaData(meta);
};
void insertChallenge();

View File

@@ -1,6 +1,4 @@
import { getArgValue } from './helpers/get-arg-value';
import { insertStep } from './commands';
import { validateMetaData } from './helpers/project-metadata';
validateMetaData();
insertStep(getArgValue(process.argv));
void insertStep(getArgValue(process.argv));

View File

@@ -3,19 +3,16 @@ import { prompt } from 'inquirer';
import { getTemplate } from './helpers/get-challenge-template';
import { newTaskPrompts } from './helpers/new-task-prompts';
import { getProjectPath } from './helpers/get-project-info';
import { validateMetaData } from './helpers/project-metadata';
import {
createChallengeFile,
insertChallengeIntoMeta,
updateTaskMeta,
updateTaskMarkdownFiles
} from './utils';
import { getChallengeOrderFromMeta } from './helpers/get-challenge-order';
import { getMetaData } from './helpers/project-metadata';
const insertChallenge = async () => {
validateMetaData();
const challenges = getChallengeOrderFromMeta();
const challenges = getMetaData().challengeOrder;
const challengeAfter = await prompt<{ id: string }>({
name: 'id',
message: 'Which challenge should come AFTER this new one?',
@@ -50,14 +47,14 @@ const insertChallenge = async () => {
createChallengeFile(challengeIdString, challengeText, path);
console.log('Finished creating new task markdown file.');
insertChallengeIntoMeta({
await insertChallengeIntoMeta({
index: indexToInsert,
id: challengeId,
title: newTaskTitle
});
console.log(`Finished inserting task into 'meta.json' file.`);
updateTaskMeta();
await updateTaskMeta();
console.log("Finished updating tasks in 'meta.json'.");
updateTaskMarkdownFiles();

View File

@@ -1,10 +1,7 @@
import { validateMetaData } from './helpers/project-metadata';
import { updateTaskMeta, updateTaskMarkdownFiles } from './utils';
const reorderTasks = () => {
validateMetaData();
updateTaskMeta();
const reorderTasks = async () => {
await updateTaskMeta();
console.log("Finished updating tasks in 'meta.json'.");
updateTaskMarkdownFiles();

View File

@@ -1,73 +0,0 @@
import { join } from 'path';
import fs from 'fs';
import { repairMeta } from './commands';
const basePath = join(
process.cwd(),
'__fixtures__' + process.env.JEST_WORKER_ID
);
const commonPath = join(basePath, 'curriculum', 'challenges');
const metaPath = join(commonPath, '_meta', 'project-repair-meta');
const superBlockPath = join(commonPath, 'english', 'superblock-repair-meta');
const projectPath = join(superBlockPath, 'project-repair-meta');
describe('Challenge utils helper scripts', () => {
beforeEach(() => {
process.env.CALLING_DIR = projectPath;
fs.mkdirSync(metaPath, { recursive: true });
fs.mkdirSync(superBlockPath, { recursive: true });
fs.mkdirSync(projectPath);
});
it('should restore the challenge order in the meta.json file', async () => {
fs.writeFileSync(
join(metaPath, 'meta.json'),
// all the challenges from step 1 to 30 in reverse order:
`{"challengeOrder": [${Array.from(
{ length: 30 },
(_, i) => `{"id": "id-${i + 1}", "title": "Step ${30 - i}"}`
).join(',')}]}`,
'utf-8'
);
// create all 30 challenges:
Array.from({ length: 30 }, (_, i) => {
fs.writeFileSync(
join(projectPath, `step-${i + 1}.md`),
`---
id: id-${i + 1}
title: Step ${30 - i}
---
`,
'utf-8'
);
});
// run the repair script:
await repairMeta();
// confirm that the meta.json file now has the correct challenge order:
const meta = JSON.parse(
fs.readFileSync(join(metaPath, 'meta.json'), 'utf-8')
) as { challengeOrder: { id: string; title: string }[] };
expect(meta.challengeOrder).toEqual(
Array.from({ length: 30 }, (_, i) => ({
id: `id-${30 - i}`,
title: `Step ${i + 1}`
}))
);
});
afterEach(() => {
delete process.env.CALLING_DIR;
try {
fs.rmSync(basePath, { recursive: true });
} catch (err) {
console.log(err);
console.log('Could not remove fixtures folder.');
}
});
});

View File

@@ -1,3 +0,0 @@
import { repairMeta } from './commands';
void (() => repairMeta())();

View File

@@ -1,10 +1,9 @@
import { prompt } from 'inquirer';
import { getMetaData, updateMetaData } from './helpers/project-metadata';
import { getChallengeOrderFromMeta } from './helpers/get-challenge-order';
const updateChallengeOrder = async () => {
const oldChallengeOrder = getChallengeOrderFromMeta();
const oldChallengeOrder = getMetaData().challengeOrder;
console.log('Current challenge order is: ');
console.table(oldChallengeOrder.map(({ title }) => ({ title })));
@@ -49,7 +48,7 @@ const updateChallengeOrder = async () => {
const meta = getMetaData();
meta.challengeOrder = newChallengeOrder;
updateMetaData(meta);
await updateMetaData(meta);
};
void (async () => await updateChallengeOrder())();

View File

@@ -1,5 +1,3 @@
import { validateMetaData } from './helpers/project-metadata';
import { updateStepTitles } from './utils';
validateMetaData();
updateStepTitles();

View File

@@ -1,8 +1,21 @@
import fs from 'fs';
import { join } from 'path';
import ObjectID from 'bson-objectid';
import glob from 'glob';
import path, { join } from 'path';
import matter from 'gray-matter';
import ObjectID from 'bson-objectid';
jest.mock('fs', () => {
return {
writeFileSync: jest.fn(),
readdirSync: jest.fn()
};
});
jest.mock('gray-matter', () => {
return {
read: jest.fn(),
stringify: jest.fn()
};
});
jest.mock('bson-objectid', () => {
return jest.fn(() => ({ toString: () => mockChallengeId }));
@@ -10,10 +23,20 @@ jest.mock('bson-objectid', () => {
jest.mock('./helpers/get-step-template', () => {
return {
getStepTemplate: jest.fn(() => 'Mock template...')
getStepTemplate: jest.fn()
};
});
const mockMeta = {
challengeOrder: [{ id: 'abc', title: 'mock title' }]
};
jest.mock('./helpers/project-metadata', () => ({
// ...jest.requireActual('./helpers/project-metadata'),
getMetaData: jest.fn(() => mockMeta),
updateMetaData: jest.fn()
}));
const mockChallengeId = '60d35cf3fe32df2ce8e31b03';
import { getStepTemplate } from './helpers/get-step-template';
import {
@@ -23,37 +46,26 @@ import {
updateStepTitles,
validateBlockName
} from './utils';
import { updateMetaData } from './helpers/project-metadata';
const basePath = join(
process.cwd(),
'__fixtures__' + process.env.JEST_WORKER_ID
);
const commonPath = join(basePath, 'curriculum', 'challenges');
const commonPath = join(basePath, 'curriculum');
const block = 'utils-project';
const metaPath = join(commonPath, '_meta', block);
const superBlockPath = join(commonPath, 'english', 'utils-superblock');
const projectPath = join(superBlockPath, block);
const projectPath = join(commonPath, 'challenges', 'english', 'blocks', block);
describe('Challenge utils helper scripts', () => {
beforeEach(() => {
fs.mkdirSync(superBlockPath, { recursive: true });
fs.mkdirSync(projectPath, { recursive: true });
fs.mkdirSync(metaPath, { recursive: true });
afterEach(() => {
jest.clearAllMocks();
});
describe('createStepFile util', () => {
it('should create next step and return its identifier', () => {
fs.writeFileSync(
join(projectPath, 'step-001.md'),
'Lorem ipsum...',
'utf-8'
);
fs.writeFileSync(
join(projectPath, 'step-002.md'),
'Lorem ipsum...',
'utf-8'
);
process.env.CALLING_DIR = projectPath;
const mockTemplate = 'Mock template...';
(getStepTemplate as jest.Mock).mockReturnValue(mockTemplate);
const step = createStepFile({
stepNum: 3,
challengeType: 0
@@ -66,15 +78,10 @@ describe('Challenge utils helper scripts', () => {
// Internal tasks
// - Should generate a template for the step that is being created
expect(getStepTemplate).toHaveBeenCalledTimes(1);
// - Should write a file with a given name and template
const files = glob.sync(`${projectPath}/*.md`);
expect(files).toEqual([
expect(fs.writeFileSync).toHaveBeenCalledWith(
`${projectPath}/${mockChallengeId}.md`,
`${projectPath}/step-001.md`,
`${projectPath}/step-002.md`
]);
mockTemplate
);
});
});
@@ -104,78 +111,31 @@ describe('Challenge utils helper scripts', () => {
describe('createChallengeFile util', () => {
it('should create the challenge', () => {
fs.writeFileSync(
join(projectPath, 'fake-challenge.md'),
'Lorem ipsum...',
'utf-8'
);
fs.writeFileSync(
join(projectPath, 'so-many-fakes.md'),
'Lorem ipsum...',
'utf-8'
);
process.env.CALLING_DIR = projectPath;
const template = 'pretend this is a template';
createChallengeFile('hi', 'pretend this is a template');
createChallengeFile('hi', template);
// - Should write a file with a given name and template
const files = glob.sync(`${projectPath}/*.md`);
expect(files).toEqual([
`${projectPath}/fake-challenge.md`,
expect(fs.writeFileSync).toHaveBeenCalledWith(
`${projectPath}/hi.md`,
`${projectPath}/so-many-fakes.md`
]);
template
);
});
});
describe('insertStepIntoMeta util', () => {
it('should update the meta with a new file id and name', () => {
fs.writeFileSync(
join(metaPath, 'meta.json'),
`{"id": "mock-id",
"challengeOrder": [
{
"id": "id-1",
"title": "Step 1"
},
{
"id": "id-2",
"title": "Step 2"
},
{
"id": "id-3",
"title": "Step 3"
}
]}`,
'utf-8'
);
it('should call updateMetaData with a new file id and name', async () => {
process.env.CALLING_DIR = projectPath;
insertStepIntoMeta({ stepNum: 3, stepId: new ObjectID(mockChallengeId) });
await insertStepIntoMeta({
stepNum: 3,
stepId: new ObjectID(mockChallengeId)
});
const meta = JSON.parse(
fs.readFileSync(join(metaPath, 'meta.json'), 'utf-8')
);
expect(meta).toEqual({
id: 'mock-id',
expect(updateMetaData).toHaveBeenCalledWith({
challengeOrder: [
{
id: 'id-1',
title: 'Step 1'
},
{
id: 'id-2',
title: 'Step 2'
},
{
id: mockChallengeId,
title: 'Step 3'
},
{
id: 'id-3',
title: 'Step 4'
}
{ id: 'abc', title: 'Step 1' }, // title gets overwritten
{ id: mockChallengeId, title: 'Step 2' }
]
});
});
@@ -183,76 +143,36 @@ describe('Challenge utils helper scripts', () => {
describe('updateStepTitles util', () => {
it('should apply meta.challengeOrder to step files', () => {
fs.writeFileSync(
join(metaPath, 'meta.json'),
`{"id": "mock-id", "challengeOrder": [{"id": "id-1", "title": "Step 1"}, {"id": "id-3", "title": "Step 2"}, {"id": "id-2", "title": "Step 3"}]}`,
'utf-8'
);
fs.writeFileSync(
join(projectPath, 'id-1.md'),
`---
id: id-1
title: Step 2
challengeType: a
dashedName: step-2
---
`,
'utf-8'
);
fs.writeFileSync(
join(projectPath, 'id-2.md'),
`---
id: id-2
title: Step 1
challengeType: b
dashedName: step-1
---
`,
'utf-8'
);
fs.writeFileSync(
join(projectPath, 'id-3.md'),
`---
id: id-3
title: Step 3
challengeType: c
dashedName: step-3
---
`,
'utf-8'
);
process.env.CALLING_DIR = projectPath;
(getStepTemplate as jest.Mock).mockReturnValue('Mock template...');
(fs.readdirSync as jest.Mock).mockReturnValue([
'name.md',
'another-name.md'
]);
(matter.read as jest.Mock).mockReturnValue({
data: { id: 'abc' },
content: 'goes here'
});
updateStepTitles();
expect(matter.read(join(projectPath, 'id-1.md')).data).toEqual({
id: 'id-1',
title: 'Step 1',
challengeType: 'a',
dashedName: 'step-1'
});
expect(matter.read(join(projectPath, 'id-2.md')).data).toEqual({
id: 'id-2',
title: 'Step 3',
challengeType: 'b',
dashedName: 'step-3'
});
expect(matter.read(join(projectPath, 'id-3.md')).data).toEqual({
id: 'id-3',
title: 'Step 2',
challengeType: 'c',
dashedName: 'step-2'
expect(fs.readdirSync).toHaveBeenCalledWith(projectPath + '/');
expect(fs.writeFileSync).toHaveBeenCalledWith(
path.join(projectPath, 'name.md'),
undefined
);
expect(fs.writeFileSync).toHaveBeenCalledWith(
path.join(projectPath, 'another-name.md'),
undefined
);
expect(matter.stringify).toHaveBeenCalledWith('goes here', {
dashedName: 'step-1',
id: 'abc',
title: 'Step 1'
});
});
});
afterEach(() => {
delete process.env.CALLING_DIR;
try {
fs.rmSync(basePath, { recursive: true });
} catch (err) {
console.log(err);
console.log('Could not remove fixtures folder.');
}
});
});

View File

@@ -15,7 +15,7 @@ import { getTemplate } from './helpers/get-challenge-template';
interface Options {
stepNum: number;
challengeType: number;
challengeType?: number;
projectPath?: string;
challengeSeeds?: Record<string, ChallengeSeed>;
isFirstChallenge?: boolean;
@@ -112,20 +112,20 @@ interface InsertChallengeOptions {
title: string;
}
function insertChallengeIntoMeta({
async function insertChallengeIntoMeta({
index,
id,
title
}: InsertChallengeOptions): void {
}: InsertChallengeOptions) {
const existingMeta = getMetaData();
const challengeOrder = [...existingMeta.challengeOrder];
// eslint-disable-next-line @typescript-eslint/no-base-to-string
challengeOrder.splice(index, 0, { id: id.toString(), title });
updateMetaData({ ...existingMeta, challengeOrder });
await updateMetaData({ ...existingMeta, challengeOrder });
}
function insertStepIntoMeta({ stepNum, stepId }: InsertOptions): void {
async function insertStepIntoMeta({ stepNum, stepId }: InsertOptions) {
const existingMeta = getMetaData();
const oldOrder = [...existingMeta.challengeOrder];
// eslint-disable-next-line @typescript-eslint/no-base-to-string
@@ -136,10 +136,10 @@ function insertStepIntoMeta({ stepNum, stepId }: InsertOptions): void {
title: `Step ${index + 1}`
}));
updateMetaData({ ...existingMeta, challengeOrder });
await updateMetaData({ ...existingMeta, challengeOrder });
}
function deleteStepFromMeta({ stepNum }: { stepNum: number }): void {
async function deleteStepFromMeta({ stepNum }: { stepNum: number }) {
const existingMeta = getMetaData();
const oldOrder = [...existingMeta.challengeOrder];
oldOrder.splice(stepNum - 1, 1);
@@ -149,17 +149,17 @@ function deleteStepFromMeta({ stepNum }: { stepNum: number }): void {
title: `Step ${index + 1}`
}));
updateMetaData({ ...existingMeta, challengeOrder });
await updateMetaData({ ...existingMeta, challengeOrder });
}
function deleteChallengeFromMeta(challengeIndex: number): void {
async function deleteChallengeFromMeta(challengeIndex: number) {
const existingMeta = getMetaData();
const challengeOrder = [...existingMeta.challengeOrder];
challengeOrder.splice(challengeIndex, 1);
updateMetaData({ ...existingMeta, challengeOrder });
await updateMetaData({ ...existingMeta, challengeOrder });
}
function updateTaskMeta() {
async function updateTaskMeta() {
const existingMeta = getMetaData();
const oldOrder = [...existingMeta.challengeOrder];
@@ -176,7 +176,7 @@ function updateTaskMeta() {
}
});
updateMetaData({ ...existingMeta, challengeOrder });
await updateMetaData({ ...existingMeta, challengeOrder });
}
const updateStepTitles = (): void => {