mirror of
https://github.com/freeCodeCamp/freeCodeCamp.git
synced 2026-05-28 18:26:54 +00: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,
|
getContentDir,
|
||||||
getBlockCreator
|
getBlockCreator
|
||||||
} = require('../../curriculum/build-curriculum');
|
} = require('../../curriculum/build-curriculum');
|
||||||
|
const { getBlockStructure } = require('../../curriculum/file-handler');
|
||||||
|
|
||||||
const { curriculumLocale } = envData;
|
const { curriculumLocale } = envData;
|
||||||
|
|
||||||
@@ -23,7 +24,7 @@ exports.replaceChallengeNode = () => {
|
|||||||
const filename = path.basename(filePath);
|
const filename = path.basename(filePath);
|
||||||
|
|
||||||
console.log(`Replacing challenge node for ${filePath}`);
|
console.log(`Replacing challenge node for ${filePath}`);
|
||||||
const meta = blockCreator.getMetaForBlock(block);
|
const meta = getBlockStructure(block);
|
||||||
|
|
||||||
return await blockCreator.createChallenge({
|
return await blockCreator.createChallenge({
|
||||||
filename,
|
filename,
|
||||||
|
|||||||
+15
-110
@@ -1,6 +1,5 @@
|
|||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const assert = require('assert');
|
|
||||||
|
|
||||||
const { isEmpty } = require('lodash');
|
const { isEmpty } = require('lodash');
|
||||||
const debug = require('debug')('fcc:build-curriculum');
|
const debug = require('debug')('fcc:build-curriculum');
|
||||||
@@ -13,14 +12,13 @@ const {
|
|||||||
|
|
||||||
const { buildCertification } = require('./build-certification');
|
const { buildCertification } = require('./build-certification');
|
||||||
const { applyFilters } = require('./utils');
|
const { applyFilters } = require('./utils');
|
||||||
|
const {
|
||||||
const CURRICULUM_DIR = __dirname;
|
getContentDir,
|
||||||
const I18N_CURRICULUM_DIR = path.resolve(
|
getLanguageConfig,
|
||||||
CURRICULUM_DIR,
|
getCurriculumStructure,
|
||||||
'i18n-curriculum',
|
getBlockStructure,
|
||||||
'curriculum'
|
getSuperblockStructure
|
||||||
);
|
} = require('./file-handler');
|
||||||
const STRUCTURE_DIR = path.resolve(CURRICULUM_DIR, 'structure');
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a BlockCreator instance for a specific language with appropriate configuration
|
* 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 getBlockCreator = (lang, skipValidation, opts) => {
|
||||||
const {
|
const {
|
||||||
blockContentDir,
|
blockContentDir,
|
||||||
blockStructureDir,
|
|
||||||
i18nBlockContentDir,
|
i18nBlockContentDir,
|
||||||
dictionariesDir,
|
dictionariesDir,
|
||||||
i18nDictionariesDir
|
i18nDictionariesDir
|
||||||
@@ -47,7 +44,6 @@ const getBlockCreator = (lang, skipValidation, opts) => {
|
|||||||
return new BlockCreator({
|
return new BlockCreator({
|
||||||
lang,
|
lang,
|
||||||
blockContentDir,
|
blockContentDir,
|
||||||
blockStructureDir,
|
|
||||||
i18nBlockContentDir,
|
i18nBlockContentDir,
|
||||||
commentTranslations: createCommentMap(
|
commentTranslations: createCommentMap(
|
||||||
dictionariesDir,
|
dictionariesDir,
|
||||||
@@ -194,84 +190,12 @@ const superBlockNames = {
|
|||||||
'dev-playground': 'dev-playground'
|
'dev-playground': 'dev-playground'
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
const superBlockToFilename = Object.entries(superBlockNames).reduce(
|
||||||
* Gets language-specific configuration paths for curriculum content
|
(map, entry) => {
|
||||||
* @param {string} lang - The language code (e.g., 'english', 'spanish', etc.)
|
return { ...map, [entry[1]]: entry[0] };
|
||||||
* @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'));
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Builds an array of superblock structures from a curriculum object
|
* Builds an array of superblock structures from a curriculum object
|
||||||
@@ -304,26 +228,6 @@ function addSuperblockStructure(superblocks) {
|
|||||||
return superblockStructures;
|
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(
|
function addBlockStructure(
|
||||||
superblocks,
|
superblocks,
|
||||||
_getBlockStructure = getBlockStructure
|
_getBlockStructure = getBlockStructure
|
||||||
@@ -383,5 +287,6 @@ module.exports = {
|
|||||||
getBlockCreator,
|
getBlockCreator,
|
||||||
getBlockStructure,
|
getBlockStructure,
|
||||||
getSuperblockStructure,
|
getSuperblockStructure,
|
||||||
createCommentMap
|
createCommentMap,
|
||||||
|
superBlockToFilename
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -226,8 +226,6 @@ class BlockCreator {
|
|||||||
/**
|
/**
|
||||||
* @param {object} options - Options object
|
* @param {object} options - Options object
|
||||||
* @param {string} options.blockContentDir - Directory containing block content files
|
* @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.i18nBlockContentDir - Directory containing i18n block content files
|
||||||
* @param {string} options.lang - Language code for the block content
|
* @param {string} options.lang - Language code for the block content
|
||||||
* @param {object} options.commentTranslations - Translations for comments in challenges
|
* @param {object} options.commentTranslations - Translations for comments in challenges
|
||||||
@@ -238,14 +236,12 @@ class BlockCreator {
|
|||||||
*/
|
*/
|
||||||
constructor({
|
constructor({
|
||||||
blockContentDir,
|
blockContentDir,
|
||||||
blockStructureDir,
|
|
||||||
i18nBlockContentDir,
|
i18nBlockContentDir,
|
||||||
lang,
|
lang,
|
||||||
commentTranslations,
|
commentTranslations,
|
||||||
skipValidation
|
skipValidation
|
||||||
}) {
|
}) {
|
||||||
this.blockContentDir = blockContentDir;
|
this.blockContentDir = blockContentDir;
|
||||||
this.blockStructureDir = blockStructureDir;
|
|
||||||
this.i18nBlockContentDir = i18nBlockContentDir;
|
this.i18nBlockContentDir = i18nBlockContentDir;
|
||||||
this.lang = lang;
|
this.lang = lang;
|
||||||
this.commentTranslations = commentTranslations;
|
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 }) {
|
async processBlock(block, { superBlock, order }) {
|
||||||
const blockName = block.dashedName;
|
const blockName = block.dashedName;
|
||||||
debug(`Processing block ${blockName} in superblock ${superBlock}`);
|
debug(`Processing block ${blockName} in superblock ${superBlock}`);
|
||||||
|
|||||||
@@ -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-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",
|
"delete-task": "CALLING_DIR=$INIT_CWD tsx --tsconfig ../tsconfig.json ../tools/challenge-helper-scripts/delete-task",
|
||||||
"lint": "tsx --tsconfig ../tsconfig.json lint-localized",
|
"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",
|
"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-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",
|
"update-step-titles": "CALLING_DIR=$INIT_CWD tsx --tsconfig ../tsconfig.json ../tools/challenge-helper-scripts/update-step-titles",
|
||||||
|
|||||||
@@ -37,9 +37,10 @@ const {
|
|||||||
prefixDoctype,
|
prefixDoctype,
|
||||||
helperVersion
|
helperVersion
|
||||||
} = require('../../client/src/templates/Challenges/utils/frame');
|
} = require('../../client/src/templates/Challenges/utils/frame');
|
||||||
const { STRUCTURE_DIR, getBlockCreator } = require('../build-curriculum');
|
|
||||||
const { curriculumSchemaValidator } = require('../schema/curriculum-schema');
|
const { curriculumSchemaValidator } = require('../schema/curriculum-schema');
|
||||||
const { validateMetaSchema } = require('../schema/meta-schema');
|
const { validateMetaSchema } = require('../schema/meta-schema');
|
||||||
|
const { getBlockStructure } = require('../file-handler');
|
||||||
const ChallengeTitles = require('./utils/challenge-titles');
|
const ChallengeTitles = require('./utils/challenge-titles');
|
||||||
const MongoIds = require('./utils/mongo-ids');
|
const MongoIds = require('./utils/mongo-ids');
|
||||||
const createPseudoWorker = require('./utils/pseudo-worker');
|
const createPseudoWorker = require('./utils/pseudo-worker');
|
||||||
@@ -168,10 +169,7 @@ async function setup() {
|
|||||||
// we can skip them.
|
// we can skip them.
|
||||||
// TODO: omit certifications from the list of challenges
|
// TODO: omit certifications from the list of challenges
|
||||||
if (dashedBlockName && !meta[dashedBlockName]) {
|
if (dashedBlockName && !meta[dashedBlockName]) {
|
||||||
meta[dashedBlockName] = await getBlockCreator(lang).getMetaForBlock(
|
meta[dashedBlockName] = getBlockStructure(dashedBlockName);
|
||||||
dashedBlockName,
|
|
||||||
STRUCTURE_DIR
|
|
||||||
);
|
|
||||||
const result = validateMetaSchema(meta[dashedBlockName]);
|
const result = validateMetaSchema(meta[dashedBlockName]);
|
||||||
|
|
||||||
if (result.error) {
|
if (result.error) {
|
||||||
|
|||||||
@@ -35,41 +35,6 @@ export enum SuperBlocks {
|
|||||||
DevPlayground = 'dev-playground'
|
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 = [
|
export const languageSuperBlocks = [
|
||||||
SuperBlocks.A2English,
|
SuperBlocks.A2English,
|
||||||
SuperBlocks.B1English,
|
SuperBlocks.B1English,
|
||||||
|
|||||||
@@ -11,8 +11,7 @@ import { availableLangs } from '../../shared/config/i18n';
|
|||||||
import { getChallengesForLang } from '../../curriculum/get-challenges';
|
import { getChallengesForLang } from '../../curriculum/get-challenges';
|
||||||
import {
|
import {
|
||||||
SuperBlocks,
|
SuperBlocks,
|
||||||
getAuditedSuperBlocks,
|
getAuditedSuperBlocks
|
||||||
superBlockToFolderMap
|
|
||||||
} from '../../shared/config/curriculum';
|
} from '../../shared/config/curriculum';
|
||||||
|
|
||||||
// TODO: re-organise the types to a common 'types' folder that can be shared
|
// TODO: re-organise the types to a common 'types' folder that can be shared
|
||||||
@@ -91,15 +90,17 @@ void (async () => {
|
|||||||
'challenges',
|
'challenges',
|
||||||
language
|
language
|
||||||
);
|
);
|
||||||
const auditedFiles = englishFilePaths.filter(file =>
|
// TODO: decide if we need to audit files at all.
|
||||||
certs.some(
|
const auditedFiles = englishFilePaths;
|
||||||
cert =>
|
// const auditedFiles = englishFilePaths.filter(file =>
|
||||||
// we're not ready to audit the new curriculum yet
|
// certs.some(
|
||||||
(cert !== SuperBlocks.JsAlgoDataStructNew ||
|
// cert =>
|
||||||
process.env.SHOW_UPCOMING_CHANGES === 'true') &&
|
// // we're not ready to audit the new curriculum yet
|
||||||
file.startsWith(superBlockToFolderMap[cert])
|
// (cert !== SuperBlocks.JsAlgoDataStructNew ||
|
||||||
)
|
// process.env.SHOW_UPCOMING_CHANGES === 'true') &&
|
||||||
);
|
// file.startsWith(superBlockToFolderMap[cert])
|
||||||
|
// )
|
||||||
|
// );
|
||||||
const noMissingFiles = await auditChallengeFiles(auditedFiles, {
|
const noMissingFiles = await auditChallengeFiles(auditedFiles, {
|
||||||
langCurriculumDirectory
|
langCurriculumDirectory
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
import fs from 'fs';
|
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 { getProjectPath } from './helpers/get-project-info';
|
||||||
import { getMetaData, updateMetaData } from './helpers/project-metadata';
|
import { getMetaData } from './helpers/project-metadata';
|
||||||
import { getChallengeOrderFromFileTree } from './helpers/get-challenge-order';
|
|
||||||
import {
|
import {
|
||||||
createStepFile,
|
createStepFile,
|
||||||
deleteStepFromMeta,
|
deleteStepFromMeta,
|
||||||
@@ -12,7 +9,7 @@ import {
|
|||||||
updateStepTitles
|
updateStepTitles
|
||||||
} from './utils';
|
} from './utils';
|
||||||
|
|
||||||
function deleteStep(stepNum: number): void {
|
async function deleteStep(stepNum: number): Promise<void> {
|
||||||
if (stepNum < 1) {
|
if (stepNum < 1) {
|
||||||
throw Error('Step not deleted. Step num must be a number greater than 0.');
|
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;
|
const stepId = challengeOrder[stepNum - 1].id;
|
||||||
|
|
||||||
fs.unlinkSync(`${getProjectPath()}${stepId}.md`);
|
fs.unlinkSync(`${getProjectPath()}${stepId}.md`);
|
||||||
deleteStepFromMeta({ stepNum });
|
await deleteStepFromMeta({ stepNum });
|
||||||
updateStepTitles();
|
updateStepTitles();
|
||||||
|
|
||||||
console.log(`Successfully deleted step #${stepNum}`);
|
console.log(`Successfully deleted step #${stepNum}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
function insertStep(stepNum: number): void {
|
async function insertStep(stepNum: number): Promise<void> {
|
||||||
if (stepNum < 1) {
|
if (stepNum < 1) {
|
||||||
throw Error('Step not inserted. New step number must be greater than 0.');
|
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
|
challengeOrder.length + 2
|
||||||
}.`
|
}.`
|
||||||
);
|
);
|
||||||
const challengeType = [SuperBlocks.SciCompPy].includes(
|
|
||||||
getMetaData().superBlock
|
|
||||||
)
|
|
||||||
? challengeTypes.python
|
|
||||||
: challengeTypes.html;
|
|
||||||
|
|
||||||
const challengeSeeds =
|
const challengeSeeds =
|
||||||
stepNum > 1
|
stepNum > 1
|
||||||
@@ -60,16 +52,15 @@ function insertStep(stepNum: number): void {
|
|||||||
|
|
||||||
const stepId = createStepFile({
|
const stepId = createStepFile({
|
||||||
stepNum,
|
stepNum,
|
||||||
challengeType,
|
|
||||||
challengeSeeds
|
challengeSeeds
|
||||||
});
|
});
|
||||||
|
|
||||||
insertStepIntoMeta({ stepNum, stepId });
|
await insertStepIntoMeta({ stepNum, stepId });
|
||||||
updateStepTitles();
|
updateStepTitles();
|
||||||
console.log(`Successfully inserted new step #${stepNum}`);
|
console.log(`Successfully inserted new step #${stepNum}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
function createEmptySteps(num: number): void {
|
async function createEmptySteps(num: number): Promise<void> {
|
||||||
if (num < 1 || num > 1000) {
|
if (num < 1 || num > 1000) {
|
||||||
throw Error(
|
throw Error(
|
||||||
`No steps created. arg 'num' must be between 1 and 1000 inclusive`
|
`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 nextStepNum = getMetaData().challengeOrder.length + 1;
|
||||||
const challengeType = [SuperBlocks.SciCompPy].includes(
|
|
||||||
getMetaData().superBlock
|
|
||||||
)
|
|
||||||
? challengeTypes.python
|
|
||||||
: challengeTypes.html;
|
|
||||||
|
|
||||||
for (let stepNum = nextStepNum; stepNum < nextStepNum + num; stepNum++) {
|
for (let stepNum = nextStepNum; stepNum < nextStepNum + num; stepNum++) {
|
||||||
const stepId = createStepFile({ stepNum, challengeType });
|
const stepId = createStepFile({ stepNum });
|
||||||
insertStepIntoMeta({ stepNum, stepId });
|
await insertStepIntoMeta({ stepNum, stepId });
|
||||||
}
|
}
|
||||||
console.log(`Successfully added ${num} steps`);
|
console.log(`Successfully added ${num} steps`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const repairMeta = async () => {
|
export { deleteStep, insertStep, createEmptySteps };
|
||||||
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 };
|
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
import { getArgValue } from './helpers/get-arg-value';
|
import { getArgValue } from './helpers/get-arg-value';
|
||||||
import { createEmptySteps } from './commands';
|
import { createEmptySteps } from './commands';
|
||||||
import { validateMetaData } from './helpers/project-metadata';
|
|
||||||
|
|
||||||
validateMetaData();
|
void createEmptySteps(getArgValue(process.argv));
|
||||||
createEmptySteps(getArgValue(process.argv));
|
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { existsSync } from 'fs';
|
|
||||||
import fs from 'fs/promises';
|
import fs from 'fs/promises';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { prompt } from 'inquirer';
|
import { prompt } from 'inquirer';
|
||||||
@@ -7,12 +6,17 @@ import ObjectID from 'bson-objectid';
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
SuperBlocks,
|
SuperBlocks,
|
||||||
languageSuperBlocks,
|
languageSuperBlocks
|
||||||
superBlockToFolderMap
|
|
||||||
} from '../../shared/config/curriculum';
|
} 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 { getBaseMeta } from './helpers/get-base-meta';
|
||||||
import { createIntroMD } from './helpers/create-intro';
|
import { createIntroMD } from './helpers/create-intro';
|
||||||
|
import { createDialogueFile, validateBlockName } from './utils';
|
||||||
|
import { updateSimpleSuperblockStructure } from './helpers/create-project';
|
||||||
|
|
||||||
const helpCategories = ['English'] as const;
|
const helpCategories = ['English'] as const;
|
||||||
|
|
||||||
@@ -46,7 +50,11 @@ async function createLanguageBlock(
|
|||||||
await updateIntroJson(superBlock, block, title);
|
await updateIntroJson(superBlock, block, title);
|
||||||
|
|
||||||
const challengeId = await createDialogueChallenge(superBlock, block);
|
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.
|
// TODO: remove once we stop relying on markdown in the client.
|
||||||
await createIntroMD(superBlock, block, title);
|
await createIntroMD(superBlock, block, title);
|
||||||
}
|
}
|
||||||
@@ -73,46 +81,34 @@ async function updateIntroJson(
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function createMetaJson(
|
async function createMetaJson(
|
||||||
superBlock: SuperBlocks,
|
|
||||||
block: string,
|
block: string,
|
||||||
title: string,
|
title: string,
|
||||||
helpCategory: string,
|
helpCategory: string,
|
||||||
challengeId: ObjectID
|
challengeId: ObjectID
|
||||||
) {
|
) {
|
||||||
const metaDir = path.resolve(__dirname, '../../curriculum/challenges/_meta');
|
|
||||||
const newMeta = getBaseMeta('Language');
|
const newMeta = getBaseMeta('Language');
|
||||||
newMeta.name = title;
|
newMeta.name = title;
|
||||||
newMeta.dashedName = block;
|
newMeta.dashedName = block;
|
||||||
newMeta.helpCategory = helpCategory;
|
newMeta.helpCategory = helpCategory;
|
||||||
newMeta.superBlock = superBlock;
|
|
||||||
newMeta.challengeOrder = [
|
newMeta.challengeOrder = [
|
||||||
// eslint-disable-next-line @typescript-eslint/no-base-to-string
|
// eslint-disable-next-line @typescript-eslint/no-base-to-string
|
||||||
{ id: challengeId.toString(), title: "Dialogue 1: I'm Tom" }
|
{ 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(
|
await writeBlockStructure(block, newMeta);
|
||||||
fs.writeFile,
|
|
||||||
path.resolve(metaDir, `${block}/meta.json`),
|
|
||||||
await format(JSON.stringify(newMeta), { parser: 'json' })
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function createDialogueChallenge(
|
async function createDialogueChallenge(
|
||||||
superBlock: SuperBlocks,
|
superBlock: SuperBlocks,
|
||||||
block: string
|
block: string
|
||||||
): Promise<ObjectID> {
|
): Promise<ObjectID> {
|
||||||
const superBlockSubPath = superBlockToFolderMap[superBlock];
|
const { blockContentDir } = getContentConfig('english') as {
|
||||||
const newChallengeDir = path.resolve(
|
blockContentDir: string;
|
||||||
__dirname,
|
};
|
||||||
`../../curriculum/challenges/english/${superBlockSubPath}/${block}`
|
|
||||||
);
|
const newChallengeDir = path.resolve(blockContentDir, block);
|
||||||
if (!existsSync(newChallengeDir)) {
|
await fs.mkdir(newChallengeDir, { recursive: true });
|
||||||
await withTrace(fs.mkdir, newChallengeDir);
|
|
||||||
}
|
|
||||||
return createDialogueFile({
|
return createDialogueFile({
|
||||||
projectPath: newChallengeDir + '/'
|
projectPath: newChallengeDir + '/'
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ const createNextChallenge = async () => {
|
|||||||
id: challengeId.toString(),
|
id: challengeId.toString(),
|
||||||
title: options.title
|
title: options.title
|
||||||
});
|
});
|
||||||
updateMetaData(meta);
|
await updateMetaData(meta);
|
||||||
};
|
};
|
||||||
|
|
||||||
void createNextChallenge();
|
void createNextChallenge();
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
import { getLastStep } from './helpers/get-last-step-file-number';
|
import { getLastStep } from './helpers/get-last-step-file-number';
|
||||||
import { insertStep } from './commands';
|
import { insertStep } from './commands';
|
||||||
import { validateMetaData } from './helpers/project-metadata';
|
|
||||||
|
|
||||||
validateMetaData();
|
void insertStep(getLastStep().stepNum + 1);
|
||||||
insertStep(getLastStep().stepNum + 1);
|
|
||||||
|
|||||||
@@ -2,11 +2,7 @@ import ObjectID from 'bson-objectid';
|
|||||||
import { getTemplate } from './helpers/get-challenge-template';
|
import { getTemplate } from './helpers/get-challenge-template';
|
||||||
import { newTaskPrompts } from './helpers/new-task-prompts';
|
import { newTaskPrompts } from './helpers/new-task-prompts';
|
||||||
import { getProjectPath } from './helpers/get-project-info';
|
import { getProjectPath } from './helpers/get-project-info';
|
||||||
import {
|
import { getMetaData, updateMetaData } from './helpers/project-metadata';
|
||||||
getMetaData,
|
|
||||||
updateMetaData,
|
|
||||||
validateMetaData
|
|
||||||
} from './helpers/project-metadata';
|
|
||||||
import {
|
import {
|
||||||
createChallengeFile,
|
createChallengeFile,
|
||||||
updateTaskMeta,
|
updateTaskMeta,
|
||||||
@@ -14,8 +10,6 @@ import {
|
|||||||
} from './utils';
|
} from './utils';
|
||||||
|
|
||||||
const createNextTask = async () => {
|
const createNextTask = async () => {
|
||||||
validateMetaData();
|
|
||||||
|
|
||||||
const { challengeType } = await newTaskPrompts();
|
const { challengeType } = await newTaskPrompts();
|
||||||
|
|
||||||
// Placeholder title, to be replaced by updateTaskMarkdownFiles
|
// Placeholder title, to be replaced by updateTaskMarkdownFiles
|
||||||
@@ -40,10 +34,10 @@ const createNextTask = async () => {
|
|||||||
id: challengeIdString,
|
id: challengeIdString,
|
||||||
title: options.title
|
title: options.title
|
||||||
});
|
});
|
||||||
updateMetaData(meta);
|
await updateMetaData(meta);
|
||||||
console.log(`Finished inserting task into 'meta.json' file.`);
|
console.log(`Finished inserting task into 'meta.json' file.`);
|
||||||
|
|
||||||
updateTaskMeta();
|
await updateTaskMeta();
|
||||||
console.log("Finished updating tasks in 'meta.json'.");
|
console.log("Finished updating tasks in 'meta.json'.");
|
||||||
|
|
||||||
updateTaskMarkdownFiles();
|
updateTaskMarkdownFiles();
|
||||||
|
|||||||
@@ -1,17 +1,19 @@
|
|||||||
import { existsSync } from 'fs';
|
|
||||||
import fs from 'fs/promises';
|
import fs from 'fs/promises';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { prompt } from 'inquirer';
|
import { prompt } from 'inquirer';
|
||||||
import { format } from 'prettier';
|
import { format } from 'prettier';
|
||||||
import ObjectID from 'bson-objectid';
|
import ObjectID from 'bson-objectid';
|
||||||
|
|
||||||
|
import { SuperBlocks } from '../../shared/config/curriculum';
|
||||||
import {
|
import {
|
||||||
SuperBlocks,
|
getContentConfig,
|
||||||
superBlockToFolderMap
|
writeBlockStructure
|
||||||
} from '../../shared/config/curriculum';
|
} from '../../curriculum/file-handler';
|
||||||
|
import { superBlockToFilename } from '../../curriculum/build-curriculum';
|
||||||
import { createStepFile, validateBlockName } from './utils';
|
import { createStepFile, validateBlockName } from './utils';
|
||||||
import { getBaseMeta } from './helpers/get-base-meta';
|
import { getBaseMeta } from './helpers/get-base-meta';
|
||||||
import { createIntroMD } from './helpers/create-intro';
|
import { createIntroMD } from './helpers/create-intro';
|
||||||
|
import { updateSimpleSuperblockStructure } from './helpers/create-project';
|
||||||
|
|
||||||
const helpCategories = [
|
const helpCategories = [
|
||||||
'HTML-CSS',
|
'HTML-CSS',
|
||||||
@@ -56,14 +58,15 @@ async function createProject(
|
|||||||
void updateIntroJson(superBlock, block, title);
|
void updateIntroJson(superBlock, block, title);
|
||||||
|
|
||||||
const challengeId = await createFirstChallenge(superBlock, block);
|
const challengeId = await createFirstChallenge(superBlock, block);
|
||||||
void createMetaJson(
|
void createMetaJson(block, title, helpCategory, challengeId);
|
||||||
superBlock,
|
const superblockFilename = (
|
||||||
block,
|
superBlockToFilename as Record<SuperBlocks, string>
|
||||||
title,
|
)[superBlock];
|
||||||
helpCategory,
|
// TODO: handle full-stack-developer (createProjects needs calling with a
|
||||||
order,
|
// chapter and module name as well)
|
||||||
challengeId
|
if (superBlock !== SuperBlocks.FullStackDeveloper) {
|
||||||
);
|
void updateSimpleSuperblockStructure(block, { order }, superblockFilename);
|
||||||
|
}
|
||||||
// TODO: remove once we stop relying on markdown in the client.
|
// TODO: remove once we stop relying on markdown in the client.
|
||||||
void createIntroMD(superBlock, block, title);
|
void createIntroMD(superBlock, block, title);
|
||||||
}
|
}
|
||||||
@@ -90,46 +93,31 @@ async function updateIntroJson(
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function createMetaJson(
|
async function createMetaJson(
|
||||||
superBlock: SuperBlocks,
|
|
||||||
block: string,
|
block: string,
|
||||||
title: string,
|
title: string,
|
||||||
helpCategory: string,
|
helpCategory: string,
|
||||||
order: number,
|
|
||||||
challengeId: ObjectID
|
challengeId: ObjectID
|
||||||
) {
|
) {
|
||||||
const metaDir = path.resolve(__dirname, '../../curriculum/challenges/_meta');
|
|
||||||
const newMeta = getBaseMeta('Step');
|
const newMeta = getBaseMeta('Step');
|
||||||
newMeta.name = title;
|
newMeta.name = title;
|
||||||
newMeta.dashedName = block;
|
newMeta.dashedName = block;
|
||||||
newMeta.helpCategory = helpCategory;
|
newMeta.helpCategory = helpCategory;
|
||||||
newMeta.order = order;
|
|
||||||
newMeta.superBlock = superBlock;
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-base-to-string
|
// eslint-disable-next-line @typescript-eslint/no-base-to-string
|
||||||
newMeta.challengeOrder = [{ id: challengeId.toString(), title: 'Step 1' }];
|
newMeta.challengeOrder = [{ id: challengeId.toString(), title: 'Step 1' }];
|
||||||
const newMetaDir = path.resolve(metaDir, block);
|
|
||||||
if (!existsSync(newMetaDir)) {
|
|
||||||
await withTrace(fs.mkdir, newMetaDir);
|
|
||||||
}
|
|
||||||
|
|
||||||
void withTrace(
|
await writeBlockStructure(block, newMeta);
|
||||||
fs.writeFile,
|
|
||||||
path.resolve(metaDir, `${block}/meta.json`),
|
|
||||||
await format(JSON.stringify(newMeta), { parser: 'json' })
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function createFirstChallenge(
|
async function createFirstChallenge(
|
||||||
superBlock: SuperBlocks,
|
superBlock: SuperBlocks,
|
||||||
block: string
|
block: string
|
||||||
): Promise<ObjectID> {
|
): Promise<ObjectID> {
|
||||||
const superBlockSubPath = superBlockToFolderMap[superBlock];
|
const { blockContentDir } = getContentConfig('english') as {
|
||||||
const newChallengeDir = path.resolve(
|
blockContentDir: string;
|
||||||
__dirname,
|
};
|
||||||
`../../curriculum/challenges/english/${superBlockSubPath}/${block}`
|
|
||||||
);
|
const newChallengeDir = path.resolve(blockContentDir, block);
|
||||||
if (!existsSync(newChallengeDir)) {
|
await fs.mkdir(newChallengeDir, { recursive: true });
|
||||||
await withTrace(fs.mkdir, newChallengeDir);
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: would be nice if the extension made sense for the challenge, but, at
|
// 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.
|
// 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 fs from 'fs/promises';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { prompt } from 'inquirer';
|
import { prompt } from 'inquirer';
|
||||||
import { format } from 'prettier';
|
import { format } from 'prettier';
|
||||||
import ObjectID from 'bson-objectid';
|
import ObjectID from 'bson-objectid';
|
||||||
|
|
||||||
|
import { SuperBlocks } from '../../shared/config/curriculum';
|
||||||
import {
|
import {
|
||||||
SuperBlocks,
|
getContentConfig,
|
||||||
superBlockToFolderMap
|
writeBlockStructure
|
||||||
} from '../../shared/config/curriculum';
|
} from '../../curriculum/file-handler';
|
||||||
|
import { superBlockToFilename } from '../../curriculum/build-curriculum';
|
||||||
import { createQuizFile, validateBlockName } from './utils';
|
import { createQuizFile, validateBlockName } from './utils';
|
||||||
import { getBaseMeta } from './helpers/get-base-meta';
|
import { getBaseMeta } from './helpers/get-base-meta';
|
||||||
import { createIntroMD } from './helpers/create-intro';
|
import { createIntroMD } from './helpers/create-intro';
|
||||||
|
import { updateSimpleSuperblockStructure } from './helpers/create-project';
|
||||||
|
|
||||||
const helpCategories = [
|
const helpCategories = [
|
||||||
'HTML-CSS',
|
'HTML-CSS',
|
||||||
@@ -57,7 +59,11 @@ async function createQuiz(
|
|||||||
title,
|
title,
|
||||||
questionCount
|
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.
|
// TODO: remove once we stop relying on markdown in the client.
|
||||||
await createIntroMD(superBlock, block, title);
|
await createIntroMD(superBlock, block, title);
|
||||||
}
|
}
|
||||||
@@ -84,30 +90,19 @@ async function updateIntroJson(
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function createMetaJson(
|
async function createMetaJson(
|
||||||
superBlock: SuperBlocks,
|
|
||||||
block: string,
|
block: string,
|
||||||
title: string,
|
title: string,
|
||||||
helpCategory: string,
|
helpCategory: string,
|
||||||
challengeId: ObjectID
|
challengeId: ObjectID
|
||||||
) {
|
) {
|
||||||
const metaDir = path.resolve(__dirname, '../../curriculum/challenges/_meta');
|
|
||||||
const newMeta = getBaseMeta('Quiz');
|
const newMeta = getBaseMeta('Quiz');
|
||||||
newMeta.name = title;
|
newMeta.name = title;
|
||||||
newMeta.dashedName = block;
|
newMeta.dashedName = block;
|
||||||
newMeta.helpCategory = helpCategory;
|
newMeta.helpCategory = helpCategory;
|
||||||
newMeta.superBlock = superBlock;
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-base-to-string
|
// eslint-disable-next-line @typescript-eslint/no-base-to-string
|
||||||
newMeta.challengeOrder = [{ id: challengeId.toString(), title: title }];
|
newMeta.challengeOrder = [{ id: challengeId.toString(), title: title }];
|
||||||
const newMetaDir = path.resolve(metaDir, block);
|
|
||||||
if (!existsSync(newMetaDir)) {
|
|
||||||
await withTrace(fs.mkdir, newMetaDir);
|
|
||||||
}
|
|
||||||
|
|
||||||
void withTrace(
|
await writeBlockStructure(block, newMeta);
|
||||||
fs.writeFile,
|
|
||||||
path.resolve(metaDir, `${block}/meta.json`),
|
|
||||||
await format(JSON.stringify(newMeta), { parser: 'json' })
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function createQuizChallenge(
|
async function createQuizChallenge(
|
||||||
@@ -116,14 +111,13 @@ async function createQuizChallenge(
|
|||||||
title: string,
|
title: string,
|
||||||
questionCount: number
|
questionCount: number
|
||||||
): Promise<ObjectID> {
|
): Promise<ObjectID> {
|
||||||
const superBlockSubPath = superBlockToFolderMap[superBlock];
|
const { blockContentDir } = getContentConfig('english') as {
|
||||||
const newChallengeDir = path.resolve(
|
blockContentDir: string;
|
||||||
__dirname,
|
};
|
||||||
`../../curriculum/challenges/english/${superBlockSubPath}/${block}`
|
|
||||||
);
|
const newChallengeDir = path.resolve(blockContentDir, block);
|
||||||
if (!existsSync(newChallengeDir)) {
|
await fs.mkdir(newChallengeDir, { recursive: true });
|
||||||
await withTrace(fs.mkdir, newChallengeDir);
|
|
||||||
}
|
|
||||||
return createQuizFile({
|
return createQuizFile({
|
||||||
projectPath: newChallengeDir + '/',
|
projectPath: newChallengeDir + '/',
|
||||||
title: title,
|
title: title,
|
||||||
|
|||||||
@@ -7,9 +7,14 @@
|
|||||||
* you want that.
|
* you want that.
|
||||||
*/
|
*/
|
||||||
import ObjectID from 'bson-objectid';
|
import ObjectID from 'bson-objectid';
|
||||||
|
|
||||||
|
import {
|
||||||
|
getBlockStructure,
|
||||||
|
writeBlockStructure
|
||||||
|
} from '../../curriculum/file-handler';
|
||||||
import { createChallengeFile } from './utils';
|
import { createChallengeFile } from './utils';
|
||||||
import { getProjectPath } from './helpers/get-project-info';
|
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
|
// eslint-disable-next-line @typescript-eslint/no-base-to-string
|
||||||
const challengeId = new ObjectID().toString();
|
const challengeId = new ObjectID().toString();
|
||||||
@@ -141,16 +146,18 @@ Watch the video
|
|||||||
|
|
||||||
const path = getProjectPath();
|
const path = getProjectPath();
|
||||||
if (
|
if (
|
||||||
!/freeCodeCamp\/curriculum\/challenges\/english\/[^/]+\/[^/]+\/$/.test(path)
|
!/freeCodeCamp\/curriculum\/challenges\/english\/blocks\/[^/]+\/$/.test(path)
|
||||||
) {
|
) {
|
||||||
throw Error(`
|
throw Error(`
|
||||||
You cannot run this script from anywhere other than a block folder of the English curriculum.
|
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.
|
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)) {
|
if (meta.challengeOrder.some(c => c.title === title)) {
|
||||||
throw Error(`
|
throw Error(`
|
||||||
A challenge with the title ${title} already exists in this block.
|
A challenge with the title ${title} already exists in this block.
|
||||||
@@ -162,8 +169,6 @@ meta.challengeOrder.push({
|
|||||||
title
|
title
|
||||||
});
|
});
|
||||||
|
|
||||||
// write the meta.json file
|
void writeBlockStructure(block, meta);
|
||||||
updateMetaData(meta);
|
|
||||||
|
|
||||||
// write the challenge file, the first argument is the filename
|
|
||||||
createChallengeFile(challengeId, template, path);
|
createChallengeFile(challengeId, template, path);
|
||||||
|
|||||||
@@ -2,13 +2,12 @@ import { unlink } from 'fs/promises';
|
|||||||
import { prompt } from 'inquirer';
|
import { prompt } from 'inquirer';
|
||||||
import { getProjectPath } from './helpers/get-project-info';
|
import { getProjectPath } from './helpers/get-project-info';
|
||||||
import { getMetaData, updateMetaData } from './helpers/project-metadata';
|
import { getMetaData, updateMetaData } from './helpers/project-metadata';
|
||||||
import { getChallengeOrderFromMeta } from './helpers/get-challenge-order';
|
|
||||||
import { getFileName } from './helpers/get-file-name';
|
import { getFileName } from './helpers/get-file-name';
|
||||||
|
|
||||||
const deleteChallenge = async () => {
|
const deleteChallenge = async () => {
|
||||||
const path = getProjectPath();
|
const path = getProjectPath();
|
||||||
|
|
||||||
const challenges = getChallengeOrderFromMeta();
|
const challenges = getMetaData().challengeOrder;
|
||||||
|
|
||||||
const challengeToDelete = (await prompt({
|
const challengeToDelete = (await prompt({
|
||||||
name: 'id',
|
name: 'id',
|
||||||
@@ -32,7 +31,7 @@ const deleteChallenge = async () => {
|
|||||||
|
|
||||||
const meta = getMetaData();
|
const meta = getMetaData();
|
||||||
meta.challengeOrder.splice(indexToDelete, 1);
|
meta.challengeOrder.splice(indexToDelete, 1);
|
||||||
updateMetaData(meta);
|
await updateMetaData(meta);
|
||||||
};
|
};
|
||||||
|
|
||||||
void deleteChallenge();
|
void deleteChallenge();
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
import { deleteStep } from './commands';
|
import { deleteStep } from './commands';
|
||||||
import { getArgValue } from './helpers/get-arg-value';
|
import { getArgValue } from './helpers/get-arg-value';
|
||||||
import { validateMetaData } from './helpers/project-metadata';
|
|
||||||
|
|
||||||
validateMetaData();
|
void deleteStep(getArgValue(process.argv));
|
||||||
deleteStep(getArgValue(process.argv));
|
|
||||||
|
|||||||
@@ -1,21 +1,18 @@
|
|||||||
import { unlink } from 'fs/promises';
|
import { unlink } from 'fs/promises';
|
||||||
import { prompt } from 'inquirer';
|
import { prompt } from 'inquirer';
|
||||||
import { getProjectPath } from './helpers/get-project-info';
|
import { getProjectPath } from './helpers/get-project-info';
|
||||||
import { getChallengeOrderFromMeta } from './helpers/get-challenge-order';
|
|
||||||
import { getFileName } from './helpers/get-file-name';
|
import { getFileName } from './helpers/get-file-name';
|
||||||
import { validateMetaData } from './helpers/project-metadata';
|
|
||||||
import {
|
import {
|
||||||
deleteChallengeFromMeta,
|
deleteChallengeFromMeta,
|
||||||
updateTaskMarkdownFiles,
|
updateTaskMarkdownFiles,
|
||||||
updateTaskMeta
|
updateTaskMeta
|
||||||
} from './utils';
|
} from './utils';
|
||||||
import { isTaskChallenge } from './helpers/task-helpers';
|
import { isTaskChallenge } from './helpers/task-helpers';
|
||||||
|
import { getMetaData } from './helpers/project-metadata';
|
||||||
|
|
||||||
const deleteTask = async () => {
|
const deleteTask = async () => {
|
||||||
validateMetaData();
|
|
||||||
|
|
||||||
const path = getProjectPath();
|
const path = getProjectPath();
|
||||||
const challenges = getChallengeOrderFromMeta();
|
const challenges = getMetaData().challengeOrder;
|
||||||
|
|
||||||
const challengeToDelete = (await prompt({
|
const challengeToDelete = (await prompt({
|
||||||
name: 'id',
|
name: 'id',
|
||||||
@@ -39,11 +36,11 @@ const deleteTask = async () => {
|
|||||||
await unlink(`${path}${fileToDelete}`);
|
await unlink(`${path}${fileToDelete}`);
|
||||||
console.log(`Finished deleting file: '${fileToDelete}'.`);
|
console.log(`Finished deleting file: '${fileToDelete}'.`);
|
||||||
|
|
||||||
deleteChallengeFromMeta(indexToDelete);
|
await deleteChallengeFromMeta(indexToDelete);
|
||||||
console.log(`Finished removing challenge from 'meta.json'.`);
|
console.log(`Finished removing challenge from 'meta.json'.`);
|
||||||
|
|
||||||
if (isTaskChallenge(challenges[indexToDelete].title)) {
|
if (isTaskChallenge(challenges[indexToDelete].title)) {
|
||||||
updateTaskMeta();
|
await updateTaskMeta();
|
||||||
console.log("Finished updating tasks in 'meta.json'.");
|
console.log("Finished updating tasks in 'meta.json'.");
|
||||||
|
|
||||||
updateTaskMarkdownFiles();
|
updateTaskMarkdownFiles();
|
||||||
|
|||||||
@@ -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']
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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: '',
|
name: '',
|
||||||
isUpcomingChange: true,
|
isUpcomingChange: true,
|
||||||
dashedName: '',
|
dashedName: '',
|
||||||
superBlock: '',
|
|
||||||
order: 42,
|
|
||||||
helpCategory: '',
|
helpCategory: '',
|
||||||
|
blockLayout: 'legacy-challenge-list',
|
||||||
challengeOrder: [
|
challengeOrder: [
|
||||||
{
|
{
|
||||||
id: '',
|
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;
|
challengeId: ObjectID;
|
||||||
challengeSeeds: Record<string, ChallengeSeed>;
|
challengeSeeds: Record<string, ChallengeSeed>;
|
||||||
stepNum: number;
|
stepNum: number;
|
||||||
challengeType: number;
|
challengeType?: number;
|
||||||
isFirstChallenge?: boolean;
|
isFirstChallenge?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -79,7 +79,7 @@ demoType: onClick`
|
|||||||
`---
|
`---
|
||||||
id: ${challengeId.toString()}
|
id: ${challengeId.toString()}
|
||||||
title: Step ${stepNum}
|
title: Step ${stepNum}
|
||||||
challengeType: ${challengeType}
|
challengeType: ${challengeType ?? 'placeholder'}
|
||||||
dashedName: step-${stepNum}${demoString}
|
dashedName: step-${stepNum}${demoString}
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -1,202 +1,18 @@
|
|||||||
import fs from 'fs';
|
|
||||||
import { join } from 'path';
|
import { join } from 'path';
|
||||||
import {
|
import { getBlockStructure } from '../../../curriculum/file-handler';
|
||||||
getMetaData,
|
import { getMetaData } from './project-metadata';
|
||||||
getProjectMetaPath,
|
|
||||||
validateMetaData
|
|
||||||
} from './project-metadata';
|
|
||||||
|
|
||||||
const basePath = join(
|
jest.mock('../../../curriculum/file-handler');
|
||||||
process.cwd(),
|
|
||||||
'__fixtures__' + process.env.JEST_WORKER_ID
|
|
||||||
);
|
|
||||||
const commonPath = join(basePath, 'curriculum', 'challenges');
|
|
||||||
|
|
||||||
const block = 'project-project-metadata';
|
const commonPath = join('curriculum', 'challenges', 'blocks');
|
||||||
const metaPath = join(commonPath, '_meta', block);
|
const block = 'block-name';
|
||||||
const superBlockPath = join(
|
|
||||||
commonPath,
|
|
||||||
'english',
|
|
||||||
'superblock-project-metadata'
|
|
||||||
);
|
|
||||||
const projectPath = join(superBlockPath, block);
|
|
||||||
|
|
||||||
describe('project-metadata helper', () => {
|
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', () => {
|
describe('getMetaData helper', () => {
|
||||||
beforeEach(() => {
|
it('should call getBlockStructure with the correct path', () => {
|
||||||
fs.writeFileSync(
|
process.env.CALLING_DIR = join(commonPath, block);
|
||||||
join(projectPath, 'step-001.md'),
|
getMetaData();
|
||||||
'Lorem ipsum...',
|
expect(getBlockStructure).toHaveBeenCalledWith(block);
|
||||||
'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 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 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 = {
|
export type Meta = {
|
||||||
name: string;
|
name: string;
|
||||||
@@ -10,65 +13,24 @@ export type Meta = {
|
|||||||
isUpcomingChange: boolean;
|
isUpcomingChange: boolean;
|
||||||
dashedName: string;
|
dashedName: string;
|
||||||
helpCategory: string;
|
helpCategory: string;
|
||||||
order: number;
|
|
||||||
time: string;
|
time: string;
|
||||||
template: string;
|
template: string;
|
||||||
required: string[];
|
required: string[];
|
||||||
superBlock: string;
|
|
||||||
challengeOrder: { id: string; title: string }[];
|
challengeOrder: { id: string; title: string }[];
|
||||||
};
|
};
|
||||||
|
|
||||||
function getMetaData(): Meta {
|
function getMetaData() {
|
||||||
const metaData = fs.readFileSync(getProjectMetaPath(), 'utf-8');
|
const block = getBlock(getProjectPath());
|
||||||
return JSON.parse(metaData) as Meta;
|
return getBlockStructure(block) as Meta;
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateMetaData(newMetaData: Record<string, unknown>): void {
|
function getBlock(filePath: string) {
|
||||||
fs.writeFileSync(getProjectMetaPath(), JSON.stringify(newMetaData, null, 2));
|
return path.basename(filePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getProjectMetaPath(): string {
|
async function updateMetaData(newMetaData: Record<string, unknown>) {
|
||||||
return path.join(
|
const block = getBlock(getProjectPath());
|
||||||
getProjectPath(),
|
await writeBlockStructure(block, newMetaData);
|
||||||
'../../..',
|
|
||||||
'_meta',
|
|
||||||
getProjectName(),
|
|
||||||
'meta.json'
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// This (and everything else) should be async, but it's fast enough
|
export { getMetaData, updateMetaData, getBlock };
|
||||||
// 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 };
|
|
||||||
|
|||||||
@@ -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]);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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 { getProjectPath } from './helpers/get-project-info';
|
||||||
import { getMetaData, updateMetaData } from './helpers/project-metadata';
|
import { getMetaData, updateMetaData } from './helpers/project-metadata';
|
||||||
import { createChallengeFile } from './utils';
|
import { createChallengeFile } from './utils';
|
||||||
import { getChallengeOrderFromMeta } from './helpers/get-challenge-order';
|
|
||||||
|
|
||||||
const insertChallenge = async () => {
|
const insertChallenge = async () => {
|
||||||
const path = getProjectPath();
|
const path = getProjectPath();
|
||||||
|
|
||||||
const options = await newChallengePrompts();
|
const options = await newChallengePrompts();
|
||||||
|
|
||||||
const challenges = getChallengeOrderFromMeta();
|
const challenges = getMetaData().challengeOrder;
|
||||||
|
|
||||||
const challengeAfter = await prompt<{ id: string }>({
|
const challengeAfter = await prompt<{ id: string }>({
|
||||||
name: 'id',
|
name: 'id',
|
||||||
@@ -38,7 +37,7 @@ const insertChallenge = async () => {
|
|||||||
id: challengeId.toString(),
|
id: challengeId.toString(),
|
||||||
title: options.title
|
title: options.title
|
||||||
});
|
});
|
||||||
updateMetaData(meta);
|
await updateMetaData(meta);
|
||||||
};
|
};
|
||||||
|
|
||||||
void insertChallenge();
|
void insertChallenge();
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
import { getArgValue } from './helpers/get-arg-value';
|
import { getArgValue } from './helpers/get-arg-value';
|
||||||
import { insertStep } from './commands';
|
import { insertStep } from './commands';
|
||||||
import { validateMetaData } from './helpers/project-metadata';
|
|
||||||
|
|
||||||
validateMetaData();
|
void insertStep(getArgValue(process.argv));
|
||||||
insertStep(getArgValue(process.argv));
|
|
||||||
|
|||||||
@@ -3,19 +3,16 @@ import { prompt } from 'inquirer';
|
|||||||
import { getTemplate } from './helpers/get-challenge-template';
|
import { getTemplate } from './helpers/get-challenge-template';
|
||||||
import { newTaskPrompts } from './helpers/new-task-prompts';
|
import { newTaskPrompts } from './helpers/new-task-prompts';
|
||||||
import { getProjectPath } from './helpers/get-project-info';
|
import { getProjectPath } from './helpers/get-project-info';
|
||||||
import { validateMetaData } from './helpers/project-metadata';
|
|
||||||
import {
|
import {
|
||||||
createChallengeFile,
|
createChallengeFile,
|
||||||
insertChallengeIntoMeta,
|
insertChallengeIntoMeta,
|
||||||
updateTaskMeta,
|
updateTaskMeta,
|
||||||
updateTaskMarkdownFiles
|
updateTaskMarkdownFiles
|
||||||
} from './utils';
|
} from './utils';
|
||||||
import { getChallengeOrderFromMeta } from './helpers/get-challenge-order';
|
import { getMetaData } from './helpers/project-metadata';
|
||||||
|
|
||||||
const insertChallenge = async () => {
|
const insertChallenge = async () => {
|
||||||
validateMetaData();
|
const challenges = getMetaData().challengeOrder;
|
||||||
|
|
||||||
const challenges = getChallengeOrderFromMeta();
|
|
||||||
const challengeAfter = await prompt<{ id: string }>({
|
const challengeAfter = await prompt<{ id: string }>({
|
||||||
name: 'id',
|
name: 'id',
|
||||||
message: 'Which challenge should come AFTER this new one?',
|
message: 'Which challenge should come AFTER this new one?',
|
||||||
@@ -50,14 +47,14 @@ const insertChallenge = async () => {
|
|||||||
createChallengeFile(challengeIdString, challengeText, path);
|
createChallengeFile(challengeIdString, challengeText, path);
|
||||||
console.log('Finished creating new task markdown file.');
|
console.log('Finished creating new task markdown file.');
|
||||||
|
|
||||||
insertChallengeIntoMeta({
|
await insertChallengeIntoMeta({
|
||||||
index: indexToInsert,
|
index: indexToInsert,
|
||||||
id: challengeId,
|
id: challengeId,
|
||||||
title: newTaskTitle
|
title: newTaskTitle
|
||||||
});
|
});
|
||||||
console.log(`Finished inserting task into 'meta.json' file.`);
|
console.log(`Finished inserting task into 'meta.json' file.`);
|
||||||
|
|
||||||
updateTaskMeta();
|
await updateTaskMeta();
|
||||||
console.log("Finished updating tasks in 'meta.json'.");
|
console.log("Finished updating tasks in 'meta.json'.");
|
||||||
|
|
||||||
updateTaskMarkdownFiles();
|
updateTaskMarkdownFiles();
|
||||||
|
|||||||
@@ -1,10 +1,7 @@
|
|||||||
import { validateMetaData } from './helpers/project-metadata';
|
|
||||||
import { updateTaskMeta, updateTaskMarkdownFiles } from './utils';
|
import { updateTaskMeta, updateTaskMarkdownFiles } from './utils';
|
||||||
|
|
||||||
const reorderTasks = () => {
|
const reorderTasks = async () => {
|
||||||
validateMetaData();
|
await updateTaskMeta();
|
||||||
|
|
||||||
updateTaskMeta();
|
|
||||||
console.log("Finished updating tasks in 'meta.json'.");
|
console.log("Finished updating tasks in 'meta.json'.");
|
||||||
|
|
||||||
updateTaskMarkdownFiles();
|
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 { prompt } from 'inquirer';
|
||||||
|
|
||||||
import { getMetaData, updateMetaData } from './helpers/project-metadata';
|
import { getMetaData, updateMetaData } from './helpers/project-metadata';
|
||||||
import { getChallengeOrderFromMeta } from './helpers/get-challenge-order';
|
|
||||||
|
|
||||||
const updateChallengeOrder = async () => {
|
const updateChallengeOrder = async () => {
|
||||||
const oldChallengeOrder = getChallengeOrderFromMeta();
|
const oldChallengeOrder = getMetaData().challengeOrder;
|
||||||
console.log('Current challenge order is: ');
|
console.log('Current challenge order is: ');
|
||||||
console.table(oldChallengeOrder.map(({ title }) => ({ title })));
|
console.table(oldChallengeOrder.map(({ title }) => ({ title })));
|
||||||
|
|
||||||
@@ -49,7 +48,7 @@ const updateChallengeOrder = async () => {
|
|||||||
|
|
||||||
const meta = getMetaData();
|
const meta = getMetaData();
|
||||||
meta.challengeOrder = newChallengeOrder;
|
meta.challengeOrder = newChallengeOrder;
|
||||||
updateMetaData(meta);
|
await updateMetaData(meta);
|
||||||
};
|
};
|
||||||
|
|
||||||
void (async () => await updateChallengeOrder())();
|
void (async () => await updateChallengeOrder())();
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
import { validateMetaData } from './helpers/project-metadata';
|
|
||||||
import { updateStepTitles } from './utils';
|
import { updateStepTitles } from './utils';
|
||||||
|
|
||||||
validateMetaData();
|
|
||||||
updateStepTitles();
|
updateStepTitles();
|
||||||
|
|||||||
@@ -1,8 +1,21 @@
|
|||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import { join } from 'path';
|
import path, { join } from 'path';
|
||||||
import ObjectID from 'bson-objectid';
|
|
||||||
import glob from 'glob';
|
|
||||||
import matter from 'gray-matter';
|
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', () => {
|
jest.mock('bson-objectid', () => {
|
||||||
return jest.fn(() => ({ toString: () => mockChallengeId }));
|
return jest.fn(() => ({ toString: () => mockChallengeId }));
|
||||||
@@ -10,10 +23,20 @@ jest.mock('bson-objectid', () => {
|
|||||||
|
|
||||||
jest.mock('./helpers/get-step-template', () => {
|
jest.mock('./helpers/get-step-template', () => {
|
||||||
return {
|
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';
|
const mockChallengeId = '60d35cf3fe32df2ce8e31b03';
|
||||||
import { getStepTemplate } from './helpers/get-step-template';
|
import { getStepTemplate } from './helpers/get-step-template';
|
||||||
import {
|
import {
|
||||||
@@ -23,37 +46,26 @@ import {
|
|||||||
updateStepTitles,
|
updateStepTitles,
|
||||||
validateBlockName
|
validateBlockName
|
||||||
} from './utils';
|
} from './utils';
|
||||||
|
import { updateMetaData } from './helpers/project-metadata';
|
||||||
|
|
||||||
const basePath = join(
|
const basePath = join(
|
||||||
process.cwd(),
|
process.cwd(),
|
||||||
'__fixtures__' + process.env.JEST_WORKER_ID
|
'__fixtures__' + process.env.JEST_WORKER_ID
|
||||||
);
|
);
|
||||||
const commonPath = join(basePath, 'curriculum', 'challenges');
|
const commonPath = join(basePath, 'curriculum');
|
||||||
|
|
||||||
const block = 'utils-project';
|
const block = 'utils-project';
|
||||||
const metaPath = join(commonPath, '_meta', block);
|
const projectPath = join(commonPath, 'challenges', 'english', 'blocks', block);
|
||||||
const superBlockPath = join(commonPath, 'english', 'utils-superblock');
|
|
||||||
const projectPath = join(superBlockPath, block);
|
|
||||||
|
|
||||||
describe('Challenge utils helper scripts', () => {
|
describe('Challenge utils helper scripts', () => {
|
||||||
beforeEach(() => {
|
afterEach(() => {
|
||||||
fs.mkdirSync(superBlockPath, { recursive: true });
|
jest.clearAllMocks();
|
||||||
fs.mkdirSync(projectPath, { recursive: true });
|
|
||||||
fs.mkdirSync(metaPath, { recursive: true });
|
|
||||||
});
|
});
|
||||||
describe('createStepFile util', () => {
|
describe('createStepFile util', () => {
|
||||||
it('should create next step and return its identifier', () => {
|
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;
|
process.env.CALLING_DIR = projectPath;
|
||||||
|
const mockTemplate = 'Mock template...';
|
||||||
|
(getStepTemplate as jest.Mock).mockReturnValue(mockTemplate);
|
||||||
const step = createStepFile({
|
const step = createStepFile({
|
||||||
stepNum: 3,
|
stepNum: 3,
|
||||||
challengeType: 0
|
challengeType: 0
|
||||||
@@ -66,15 +78,10 @@ describe('Challenge utils helper scripts', () => {
|
|||||||
// Internal tasks
|
// Internal tasks
|
||||||
// - Should generate a template for the step that is being created
|
// - Should generate a template for the step that is being created
|
||||||
expect(getStepTemplate).toHaveBeenCalledTimes(1);
|
expect(getStepTemplate).toHaveBeenCalledTimes(1);
|
||||||
|
expect(fs.writeFileSync).toHaveBeenCalledWith(
|
||||||
// - Should write a file with a given name and template
|
|
||||||
const files = glob.sync(`${projectPath}/*.md`);
|
|
||||||
|
|
||||||
expect(files).toEqual([
|
|
||||||
`${projectPath}/${mockChallengeId}.md`,
|
`${projectPath}/${mockChallengeId}.md`,
|
||||||
`${projectPath}/step-001.md`,
|
mockTemplate
|
||||||
`${projectPath}/step-002.md`
|
);
|
||||||
]);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -104,78 +111,31 @@ describe('Challenge utils helper scripts', () => {
|
|||||||
|
|
||||||
describe('createChallengeFile util', () => {
|
describe('createChallengeFile util', () => {
|
||||||
it('should create the challenge', () => {
|
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;
|
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
|
// - Should write a file with a given name and template
|
||||||
const files = glob.sync(`${projectPath}/*.md`);
|
expect(fs.writeFileSync).toHaveBeenCalledWith(
|
||||||
|
|
||||||
expect(files).toEqual([
|
|
||||||
`${projectPath}/fake-challenge.md`,
|
|
||||||
`${projectPath}/hi.md`,
|
`${projectPath}/hi.md`,
|
||||||
`${projectPath}/so-many-fakes.md`
|
template
|
||||||
]);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('insertStepIntoMeta util', () => {
|
describe('insertStepIntoMeta util', () => {
|
||||||
it('should update the meta with a new file id and name', () => {
|
it('should call updateMetaData with a new file id and name', async () => {
|
||||||
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'
|
|
||||||
);
|
|
||||||
process.env.CALLING_DIR = projectPath;
|
process.env.CALLING_DIR = projectPath;
|
||||||
|
|
||||||
insertStepIntoMeta({ stepNum: 3, stepId: new ObjectID(mockChallengeId) });
|
await insertStepIntoMeta({
|
||||||
|
stepNum: 3,
|
||||||
|
stepId: new ObjectID(mockChallengeId)
|
||||||
|
});
|
||||||
|
|
||||||
const meta = JSON.parse(
|
expect(updateMetaData).toHaveBeenCalledWith({
|
||||||
fs.readFileSync(join(metaPath, 'meta.json'), 'utf-8')
|
|
||||||
);
|
|
||||||
expect(meta).toEqual({
|
|
||||||
id: 'mock-id',
|
|
||||||
challengeOrder: [
|
challengeOrder: [
|
||||||
{
|
{ id: 'abc', title: 'Step 1' }, // title gets overwritten
|
||||||
id: 'id-1',
|
{ id: mockChallengeId, title: 'Step 2' }
|
||||||
title: 'Step 1'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'id-2',
|
|
||||||
title: 'Step 2'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: mockChallengeId,
|
|
||||||
title: 'Step 3'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'id-3',
|
|
||||||
title: 'Step 4'
|
|
||||||
}
|
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -183,76 +143,36 @@ describe('Challenge utils helper scripts', () => {
|
|||||||
|
|
||||||
describe('updateStepTitles util', () => {
|
describe('updateStepTitles util', () => {
|
||||||
it('should apply meta.challengeOrder to step files', () => {
|
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;
|
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();
|
updateStepTitles();
|
||||||
|
|
||||||
expect(matter.read(join(projectPath, 'id-1.md')).data).toEqual({
|
expect(fs.readdirSync).toHaveBeenCalledWith(projectPath + '/');
|
||||||
id: 'id-1',
|
expect(fs.writeFileSync).toHaveBeenCalledWith(
|
||||||
title: 'Step 1',
|
path.join(projectPath, 'name.md'),
|
||||||
challengeType: 'a',
|
undefined
|
||||||
dashedName: 'step-1'
|
);
|
||||||
});
|
expect(fs.writeFileSync).toHaveBeenCalledWith(
|
||||||
expect(matter.read(join(projectPath, 'id-2.md')).data).toEqual({
|
path.join(projectPath, 'another-name.md'),
|
||||||
id: 'id-2',
|
undefined
|
||||||
title: 'Step 3',
|
);
|
||||||
challengeType: 'b',
|
expect(matter.stringify).toHaveBeenCalledWith('goes here', {
|
||||||
dashedName: 'step-3'
|
dashedName: 'step-1',
|
||||||
});
|
id: 'abc',
|
||||||
expect(matter.read(join(projectPath, 'id-3.md')).data).toEqual({
|
title: 'Step 1'
|
||||||
id: 'id-3',
|
|
||||||
title: 'Step 2',
|
|
||||||
challengeType: 'c',
|
|
||||||
dashedName: 'step-2'
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
delete process.env.CALLING_DIR;
|
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 {
|
interface Options {
|
||||||
stepNum: number;
|
stepNum: number;
|
||||||
challengeType: number;
|
challengeType?: number;
|
||||||
projectPath?: string;
|
projectPath?: string;
|
||||||
challengeSeeds?: Record<string, ChallengeSeed>;
|
challengeSeeds?: Record<string, ChallengeSeed>;
|
||||||
isFirstChallenge?: boolean;
|
isFirstChallenge?: boolean;
|
||||||
@@ -112,20 +112,20 @@ interface InsertChallengeOptions {
|
|||||||
title: string;
|
title: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
function insertChallengeIntoMeta({
|
async function insertChallengeIntoMeta({
|
||||||
index,
|
index,
|
||||||
id,
|
id,
|
||||||
title
|
title
|
||||||
}: InsertChallengeOptions): void {
|
}: InsertChallengeOptions) {
|
||||||
const existingMeta = getMetaData();
|
const existingMeta = getMetaData();
|
||||||
const challengeOrder = [...existingMeta.challengeOrder];
|
const challengeOrder = [...existingMeta.challengeOrder];
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-base-to-string
|
// eslint-disable-next-line @typescript-eslint/no-base-to-string
|
||||||
challengeOrder.splice(index, 0, { id: id.toString(), title });
|
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 existingMeta = getMetaData();
|
||||||
const oldOrder = [...existingMeta.challengeOrder];
|
const oldOrder = [...existingMeta.challengeOrder];
|
||||||
// eslint-disable-next-line @typescript-eslint/no-base-to-string
|
// eslint-disable-next-line @typescript-eslint/no-base-to-string
|
||||||
@@ -136,10 +136,10 @@ function insertStepIntoMeta({ stepNum, stepId }: InsertOptions): void {
|
|||||||
title: `Step ${index + 1}`
|
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 existingMeta = getMetaData();
|
||||||
const oldOrder = [...existingMeta.challengeOrder];
|
const oldOrder = [...existingMeta.challengeOrder];
|
||||||
oldOrder.splice(stepNum - 1, 1);
|
oldOrder.splice(stepNum - 1, 1);
|
||||||
@@ -149,17 +149,17 @@ function deleteStepFromMeta({ stepNum }: { stepNum: number }): void {
|
|||||||
title: `Step ${index + 1}`
|
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 existingMeta = getMetaData();
|
||||||
const challengeOrder = [...existingMeta.challengeOrder];
|
const challengeOrder = [...existingMeta.challengeOrder];
|
||||||
challengeOrder.splice(challengeIndex, 1);
|
challengeOrder.splice(challengeIndex, 1);
|
||||||
updateMetaData({ ...existingMeta, challengeOrder });
|
await updateMetaData({ ...existingMeta, challengeOrder });
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateTaskMeta() {
|
async function updateTaskMeta() {
|
||||||
const existingMeta = getMetaData();
|
const existingMeta = getMetaData();
|
||||||
const oldOrder = [...existingMeta.challengeOrder];
|
const oldOrder = [...existingMeta.challengeOrder];
|
||||||
|
|
||||||
@@ -176,7 +176,7 @@ function updateTaskMeta() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
updateMetaData({ ...existingMeta, challengeOrder });
|
await updateMetaData({ ...existingMeta, challengeOrder });
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateStepTitles = (): void => {
|
const updateStepTitles = (): void => {
|
||||||
|
|||||||
Reference in New Issue
Block a user