mirror of
https://github.com/freeCodeCamp/freeCodeCamp.git
synced 2026-04-29 16:00:55 -04:00
fix(tools): curriculum command line helpers (#61831)
This commit is contained in:
committed by
GitHub
parent
c58ba56eeb
commit
10c565828e
@@ -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,
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
@@ -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
199
curriculum/file-handler.js
Normal 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;
|
||||
@@ -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",
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
});
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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 + '/'
|
||||
});
|
||||
|
||||
@@ -22,7 +22,7 @@ const createNextChallenge = async () => {
|
||||
id: challengeId.toString(),
|
||||
title: options.title
|
||||
});
|
||||
updateMetaData(meta);
|
||||
await updateMetaData(meta);
|
||||
};
|
||||
|
||||
void createNextChallenge();
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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();
|
||||
|
||||
186
tools/challenge-helper-scripts/helpers/create-project.test.ts
Normal file
186
tools/challenge-helper-scripts/helpers/create-project.test.ts
Normal 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']
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
94
tools/challenge-helper-scripts/helpers/create-project.ts
Normal file
94
tools/challenge-helper-scripts/helpers/create-project.ts
Normal 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);
|
||||
}
|
||||
@@ -2,9 +2,8 @@ const baseMeta = {
|
||||
name: '',
|
||||
isUpcomingChange: true,
|
||||
dashedName: '',
|
||||
superBlock: '',
|
||||
order: 42,
|
||||
helpCategory: '',
|
||||
blockLayout: 'legacy-challenge-list',
|
||||
challengeOrder: [
|
||||
{
|
||||
id: '',
|
||||
|
||||
@@ -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.');
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -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
|
||||
}));
|
||||
};
|
||||
@@ -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}
|
||||
---
|
||||
|
||||
|
||||
@@ -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.');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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 };
|
||||
|
||||
36
tools/challenge-helper-scripts/helpers/utils.test.ts
Normal file
36
tools/challenge-helper-scripts/helpers/utils.test.ts
Normal 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]);
|
||||
});
|
||||
});
|
||||
8
tools/challenge-helper-scripts/helpers/utils.ts
Normal file
8
tools/challenge-helper-scripts/helpers/utils.ts
Normal 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;
|
||||
});
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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.');
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -1,3 +0,0 @@
|
||||
import { repairMeta } from './commands';
|
||||
|
||||
void (() => repairMeta())();
|
||||
@@ -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())();
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { validateMetaData } from './helpers/project-metadata';
|
||||
import { updateStepTitles } from './utils';
|
||||
|
||||
validateMetaData();
|
||||
updateStepTitles();
|
||||
|
||||
@@ -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.');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
Reference in New Issue
Block a user