mirror of
https://github.com/freeCodeCamp/freeCodeCamp.git
synced 2025-12-19 18:18:27 -05:00
refactor: migrate (some) curriculum files to TypeScript (#62228)
Co-authored-by: Huyen Nguyen <25715018+huyenltnguyen@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
0d44fff1ff
commit
4a635c1b32
2
.github/workflows/e2e-playwright.yml
vendored
2
.github/workflows/e2e-playwright.yml
vendored
@@ -155,7 +155,7 @@ jobs:
|
|||||||
- name: Install and Build
|
- name: Install and Build
|
||||||
run: |
|
run: |
|
||||||
pnpm install
|
pnpm install
|
||||||
pnpm run create:shared
|
pnpm compile:ts
|
||||||
pnpm run build:curriculum
|
pnpm run build:curriculum
|
||||||
|
|
||||||
- name: Start apps
|
- name: Start apps
|
||||||
|
|||||||
2
.github/workflows/e2e-third-party.yml
vendored
2
.github/workflows/e2e-third-party.yml
vendored
@@ -138,7 +138,7 @@ jobs:
|
|||||||
- name: Install and Build
|
- name: Install and Build
|
||||||
run: |
|
run: |
|
||||||
pnpm install
|
pnpm install
|
||||||
pnpm run create:shared
|
pnpm compile:ts
|
||||||
pnpm run build:curriculum
|
pnpm run build:curriculum
|
||||||
|
|
||||||
- name: Start apps
|
- name: Start apps
|
||||||
|
|||||||
2
.github/workflows/node.js-tests.yml
vendored
2
.github/workflows/node.js-tests.yml
vendored
@@ -68,7 +68,7 @@ jobs:
|
|||||||
- name: Lint Source Files
|
- name: Lint Source Files
|
||||||
run: |
|
run: |
|
||||||
echo pnpm version $(pnpm -v)
|
echo pnpm version $(pnpm -v)
|
||||||
pnpm run create:shared
|
pnpm compile:ts
|
||||||
pnpm run build:curriculum
|
pnpm run build:curriculum
|
||||||
pnpm run lint
|
pnpm run lint
|
||||||
|
|
||||||
|
|||||||
6
.gitignore
vendored
6
.gitignore
vendored
@@ -152,7 +152,8 @@ jspm_packages/
|
|||||||
.netlify
|
.netlify
|
||||||
|
|
||||||
### Generated config files ###
|
### Generated config files ###
|
||||||
shared/config/curriculum.json
|
shared/tsconfig.tsbuildinfo
|
||||||
|
curriculum/tsconfig.tsbuildinfo
|
||||||
|
|
||||||
### Old Generated files ###
|
### Old Generated files ###
|
||||||
# These files are no longer generated by the client, but can
|
# These files are no longer generated by the client, but can
|
||||||
@@ -195,7 +196,7 @@ curriculum/curricula.json
|
|||||||
### Additional Folders ###
|
### Additional Folders ###
|
||||||
curriculum/dist
|
curriculum/dist
|
||||||
curriculum/build
|
curriculum/build
|
||||||
curriculum/test/blocks-generated
|
curriculum/src/test/blocks-generated
|
||||||
shared-dist
|
shared-dist
|
||||||
|
|
||||||
### Playwright ###
|
### Playwright ###
|
||||||
@@ -204,3 +205,4 @@ shared-dist
|
|||||||
|
|
||||||
### Shadow Testing Log Files Folder ###
|
### Shadow Testing Log Files Folder ###
|
||||||
api/logs/
|
api/logs/
|
||||||
|
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ tasks:
|
|||||||
cp sample.env .env &&
|
cp sample.env .env &&
|
||||||
pnpm install &&
|
pnpm install &&
|
||||||
gp sync-done pnpm-install &&
|
gp sync-done pnpm-install &&
|
||||||
|
pnpm compile:ts &&
|
||||||
pnpm run build:curriculum &&
|
pnpm run build:curriculum &&
|
||||||
gp ports await 27017
|
gp ports await 27017
|
||||||
command: >
|
command: >
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { readFileSync } from 'fs';
|
|||||||
import { fileURLToPath } from 'node:url';
|
import { fileURLToPath } from 'node:url';
|
||||||
import { join, dirname } from 'path';
|
import { join, dirname } from 'path';
|
||||||
|
|
||||||
const CURRICULUM_PATH = '../../../shared/config/curriculum.json';
|
const CURRICULUM_PATH = '../../../shared-dist/config/curriculum.json';
|
||||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||||
// Curriculum is read using fs, because it is too large for VSCode's LSP to handle type inference which causes annoying behavior.
|
// Curriculum is read using fs, because it is too large for VSCode's LSP to handle type inference which causes annoying behavior.
|
||||||
const curriculum = JSON.parse(
|
const curriculum = JSON.parse(
|
||||||
|
|||||||
@@ -23,7 +23,7 @@
|
|||||||
"build": "NODE_OPTIONS=\"--max-old-space-size=7168 --no-deprecation\" gatsby build --prefix-paths",
|
"build": "NODE_OPTIONS=\"--max-old-space-size=7168 --no-deprecation\" gatsby build --prefix-paths",
|
||||||
"build:scripts": "pnpm run -F=browser-scripts build",
|
"build:scripts": "pnpm run -F=browser-scripts build",
|
||||||
"clean": "gatsby clean",
|
"clean": "gatsby clean",
|
||||||
"common-setup": "pnpm -w run create:shared && pnpm run create:env && pnpm run create:trending && pnpm run create:search-placeholder",
|
"common-setup": "pnpm -w run compile:ts && pnpm run create:env && pnpm run create:trending && pnpm run create:search-placeholder",
|
||||||
"create:env": "DEBUG=fcc:* tsx ./tools/create-env.ts",
|
"create:env": "DEBUG=fcc:* tsx ./tools/create-env.ts",
|
||||||
"create:trending": "tsx ./tools/download-trending.ts",
|
"create:trending": "tsx ./tools/download-trending.ts",
|
||||||
"create:search-placeholder": "tsx ./tools/generate-search-placeholder",
|
"create:search-placeholder": "tsx ./tools/generate-search-placeholder",
|
||||||
|
|||||||
@@ -2,20 +2,24 @@ const path = require('path');
|
|||||||
|
|
||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
|
|
||||||
const { getChallengesForLang } = require('../../curriculum/get-challenges');
|
const {
|
||||||
|
getChallengesForLang
|
||||||
|
} = require('../../curriculum/dist/get-challenges.js');
|
||||||
|
|
||||||
const {
|
const {
|
||||||
getContentDir,
|
|
||||||
getBlockCreator,
|
getBlockCreator,
|
||||||
getSuperblocks,
|
getSuperblocks,
|
||||||
superBlockToFilename
|
superBlockToFilename
|
||||||
} = require('../../curriculum/build-curriculum');
|
} = require('../../curriculum/dist/build-curriculum.js');
|
||||||
const {
|
const {
|
||||||
|
getContentDir,
|
||||||
getBlockStructure,
|
getBlockStructure,
|
||||||
getSuperblockStructure
|
getSuperblockStructure
|
||||||
} = require('../../curriculum/file-handler');
|
} = require('../../curriculum/dist/file-handler.js');
|
||||||
const { transformSuperBlock } = require('../../curriculum/build-superblock');
|
const {
|
||||||
const { getSuperOrder } = require('../../curriculum/utils');
|
transformSuperBlock
|
||||||
|
} = require('../../curriculum/dist/build-superblock.js');
|
||||||
|
const { getSuperOrder } = require('../../curriculum/dist/utils.js');
|
||||||
|
|
||||||
const curriculumLocale = process.env.CURRICULUM_LOCALE || 'english';
|
const curriculumLocale = process.env.CURRICULUM_LOCALE || 'english';
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +0,0 @@
|
|||||||
const fs = require('fs');
|
|
||||||
const yaml = require('js-yaml');
|
|
||||||
|
|
||||||
const buildCertification = filePath => ({
|
|
||||||
challenges: [yaml.load(fs.readFileSync(filePath, 'utf8'))]
|
|
||||||
});
|
|
||||||
|
|
||||||
module.exports = { buildCertification };
|
|
||||||
@@ -1,207 +0,0 @@
|
|||||||
const path = require('node:path');
|
|
||||||
const assert = require('node:assert');
|
|
||||||
const fs = require('node:fs');
|
|
||||||
const fsP = require('node:fs/promises');
|
|
||||||
|
|
||||||
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 getBlockStructureDir() {
|
|
||||||
return BLOCK_STRUCTURE_DIR;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getBlockStructure(block) {
|
|
||||||
return JSON.parse(fs.readFileSync(getBlockStructurePath(block), 'utf8'));
|
|
||||||
}
|
|
||||||
|
|
||||||
async function writeBlockStructure(block, structure) {
|
|
||||||
// dynamically importing prettier because Gatsby build and develop fail when
|
|
||||||
// it's required.
|
|
||||||
const prettier = await import('prettier');
|
|
||||||
const content = await prettier.format(JSON.stringify(structure), {
|
|
||||||
parser: 'json'
|
|
||||||
});
|
|
||||||
await fsP.writeFile(getBlockStructurePath(block), content, 'utf8');
|
|
||||||
}
|
|
||||||
|
|
||||||
async function writeSuperblockStructure(superblock, structure) {
|
|
||||||
// dynamically importing prettier because Gatsby build and develop fail when
|
|
||||||
// it's required.
|
|
||||||
const prettier = await import('prettier');
|
|
||||||
const content = await prettier.format(JSON.stringify(structure), {
|
|
||||||
parser: 'json'
|
|
||||||
});
|
|
||||||
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 } = {
|
|
||||||
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 i18n block content directory: ${i18nBlockContentDir}`);
|
|
||||||
debug(`Using dictionaries directory: ${dictionariesDir}`);
|
|
||||||
debug(`Using i18n dictionaries directory: ${i18nDictionariesDir}`);
|
|
||||||
|
|
||||||
return {
|
|
||||||
contentDir,
|
|
||||||
i18nContentDir,
|
|
||||||
blockContentDir,
|
|
||||||
i18nBlockContentDir,
|
|
||||||
dictionariesDir,
|
|
||||||
i18nDictionariesDir
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
exports.getContentConfig = getContentConfig;
|
|
||||||
exports.getContentDir = getContentDir;
|
|
||||||
exports.getBlockStructureDir = getBlockStructureDir;
|
|
||||||
exports.getBlockStructure = getBlockStructure;
|
|
||||||
exports.getBlockStructurePath = getBlockStructurePath;
|
|
||||||
exports.getSuperblockStructure = getSuperblockStructure;
|
|
||||||
exports.getCurriculumStructure = getCurriculumStructure;
|
|
||||||
exports.writeBlockStructure = writeBlockStructure;
|
|
||||||
exports.writeSuperblockStructure = writeSuperblockStructure;
|
|
||||||
exports.getLanguageConfig = getLanguageConfig;
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
const fs = require('fs');
|
|
||||||
const path = require('path');
|
|
||||||
const util = require('util');
|
|
||||||
|
|
||||||
const { curriculum: curriculumLangs } =
|
|
||||||
require('../shared-dist/config/i18n').availableLangs;
|
|
||||||
const { buildCurriculum } = require('./build-curriculum');
|
|
||||||
|
|
||||||
const access = util.promisify(fs.access);
|
|
||||||
|
|
||||||
exports.getChallengesForLang = async function getChallengesForLang(
|
|
||||||
lang,
|
|
||||||
filters
|
|
||||||
) {
|
|
||||||
const invalidLang = !curriculumLangs.includes(lang);
|
|
||||||
if (invalidLang)
|
|
||||||
throw Error(`${lang} is not an accepted language.
|
|
||||||
Accepted languages are ${curriculumLangs.join(', ')}`);
|
|
||||||
|
|
||||||
return buildCurriculum(lang, filters);
|
|
||||||
};
|
|
||||||
|
|
||||||
async function hasEnglishSource(basePath, translationPath) {
|
|
||||||
const englishRoot = path.resolve(__dirname, basePath, 'english');
|
|
||||||
return await access(
|
|
||||||
path.join(englishRoot, translationPath),
|
|
||||||
fs.constants.F_OK
|
|
||||||
)
|
|
||||||
.then(() => true)
|
|
||||||
.catch(() => false);
|
|
||||||
}
|
|
||||||
|
|
||||||
exports.hasEnglishSource = hasEnglishSource;
|
|
||||||
@@ -31,17 +31,20 @@
|
|||||||
"delete-step": "CALLING_DIR=$INIT_CWD tsx --tsconfig ../tsconfig.json ../tools/challenge-helper-scripts/delete-step",
|
"delete-step": "CALLING_DIR=$INIT_CWD tsx --tsconfig ../tsconfig.json ../tools/challenge-helper-scripts/delete-step",
|
||||||
"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 src/lint-localized",
|
||||||
"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",
|
||||||
"test-gen": "node ./test/utils/generate-block-tests.mjs",
|
"test-gen": "tsx ./src/test/utils/generate-block-tests.ts",
|
||||||
"test": "NODE_OPTIONS='--max-old-space-size=7168' pnpm -s test-gen && vitest -c test/vitest.config.mjs"
|
"test": "NODE_OPTIONS='--max-old-space-size=7168' pnpm -s test-gen && vitest -c src/test/vitest.config.mjs"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "7.23.7",
|
"@babel/core": "7.23.7",
|
||||||
"@babel/register": "7.23.7",
|
"@babel/register": "7.23.7",
|
||||||
"@types/polka": "^0.5.7",
|
"@types/polka": "^0.5.7",
|
||||||
|
"@total-typescript/ts-reset": "^0.6.1",
|
||||||
|
"@types/debug": "^4.1.12",
|
||||||
|
"@types/js-yaml": "4.0.5",
|
||||||
"@types/string-similarity": "^4.0.2",
|
"@types/string-similarity": "^4.0.2",
|
||||||
"@vitest/ui": "^3.2.4",
|
"@vitest/ui": "^3.2.4",
|
||||||
"chai": "4.4.1",
|
"chai": "4.4.1",
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import path from 'path';
|
|||||||
import { fileURLToPath } from 'url';
|
import { fileURLToPath } from 'url';
|
||||||
import { describe, test, expect } from 'vitest';
|
import { describe, test, expect } from 'vitest';
|
||||||
|
|
||||||
import { allCerts } from '../client/config/cert-and-project-map.js';
|
import { allCerts } from '../../client/config/cert-and-project-map.js';
|
||||||
import { buildCertification } from './build-certification.js';
|
import { buildCertification } from './build-certification.js';
|
||||||
|
|
||||||
const __filename = fileURLToPath(import.meta.url);
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
@@ -12,6 +12,7 @@ const __dirname = path.dirname(__filename);
|
|||||||
describe('build-certification', () => {
|
describe('build-certification', () => {
|
||||||
const certificationsDir = path.join(
|
const certificationsDir = path.join(
|
||||||
__dirname,
|
__dirname,
|
||||||
|
'..',
|
||||||
'challenges/english/certifications'
|
'challenges/english/certifications'
|
||||||
);
|
);
|
||||||
const yamlFiles = fs
|
const yamlFiles = fs
|
||||||
6
curriculum/src/build-certification.ts
Normal file
6
curriculum/src/build-certification.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import { readFileSync } from 'fs';
|
||||||
|
import { load } from 'js-yaml';
|
||||||
|
|
||||||
|
export const buildCertification = (filePath: string) => ({
|
||||||
|
challenges: [load(readFileSync(filePath, 'utf8'))]
|
||||||
|
});
|
||||||
@@ -1,22 +1,27 @@
|
|||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
import { fileURLToPath } from 'node:url';
|
|
||||||
import { describe, it, expect, vi } from 'vitest';
|
import { describe, it, expect, vi } from 'vitest';
|
||||||
|
|
||||||
|
import { SuperBlocks } from '../../shared/config/curriculum.js';
|
||||||
import {
|
import {
|
||||||
createCommentMap,
|
createCommentMap,
|
||||||
addBlockStructure,
|
addBlockStructure,
|
||||||
getSuperblocks
|
getSuperblocks,
|
||||||
|
superBlockNames
|
||||||
} from './build-curriculum.js';
|
} from './build-curriculum.js';
|
||||||
import { getCurriculumStructure } from './file-handler.js';
|
import { getCurriculumStructure } from './file-handler.js';
|
||||||
|
|
||||||
vi.mock('./file-handler');
|
vi.mock('./file-handler');
|
||||||
const __filename = fileURLToPath(import.meta.url);
|
|
||||||
const __dirname = path.dirname(__filename);
|
|
||||||
|
|
||||||
describe('createCommentMap', () => {
|
describe('createCommentMap', () => {
|
||||||
const dictionaryDir = path.resolve(__dirname, '__fixtures__', 'dictionaries');
|
const dictionaryDir = path.resolve(
|
||||||
|
import.meta.dirname,
|
||||||
|
'..',
|
||||||
|
'__fixtures__',
|
||||||
|
'dictionaries'
|
||||||
|
);
|
||||||
const incompleteDictDir = path.resolve(
|
const incompleteDictDir = path.resolve(
|
||||||
__dirname,
|
import.meta.dirname,
|
||||||
|
'..',
|
||||||
'__fixtures__',
|
'__fixtures__',
|
||||||
'incomplete-dicts'
|
'incomplete-dicts'
|
||||||
);
|
);
|
||||||
@@ -169,3 +174,13 @@ describe('getSuperblocks', () => {
|
|||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('superBlockNames', () => {
|
||||||
|
it('should have mappings for each SuperBlock', () => {
|
||||||
|
const superBlocks = Object.values(SuperBlocks).sort(); // sorting to make comparison clearer
|
||||||
|
const names = Object.values(superBlockNames).sort();
|
||||||
|
|
||||||
|
expect(names).toHaveLength(superBlocks.length);
|
||||||
|
expect(names).toEqual(expect.arrayContaining(superBlocks));
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,26 +1,36 @@
|
|||||||
const fs = require('fs');
|
import { readdirSync, readFileSync, existsSync } from 'fs';
|
||||||
const path = require('path');
|
import { resolve, basename } from 'path';
|
||||||
|
|
||||||
const { isEmpty, isUndefined } = require('lodash');
|
import { isEmpty, isUndefined } from 'lodash';
|
||||||
const debug = require('debug')('fcc:build-curriculum');
|
import debug from 'debug';
|
||||||
|
|
||||||
const {
|
import type { CommentDictionary } from '../../tools/challenge-parser/translation-parser/index.js';
|
||||||
|
import { SuperBlocks } from '../../shared-dist/config/curriculum.js';
|
||||||
|
import {
|
||||||
SuperblockCreator,
|
SuperblockCreator,
|
||||||
BlockCreator,
|
BlockCreator,
|
||||||
transformSuperBlock
|
transformSuperBlock,
|
||||||
} = require('./build-superblock');
|
BlockInfo
|
||||||
|
} from './build-superblock.js';
|
||||||
|
|
||||||
const { buildCertification } = require('./build-certification');
|
import { buildCertification } from './build-certification.js';
|
||||||
const { applyFilters, closestFilters, getSuperOrder } = require('./utils');
|
import {
|
||||||
const {
|
applyFilters,
|
||||||
|
closestFilters,
|
||||||
|
Filter,
|
||||||
|
getSuperOrder
|
||||||
|
} from './utils.js';
|
||||||
|
import {
|
||||||
getContentDir,
|
getContentDir,
|
||||||
getLanguageConfig,
|
getLanguageConfig,
|
||||||
getCurriculumStructure,
|
getCurriculumStructure,
|
||||||
getBlockStructure,
|
getBlockStructure,
|
||||||
getSuperblockStructure,
|
getSuperblockStructure,
|
||||||
getBlockStructurePath,
|
getBlockStructurePath,
|
||||||
getBlockStructureDir
|
getBlockStructureDir,
|
||||||
} = require('./file-handler');
|
type BlockStructure
|
||||||
|
} from './file-handler.js';
|
||||||
|
const log = debug('fcc:build-curriculum');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a BlockCreator instance for a specific language with appropriate configuration
|
* Creates a BlockCreator instance for a specific language with appropriate configuration
|
||||||
@@ -32,7 +42,11 @@ const {
|
|||||||
* @param {string} [opts.structureDir] - Directory containing curriculum structure
|
* @param {string} [opts.structureDir] - Directory containing curriculum structure
|
||||||
* @returns {BlockCreator} A configured BlockCreator instance
|
* @returns {BlockCreator} A configured BlockCreator instance
|
||||||
*/
|
*/
|
||||||
const getBlockCreator = (lang, skipValidation, opts) => {
|
export const getBlockCreator = (
|
||||||
|
lang: string,
|
||||||
|
skipValidation?: boolean,
|
||||||
|
opts?: { baseDir: string; i18nBaseDir: string; structureDir: string }
|
||||||
|
) => {
|
||||||
const {
|
const {
|
||||||
blockContentDir,
|
blockContentDir,
|
||||||
i18nBlockContentDir,
|
i18nBlockContentDir,
|
||||||
@@ -63,9 +77,12 @@ const getBlockCreator = (lang, skipValidation, opts) => {
|
|||||||
* @param {string} params.text - The fallback English text to use if translation not found
|
* @param {string} params.text - The fallback English text to use if translation not found
|
||||||
* @returns {Object} Object mapping language codes to translated text or fallback English text
|
* @returns {Object} Object mapping language codes to translated text or fallback English text
|
||||||
*/
|
*/
|
||||||
function getTranslationEntry(dicts, { engId, text }) {
|
export function getTranslationEntry(
|
||||||
|
dicts: Record<string, Record<string, unknown>>,
|
||||||
|
{ engId, text }: { engId: string; text: string }
|
||||||
|
) {
|
||||||
return Object.keys(dicts).reduce((acc, lang) => {
|
return Object.keys(dicts).reduce((acc, lang) => {
|
||||||
const entry = dicts[lang][engId];
|
const entry = dicts[lang]?.[engId];
|
||||||
if (entry) {
|
if (entry) {
|
||||||
return { ...acc, [lang]: entry };
|
return { ...acc, [lang]: entry };
|
||||||
} else {
|
} else {
|
||||||
@@ -81,19 +98,18 @@ function getTranslationEntry(dicts, { engId, text }) {
|
|||||||
* @param {string} targetDictionariesDir - Path to the target (i18n or english) dictionaries directory
|
* @param {string} targetDictionariesDir - Path to the target (i18n or english) dictionaries directory
|
||||||
* @returns {Object} Object mapping English comment text to translations in all languages
|
* @returns {Object} Object mapping English comment text to translations in all languages
|
||||||
*/
|
*/
|
||||||
function createCommentMap(dictionariesDir, targetDictionariesDir) {
|
export function createCommentMap(
|
||||||
debug(
|
dictionariesDir: string,
|
||||||
|
targetDictionariesDir: string
|
||||||
|
): CommentDictionary {
|
||||||
|
log(
|
||||||
`Creating comment map from ${dictionariesDir} and ${targetDictionariesDir}`
|
`Creating comment map from ${dictionariesDir} and ${targetDictionariesDir}`
|
||||||
);
|
);
|
||||||
const languages = fs.readdirSync(targetDictionariesDir);
|
const languages = readdirSync(targetDictionariesDir);
|
||||||
|
|
||||||
const dictionaries = languages.reduce((acc, lang) => {
|
const dictionaries = languages.reduce((acc, lang) => {
|
||||||
const commentsPath = path.resolve(
|
const commentsPath = resolve(targetDictionariesDir, lang, 'comments.json');
|
||||||
targetDictionariesDir,
|
const commentsData = JSON.parse(readFileSync(commentsPath, 'utf8'));
|
||||||
lang,
|
|
||||||
'comments.json'
|
|
||||||
);
|
|
||||||
const commentsData = JSON.parse(fs.readFileSync(commentsPath, 'utf8'));
|
|
||||||
return {
|
return {
|
||||||
...acc,
|
...acc,
|
||||||
[lang]: commentsData
|
[lang]: commentsData
|
||||||
@@ -101,22 +117,15 @@ function createCommentMap(dictionariesDir, targetDictionariesDir) {
|
|||||||
}, {});
|
}, {});
|
||||||
|
|
||||||
const COMMENTS_TO_TRANSLATE = JSON.parse(
|
const COMMENTS_TO_TRANSLATE = JSON.parse(
|
||||||
fs.readFileSync(
|
readFileSync(resolve(dictionariesDir, 'english', 'comments.json'), 'utf8')
|
||||||
path.resolve(dictionariesDir, 'english', 'comments.json'),
|
) as Record<string, string>;
|
||||||
'utf8'
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
const COMMENTS_TO_NOT_TRANSLATE = JSON.parse(
|
const COMMENTS_TO_NOT_TRANSLATE = JSON.parse(
|
||||||
fs.readFileSync(
|
readFileSync(
|
||||||
path.resolve(
|
resolve(dictionariesDir, 'english', 'comments-to-not-translate.json'),
|
||||||
dictionariesDir,
|
|
||||||
'english',
|
|
||||||
'comments-to-not-translate.json'
|
|
||||||
),
|
|
||||||
'utf8'
|
'utf8'
|
||||||
)
|
)
|
||||||
);
|
) as Record<string, string>;
|
||||||
|
|
||||||
// map from english comment text to translations
|
// map from english comment text to translations
|
||||||
const translatedCommentMap = Object.entries(COMMENTS_TO_TRANSLATE).reduce(
|
const translatedCommentMap = Object.entries(COMMENTS_TO_TRANSLATE).reduce(
|
||||||
@@ -126,7 +135,7 @@ function createCommentMap(dictionariesDir, targetDictionariesDir) {
|
|||||||
[text]: getTranslationEntry(dictionaries, { engId: id, text })
|
[text]: getTranslationEntry(dictionaries, { engId: id, text })
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
{}
|
{} as CommentDictionary
|
||||||
);
|
);
|
||||||
|
|
||||||
// map from english comment text to itself
|
// map from english comment text to itself
|
||||||
@@ -144,63 +153,62 @@ function createCommentMap(dictionariesDir, targetDictionariesDir) {
|
|||||||
...acc,
|
...acc,
|
||||||
[text]: englishEntry
|
[text]: englishEntry
|
||||||
};
|
};
|
||||||
}, {});
|
}, {} as CommentDictionary);
|
||||||
|
|
||||||
const allComments = { ...translatedCommentMap, ...untranslatableCommentMap };
|
const allComments = { ...translatedCommentMap, ...untranslatableCommentMap };
|
||||||
|
|
||||||
// the english entries need to be added here, because english is not in
|
// the english entries need to be added here, because english is not in
|
||||||
// languages
|
// languages
|
||||||
Object.keys(allComments).forEach(comment => {
|
Object.keys(allComments).forEach(comment => {
|
||||||
allComments[comment].english = comment;
|
allComments[comment]!.english = comment;
|
||||||
});
|
});
|
||||||
|
|
||||||
return allComments;
|
return allComments;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Map of superblock filenames to their SuperBlocks enum values
|
// Map of superblock filenames to their SuperBlocks enum values
|
||||||
const superBlockNames = {
|
export const superBlockNames = {
|
||||||
'responsive-web-design': 'responsive-web-design',
|
'responsive-web-design': SuperBlocks.RespWebDesign,
|
||||||
'javascript-algorithms-and-data-structures':
|
'javascript-algorithms-and-data-structures': SuperBlocks.JsAlgoDataStruct,
|
||||||
'javascript-algorithms-and-data-structures',
|
'front-end-development-libraries': SuperBlocks.FrontEndDevLibs,
|
||||||
'front-end-development-libraries': 'front-end-development-libraries',
|
'data-visualization': SuperBlocks.DataVis,
|
||||||
'data-visualization': 'data-visualization',
|
'back-end-development-and-apis': SuperBlocks.BackEndDevApis,
|
||||||
'back-end-development-and-apis': 'back-end-development-and-apis',
|
'quality-assurance': SuperBlocks.QualityAssurance,
|
||||||
'quality-assurance': 'quality-assurance',
|
'scientific-computing-with-python': SuperBlocks.SciCompPy,
|
||||||
'scientific-computing-with-python': 'scientific-computing-with-python',
|
'data-analysis-with-python': SuperBlocks.DataAnalysisPy,
|
||||||
'data-analysis-with-python': 'data-analysis-with-python',
|
'information-security': SuperBlocks.InfoSec,
|
||||||
'information-security': 'information-security',
|
'coding-interview-prep': SuperBlocks.CodingInterviewPrep,
|
||||||
'coding-interview-prep': 'coding-interview-prep',
|
'machine-learning-with-python': SuperBlocks.MachineLearningPy,
|
||||||
'machine-learning-with-python': 'machine-learning-with-python',
|
'relational-databases': SuperBlocks.RelationalDb,
|
||||||
'relational-databases': 'relational-database',
|
'responsive-web-design-22': SuperBlocks.RespWebDesignNew,
|
||||||
'responsive-web-design-22': '2022/responsive-web-design',
|
|
||||||
'javascript-algorithms-and-data-structures-22':
|
'javascript-algorithms-and-data-structures-22':
|
||||||
'javascript-algorithms-and-data-structures-v8',
|
SuperBlocks.JsAlgoDataStructNew,
|
||||||
'the-odin-project': 'the-odin-project',
|
'javascript-v9': SuperBlocks.JsV9,
|
||||||
'college-algebra-with-python': 'college-algebra-with-python',
|
'the-odin-project': SuperBlocks.TheOdinProject,
|
||||||
'project-euler': 'project-euler',
|
'college-algebra-with-python': SuperBlocks.CollegeAlgebraPy,
|
||||||
'foundational-c-sharp-with-microsoft': 'foundational-c-sharp-with-microsoft',
|
'project-euler': SuperBlocks.ProjectEuler,
|
||||||
'a2-english-for-developers': 'a2-english-for-developers',
|
'foundational-c-sharp-with-microsoft': SuperBlocks.FoundationalCSharp,
|
||||||
'rosetta-code': 'rosetta-code',
|
'a2-english-for-developers': SuperBlocks.A2English,
|
||||||
'python-for-everybody': 'python-for-everybody',
|
'rosetta-code': SuperBlocks.RosettaCode,
|
||||||
'b1-english-for-developers': 'b1-english-for-developers',
|
'python-for-everybody': SuperBlocks.PythonForEverybody,
|
||||||
'full-stack-developer': 'full-stack-developer',
|
'b1-english-for-developers': SuperBlocks.B1English,
|
||||||
'a1-professional-spanish': 'a1-professional-spanish',
|
'full-stack-developer': SuperBlocks.FullStackDeveloper,
|
||||||
'a2-professional-spanish': 'a2-professional-spanish',
|
'a1-professional-spanish': SuperBlocks.A1Spanish,
|
||||||
'a2-professional-chinese': 'a2-professional-chinese',
|
'a2-professional-spanish': SuperBlocks.A2Spanish,
|
||||||
'basic-html': 'basic-html',
|
'a2-professional-chinese': SuperBlocks.A2Chinese,
|
||||||
'semantic-html': 'semantic-html',
|
'basic-html': SuperBlocks.BasicHtml,
|
||||||
'a1-professional-chinese': 'a1-professional-chinese',
|
'semantic-html': SuperBlocks.SemanticHtml,
|
||||||
'dev-playground': 'dev-playground',
|
'a1-professional-chinese': SuperBlocks.A1Chinese,
|
||||||
'full-stack-open': 'full-stack-open',
|
'dev-playground': SuperBlocks.DevPlayground,
|
||||||
'responsive-web-design-v9': 'responsive-web-design-v9',
|
'full-stack-open': SuperBlocks.FullStackOpen,
|
||||||
'javascript-v9': 'javascript-v9',
|
'responsive-web-design-v9': SuperBlocks.RespWebDesignV9,
|
||||||
'front-end-development-libraries-v9': 'front-end-development-libraries-v9',
|
'front-end-development-libraries-v9': SuperBlocks.FrontEndDevLibsV9,
|
||||||
'python-v9': 'python-v9',
|
'python-v9': SuperBlocks.PythonV9,
|
||||||
'relational-databases-v9': 'relational-databases-v9',
|
'relational-databases-v9': SuperBlocks.RelationalDbV9,
|
||||||
'back-end-development-and-apis-v9': 'back-end-development-and-apis-v9'
|
'back-end-development-and-apis-v9': SuperBlocks.BackEndDevApisV9
|
||||||
};
|
};
|
||||||
|
|
||||||
const superBlockToFilename = Object.entries(superBlockNames).reduce(
|
export const superBlockToFilename = Object.entries(superBlockNames).reduce(
|
||||||
(map, entry) => {
|
(map, entry) => {
|
||||||
return { ...map, [entry[1]]: entry[0] };
|
return { ...map, [entry[1]]: entry[0] };
|
||||||
},
|
},
|
||||||
@@ -210,41 +218,44 @@ const superBlockToFilename = Object.entries(superBlockNames).reduce(
|
|||||||
/**
|
/**
|
||||||
* Builds an array of superblock structures from a curriculum object
|
* Builds an array of superblock structures from a curriculum object
|
||||||
|
|
||||||
* @param {string[]} superblocks - Array of superblock filename strings
|
* @param {string[]} superBlockFilenames - Array of superblock filename strings
|
||||||
* @returns {Array<Object>} Array of superblock structure objects with filename, name, and blocks
|
* @returns {Array<Object>} Array of superblock structure objects with filename, name, and blocks
|
||||||
* @throws {Error} When a superblock file is not found
|
* @throws {Error} When a superblock file is not found
|
||||||
*/
|
*/
|
||||||
function addSuperblockStructure(
|
export function addSuperblockStructure(
|
||||||
superblocks,
|
superBlockFilenames: string[],
|
||||||
showComingSoon = process.env.SHOW_UPCOMING_CHANGES === 'true'
|
showComingSoon = process.env.SHOW_UPCOMING_CHANGES === 'true'
|
||||||
) {
|
) {
|
||||||
debug(`Building structure for ${superblocks.length} superblocks`);
|
log(`Building structure for ${superBlockFilenames.length} superblocks`);
|
||||||
|
|
||||||
const superblockStructures = superblocks.map(superblockFilename => {
|
const superblockStructures = superBlockFilenames.map(filename => {
|
||||||
const superblockName = superBlockNames[superblockFilename];
|
const superblockName =
|
||||||
|
superBlockNames[filename as keyof typeof superBlockNames];
|
||||||
if (!superblockName) {
|
if (!superblockName) {
|
||||||
throw new Error(`Superblock name not found for ${superblockFilename}`);
|
throw new Error(`Superblock name not found for ${filename}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
name: superblockName,
|
name: superblockName,
|
||||||
blocks: transformSuperBlock(getSuperblockStructure(superblockFilename), {
|
blocks: transformSuperBlock(getSuperblockStructure(filename), {
|
||||||
showComingSoon
|
showComingSoon
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
debug(
|
log(
|
||||||
`Successfully built ${superblockStructures.length} superblock structures`
|
`Successfully built ${superblockStructures.length} superblock structures`
|
||||||
);
|
);
|
||||||
|
|
||||||
return superblockStructures;
|
return superblockStructures;
|
||||||
}
|
}
|
||||||
|
|
||||||
function addBlockStructure(
|
type ProcessedBlock = BlockInfo & BlockStructure;
|
||||||
superblocks,
|
|
||||||
|
export function addBlockStructure(
|
||||||
|
superblocks: { name: SuperBlocks; blocks: BlockInfo[] }[],
|
||||||
_getBlockStructure = getBlockStructure
|
_getBlockStructure = getBlockStructure
|
||||||
) {
|
): { name: SuperBlocks; blocks: ProcessedBlock[] }[] {
|
||||||
return superblocks.map(superblock => ({
|
return superblocks.map(superblock => ({
|
||||||
...superblock,
|
...superblock,
|
||||||
blocks: superblock.blocks.map((block, index) => ({
|
blocks: superblock.blocks.map((block, index) => ({
|
||||||
@@ -260,8 +271,8 @@ function addBlockStructure(
|
|||||||
* Returns a list of all the superblocks that contain the given block
|
* Returns a list of all the superblocks that contain the given block
|
||||||
* @param {string} block
|
* @param {string} block
|
||||||
*/
|
*/
|
||||||
function getSuperblocks(
|
export function getSuperblocks(
|
||||||
block,
|
block: string,
|
||||||
_addSuperblockStructure = addSuperblockStructure
|
_addSuperblockStructure = addSuperblockStructure
|
||||||
) {
|
) {
|
||||||
const { superblocks } = getCurriculumStructure();
|
const { superblocks } = getCurriculumStructure();
|
||||||
@@ -274,23 +285,23 @@ function getSuperblocks(
|
|||||||
.map(({ name }) => name);
|
.map(({ name }) => name);
|
||||||
}
|
}
|
||||||
|
|
||||||
function validateBlocks(superblocks, blockStructureDir) {
|
function validateBlocks(superblocks: SuperBlocks[], blockStructureDir: string) {
|
||||||
const withSuperblockStructure = addSuperblockStructure(superblocks, true);
|
const withSuperblockStructure = addSuperblockStructure(superblocks, true);
|
||||||
const blockInSuperblocks = withSuperblockStructure
|
const blockInSuperblocks = withSuperblockStructure
|
||||||
.flatMap(({ blocks }) => blocks)
|
.flatMap(({ blocks }) => blocks)
|
||||||
.map(b => b.dashedName);
|
.map(b => b.dashedName);
|
||||||
for (const block of blockInSuperblocks) {
|
for (const block of blockInSuperblocks) {
|
||||||
const blockPath = getBlockStructurePath(block);
|
const blockPath = getBlockStructurePath(block);
|
||||||
if (!fs.existsSync(blockPath)) {
|
if (!existsSync(blockPath)) {
|
||||||
throw Error(
|
throw Error(
|
||||||
`Block "${block}" is in a superblock, but has no block structure file at ${blockPath}`
|
`Block "${block}" is in a superblock, but has no block structure file at ${blockPath}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const blockStructureFiles = fs
|
const blockStructureFiles = readdirSync(blockStructureDir).map(file =>
|
||||||
.readdirSync(blockStructureDir)
|
basename(file, '.json')
|
||||||
.map(file => path.basename(file, '.json'));
|
);
|
||||||
|
|
||||||
for (const block of blockStructureFiles) {
|
for (const block of blockStructureFiles) {
|
||||||
if (!blockInSuperblocks.includes(block)) {
|
if (!blockInSuperblocks.includes(block)) {
|
||||||
@@ -301,40 +312,45 @@ function validateBlocks(superblocks, blockStructureDir) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function parseCurriculumStructure(filters) {
|
export async function parseCurriculumStructure(filter?: Filter) {
|
||||||
const curriculum = getCurriculumStructure();
|
const curriculum = getCurriculumStructure();
|
||||||
const blockStructureDir = getBlockStructureDir();
|
const blockStructureDir = getBlockStructureDir();
|
||||||
if (isEmpty(curriculum.superblocks))
|
if (isEmpty(curriculum.superblocks))
|
||||||
throw Error('No superblocks found in curriculum.json');
|
throw Error('No superblocks found in curriculum.json');
|
||||||
if (isEmpty(curriculum.certifications))
|
if (isEmpty(curriculum.certifications))
|
||||||
throw Error('No certifications found in curriculum.json');
|
throw Error('No certifications found in curriculum.json');
|
||||||
debug(`Found ${curriculum.superblocks.length} superblocks to build`);
|
log(`Found ${curriculum.superblocks.length} superblocks to build`);
|
||||||
debug(`Found ${curriculum.certifications.length} certifications to build`);
|
log(`Found ${curriculum.certifications.length} certifications to build`);
|
||||||
|
|
||||||
validateBlocks(curriculum.superblocks, blockStructureDir);
|
validateBlocks(curriculum.superblocks, blockStructureDir);
|
||||||
|
|
||||||
const superblockList = addBlockStructure(
|
const superblockList = addBlockStructure(
|
||||||
addSuperblockStructure(curriculum.superblocks)
|
addSuperblockStructure(curriculum.superblocks)
|
||||||
);
|
);
|
||||||
const refinedFilters = closestFilters(filters, superblockList);
|
const refinedFilter = closestFilters(superblockList, filter);
|
||||||
const fullSuperblockList = applyFilters(superblockList, refinedFilters);
|
const fullSuperblockList = applyFilters(superblockList, refinedFilter);
|
||||||
return {
|
return {
|
||||||
fullSuperblockList,
|
fullSuperblockList,
|
||||||
certifications: curriculum.certifications
|
certifications: curriculum.certifications
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async function buildCurriculum(lang, filters) {
|
export async function buildCurriculum(lang: string, filters?: Filter) {
|
||||||
const contentDir = getContentDir(lang);
|
const contentDir = getContentDir(lang);
|
||||||
|
|
||||||
const builder = new SuperblockCreator({
|
const builder = new SuperblockCreator(
|
||||||
blockCreator: getBlockCreator(lang, !isEmpty(filters))
|
getBlockCreator(lang, !isEmpty(filters))
|
||||||
});
|
);
|
||||||
|
|
||||||
const { fullSuperblockList, certifications } =
|
const { fullSuperblockList, certifications } =
|
||||||
await parseCurriculumStructure(filters);
|
await parseCurriculumStructure(filters);
|
||||||
|
|
||||||
const fullCurriculum = { certifications: { blocks: {} } };
|
const fullCurriculum: {
|
||||||
|
[key: string]: unknown;
|
||||||
|
certifications: { blocks: { [key: string]: unknown } };
|
||||||
|
} = {
|
||||||
|
certifications: { blocks: {} }
|
||||||
|
};
|
||||||
|
|
||||||
const liveSuperblocks = fullSuperblockList.filter(({ name }) => {
|
const liveSuperblocks = fullSuperblockList.filter(({ name }) => {
|
||||||
const superOrder = getSuperOrder(name);
|
const superOrder = getSuperOrder(name);
|
||||||
@@ -353,27 +369,13 @@ async function buildCurriculum(lang, filters) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (const cert of certifications) {
|
for (const cert of certifications) {
|
||||||
const certPath = path.resolve(contentDir, 'certifications', `${cert}.yml`);
|
const certPath = resolve(contentDir, 'certifications', `${cert}.yml`);
|
||||||
if (!fs.existsSync(certPath)) {
|
if (!existsSync(certPath)) {
|
||||||
throw Error(`Certification file not found: ${certPath}`);
|
throw Error(`Certification file not found: ${certPath}`);
|
||||||
}
|
}
|
||||||
debug(`=== Processing certification ${cert} ===`);
|
log(`=== Processing certification ${cert} ===`);
|
||||||
fullCurriculum.certifications.blocks[cert] = buildCertification(certPath);
|
fullCurriculum.certifications.blocks[cert] = buildCertification(certPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
return fullCurriculum;
|
return fullCurriculum;
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
addBlockStructure,
|
|
||||||
buildCurriculum,
|
|
||||||
getContentDir,
|
|
||||||
getBlockCreator,
|
|
||||||
getBlockStructure,
|
|
||||||
getSuperblockStructure,
|
|
||||||
createCommentMap,
|
|
||||||
superBlockToFilename,
|
|
||||||
getSuperblocks,
|
|
||||||
addSuperblockStructure,
|
|
||||||
parseCurriculumStructure
|
|
||||||
};
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { describe, test, expect, vi } from 'vitest';
|
import { describe, test, expect, vi } from 'vitest';
|
||||||
import { isPoly } from '../shared-dist/utils/polyvinyl.js';
|
import { isPoly } from '../../shared-dist/utils/polyvinyl.js';
|
||||||
import {
|
import {
|
||||||
validateChallenges,
|
validateChallenges,
|
||||||
buildBlock,
|
buildBlock,
|
||||||
@@ -565,9 +565,7 @@ describe('SuperblockCreator class', () => {
|
|||||||
{ dashedName: 'block-3' }
|
{ dashedName: 'block-3' }
|
||||||
];
|
];
|
||||||
|
|
||||||
const parser = new SuperblockCreator({
|
const parser = new SuperblockCreator(mockBlockCreator);
|
||||||
blockCreator: mockBlockCreator
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await parser.processSuperblock({
|
const result = await parser.processSuperblock({
|
||||||
blocks,
|
blocks,
|
||||||
@@ -1,37 +1,58 @@
|
|||||||
const fs = require('fs');
|
import { existsSync, readdirSync } from 'fs';
|
||||||
const path = require('path');
|
import { resolve } from 'path';
|
||||||
const { isEmpty } = require('lodash');
|
import { isEmpty } from 'lodash';
|
||||||
const debug = require('debug')('fcc:build-superblock');
|
import debug from 'debug';
|
||||||
|
|
||||||
const { parseMD } = require('../tools/challenge-parser/parser');
|
import { parseMD } from '../../tools/challenge-parser/parser';
|
||||||
const { createPoly } = require('../shared-dist/utils/polyvinyl');
|
import { createPoly } from '../../shared-dist/utils/polyvinyl';
|
||||||
const { isAuditedSuperBlock } = require('../shared-dist/utils/is-audited');
|
import { isAuditedSuperBlock } from '../../shared-dist/utils/is-audited';
|
||||||
const {
|
import {
|
||||||
|
CommentDictionary,
|
||||||
translateCommentsInChallenge
|
translateCommentsInChallenge
|
||||||
} = require('../tools/challenge-parser/translation-parser');
|
} from '../../tools/challenge-parser/translation-parser';
|
||||||
const { getSuperOrder } = require('./utils');
|
import { SuperBlocks } from '../../shared-dist/config/curriculum';
|
||||||
|
import type { Chapter } from '../../shared-dist/config/chapters';
|
||||||
|
import { Certification } from '../../shared-dist/config/certification-settings';
|
||||||
|
import { getSuperOrder } from './utils.js';
|
||||||
|
import type {
|
||||||
|
BlockStructure,
|
||||||
|
Challenge,
|
||||||
|
ChallengeFile
|
||||||
|
} from './file-handler.js';
|
||||||
|
|
||||||
const duplicates = xs => xs.filter((x, i) => xs.indexOf(x) !== i);
|
const log = debug('fcc:build-superblock');
|
||||||
|
|
||||||
const createValidator = throwOnError => fn => {
|
const duplicates = <T>(xs: T[]) => xs.filter((x, i) => xs.indexOf(x) !== i);
|
||||||
|
|
||||||
|
const createValidator = (throwOnError?: boolean) => (fn: () => void) => {
|
||||||
try {
|
try {
|
||||||
fn();
|
fn();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (throwOnError) {
|
if (throwOnError) {
|
||||||
throw error;
|
throw error;
|
||||||
} else {
|
} else {
|
||||||
console.error(error.message);
|
console.error((error as Error).message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
interface Meta extends BlockStructure {
|
||||||
|
order: number;
|
||||||
|
superBlock: SuperBlocks;
|
||||||
|
superOrder: number;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validates challenges against meta.json challengeOrder
|
* Validates challenges against meta.json challengeOrder
|
||||||
* @param {Array<object>} foundChallenges - Array of challenge objects
|
* @param {Array<object>} foundChallenges - Array of challenge objects
|
||||||
* @param {object} meta - Meta object with challengeOrder array
|
* @param {object} meta - Meta object with challengeOrder array
|
||||||
* @throws {Error} If validation fails (missing challenges, duplicates, etc.)
|
* @throws {Error} If validation fails (missing challenges, duplicates, etc.)
|
||||||
*/
|
*/
|
||||||
function validateChallenges(foundChallenges, meta, throwOnError) {
|
export function validateChallenges(
|
||||||
|
foundChallenges: Challenge[],
|
||||||
|
meta: { challengeOrder: Challenge[]; dashedName: string },
|
||||||
|
throwOnError?: boolean
|
||||||
|
) {
|
||||||
const metaChallengeIds = new Set(meta.challengeOrder.map(c => c.id));
|
const metaChallengeIds = new Set(meta.challengeOrder.map(c => c.id));
|
||||||
const foundChallengeIds = new Set(foundChallenges.map(c => c.id));
|
const foundChallengeIds = new Set(foundChallenges.map(c => c.id));
|
||||||
|
|
||||||
@@ -98,7 +119,7 @@ function validateChallenges(foundChallenges, meta, throwOnError) {
|
|||||||
* @param {object} meta - Meta object with name, dashedName, and challengeOrder
|
* @param {object} meta - Meta object with name, dashedName, and challengeOrder
|
||||||
* @returns {object} Block object with ordered challenges
|
* @returns {object} Block object with ordered challenges
|
||||||
*/
|
*/
|
||||||
function buildBlock(foundChallenges, meta) {
|
export function buildBlock(foundChallenges: Challenge[], meta: Meta) {
|
||||||
const challenges = meta.challengeOrder.map(challengeInfo => {
|
const challenges = meta.challengeOrder.map(challengeInfo => {
|
||||||
const challenge = foundChallenges.find(c => c.id === challengeInfo.id);
|
const challenge = foundChallenges.find(c => c.id === challengeInfo.id);
|
||||||
if (!challenge) {
|
if (!challenge) {
|
||||||
@@ -122,7 +143,10 @@ function buildBlock(foundChallenges, meta) {
|
|||||||
* @param {object} meta - The meta information object
|
* @param {object} meta - The meta information object
|
||||||
* @returns {object} The challenge object with added meta information
|
* @returns {object} The challenge object with added meta information
|
||||||
*/
|
*/
|
||||||
function addMetaToChallenge(challenge, meta) {
|
export function addMetaToChallenge(
|
||||||
|
challenge: Partial<Challenge>,
|
||||||
|
meta: Meta
|
||||||
|
): Challenge {
|
||||||
const challengeOrderIndex = meta.challengeOrder.findIndex(
|
const challengeOrderIndex = meta.challengeOrder.findIndex(
|
||||||
({ id }) => id === challenge.id
|
({ id }) => id === challenge.id
|
||||||
);
|
);
|
||||||
@@ -168,9 +192,22 @@ function addMetaToChallenge(challenge, meta) {
|
|||||||
const hasDupe = dupeCertifications.find(
|
const hasDupe = dupeCertifications.find(
|
||||||
cert => cert.dupe === meta.superBlock
|
cert => cert.dupe === meta.superBlock
|
||||||
);
|
);
|
||||||
challenge.certification = hasDupe ? hasDupe.certification : meta.superBlock;
|
|
||||||
|
|
||||||
return challenge;
|
const maybeCert = (
|
||||||
|
hasDupe ? hasDupe.certification : meta.superBlock
|
||||||
|
) as Certification;
|
||||||
|
|
||||||
|
challenge.certification = maybeCert;
|
||||||
|
// TODO: reimplement after updating the client to expect Certification | null
|
||||||
|
// if (isCertification(maybeCert)) {
|
||||||
|
// challenge.certification = maybeCert;
|
||||||
|
// } else {
|
||||||
|
// throw Error(
|
||||||
|
// `Superblock ${meta.superBlock} does not map to a certification`
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
|
||||||
|
return challenge as Challenge;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -178,7 +215,7 @@ function addMetaToChallenge(challenge, meta) {
|
|||||||
* @param {Array<object>} files - Array of challenge file objects
|
* @param {Array<object>} files - Array of challenge file objects
|
||||||
* @returns {Array<object>} Array of polyvinyl objects with seed property
|
* @returns {Array<object>} Array of polyvinyl objects with seed property
|
||||||
*/
|
*/
|
||||||
function challengeFilesToPolys(files) {
|
export function challengeFilesToPolys(files: ChallengeFile[]) {
|
||||||
return files.reduce((challengeFiles, challengeFile) => {
|
return files.reduce((challengeFiles, challengeFile) => {
|
||||||
return [
|
return [
|
||||||
...challengeFiles,
|
...challengeFiles,
|
||||||
@@ -187,7 +224,7 @@ function challengeFilesToPolys(files) {
|
|||||||
seed: challengeFile.contents.slice(0)
|
seed: challengeFile.contents.slice(0)
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
}, []);
|
}, [] as ChallengeFile[]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -195,7 +232,7 @@ function challengeFilesToPolys(files) {
|
|||||||
* @param {object} challenge - The challenge object to fix
|
* @param {object} challenge - The challenge object to fix
|
||||||
* @returns {object} The challenge object with fixed properties
|
* @returns {object} The challenge object with fixed properties
|
||||||
*/
|
*/
|
||||||
function fixChallengeProperties(challenge) {
|
export function fixChallengeProperties(challenge: Challenge) {
|
||||||
const fixedChallenge = {
|
const fixedChallenge = {
|
||||||
...challenge
|
...challenge
|
||||||
};
|
};
|
||||||
@@ -219,10 +256,10 @@ function fixChallengeProperties(challenge) {
|
|||||||
* @param {object} meta - The meta information object
|
* @param {object} meta - The meta information object
|
||||||
* @returns {object} The finalized challenge object
|
* @returns {object} The finalized challenge object
|
||||||
*/
|
*/
|
||||||
function finalizeChallenge(challenge, meta) {
|
export function finalizeChallenge(challenge: Challenge, meta: Meta) {
|
||||||
return addMetaToChallenge(fixChallengeProperties(challenge), meta);
|
return addMetaToChallenge(fixChallengeProperties(challenge), meta);
|
||||||
}
|
}
|
||||||
class BlockCreator {
|
export 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
|
||||||
@@ -234,12 +271,25 @@ class BlockCreator {
|
|||||||
* This class is responsible for reading block directories, parsing challenges, and validating them
|
* This class is responsible for reading block directories, parsing challenges, and validating them
|
||||||
* against the meta information.
|
* against the meta information.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
blockContentDir: string;
|
||||||
|
i18nBlockContentDir: string;
|
||||||
|
lang: string;
|
||||||
|
commentTranslations: CommentDictionary;
|
||||||
|
skipValidation: boolean | undefined;
|
||||||
|
|
||||||
constructor({
|
constructor({
|
||||||
blockContentDir,
|
blockContentDir,
|
||||||
i18nBlockContentDir,
|
i18nBlockContentDir,
|
||||||
lang,
|
lang,
|
||||||
commentTranslations,
|
commentTranslations,
|
||||||
skipValidation
|
skipValidation
|
||||||
|
}: {
|
||||||
|
blockContentDir: string;
|
||||||
|
i18nBlockContentDir: string;
|
||||||
|
lang: string;
|
||||||
|
commentTranslations: CommentDictionary;
|
||||||
|
skipValidation?: boolean;
|
||||||
}) {
|
}) {
|
||||||
this.blockContentDir = blockContentDir;
|
this.blockContentDir = blockContentDir;
|
||||||
this.i18nBlockContentDir = i18nBlockContentDir;
|
this.i18nBlockContentDir = i18nBlockContentDir;
|
||||||
@@ -259,18 +309,22 @@ class BlockCreator {
|
|||||||
* @returns {Promise<object>} The finalized challenge object
|
* @returns {Promise<object>} The finalized challenge object
|
||||||
*/
|
*/
|
||||||
async createChallenge(
|
async createChallenge(
|
||||||
{ filename, block, meta, isAudited },
|
{
|
||||||
|
filename,
|
||||||
|
block,
|
||||||
|
meta,
|
||||||
|
isAudited
|
||||||
|
}: { filename: string; block: string; meta: Meta; isAudited: boolean },
|
||||||
parser = parseMD
|
parser = parseMD
|
||||||
) {
|
) {
|
||||||
debug(
|
log(
|
||||||
`Creating challenge from file: ${filename} in block: ${block}, using lang: ${this.lang}`
|
`Creating challenge from file: ${filename} in block: ${block}, using lang: ${this.lang}`
|
||||||
);
|
);
|
||||||
|
|
||||||
const englishPath = path.resolve(this.blockContentDir, block, filename);
|
const englishPath = resolve(this.blockContentDir, block, filename);
|
||||||
const i18nPath = path.resolve(this.i18nBlockContentDir, block, filename);
|
const i18nPath = resolve(this.i18nBlockContentDir, block, filename);
|
||||||
|
|
||||||
const langUsed =
|
const langUsed = isAudited && existsSync(i18nPath) ? this.lang : 'english';
|
||||||
isAudited && fs.existsSync(i18nPath) ? this.lang : 'english';
|
|
||||||
|
|
||||||
const challengePath = langUsed === 'english' ? englishPath : i18nPath;
|
const challengePath = langUsed === 'english' ? englishPath : i18nPath;
|
||||||
|
|
||||||
@@ -292,11 +346,11 @@ class BlockCreator {
|
|||||||
* @param {boolean} isAudited - Whether the block is audited for i18n
|
* @param {boolean} isAudited - Whether the block is audited for i18n
|
||||||
* @returns {Promise<Array<object>>} Array of challenge objects
|
* @returns {Promise<Array<object>>} Array of challenge objects
|
||||||
*/
|
*/
|
||||||
async readBlockChallenges(block, meta, isAudited) {
|
async readBlockChallenges(block: string, meta: Meta, isAudited: boolean) {
|
||||||
const blockDir = path.resolve(this.blockContentDir, block);
|
const blockDir = resolve(this.blockContentDir, block);
|
||||||
const challengeFiles = fs
|
const challengeFiles = readdirSync(blockDir).filter(file =>
|
||||||
.readdirSync(blockDir)
|
file.endsWith('.md')
|
||||||
.filter(file => file.endsWith('.md'));
|
);
|
||||||
|
|
||||||
return await Promise.all(
|
return await Promise.all(
|
||||||
challengeFiles.map(filename =>
|
challengeFiles.map(filename =>
|
||||||
@@ -305,13 +359,16 @@ class BlockCreator {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async processBlock(block, { superBlock, order }) {
|
async processBlock(
|
||||||
|
block: BlockStructure,
|
||||||
|
{ superBlock, order }: { superBlock: SuperBlocks; order: number }
|
||||||
|
) {
|
||||||
const blockName = block.dashedName;
|
const blockName = block.dashedName;
|
||||||
debug(`Processing block ${blockName} in superblock ${superBlock}`);
|
log(`Processing block ${blockName} in superblock ${superBlock}`);
|
||||||
|
|
||||||
// Check if block directory exists
|
// Check if block directory exists
|
||||||
const blockContentDir = path.resolve(this.blockContentDir, blockName);
|
const blockContentDir = resolve(this.blockContentDir, blockName);
|
||||||
if (!fs.existsSync(blockContentDir)) {
|
if (!existsSync(blockContentDir)) {
|
||||||
throw Error(`Block directory not found: ${blockContentDir}`);
|
throw Error(`Block directory not found: ${blockContentDir}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -319,11 +376,13 @@ class BlockCreator {
|
|||||||
block.isUpcomingChange &&
|
block.isUpcomingChange &&
|
||||||
process.env.SHOW_UPCOMING_CHANGES !== 'true'
|
process.env.SHOW_UPCOMING_CHANGES !== 'true'
|
||||||
) {
|
) {
|
||||||
debug(`Ignoring upcoming block ${blockName}`);
|
log(`Ignoring upcoming block ${blockName}`);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const superOrder = getSuperOrder(superBlock);
|
const superOrder = getSuperOrder(superBlock);
|
||||||
|
if (superOrder === undefined)
|
||||||
|
throw Error(`Superblock not found: ${superBlock}`);
|
||||||
const meta = {
|
const meta = {
|
||||||
...block,
|
...block,
|
||||||
superOrder,
|
superOrder,
|
||||||
@@ -332,7 +391,7 @@ class BlockCreator {
|
|||||||
...(block.chapter && { chapter: block.chapter }),
|
...(block.chapter && { chapter: block.chapter }),
|
||||||
...(block.module && { module: block.module })
|
...(block.module && { module: block.module })
|
||||||
};
|
};
|
||||||
const isAudited = isAuditedSuperBlock(this.lang, superBlock);
|
const isAudited = isAuditedSuperBlock(this.lang, superBlock as SuperBlocks);
|
||||||
|
|
||||||
// Read challenges from directory
|
// Read challenges from directory
|
||||||
const foundChallenges = await this.readBlockChallenges(
|
const foundChallenges = await this.readBlockChallenges(
|
||||||
@@ -340,11 +399,11 @@ class BlockCreator {
|
|||||||
meta,
|
meta,
|
||||||
isAudited
|
isAudited
|
||||||
);
|
);
|
||||||
debug(`Found ${foundChallenges.length} challenge files in directory`);
|
log(`Found ${foundChallenges.length} challenge files in directory`);
|
||||||
|
|
||||||
// Log found challenges
|
// Log found challenges
|
||||||
foundChallenges.forEach(challenge => {
|
foundChallenges.forEach(challenge => {
|
||||||
debug(`Found challenge: ${challenge.title} (${challenge.id})`);
|
log(`Found challenge: ${challenge.title} (${challenge.id})`);
|
||||||
});
|
});
|
||||||
|
|
||||||
const throwOnError = this.lang === 'english';
|
const throwOnError = this.lang === 'english';
|
||||||
@@ -355,7 +414,7 @@ class BlockCreator {
|
|||||||
// Build the block object
|
// Build the block object
|
||||||
const blockResult = buildBlock(foundChallenges, meta);
|
const blockResult = buildBlock(foundChallenges, meta);
|
||||||
|
|
||||||
debug(
|
log(
|
||||||
`Completed block "${meta.name}" with ${blockResult.challenges.length} challenges (${blockResult.challenges.filter(c => !c.missing).length} built successfully)`
|
`Completed block "${meta.name}" with ${blockResult.challenges.length} challenges (${blockResult.challenges.filter(c => !c.missing).length} built successfully)`
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -363,20 +422,29 @@ class BlockCreator {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class SuperblockCreator {
|
export class SuperblockCreator {
|
||||||
/**
|
/**
|
||||||
* @param {object} options - Options object
|
* @param {object} options - Options object
|
||||||
* @param {BlockCreator} options.blockCreator - Instance of BlockCreator
|
* @param {BlockCreator} options.blockCreator - Instance of BlockCreator
|
||||||
*/
|
*/
|
||||||
constructor({ blockCreator }) {
|
|
||||||
|
blockCreator: BlockCreator;
|
||||||
|
|
||||||
|
constructor(blockCreator: BlockCreator) {
|
||||||
this.blockCreator = blockCreator;
|
this.blockCreator = blockCreator;
|
||||||
}
|
}
|
||||||
|
|
||||||
async processSuperblock({ blocks, name }) {
|
async processSuperblock({
|
||||||
const superBlock = { blocks: {} };
|
blocks,
|
||||||
|
name
|
||||||
|
}: {
|
||||||
|
blocks: BlockStructure[];
|
||||||
|
name: SuperBlocks;
|
||||||
|
}) {
|
||||||
|
const superBlock: { blocks: Record<string, unknown> } = { blocks: {} };
|
||||||
|
|
||||||
for (let i = 0; i < blocks.length; i++) {
|
for (let i = 0; i < blocks.length; i++) {
|
||||||
const block = blocks[i];
|
const block: BlockStructure = blocks[i]!;
|
||||||
const blockResult = await this.blockCreator.processBlock(block, {
|
const blockResult = await this.blockCreator.processBlock(block, {
|
||||||
superBlock: name,
|
superBlock: name,
|
||||||
order: i
|
order: i
|
||||||
@@ -386,23 +454,32 @@ class SuperblockCreator {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
debug(
|
log(
|
||||||
`Completed parsing superblock. Total blocks: ${Object.keys(superBlock.blocks).length}`
|
`Completed parsing superblock. Total blocks: ${Object.keys(superBlock.blocks).length}`
|
||||||
);
|
);
|
||||||
return superBlock;
|
return superBlock;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type BlockInfo = {
|
||||||
|
dashedName: string;
|
||||||
|
chapter?: string;
|
||||||
|
module?: string;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Transforms superblock data to extract blocks array
|
* Transforms superblock data to extract blocks array
|
||||||
* @param {object} superblockData - The superblock data object
|
* @param {object} superblockData - The superblock data object
|
||||||
* @returns {object[]} Array of block objects with dashedName, chapter, and module properties
|
* @returns {object[]} Array of block objects with dashedName, chapter, and module properties
|
||||||
*/
|
*/
|
||||||
function transformSuperBlock(
|
export function transformSuperBlock(
|
||||||
superblockData,
|
superblockData: {
|
||||||
|
blocks?: string[];
|
||||||
|
chapters?: Chapter[];
|
||||||
|
},
|
||||||
{ showComingSoon } = { showComingSoon: false }
|
{ showComingSoon } = { showComingSoon: false }
|
||||||
) {
|
) {
|
||||||
let blocks = [];
|
let blocks: BlockInfo[] = [];
|
||||||
|
|
||||||
// Handle simple blocks array format
|
// Handle simple blocks array format
|
||||||
if (superblockData.blocks) {
|
if (superblockData.blocks) {
|
||||||
@@ -442,17 +519,6 @@ function transformSuperBlock(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const blockNames = blocks.map(block => block.dashedName);
|
const blockNames = blocks.map(block => block.dashedName);
|
||||||
debug(`Found ${blocks.length} blocks: ${blockNames.join(', ')}`);
|
log(`Found ${blocks.length} blocks: ${blockNames.join(', ')}`);
|
||||||
return blocks;
|
return blocks;
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
SuperblockCreator,
|
|
||||||
BlockCreator,
|
|
||||||
addMetaToChallenge,
|
|
||||||
validateChallenges,
|
|
||||||
buildBlock,
|
|
||||||
finalizeChallenge,
|
|
||||||
transformSuperBlock,
|
|
||||||
fixChallengeProperties
|
|
||||||
};
|
|
||||||
276
curriculum/src/file-handler.ts
Normal file
276
curriculum/src/file-handler.ts
Normal file
@@ -0,0 +1,276 @@
|
|||||||
|
import { dirname, resolve } from 'node:path';
|
||||||
|
import assert from 'node:assert';
|
||||||
|
import { existsSync, readFileSync } from 'node:fs';
|
||||||
|
import { writeFile } from 'node:fs/promises';
|
||||||
|
import { fileURLToPath } from 'node:url';
|
||||||
|
import debug from 'debug';
|
||||||
|
|
||||||
|
import type { Chapter } from '../../shared-dist/config/chapters.js';
|
||||||
|
import type { SuperBlocks } from '../../shared-dist/config/curriculum.js';
|
||||||
|
import type { Certification } from '../../shared-dist/config/certification-settings.js';
|
||||||
|
|
||||||
|
const log = debug('fcc:file-handler');
|
||||||
|
|
||||||
|
let __dirnameCompat: string;
|
||||||
|
|
||||||
|
if (typeof __dirname !== 'undefined') {
|
||||||
|
// CJS
|
||||||
|
__dirnameCompat = __dirname;
|
||||||
|
} else {
|
||||||
|
// ESM – wrap in Function so CJS parsers don't see it
|
||||||
|
const metaUrl = new Function('return import.meta.url')() as string;
|
||||||
|
__dirnameCompat = dirname(fileURLToPath(metaUrl));
|
||||||
|
}
|
||||||
|
|
||||||
|
const CURRICULUM_DIR = resolve(__dirnameCompat, '..');
|
||||||
|
const I18N_CURRICULUM_DIR = resolve(
|
||||||
|
CURRICULUM_DIR,
|
||||||
|
'i18n-curriculum',
|
||||||
|
'curriculum'
|
||||||
|
);
|
||||||
|
const STRUCTURE_DIR = resolve(CURRICULUM_DIR, 'structure');
|
||||||
|
const BLOCK_STRUCTURE_DIR = 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
|
||||||
|
*/
|
||||||
|
export function getContentConfig(
|
||||||
|
lang: string,
|
||||||
|
{ baseDir, i18nBaseDir } = {
|
||||||
|
baseDir: CURRICULUM_DIR,
|
||||||
|
i18nBaseDir: I18N_CURRICULUM_DIR
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
const contentDir = resolve(baseDir, 'challenges', 'english');
|
||||||
|
const i18nContentDir = resolve(i18nBaseDir, 'challenges', lang);
|
||||||
|
const blockContentDir = resolve(contentDir, 'blocks');
|
||||||
|
const i18nBlockContentDir = resolve(i18nContentDir, 'blocks');
|
||||||
|
const dictionariesDir = resolve(baseDir, 'dictionaries');
|
||||||
|
const i18nDictionariesDir = resolve(i18nBaseDir, 'dictionaries');
|
||||||
|
|
||||||
|
if (lang !== 'english') {
|
||||||
|
assert(
|
||||||
|
existsSync(i18nContentDir),
|
||||||
|
`i18n content directory does not exist: ${i18nContentDir}`
|
||||||
|
);
|
||||||
|
assert(
|
||||||
|
existsSync(i18nBlockContentDir),
|
||||||
|
`i18n block content directory does not exist: ${i18nBlockContentDir}`
|
||||||
|
);
|
||||||
|
assert(
|
||||||
|
existsSync(i18nDictionariesDir),
|
||||||
|
`i18n dictionaries directory does not exist: ${i18nDictionariesDir}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
log(`Using content directory: ${contentDir}`);
|
||||||
|
log(`Using i18n content directory: ${i18nContentDir}`);
|
||||||
|
log(`Using block content directory: ${blockContentDir}`);
|
||||||
|
log(`Using i18n block content directory: ${i18nBlockContentDir}`);
|
||||||
|
log(`Using dictionaries directory: ${dictionariesDir}`);
|
||||||
|
log(`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
|
||||||
|
*/
|
||||||
|
export function getContentDir(lang: string) {
|
||||||
|
const { contentDir, i18nContentDir } = getContentConfig(lang);
|
||||||
|
|
||||||
|
return lang === 'english' ? contentDir : i18nContentDir;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getCurriculumStructure() {
|
||||||
|
const curriculumPath = resolve(STRUCTURE_DIR, 'curriculum.json');
|
||||||
|
if (!existsSync(curriculumPath)) {
|
||||||
|
throw new Error(`Curriculum file not found: ${curriculumPath}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return JSON.parse(readFileSync(curriculumPath, 'utf8')) as {
|
||||||
|
superblocks: SuperBlocks[];
|
||||||
|
certifications: string[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getBlockStructurePath(block: string) {
|
||||||
|
return resolve(BLOCK_STRUCTURE_DIR, `${block}.json`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getBlockStructureDir() {
|
||||||
|
return BLOCK_STRUCTURE_DIR;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ChallengeFile = {
|
||||||
|
contents: string;
|
||||||
|
ext: string;
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Challenge = {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
// infer other properties:
|
||||||
|
description?: string;
|
||||||
|
instructions?: string;
|
||||||
|
questions?: string[];
|
||||||
|
block?: string;
|
||||||
|
blockType?: string;
|
||||||
|
blockLayout?: string;
|
||||||
|
hasEditableBoundaries?: boolean;
|
||||||
|
order?: number;
|
||||||
|
superBlock?: SuperBlocks;
|
||||||
|
superOrder?: number;
|
||||||
|
challengeOrder?: number;
|
||||||
|
isLastChallengeInBlock?: boolean;
|
||||||
|
required?: string[];
|
||||||
|
template?: string;
|
||||||
|
helpCategory?: string;
|
||||||
|
usesMultifileEditor?: boolean;
|
||||||
|
disableLoopProtectTests?: boolean;
|
||||||
|
disableLoopProtectPreview?: boolean;
|
||||||
|
chapter?: string;
|
||||||
|
module?: string;
|
||||||
|
certification?: Certification;
|
||||||
|
translationPending?: boolean;
|
||||||
|
missing?: boolean;
|
||||||
|
challengeFiles?: ChallengeFile[];
|
||||||
|
solutions?: ChallengeFile[][];
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface BlockStructure {
|
||||||
|
name: string;
|
||||||
|
hasEditableBoundaries?: boolean;
|
||||||
|
required?: string[];
|
||||||
|
template?: string;
|
||||||
|
helpCategory?: string;
|
||||||
|
usesMultifileEditor?: boolean;
|
||||||
|
disableLoopProtectTests?: boolean;
|
||||||
|
disableLoopProtectPreview?: boolean;
|
||||||
|
blockLayout: string;
|
||||||
|
blockType: string;
|
||||||
|
challengeOrder: Challenge[];
|
||||||
|
dashedName: string;
|
||||||
|
isUpcomingChange?: boolean;
|
||||||
|
chapter?: string;
|
||||||
|
module?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getBlockStructure(block: string) {
|
||||||
|
return JSON.parse(
|
||||||
|
readFileSync(getBlockStructurePath(block), 'utf8')
|
||||||
|
) as BlockStructure;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function writeBlockStructure(block: string, structure: unknown) {
|
||||||
|
// dynamically importing prettier because Gatsby build and develop fail when
|
||||||
|
// it's required.
|
||||||
|
const prettier = await import('prettier');
|
||||||
|
const content = await prettier.format(JSON.stringify(structure), {
|
||||||
|
parser: 'json'
|
||||||
|
});
|
||||||
|
await writeFile(getBlockStructurePath(block), content, 'utf8');
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function writeSuperblockStructure(
|
||||||
|
superblock: string,
|
||||||
|
structure: unknown
|
||||||
|
) {
|
||||||
|
// dynamically importing prettier because Gatsby build and develop fail when
|
||||||
|
// it's required.
|
||||||
|
const prettier = await import('prettier');
|
||||||
|
const content = await prettier.format(JSON.stringify(structure), {
|
||||||
|
parser: 'json'
|
||||||
|
});
|
||||||
|
await writeFile(getSuperblockStructurePath(superblock), content);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getSuperblockStructure(superblockFilename: string) {
|
||||||
|
const superblockPath = getSuperblockStructurePath(superblockFilename);
|
||||||
|
|
||||||
|
if (!existsSync(superblockPath)) {
|
||||||
|
throw Error(`Superblock file not found: ${superblockPath}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return JSON.parse(readFileSync(superblockPath, 'utf8')) as {
|
||||||
|
blocks?: string[];
|
||||||
|
chapters?: Chapter[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getSuperblockStructurePath(superblockFilename: string) {
|
||||||
|
return 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
|
||||||
|
*/
|
||||||
|
export function getLanguageConfig(
|
||||||
|
lang: string,
|
||||||
|
{ baseDir, i18nBaseDir } = {
|
||||||
|
baseDir: CURRICULUM_DIR,
|
||||||
|
i18nBaseDir: I18N_CURRICULUM_DIR
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
const contentDir = resolve(baseDir, 'challenges', 'english');
|
||||||
|
const i18nContentDir = resolve(i18nBaseDir, 'challenges', lang);
|
||||||
|
const blockContentDir = resolve(contentDir, 'blocks');
|
||||||
|
const i18nBlockContentDir = resolve(i18nContentDir, 'blocks');
|
||||||
|
const dictionariesDir = resolve(baseDir, 'dictionaries');
|
||||||
|
const i18nDictionariesDir = resolve(i18nBaseDir, 'dictionaries');
|
||||||
|
|
||||||
|
if (lang !== 'english') {
|
||||||
|
assert(
|
||||||
|
existsSync(i18nContentDir),
|
||||||
|
`i18n content directory does not exist: ${i18nContentDir}`
|
||||||
|
);
|
||||||
|
assert(
|
||||||
|
existsSync(i18nBlockContentDir),
|
||||||
|
`i18n block content directory does not exist: ${i18nBlockContentDir}`
|
||||||
|
);
|
||||||
|
assert(
|
||||||
|
existsSync(i18nDictionariesDir),
|
||||||
|
`i18n dictionaries directory does not exist: ${i18nDictionariesDir}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
log(`Using content directory: ${contentDir}`);
|
||||||
|
log(`Using i18n content directory: ${i18nContentDir}`);
|
||||||
|
log(`Using block content directory: ${blockContentDir}`);
|
||||||
|
log(`Using i18n block content directory: ${i18nBlockContentDir}`);
|
||||||
|
log(`Using dictionaries directory: ${dictionariesDir}`);
|
||||||
|
log(`Using i18n dictionaries directory: ${i18nDictionariesDir}`);
|
||||||
|
|
||||||
|
return {
|
||||||
|
contentDir,
|
||||||
|
i18nContentDir,
|
||||||
|
blockContentDir,
|
||||||
|
i18nBlockContentDir,
|
||||||
|
dictionariesDir,
|
||||||
|
i18nDictionariesDir
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -5,7 +5,7 @@ import { hasEnglishSource, getChallengesForLang } from './get-challenges.js';
|
|||||||
const EXISTING_CHALLENGE_PATH = 'challenge.md';
|
const EXISTING_CHALLENGE_PATH = 'challenge.md';
|
||||||
const MISSING_CHALLENGE_PATH = 'no/challenge.md';
|
const MISSING_CHALLENGE_PATH = 'no/challenge.md';
|
||||||
|
|
||||||
const basePath = '__fixtures__';
|
const basePath = '../__fixtures__';
|
||||||
|
|
||||||
describe('create non-English challenge', () => {
|
describe('create non-English challenge', () => {
|
||||||
describe('getChallengesForLang', () => {
|
describe('getChallengesForLang', () => {
|
||||||
36
curriculum/src/get-challenges.ts
Normal file
36
curriculum/src/get-challenges.ts
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import { access as _access, constants } from 'fs';
|
||||||
|
import { resolve, join } from 'path';
|
||||||
|
import { promisify } from 'util';
|
||||||
|
|
||||||
|
import { availableLangs } from '../../shared-dist/config/i18n.js';
|
||||||
|
import { buildCurriculum } from './build-curriculum.js';
|
||||||
|
|
||||||
|
const { curriculum: curriculumLangs } = availableLangs;
|
||||||
|
|
||||||
|
const access = promisify(_access);
|
||||||
|
|
||||||
|
export async function getChallengesForLang(
|
||||||
|
lang: string,
|
||||||
|
filters?: {
|
||||||
|
superBlock?: string;
|
||||||
|
block?: string;
|
||||||
|
challengeId?: string;
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
const invalidLang = !curriculumLangs.includes(lang);
|
||||||
|
if (invalidLang)
|
||||||
|
throw Error(`${lang} is not an accepted language.
|
||||||
|
Accepted languages are ${curriculumLangs.join(', ')}`);
|
||||||
|
|
||||||
|
return buildCurriculum(lang, filters);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function hasEnglishSource(
|
||||||
|
basePath: string,
|
||||||
|
translationPath: string
|
||||||
|
) {
|
||||||
|
const englishRoot = resolve(__dirname, basePath, 'english');
|
||||||
|
return await access(join(englishRoot, translationPath), constants.F_OK)
|
||||||
|
.then(() => true)
|
||||||
|
.catch(() => false);
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
var glob = require('glob');
|
var glob = require('glob');
|
||||||
const lint = require('../tools/scripts/lint');
|
const lint = require('../../tools/scripts/lint');
|
||||||
const { testedLang } = require('./utils');
|
const { testedLang } = require('./utils');
|
||||||
|
|
||||||
glob(`challenges/${testedLang()}/**/*.md`, (err, files) => {
|
glob(`challenges/${testedLang()}/**/*.md`, (err, files) => {
|
||||||
1
curriculum/src/reset.d.ts
vendored
Normal file
1
curriculum/src/reset.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
import '@total-typescript/ts-reset';
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
import { assert, describe, it, vi } from 'vitest';
|
import { assert, describe, it, vi } from 'vitest';
|
||||||
import { testedLang } from '../utils';
|
|
||||||
|
|
||||||
vi.stubEnv('SHOW_UPCOMING_CHANGES', 'true');
|
vi.stubEnv('SHOW_UPCOMING_CHANGES', 'true');
|
||||||
|
|
||||||
// We need to use dynamic import here to ensure the environment variable is set
|
// We need to use dynamic imports here to ensure the environment variable is set
|
||||||
// before the module is loaded.
|
// before the module is loaded.
|
||||||
|
const { testedLang } = await import('../utils.js');
|
||||||
const { getChallenges } = await import('./test-challenges.js');
|
const { getChallenges } = await import('./test-challenges.js');
|
||||||
|
|
||||||
describe('Daily Coding Challenges', async () => {
|
describe('Daily Coding Challenges', async () => {
|
||||||
@@ -1,5 +1,3 @@
|
|||||||
import { createRequire } from 'node:module';
|
|
||||||
|
|
||||||
import { describe, it, beforeAll, expect } from 'vitest';
|
import { describe, it, beforeAll, expect } from 'vitest';
|
||||||
import { assert, AssertionError } from 'chai';
|
import { assert, AssertionError } from 'chai';
|
||||||
import jsdom from 'jsdom';
|
import jsdom from 'jsdom';
|
||||||
@@ -8,28 +6,26 @@ import lodash from 'lodash';
|
|||||||
import {
|
import {
|
||||||
buildChallenge,
|
buildChallenge,
|
||||||
runnerTypes
|
runnerTypes
|
||||||
} from '../../client/src/templates/Challenges/utils/build';
|
} from '../../../client/src/templates/Challenges/utils/build';
|
||||||
import {
|
import {
|
||||||
challengeTypes,
|
challengeTypes,
|
||||||
hasNoSolution
|
hasNoSolution
|
||||||
} from '../../shared/config/challenge-types';
|
} from '../../../shared/config/challenge-types';
|
||||||
import { getLines } from '../../shared/utils/get-lines';
|
import { getLines } from '../../../shared/utils/get-lines';
|
||||||
import { prefixDoctype } from '../../client/src/templates/Challenges/utils/frame';
|
import { prefixDoctype } from '../../../client/src/templates/Challenges/utils/frame';
|
||||||
|
|
||||||
const require = createRequire(import.meta.url);
|
import { getChallengesForLang } from '../get-challenges.js';
|
||||||
|
import { challengeSchemaValidator } from '../../schema/challenge-schema.js';
|
||||||
|
import { testedLang } from '../utils.js';
|
||||||
|
|
||||||
const { getChallengesForLang } = require('../get-challenges');
|
import { curriculumSchemaValidator } from '../../schema/curriculum-schema.js';
|
||||||
const { challengeSchemaValidator } = require('../schema/challenge-schema');
|
import { validateMetaSchema } from '../../schema/meta-schema.js';
|
||||||
const { testedLang } = require('../utils');
|
import { getBlockStructure } from '../file-handler.js';
|
||||||
|
import ChallengeTitles from './utils/challenge-titles.js';
|
||||||
|
import MongoIds from './utils/mongo-ids.js';
|
||||||
|
import createPseudoWorker from './utils/pseudo-worker.js';
|
||||||
|
|
||||||
const { curriculumSchemaValidator } = require('../schema/curriculum-schema');
|
import { sortChallenges } from './utils/sort-challenges.js';
|
||||||
const { validateMetaSchema } = require('../schema/meta-schema');
|
|
||||||
const { getBlockStructure } = require('../file-handler');
|
|
||||||
const ChallengeTitles = require('./utils/challenge-titles');
|
|
||||||
const MongoIds = require('./utils/mongo-ids');
|
|
||||||
const createPseudoWorker = require('./utils/pseudo-worker');
|
|
||||||
|
|
||||||
const { sortChallenges } = require('./utils/sort-challenges');
|
|
||||||
|
|
||||||
const { flatten, isEmpty, cloneDeep } = lodash;
|
const { flatten, isEmpty, cloneDeep } = lodash;
|
||||||
|
|
||||||
@@ -4,10 +4,19 @@ import path from 'node:path';
|
|||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
|
|
||||||
import { parseCurriculumStructure } from '../../build-curriculum.js';
|
import { parseCurriculumStructure } from '../../build-curriculum.js';
|
||||||
|
import { Filter } from '../../utils.js';
|
||||||
|
|
||||||
const __dirname = import.meta.dirname;
|
let __dirnameCompat: string;
|
||||||
|
|
||||||
const testFilter = {
|
if (typeof __dirname !== 'undefined') {
|
||||||
|
// CJS
|
||||||
|
__dirnameCompat = __dirname;
|
||||||
|
} else {
|
||||||
|
// ESM – wrap in Function so CJS parsers don't see it
|
||||||
|
__dirnameCompat = new Function('return import.meta.dirname')() as string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const testFilter: Filter = {
|
||||||
block: process.env.FCC_BLOCK ? process.env.FCC_BLOCK.trim() : undefined,
|
block: process.env.FCC_BLOCK ? process.env.FCC_BLOCK.trim() : undefined,
|
||||||
challengeId: process.env.FCC_CHALLENGE_ID
|
challengeId: process.env.FCC_CHALLENGE_ID
|
||||||
? process.env.FCC_CHALLENGE_ID.trim()
|
? process.env.FCC_CHALLENGE_ID.trim()
|
||||||
@@ -17,7 +26,7 @@ const testFilter = {
|
|||||||
: undefined
|
: undefined
|
||||||
};
|
};
|
||||||
|
|
||||||
const GENERATED_DIR = path.resolve(__dirname, '../blocks-generated');
|
const GENERATED_DIR = path.resolve(__dirnameCompat, '../blocks-generated');
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
// clean and recreate directory
|
// clean and recreate directory
|
||||||
@@ -39,7 +48,7 @@ async function main() {
|
|||||||
console.log(`Generated ${blocks.length} block test file(s).`);
|
console.log(`Generated ${blocks.length} block test file(s).`);
|
||||||
}
|
}
|
||||||
|
|
||||||
function generateSingleBlockFile(testFilter) {
|
function generateSingleBlockFile(testFilter: Filter) {
|
||||||
return `import { defineTestsForBlock } from '../test-challenges.js';
|
return `import { defineTestsForBlock } from '../test-challenges.js';
|
||||||
|
|
||||||
await defineTestsForBlock(${JSON.stringify(testFilter)});
|
await defineTestsForBlock(${JSON.stringify(testFilter)});
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { describe, it, expect } from 'vitest';
|
import { describe, it, expect } from 'vitest';
|
||||||
|
|
||||||
import { shuffleArray } from '../../../shared-dist/utils/shuffle-array.js';
|
import { shuffleArray } from '../../../../shared-dist/utils/shuffle-array.js';
|
||||||
import { sortChallenges } from './sort-challenges.js';
|
import { sortChallenges } from './sort-challenges.js';
|
||||||
|
|
||||||
const challenges = [
|
const challenges = [
|
||||||
@@ -4,9 +4,9 @@ import sirv from 'sirv';
|
|||||||
import polka from 'polka';
|
import polka from 'polka';
|
||||||
import puppeteer from 'puppeteer';
|
import puppeteer from 'puppeteer';
|
||||||
|
|
||||||
import { helperVersion } from '../../client/src/templates/Challenges/utils/frame';
|
import { helperVersion } from '../../../client/src/templates/Challenges/utils/frame';
|
||||||
|
|
||||||
const clientPath = path.resolve(__dirname, '../../client');
|
const clientPath = path.resolve(__dirname, '../../../client');
|
||||||
|
|
||||||
async function createBrowser() {
|
async function createBrowser() {
|
||||||
return puppeteer.launch({
|
return puppeteer.launch({
|
||||||
@@ -2,12 +2,12 @@ import { defineConfig } from 'vitest/config';
|
|||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
test: {
|
test: {
|
||||||
include: ['test/blocks-generated/**/*.test.js'],
|
include: ['src/test/blocks-generated/**/*.test.js'],
|
||||||
environment: 'node',
|
environment: 'node',
|
||||||
hookTimeout: 60000,
|
hookTimeout: 60000,
|
||||||
testTimeout: 30000,
|
testTimeout: 30000,
|
||||||
isolate: false,
|
isolate: false,
|
||||||
globalSetup: 'test/vitest-global-setup.mjs',
|
globalSetup: 'src/test/vitest-global-setup.mjs',
|
||||||
setupFiles: 'test/vitest-setup.mjs'
|
setupFiles: 'src/test/vitest-setup.mjs'
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -2,7 +2,7 @@ import path from 'path';
|
|||||||
import { config } from 'dotenv';
|
import { config } from 'dotenv';
|
||||||
import { describe, it, expect } from 'vitest';
|
import { describe, it, expect } from 'vitest';
|
||||||
|
|
||||||
import { SuperBlocks } from '../shared-dist/config/curriculum';
|
import { SuperBlocks } from '../../shared-dist/config/curriculum';
|
||||||
import {
|
import {
|
||||||
closestFilters,
|
closestFilters,
|
||||||
closestMatch,
|
closestMatch,
|
||||||
@@ -11,7 +11,7 @@ import {
|
|||||||
filterByChallengeId,
|
filterByChallengeId,
|
||||||
filterBySuperblock,
|
filterBySuperblock,
|
||||||
getSuperOrder
|
getSuperOrder
|
||||||
} from './utils';
|
} from './utils.js';
|
||||||
|
|
||||||
config({ path: path.resolve(__dirname, '../.env') });
|
config({ path: path.resolve(__dirname, '../.env') });
|
||||||
|
|
||||||
@@ -65,12 +65,6 @@ describe('createSuperOrder', () => {
|
|||||||
it('should create the correct object given an array of SuperBlocks', () => {
|
it('should create the correct object given an array of SuperBlocks', () => {
|
||||||
expect(superOrder).toStrictEqual(fullSuperOrder);
|
expect(superOrder).toStrictEqual(fullSuperOrder);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('throws when not given an array of SuperBlocks', () => {
|
|
||||||
expect(() => createSuperOrder()).toThrow();
|
|
||||||
expect(() => createSuperOrder(null)).toThrow();
|
|
||||||
expect(() => createSuperOrder('')).toThrow();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('getSuperOrder', () => {
|
describe('getSuperOrder', () => {
|
||||||
@@ -79,8 +73,6 @@ describe('getSuperOrder', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('returns undefined for unknown curriculum', () => {
|
it('returns undefined for unknown curriculum', () => {
|
||||||
expect(getSuperOrder()).toBeUndefined();
|
|
||||||
expect(getSuperOrder(null)).toBeUndefined();
|
|
||||||
expect(getSuperOrder('')).toBeUndefined();
|
expect(getSuperOrder('')).toBeUndefined();
|
||||||
expect(getSuperOrder('respansive-wib-desoin')).toBeUndefined();
|
expect(getSuperOrder('respansive-wib-desoin')).toBeUndefined();
|
||||||
expect(getSuperOrder('certifications')).toBeUndefined();
|
expect(getSuperOrder('certifications')).toBeUndefined();
|
||||||
@@ -299,18 +291,21 @@ describe('filter utils', () => {
|
|||||||
{
|
{
|
||||||
name: 'responsive-web-design',
|
name: 'responsive-web-design',
|
||||||
blocks: [
|
blocks: [
|
||||||
{ dashedName: 'basic-html-and-html5' },
|
{ dashedName: 'basic-html-and-html5', challengeOrder: [] },
|
||||||
{ dashedName: 'css-flexbox' }
|
{ dashedName: 'css-flexbox', challengeOrder: [] }
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'javascript-algorithms-and-data-structures',
|
name: 'javascript-algorithms-and-data-structures',
|
||||||
blocks: [{ dashedName: 'basic-javascript' }, { dashedName: 'es6' }]
|
blocks: [
|
||||||
|
{ dashedName: 'basic-javascript', challengeOrder: [] },
|
||||||
|
{ dashedName: 'es6', challengeOrder: [] }
|
||||||
|
]
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
closestFilters({ superBlock: 'responsiv web design' }, superblocks)
|
closestFilters(superblocks, { superBlock: 'responsiv web design' })
|
||||||
).toEqual({ superBlock: 'responsive-web-design' });
|
).toEqual({ superBlock: 'responsive-web-design' });
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -319,17 +314,20 @@ describe('filter utils', () => {
|
|||||||
{
|
{
|
||||||
name: 'responsive-web-design',
|
name: 'responsive-web-design',
|
||||||
blocks: [
|
blocks: [
|
||||||
{ dashedName: 'basic-html-and-html5' },
|
{ dashedName: 'basic-html-and-html5', challengeOrder: [] },
|
||||||
{ dashedName: 'css-flexbox' }
|
{ dashedName: 'css-flexbox', challengeOrder: [] }
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'javascript-algorithms-and-data-structures',
|
name: 'javascript-algorithms-and-data-structures',
|
||||||
blocks: [{ dashedName: 'basic-javascript' }, { dashedName: 'es6' }]
|
blocks: [
|
||||||
|
{ dashedName: 'basic-javascript', challengeOrder: [] },
|
||||||
|
{ dashedName: 'es6', challengeOrder: [] }
|
||||||
|
]
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
expect(closestFilters({ block: 'basic-javascr' }, superblocks)).toEqual({
|
expect(closestFilters(superblocks, { block: 'basic-javascr' })).toEqual({
|
||||||
block: 'basic-javascript'
|
block: 'basic-javascript'
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -1,16 +1,18 @@
|
|||||||
const path = require('path');
|
import { resolve } from 'path';
|
||||||
|
|
||||||
const comparison = require('string-similarity');
|
import comparison from 'string-similarity';
|
||||||
|
import { config } from 'dotenv';
|
||||||
|
|
||||||
const { generateSuperBlockList } = require('../shared-dist/config/curriculum');
|
import { generateSuperBlockList } from '../../shared-dist/config/curriculum.js';
|
||||||
|
|
||||||
require('dotenv').config({ path: path.resolve(__dirname, '../.env') });
|
config({ path: resolve(__dirname, '../../.env') });
|
||||||
|
|
||||||
|
import { availableLangs } from '../../shared-dist/config/i18n.js';
|
||||||
|
|
||||||
const { availableLangs } = require('../shared-dist/config/i18n');
|
|
||||||
const curriculumLangs = availableLangs.curriculum;
|
const curriculumLangs = availableLangs.curriculum;
|
||||||
|
|
||||||
// checks that the CURRICULUM_LOCALE exists and is an available language
|
// checks that the CURRICULUM_LOCALE exists and is an available language
|
||||||
exports.testedLang = function testedLang() {
|
export function testedLang() {
|
||||||
if (process.env.CURRICULUM_LOCALE) {
|
if (process.env.CURRICULUM_LOCALE) {
|
||||||
if (curriculumLangs.includes(process.env.CURRICULUM_LOCALE)) {
|
if (curriculumLangs.includes(process.env.CURRICULUM_LOCALE)) {
|
||||||
return process.env.CURRICULUM_LOCALE;
|
return process.env.CURRICULUM_LOCALE;
|
||||||
@@ -21,10 +23,10 @@ exports.testedLang = function testedLang() {
|
|||||||
} else {
|
} else {
|
||||||
throw Error('LOCALE must be set for testing');
|
throw Error('LOCALE must be set for testing');
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
function createSuperOrder(superBlocks) {
|
export function createSuperOrder(superBlocks: string[]) {
|
||||||
const superOrder = {};
|
const superOrder: { [sb: string]: number } = {};
|
||||||
|
|
||||||
superBlocks.forEach((superBlock, i) => {
|
superBlocks.forEach((superBlock, i) => {
|
||||||
superOrder[superBlock] = i;
|
superOrder[superBlock] = i;
|
||||||
@@ -33,8 +35,8 @@ function createSuperOrder(superBlocks) {
|
|||||||
return superOrder;
|
return superOrder;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getSuperOrder(
|
export function getSuperOrder(
|
||||||
superblock,
|
superblock: string,
|
||||||
showUpcomingChanges = process.env.SHOW_UPCOMING_CHANGES === 'true'
|
showUpcomingChanges = process.env.SHOW_UPCOMING_CHANGES === 'true'
|
||||||
) {
|
) {
|
||||||
const flatSuperBlockMap = generateSuperBlockList({
|
const flatSuperBlockMap = generateSuperBlockList({
|
||||||
@@ -54,7 +56,10 @@ function getSuperOrder(
|
|||||||
* @param {string} [options.block] - The dashedName of the block to filter for (in kebab case).
|
* @param {string} [options.block] - The dashedName of the block to filter for (in kebab case).
|
||||||
* @returns {Array<Object>} Array with one superblock containing the specified block, or the original array if block is not provided.
|
* @returns {Array<Object>} Array with one superblock containing the specified block, or the original array if block is not provided.
|
||||||
*/
|
*/
|
||||||
function filterByBlock(superblocks, { block } = {}) {
|
export function filterByBlock<T extends { blocks: { dashedName: string }[] }>(
|
||||||
|
superblocks: T[],
|
||||||
|
{ block }: { block?: string } = {}
|
||||||
|
): T[] {
|
||||||
if (!block) return superblocks;
|
if (!block) return superblocks;
|
||||||
|
|
||||||
const superblock = superblocks
|
const superblock = superblocks
|
||||||
@@ -76,7 +81,10 @@ function filterByBlock(superblocks, { block } = {}) {
|
|||||||
* @param {string} [options.superBlock] - The name of the superblock to filter for.
|
* @param {string} [options.superBlock] - The name of the superblock to filter for.
|
||||||
* @returns {Array<Object>} Filtered array of superblocks containing only the specified superblock, or the original array if superBlock is not provided.
|
* @returns {Array<Object>} Filtered array of superblocks containing only the specified superblock, or the original array if superBlock is not provided.
|
||||||
*/
|
*/
|
||||||
function filterBySuperblock(superblocks, { superBlock } = {}) {
|
export function filterBySuperblock<T extends { name: string }>(
|
||||||
|
superblocks: T[],
|
||||||
|
{ superBlock }: { superBlock?: string } = {}
|
||||||
|
): T[] {
|
||||||
if (!superBlock) return superblocks;
|
if (!superBlock) return superblocks;
|
||||||
return superblocks.filter(({ name }) => name === superBlock);
|
return superblocks.filter(({ name }) => name === superBlock);
|
||||||
}
|
}
|
||||||
@@ -88,15 +96,20 @@ function filterBySuperblock(superblocks, { superBlock } = {}) {
|
|||||||
* @param {string} [options.challengeId] - The specific challenge id to filter for
|
* @param {string} [options.challengeId] - The specific challenge id to filter for
|
||||||
* @returns {Array<Object>} Filtered superblocks containing only the matching challenge
|
* @returns {Array<Object>} Filtered superblocks containing only the matching challenge
|
||||||
*/
|
*/
|
||||||
function filterByChallengeId(superblocks, { challengeId } = {}) {
|
export function filterByChallengeId<
|
||||||
|
T extends { blocks: { challengeOrder: { id: string }[] }[] }
|
||||||
|
>(superblocks: T[], { challengeId }: { challengeId?: string } = {}): T[] {
|
||||||
if (!challengeId) {
|
if (!challengeId) {
|
||||||
return superblocks;
|
return superblocks;
|
||||||
}
|
}
|
||||||
|
|
||||||
const findChallengeIndex = (challengeOrder, id) =>
|
const findChallengeIndex = (challengeOrder: { id: string }[], id: string) =>
|
||||||
challengeOrder.findIndex(challenge => challenge.id === id);
|
challengeOrder.findIndex(challenge => challenge.id === id);
|
||||||
|
|
||||||
const filterChallengeOrder = (challengeOrder, id) => {
|
const filterChallengeOrder = (
|
||||||
|
challengeOrder: { id: string }[],
|
||||||
|
id: string
|
||||||
|
) => {
|
||||||
const index = findChallengeIndex(challengeOrder, id);
|
const index = findChallengeIndex(challengeOrder, id);
|
||||||
if (index === -1) return [];
|
if (index === -1) return [];
|
||||||
|
|
||||||
@@ -121,20 +134,45 @@ function filterByChallengeId(superblocks, { challengeId } = {}) {
|
|||||||
.filter(superblock => superblock.blocks.length > 0);
|
.filter(superblock => superblock.blocks.length > 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
const createFilterPipeline = filterFunctions => (data, filters) =>
|
export interface Filter {
|
||||||
filterFunctions.reduce((acc, filterFn) => filterFn(acc, filters), data);
|
superBlock?: string;
|
||||||
|
block?: string;
|
||||||
|
challengeId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
const applyFilters = createFilterPipeline([
|
interface Filterable {
|
||||||
|
name: string;
|
||||||
|
blocks: {
|
||||||
|
challengeOrder: { id: string }[];
|
||||||
|
dashedName: string;
|
||||||
|
}[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GenericFilterFunction {
|
||||||
|
<T extends Filterable>(data: T[], filters?: Filter): T[];
|
||||||
|
}
|
||||||
|
|
||||||
|
function createFilterPipeline<T extends Filterable>(
|
||||||
|
filterFunctions: GenericFilterFunction[]
|
||||||
|
): (data: T[], filters?: Filter) => T[] {
|
||||||
|
return (data: T[], filters?: Filter) =>
|
||||||
|
filterFunctions.reduce((acc, filterFn) => filterFn(acc, filters), data);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const applyFilters: GenericFilterFunction = createFilterPipeline([
|
||||||
filterBySuperblock,
|
filterBySuperblock,
|
||||||
filterByBlock,
|
filterByBlock,
|
||||||
filterByChallengeId
|
filterByChallengeId
|
||||||
]);
|
]);
|
||||||
|
|
||||||
function closestMatch(target, xs) {
|
export function closestMatch(target: string, xs: string[]): string {
|
||||||
return comparison.findBestMatch(target.toLowerCase(), xs).bestMatch.target;
|
return comparison.findBestMatch(target.toLowerCase(), xs).bestMatch.target;
|
||||||
}
|
}
|
||||||
|
|
||||||
function closestFilters(target, superblocks) {
|
export function closestFilters(
|
||||||
|
superblocks: Filterable[],
|
||||||
|
target?: Filter
|
||||||
|
): Filter | undefined {
|
||||||
if (target?.superBlock) {
|
if (target?.superBlock) {
|
||||||
const superblockNames = superblocks.map(({ name }) => name);
|
const superblockNames = superblocks.map(({ name }) => name);
|
||||||
return {
|
return {
|
||||||
@@ -155,12 +193,3 @@ function closestFilters(target, superblocks) {
|
|||||||
|
|
||||||
return target;
|
return target;
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.closestFilters = closestFilters;
|
|
||||||
exports.closestMatch = closestMatch;
|
|
||||||
exports.createSuperOrder = createSuperOrder;
|
|
||||||
exports.filterByBlock = filterByBlock;
|
|
||||||
exports.filterBySuperblock = filterBySuperblock;
|
|
||||||
exports.filterByChallengeId = filterByChallengeId;
|
|
||||||
exports.getSuperOrder = getSuperOrder;
|
|
||||||
exports.applyFilters = applyFilters;
|
|
||||||
17
curriculum/tsconfig.json
Normal file
17
curriculum/tsconfig.json
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"references": [{ "path": "../shared/tsconfig.json" }],
|
||||||
|
"compilerOptions": {
|
||||||
|
"rootDir": "./src",
|
||||||
|
"outDir": "./dist",
|
||||||
|
"declaration": true,
|
||||||
|
"target": "es2022",
|
||||||
|
"module": "nodenext",
|
||||||
|
"strict": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"noEmit": false,
|
||||||
|
"noUncheckedIndexedAccess": true,
|
||||||
|
"skipLibCheck": true // vitest cannot import cjs. The options are 1) migrate to esm, 2) don't type check tests and 3) skip lib checks
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,6 @@ import { defineConfig } from 'vitest/config';
|
|||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
test: {
|
test: {
|
||||||
exclude: ['test/blocks-generated/**/*.test.js']
|
exclude: ['src/test/blocks-generated/**/*.test.js', 'dist']
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ RUN cd api && pnpm prisma generate
|
|||||||
ARG SHOW_UPCOMING_CHANGES=false
|
ARG SHOW_UPCOMING_CHANGES=false
|
||||||
ENV SHOW_UPCOMING_CHANGES=$SHOW_UPCOMING_CHANGES
|
ENV SHOW_UPCOMING_CHANGES=$SHOW_UPCOMING_CHANGES
|
||||||
|
|
||||||
RUN pnpm create:shared
|
RUN pnpm compile:ts
|
||||||
RUN pnpm build:curriculum
|
RUN pnpm build:curriculum
|
||||||
RUN pnpm -F=api build
|
RUN pnpm -F=api build
|
||||||
|
|
||||||
@@ -52,7 +52,7 @@ USER node
|
|||||||
WORKDIR /home/node/fcc
|
WORKDIR /home/node/fcc
|
||||||
COPY --from=builder --chown=node:node /home/node/build/api/dist/ ./
|
COPY --from=builder --chown=node:node /home/node/build/api/dist/ ./
|
||||||
COPY --from=builder --chown=node:node /home/node/build/api/package.json api/
|
COPY --from=builder --chown=node:node /home/node/build/api/package.json api/
|
||||||
COPY --from=builder --chown=node:node /home/node/build/shared/config/curriculum.json shared/config/
|
COPY --from=builder --chown=node:node /home/node/build/shared-dist/config/curriculum.json shared-dist/config/
|
||||||
|
|
||||||
COPY --from=deps --chown=node:node /home/node/build/node_modules/ node_modules/
|
COPY --from=deps --chown=node:node /home/node/build/node_modules/ node_modules/
|
||||||
COPY --from=deps --chown=node:node /home/node/build/api/node_modules/ api/node_modules/
|
COPY --from=deps --chown=node:node /home/node/build/api/node_modules/ api/node_modules/
|
||||||
|
|||||||
@@ -33,10 +33,12 @@ export default tseslint.config(
|
|||||||
'client/.cache/**/*',
|
'client/.cache/**/*',
|
||||||
'client/public/**/*',
|
'client/public/**/*',
|
||||||
'shared/**/*.js',
|
'shared/**/*.js',
|
||||||
|
'shared/**/*.d.ts',
|
||||||
'docs/**/*.md',
|
'docs/**/*.md',
|
||||||
'**/playwright*.config.ts',
|
'**/playwright*.config.ts',
|
||||||
'playwright/**/*',
|
'playwright/**/*',
|
||||||
'shared-dist/**/*'
|
'shared-dist/**/*',
|
||||||
|
'curriculum/dist/**/*'
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
js.configs.recommended,
|
js.configs.recommended,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://cdn.jsdelivr.net/npm/knip@5/schema.json",
|
"$schema": "https://cdn.jsdelivr.net/npm/knip@5/schema.json",
|
||||||
"ignoreBinaries": ["create:shared", "install-puppeteer", "pm2"],
|
"ignoreBinaries": ["compile:ts", "install-puppeteer", "pm2"],
|
||||||
"workspaces": {
|
"workspaces": {
|
||||||
".": {
|
".": {
|
||||||
"playwright": ["playwright.config.ts"],
|
"playwright": ["playwright.config.ts"],
|
||||||
|
|||||||
20
package.json
20
package.json
@@ -19,9 +19,9 @@
|
|||||||
"author": "freeCodeCamp <team@freecodecamp.org>",
|
"author": "freeCodeCamp <team@freecodecamp.org>",
|
||||||
"main": "none",
|
"main": "none",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"audit-challenges": "pnpm run create:shared && tsx tools/challenge-auditor/index.ts",
|
"audit-challenges": "pnpm run compile:ts && tsx tools/challenge-auditor/index.ts",
|
||||||
"analyze-bundle": "webpack-bundle-analyzer",
|
"analyze-bundle": "webpack-bundle-analyzer",
|
||||||
"prebuild": "npm-run-all create:shared",
|
"prebuild": "npm-run-all compile:ts",
|
||||||
"build": "npm-run-all -p build:*",
|
"build": "npm-run-all -p build:*",
|
||||||
"build-workers": "cd ./client && pnpm run prebuild",
|
"build-workers": "cd ./client && pnpm run prebuild",
|
||||||
"build:client": "cd ./client && pnpm run build",
|
"build:client": "cd ./client && pnpm run build",
|
||||||
@@ -34,13 +34,13 @@
|
|||||||
"clean-and-develop": "pnpm run clean && pnpm install && pnpm run develop",
|
"clean-and-develop": "pnpm run clean && pnpm install && pnpm run develop",
|
||||||
"clean:api": "cd api && pnpm clean",
|
"clean:api": "cd api && pnpm clean",
|
||||||
"clean:client": "cd ./client && pnpm run clean",
|
"clean:client": "cd ./client && pnpm run clean",
|
||||||
"clean:curriculum": "rm -rf ./shared/config/curriculum.json",
|
"clean:curriculum": "rm -rf ./shared-dist/config/curriculum.json",
|
||||||
"clean:packages": "find . -name 'node_modules' -type d -prune -exec rm -rf '{}' +",
|
"clean:packages": "find . -name 'node_modules' -type d -prune -exec rm -rf '{}' +",
|
||||||
"create:shared": "tsc -p shared",
|
"compile:ts": "tsc --build --clean curriculum && tsc --build curriculum",
|
||||||
"create-new-project": "cd ./tools/challenge-helper-scripts/ && pnpm run create-project",
|
"create-new-project": "cd ./tools/challenge-helper-scripts/ && pnpm run create-project",
|
||||||
"create-new-language-block": "cd ./tools/challenge-helper-scripts/ && pnpm run create-language-block",
|
"create-new-language-block": "cd ./tools/challenge-helper-scripts/ && pnpm run create-language-block",
|
||||||
"create-new-quiz": "cd ./tools/challenge-helper-scripts/ && pnpm run create-quiz",
|
"create-new-quiz": "cd ./tools/challenge-helper-scripts/ && pnpm run create-quiz",
|
||||||
"predevelop": "npm-run-all -p create:shared -s build:curriculum",
|
"predevelop": "npm-run-all -p compile:ts -s build:curriculum",
|
||||||
"develop": "npm-run-all -p develop:*",
|
"develop": "npm-run-all -p develop:*",
|
||||||
"develop:client": "cd ./client && pnpm run develop",
|
"develop:client": "cd ./client && pnpm run develop",
|
||||||
"develop:api": "cd ./api && pnpm run develop",
|
"develop:api": "cd ./api && pnpm run develop",
|
||||||
@@ -51,13 +51,13 @@
|
|||||||
"knip": "npx -y knip@5 --include files",
|
"knip": "npx -y knip@5 --include files",
|
||||||
"knip:all": "npx -y knip@5 ",
|
"knip:all": "npx -y knip@5 ",
|
||||||
"prelint": "pnpm run -F=client predevelop",
|
"prelint": "pnpm run -F=client predevelop",
|
||||||
"lint": "NODE_OPTIONS=\"--max-old-space-size=7168\" npm-run-all create:shared -p lint:*",
|
"lint": "NODE_OPTIONS=\"--max-old-space-size=7168\" npm-run-all compile:ts -p lint:*",
|
||||||
"lint:challenges": "cd ./curriculum && pnpm run lint",
|
"lint:challenges": "cd ./curriculum && pnpm run lint",
|
||||||
"lint:js": "eslint --cache --max-warnings 0 .",
|
"lint:js": "eslint --cache --max-warnings 0 .",
|
||||||
"lint:ts": "tsc && tsc -p shared && tsc -p api && tsc -p client",
|
"lint:ts": "tsc && tsc -p shared && tsc -p api && tsc -p client && tsc -p curriculum",
|
||||||
"lint:prettier": "prettier --list-different .",
|
"lint:prettier": "prettier --list-different .",
|
||||||
"lint:css": "stylelint '**/*.css'",
|
"lint:css": "stylelint '**/*.css'",
|
||||||
"preseed": "npm-run-all create:shared",
|
"preseed": "npm-run-all compile:ts",
|
||||||
"playwright:install-build-tools": "npx playwright install --with-deps",
|
"playwright:install-build-tools": "npx playwright install --with-deps",
|
||||||
"rename-challenges": "tsx tools/challenge-helper-scripts/rename-challenge-files.ts",
|
"rename-challenges": "tsx tools/challenge-helper-scripts/rename-challenge-files.ts",
|
||||||
"seed": "pnpm seed:surveys && pnpm seed:exams && DEBUG=fcc:* node ./tools/scripts/seed/seed-demo-user",
|
"seed": "pnpm seed:surveys && pnpm seed:exams && DEBUG=fcc:* node ./tools/scripts/seed/seed-demo-user",
|
||||||
@@ -69,8 +69,8 @@
|
|||||||
"seed:ms-username": "DEBUG=fcc:* node ./tools/scripts/seed/seed-ms-username",
|
"seed:ms-username": "DEBUG=fcc:* node ./tools/scripts/seed/seed-ms-username",
|
||||||
"serve:client": "cd ./client && pnpm run serve",
|
"serve:client": "cd ./client && pnpm run serve",
|
||||||
"serve:client-ci": "cd ./client && pnpm run serve-ci",
|
"serve:client-ci": "cd ./client && pnpm run serve-ci",
|
||||||
"start": "npm-run-all create:shared -p develop:server serve:client",
|
"start": "npm-run-all compile:ts -p develop:server serve:client",
|
||||||
"test": "NODE_OPTIONS='--max-old-space-size=7168' run-s create:shared build:curriculum build-workers test:**",
|
"test": "NODE_OPTIONS='--max-old-space-size=7168' run-s compile:ts build:curriculum build-workers test:**",
|
||||||
"test:api": "cd api && pnpm test",
|
"test:api": "cd api && pnpm test",
|
||||||
"test:tools:challenge-helper-scripts": "cd ./tools/challenge-helper-scripts && pnpm test run",
|
"test:tools:challenge-helper-scripts": "cd ./tools/challenge-helper-scripts && pnpm test run",
|
||||||
"test:tools:scripts-build": "cd ./tools/scripts/build && pnpm test run",
|
"test:tools:scripts-build": "cd ./tools/scripts/build && pnpm test run",
|
||||||
|
|||||||
27
pnpm-lock.yaml
generated
27
pnpm-lock.yaml
generated
@@ -712,6 +712,15 @@ importers:
|
|||||||
'@babel/register':
|
'@babel/register':
|
||||||
specifier: 7.23.7
|
specifier: 7.23.7
|
||||||
version: 7.23.7(@babel/core@7.23.7)
|
version: 7.23.7(@babel/core@7.23.7)
|
||||||
|
'@total-typescript/ts-reset':
|
||||||
|
specifier: ^0.6.1
|
||||||
|
version: 0.6.1
|
||||||
|
'@types/debug':
|
||||||
|
specifier: ^4.1.12
|
||||||
|
version: 4.1.12
|
||||||
|
'@types/js-yaml':
|
||||||
|
specifier: 4.0.5
|
||||||
|
version: 4.0.5
|
||||||
'@types/polka':
|
'@types/polka':
|
||||||
specifier: ^0.5.7
|
specifier: ^0.5.7
|
||||||
version: 0.5.7
|
version: 0.5.7
|
||||||
@@ -4434,6 +4443,9 @@ packages:
|
|||||||
'@total-typescript/ts-reset@0.5.1':
|
'@total-typescript/ts-reset@0.5.1':
|
||||||
resolution: {integrity: sha512-AqlrT8YA1o7Ff5wPfMOL0pvL+1X+sw60NN6CcOCqs658emD6RfiXhF7Gu9QcfKBH7ELY2nInLhKSCWVoNL70MQ==}
|
resolution: {integrity: sha512-AqlrT8YA1o7Ff5wPfMOL0pvL+1X+sw60NN6CcOCqs658emD6RfiXhF7Gu9QcfKBH7ELY2nInLhKSCWVoNL70MQ==}
|
||||||
|
|
||||||
|
'@total-typescript/ts-reset@0.6.1':
|
||||||
|
resolution: {integrity: sha512-cka47fVSo6lfQDIATYqb/vO1nvFfbPw7uWLayIXIhGETj0wcOOlrlkobOMDNQOFr9QOafegUPq13V2+6vtD7yg==}
|
||||||
|
|
||||||
'@trysound/sax@0.2.0':
|
'@trysound/sax@0.2.0':
|
||||||
resolution: {integrity: sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==}
|
resolution: {integrity: sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==}
|
||||||
engines: {node: '>=10.13.0'}
|
engines: {node: '>=10.13.0'}
|
||||||
@@ -4515,6 +4527,9 @@ packages:
|
|||||||
'@types/debug@0.0.30':
|
'@types/debug@0.0.30':
|
||||||
resolution: {integrity: sha512-orGL5LXERPYsLov6CWs3Fh6203+dXzJkR7OnddIr2514Hsecwc8xRpzCapshBbKFImCsvS/mk6+FWiN5LyZJAQ==}
|
resolution: {integrity: sha512-orGL5LXERPYsLov6CWs3Fh6203+dXzJkR7OnddIr2514Hsecwc8xRpzCapshBbKFImCsvS/mk6+FWiN5LyZJAQ==}
|
||||||
|
|
||||||
|
'@types/debug@4.1.12':
|
||||||
|
resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==}
|
||||||
|
|
||||||
'@types/debug@4.1.9':
|
'@types/debug@4.1.9':
|
||||||
resolution: {integrity: sha512-8Hz50m2eoS56ldRlepxSBa6PWEVCtzUo/92HgLc2qTMnotJNIm7xP+UZhyWoYsyOdd5dxZ+NZLb24rsKyFs2ow==}
|
resolution: {integrity: sha512-8Hz50m2eoS56ldRlepxSBa6PWEVCtzUo/92HgLc2qTMnotJNIm7xP+UZhyWoYsyOdd5dxZ+NZLb24rsKyFs2ow==}
|
||||||
|
|
||||||
@@ -19127,6 +19142,8 @@ snapshots:
|
|||||||
|
|
||||||
'@total-typescript/ts-reset@0.5.1': {}
|
'@total-typescript/ts-reset@0.5.1': {}
|
||||||
|
|
||||||
|
'@total-typescript/ts-reset@0.6.1': {}
|
||||||
|
|
||||||
'@trysound/sax@0.2.0': {}
|
'@trysound/sax@0.2.0': {}
|
||||||
|
|
||||||
'@turist/fetch@7.2.0(node-fetch@2.7.0)':
|
'@turist/fetch@7.2.0(node-fetch@2.7.0)':
|
||||||
@@ -19231,6 +19248,10 @@ snapshots:
|
|||||||
|
|
||||||
'@types/debug@0.0.30': {}
|
'@types/debug@0.0.30': {}
|
||||||
|
|
||||||
|
'@types/debug@4.1.12':
|
||||||
|
dependencies:
|
||||||
|
'@types/ms': 0.7.32
|
||||||
|
|
||||||
'@types/debug@4.1.9':
|
'@types/debug@4.1.9':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/ms': 0.7.32
|
'@types/ms': 0.7.32
|
||||||
@@ -26857,7 +26878,7 @@ snapshots:
|
|||||||
|
|
||||||
micromark@3.2.0:
|
micromark@3.2.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/debug': 4.1.9
|
'@types/debug': 4.1.12
|
||||||
debug: 4.3.4(supports-color@8.1.1)
|
debug: 4.3.4(supports-color@8.1.1)
|
||||||
decode-named-character-reference: 1.0.2
|
decode-named-character-reference: 1.0.2
|
||||||
micromark-core-commonmark: 1.1.0
|
micromark-core-commonmark: 1.1.0
|
||||||
@@ -31148,6 +31169,7 @@ snapshots:
|
|||||||
vite-node: 3.2.4(@types/node@20.12.8)(jiti@2.6.1)(terser@5.28.1)(tsx@4.19.1)(yaml@2.8.0)
|
vite-node: 3.2.4(@types/node@20.12.8)(jiti@2.6.1)(terser@5.28.1)(tsx@4.19.1)(yaml@2.8.0)
|
||||||
why-is-node-running: 2.3.0
|
why-is-node-running: 2.3.0
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
|
'@types/debug': 4.1.12
|
||||||
'@types/node': 20.12.8
|
'@types/node': 20.12.8
|
||||||
'@vitest/ui': 3.2.4(vitest@3.2.4)
|
'@vitest/ui': 3.2.4(vitest@3.2.4)
|
||||||
jsdom: 16.7.0
|
jsdom: 16.7.0
|
||||||
@@ -31191,6 +31213,7 @@ snapshots:
|
|||||||
vite-node: 3.2.4(@types/node@20.12.8)(jiti@2.6.1)(terser@5.28.1)(tsx@4.19.1)(yaml@2.8.0)
|
vite-node: 3.2.4(@types/node@20.12.8)(jiti@2.6.1)(terser@5.28.1)(tsx@4.19.1)(yaml@2.8.0)
|
||||||
why-is-node-running: 2.3.0
|
why-is-node-running: 2.3.0
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
|
'@types/debug': 4.1.12
|
||||||
'@types/node': 20.12.8
|
'@types/node': 20.12.8
|
||||||
'@vitest/ui': 3.2.4(vitest@3.2.4)
|
'@vitest/ui': 3.2.4(vitest@3.2.4)
|
||||||
jsdom: 26.1.0
|
jsdom: 26.1.0
|
||||||
@@ -31234,6 +31257,7 @@ snapshots:
|
|||||||
vite-node: 3.2.4(@types/node@20.12.8)(jiti@2.6.1)(terser@5.28.1)(tsx@4.19.1)(yaml@2.8.0)
|
vite-node: 3.2.4(@types/node@20.12.8)(jiti@2.6.1)(terser@5.28.1)(tsx@4.19.1)(yaml@2.8.0)
|
||||||
why-is-node-running: 2.3.0
|
why-is-node-running: 2.3.0
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
|
'@types/debug': 4.1.12
|
||||||
'@types/node': 20.12.8
|
'@types/node': 20.12.8
|
||||||
'@vitest/ui': 3.2.4(vitest@3.2.4)
|
'@vitest/ui': 3.2.4(vitest@3.2.4)
|
||||||
jsdom: 26.1.0
|
jsdom: 26.1.0
|
||||||
@@ -31277,6 +31301,7 @@ snapshots:
|
|||||||
vite-node: 3.2.4(@types/node@20.12.8)(jiti@2.6.1)(terser@5.28.1)(tsx@4.19.1)(yaml@2.8.0)
|
vite-node: 3.2.4(@types/node@20.12.8)(jiti@2.6.1)(terser@5.28.1)(tsx@4.19.1)(yaml@2.8.0)
|
||||||
why-is-node-running: 2.3.0
|
why-is-node-running: 2.3.0
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
|
'@types/debug': 4.1.12
|
||||||
'@types/node': 20.12.8
|
'@types/node': 20.12.8
|
||||||
'@vitest/ui': 3.2.4(vitest@3.2.4)
|
'@vitest/ui': 3.2.4(vitest@3.2.4)
|
||||||
jsdom: 26.1.0
|
jsdom: 26.1.0
|
||||||
|
|||||||
@@ -47,6 +47,10 @@ export enum Certification {
|
|||||||
LegacyFullStack = 'full-stack'
|
LegacyFullStack = 'full-stack'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isCertification(x: string): x is Certification {
|
||||||
|
return Object.values(Certification).includes(x as Certification);
|
||||||
|
}
|
||||||
|
|
||||||
// "Current" certifications are the subset of standard certifications that are
|
// "Current" certifications are the subset of standard certifications that are
|
||||||
// live and not legacy.
|
// live and not legacy.
|
||||||
export const currentCertifications = [
|
export const currentCertifications = [
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
"extends": "../tsconfig-base.json",
|
"extends": "../tsconfig-base.json",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"outDir": "../shared-dist",
|
"outDir": "../shared-dist",
|
||||||
"declaration": true,
|
"composite": true,
|
||||||
"noEmit": false,
|
"noEmit": false,
|
||||||
"module": "CommonJS"
|
"module": "CommonJS"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ const envPath = resolve(__dirname, '../../.env');
|
|||||||
config({ path: envPath });
|
config({ path: envPath });
|
||||||
|
|
||||||
import { availableLangs } from '../../shared/config/i18n';
|
import { availableLangs } from '../../shared/config/i18n';
|
||||||
import { getChallengesForLang } from '../../curriculum/get-challenges';
|
import { getChallengesForLang } from '../../curriculum/src/get-challenges';
|
||||||
import {
|
import {
|
||||||
SuperBlocks,
|
SuperBlocks,
|
||||||
getAuditedSuperBlocks
|
getAuditedSuperBlocks
|
||||||
|
|||||||
@@ -15,8 +15,8 @@ import {
|
|||||||
getContentConfig,
|
getContentConfig,
|
||||||
writeBlockStructure,
|
writeBlockStructure,
|
||||||
getSuperblockStructure
|
getSuperblockStructure
|
||||||
} from '../../curriculum/file-handler';
|
} from '../../curriculum/src/file-handler';
|
||||||
import { superBlockToFilename } from '../../curriculum/build-curriculum';
|
import { superBlockToFilename } from '../../curriculum/src/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 {
|
import {
|
||||||
|
|||||||
@@ -13,8 +13,8 @@ import { BlockLayouts, BlockTypes } from '../../shared/config/blocks';
|
|||||||
import {
|
import {
|
||||||
getContentConfig,
|
getContentConfig,
|
||||||
writeBlockStructure
|
writeBlockStructure
|
||||||
} from '../../curriculum/file-handler';
|
} from '../../curriculum/src/file-handler';
|
||||||
import { superBlockToFilename } from '../../curriculum/build-curriculum';
|
import { superBlockToFilename } from '../../curriculum/src/build-curriculum';
|
||||||
import {
|
import {
|
||||||
createQuizFile,
|
createQuizFile,
|
||||||
createStepFile,
|
createStepFile,
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ import { SuperBlocks } from '../../shared/config/curriculum';
|
|||||||
import {
|
import {
|
||||||
getContentConfig,
|
getContentConfig,
|
||||||
writeBlockStructure
|
writeBlockStructure
|
||||||
} from '../../curriculum/file-handler';
|
} from '../../curriculum/src/file-handler';
|
||||||
import { superBlockToFilename } from '../../curriculum/build-curriculum';
|
import { superBlockToFilename } from '../../curriculum/src/build-curriculum';
|
||||||
import { createQuizFile, getAllBlocks, validateBlockName } from './utils';
|
import { createQuizFile, getAllBlocks, 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';
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import ObjectID from 'bson-objectid';
|
|||||||
import {
|
import {
|
||||||
getBlockStructure,
|
getBlockStructure,
|
||||||
writeBlockStructure
|
writeBlockStructure
|
||||||
} from '../../curriculum/file-handler';
|
} from '../../curriculum/src/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 { getBlock, type Meta } from './helpers/project-metadata';
|
import { getBlock, type Meta } from './helpers/project-metadata';
|
||||||
|
|||||||
@@ -2,13 +2,13 @@ import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|||||||
import {
|
import {
|
||||||
getSuperblockStructure,
|
getSuperblockStructure,
|
||||||
writeSuperblockStructure
|
writeSuperblockStructure
|
||||||
} from '../../../curriculum/file-handler';
|
} from '../../../curriculum/src/file-handler';
|
||||||
import {
|
import {
|
||||||
updateChapterModuleSuperblockStructure,
|
updateChapterModuleSuperblockStructure,
|
||||||
updateSimpleSuperblockStructure
|
updateSimpleSuperblockStructure
|
||||||
} from './create-project';
|
} from './create-project';
|
||||||
|
|
||||||
vi.mock('../../../curriculum/file-handler');
|
vi.mock('../../../curriculum/src/file-handler');
|
||||||
|
|
||||||
const mockGetSuperblockStructure = vi.mocked(getSuperblockStructure);
|
const mockGetSuperblockStructure = vi.mocked(getSuperblockStructure);
|
||||||
const mockWriteSuperblockStructure = vi.mocked(writeSuperblockStructure);
|
const mockWriteSuperblockStructure = vi.mocked(writeSuperblockStructure);
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
import {
|
import {
|
||||||
getSuperblockStructure,
|
getSuperblockStructure,
|
||||||
writeSuperblockStructure
|
writeSuperblockStructure
|
||||||
} from '../../../curriculum/file-handler';
|
} from '../../../curriculum/src/file-handler';
|
||||||
import { insertInto } from './utils';
|
import { insertInto } from './utils';
|
||||||
|
|
||||||
export async function updateSimpleSuperblockStructure(
|
export async function updateSimpleSuperblockStructure(
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { join } from 'path';
|
import { join } from 'path';
|
||||||
import { describe, it, expect, vi } from 'vitest';
|
import { describe, it, expect, vi } from 'vitest';
|
||||||
import { getBlockStructure } from '../../../curriculum/file-handler';
|
import { getBlockStructure } from '../../../curriculum/src/file-handler';
|
||||||
import { getMetaData } from './project-metadata';
|
import { getMetaData } from './project-metadata';
|
||||||
|
|
||||||
vi.mock('../../../curriculum/file-handler');
|
vi.mock('../../../curriculum/src/file-handler');
|
||||||
|
|
||||||
const commonPath = join('curriculum', 'challenges', 'blocks');
|
const commonPath = join('curriculum', 'challenges', 'blocks');
|
||||||
const block = 'block-name';
|
const block = 'block-name';
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import path from 'path';
|
|||||||
import {
|
import {
|
||||||
getBlockStructure,
|
getBlockStructure,
|
||||||
writeBlockStructure
|
writeBlockStructure
|
||||||
} from '../../../curriculum/file-handler';
|
} from '../../../curriculum/src/file-handler';
|
||||||
import { getProjectPath } from './get-project-info';
|
import { getProjectPath } from './get-project-info';
|
||||||
|
|
||||||
export type Meta = {
|
export type Meta = {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import matter from 'gray-matter';
|
|||||||
import { uniq } from 'lodash';
|
import { uniq } from 'lodash';
|
||||||
|
|
||||||
import { challengeTypes } from '../../shared/config/challenge-types';
|
import { challengeTypes } from '../../shared/config/challenge-types';
|
||||||
import { parseCurriculumStructure } from '../../curriculum/build-curriculum';
|
import { parseCurriculumStructure } from '../../curriculum/src/build-curriculum';
|
||||||
import { parseMDSync } from '../challenge-parser/parser';
|
import { parseMDSync } from '../challenge-parser/parser';
|
||||||
import { getMetaData, updateMetaData } from './helpers/project-metadata';
|
import { getMetaData, updateMetaData } from './helpers/project-metadata';
|
||||||
import { getProjectPath } from './helpers/get-project-info';
|
import { getProjectPath } from './helpers/get-project-info';
|
||||||
|
|||||||
40
tools/challenge-parser/parser/index.d.ts
vendored
Normal file
40
tools/challenge-parser/parser/index.d.ts
vendored
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
// TypeScript declaration file for challenge-parser/parser
|
||||||
|
// This module exports functions to parse challenge markdown files
|
||||||
|
|
||||||
|
type ChallengeFile = {
|
||||||
|
name: string;
|
||||||
|
contents: string;
|
||||||
|
ext: string;
|
||||||
|
editableRegionBoundaries: number[];
|
||||||
|
head?: string;
|
||||||
|
tail?: string;
|
||||||
|
};
|
||||||
|
export interface ParsedChallenge {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
challengeType: number;
|
||||||
|
description?: string;
|
||||||
|
instructions?: string;
|
||||||
|
questions?: string[];
|
||||||
|
challengeFiles?: ChallengeFile[];
|
||||||
|
solutions?: {
|
||||||
|
contents: string;
|
||||||
|
ext: string;
|
||||||
|
name: string;
|
||||||
|
}[][];
|
||||||
|
[key: string]: unknown; // Allow for additional properties that may be added by plugins
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses a markdown challenge file asynchronously
|
||||||
|
* @param filename - Path to the markdown file to parse
|
||||||
|
* @returns Promise that resolves to the parsed challenge data
|
||||||
|
*/
|
||||||
|
export function parseMD(filename: string): Promise<ParsedChallenge>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses a markdown challenge file synchronously
|
||||||
|
* @param filename - Path to the markdown file to parse
|
||||||
|
* @returns The parsed challenge data
|
||||||
|
*/
|
||||||
|
export function parseMDSync(filename: string): ParsedChallenge;
|
||||||
40
tools/challenge-parser/translation-parser/index.d.ts
vendored
Normal file
40
tools/challenge-parser/translation-parser/index.d.ts
vendored
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
export interface ChallengeFile {
|
||||||
|
contents: string;
|
||||||
|
ext: string;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Challenge {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
challengeFiles?: ChallengeFile[];
|
||||||
|
[key: string]: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CommentDictionary {
|
||||||
|
[comment: string]: {
|
||||||
|
[lang: string]: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function translateComments(
|
||||||
|
text: string,
|
||||||
|
lang: string,
|
||||||
|
dict: CommentDictionary,
|
||||||
|
codeLang: string
|
||||||
|
): { text: string };
|
||||||
|
|
||||||
|
export function translateCommentsInChallenge(
|
||||||
|
challenge: Challenge,
|
||||||
|
lang: string,
|
||||||
|
dict: CommentDictionary
|
||||||
|
): Challenge;
|
||||||
|
|
||||||
|
export function translateGeneric(
|
||||||
|
input: { text: string },
|
||||||
|
config: {
|
||||||
|
knownComments: string[];
|
||||||
|
dict: CommentDictionary;
|
||||||
|
lang: string;
|
||||||
|
}
|
||||||
|
): { text: string };
|
||||||
@@ -20,7 +20,9 @@ exports.translateCommentsInChallenge = (challenge, lang, dict) => {
|
|||||||
if (challClone?.challengeFiles) {
|
if (challClone?.challengeFiles) {
|
||||||
challClone.challengeFiles.forEach(challengeFile => {
|
challClone.challengeFiles.forEach(challengeFile => {
|
||||||
if (challengeFile.contents) {
|
if (challengeFile.contents) {
|
||||||
let { text } = this.translateComments(
|
// It cannot be this.translateComments because 'this' does not exist
|
||||||
|
// when imported into an ES module.
|
||||||
|
let { text } = exports.translateComments(
|
||||||
challengeFile.contents,
|
challengeFile.contents,
|
||||||
lang,
|
lang,
|
||||||
dict,
|
dict,
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
const chokidar = require('chokidar');
|
const chokidar = require('chokidar');
|
||||||
const { getSuperblockStructure } = require('../../../curriculum/file-handler');
|
const {
|
||||||
|
getSuperblockStructure
|
||||||
|
} = require('../../../curriculum/dist/file-handler');
|
||||||
const {
|
const {
|
||||||
superBlockToFilename
|
superBlockToFilename
|
||||||
} = require('../../../curriculum/build-curriculum');
|
} = require('../../../curriculum/dist/build-curriculum');
|
||||||
|
|
||||||
const { createChallengeNode } = require('./create-challenge-nodes');
|
const { createChallengeNode } = require('./create-challenge-nodes');
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
|
|
||||||
import { getChallengesForLang } from '../../../curriculum/get-challenges';
|
import { getChallengesForLang } from '../../../curriculum/src/get-challenges';
|
||||||
import {
|
import {
|
||||||
buildExtCurriculumDataV1,
|
buildExtCurriculumDataV1,
|
||||||
type Curriculum as CurriculumV1,
|
type Curriculum as CurriculumV1,
|
||||||
@@ -13,7 +13,7 @@ import {
|
|||||||
type CurriculumProps as CurriculumPropsV2
|
type CurriculumProps as CurriculumPropsV2
|
||||||
} from './build-external-curricula-data-v2';
|
} from './build-external-curricula-data-v2';
|
||||||
|
|
||||||
const globalConfigPath = path.resolve(__dirname, '../../../shared/config');
|
const globalConfigPath = path.resolve(__dirname, '../../../shared-dist/config');
|
||||||
|
|
||||||
// We are defaulting to English because the ids for the challenges are same
|
// We are defaulting to English because the ids for the challenges are same
|
||||||
// across all languages.
|
// across all languages.
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { submitTypes } from '../../../shared-dist/config/challenge-types';
|
|||||||
import { type ChallengeNode } from '../../../client/src/redux/prop-types';
|
import { type ChallengeNode } from '../../../client/src/redux/prop-types';
|
||||||
import { SuperBlocks } from '../../../shared-dist/config/curriculum';
|
import { SuperBlocks } from '../../../shared-dist/config/curriculum';
|
||||||
import type { Chapter } from '../../../shared-dist/config/chapters';
|
import type { Chapter } from '../../../shared-dist/config/chapters';
|
||||||
import { getSuperblockStructure } from '../../../curriculum/build-curriculum';
|
import { getSuperblockStructure } from '../../../curriculum/src/file-handler';
|
||||||
|
|
||||||
export type CurriculumIntros =
|
export type CurriculumIntros =
|
||||||
| BlockBasedCurriculumIntros
|
| BlockBasedCurriculumIntros
|
||||||
|
|||||||
Reference in New Issue
Block a user