diff --git a/client/tools/external-curriculum/build-external-curricula-data-v1.test.ts b/client/tools/external-curriculum/build-external-curricula-data-v1.test.ts deleted file mode 100644 index 784309ced45..00000000000 --- a/client/tools/external-curriculum/build-external-curricula-data-v1.test.ts +++ /dev/null @@ -1,173 +0,0 @@ -import path from 'path'; -import fs from 'fs'; - -import readdirp from 'readdirp'; -import { describe, test, expect } from 'vitest'; - -import intros from '../../i18n/locales/english/intro.json'; -import { - SuperBlocks, - SuperBlockStage, - superBlockStages -} from '@freecodecamp/shared/config/curriculum'; -import { - superblockSchemaValidator, - availableSuperBlocksValidator -} from './external-data-schema-v1'; -import { - type Curriculum, - type GeneratedCurriculumProps, - orderedSuperBlockInfo -} from './build-external-curricula-data-v1'; - -const VERSION = 'v1'; - -describe('external curriculum data build', () => { - const clientStaticPath = path.resolve(__dirname, '../../../client/static'); - - const validateSuperBlock = superblockSchemaValidator(); - - test("the external curriculum data should be in the client's static directory", () => { - expect( - fs.existsSync(`${clientStaticPath}/curriculum-data/${VERSION}`) - ).toBe(true); - - expect( - fs.readdirSync(`${clientStaticPath}/curriculum-data/${VERSION}`).length - ).toBeGreaterThan(0); - }); - - test('there should be an endpoint to request submit types from', () => { - expect( - fs.existsSync(`${clientStaticPath}/curriculum-data/submit-types.json`) - ).toBeTruthy(); - }); - - test('the available-superblocks file should have the correct structure', async () => { - const validateAvailableSuperBlocks = availableSuperBlocksValidator(); - const availableSuperblocks: unknown = JSON.parse( - await fs.promises.readFile( - `${clientStaticPath}/curriculum-data/${VERSION}/available-superblocks.json`, - 'utf-8' - ) - ); - - const result = validateAvailableSuperBlocks(availableSuperblocks); - - expect(result.error?.details).toBeUndefined(); - expect(result.error).toBeFalsy(); - }); - - test('the super block files generated should have the correct schema', async () => { - const fileArray = ( - await readdirp.promise(`${clientStaticPath}/curriculum-data/${VERSION}`, { - directoryFilter: ['!challenges'], - fileFilter: entry => { - // path without extension: - const filePath = entry.path.replace(/\.json$/, ''); - // The directory contains super block files and other curriculum-related files. - // We're only interested in super block ones. - const superBlocks = Object.values(SuperBlocks); - return superBlocks.includes(filePath); - } - }) - ).map(file => file.path); - - expect(fileArray.length).toBeGreaterThan(0); - - fileArray.forEach(fileInArray => { - const fileContent = fs.readFileSync( - `${clientStaticPath}/curriculum-data/${VERSION}/${fileInArray}`, - 'utf-8' - ); - - const result = validateSuperBlock(JSON.parse(fileContent)); - - expect(result.error?.details).toBeUndefined(); - expect(result.error).toBeFalsy(); - }); - }); - - test('super blocks and blocks should have the correct data', async () => { - const superBlockFiles = ( - await readdirp.promise(`${clientStaticPath}/curriculum-data/${VERSION}`, { - directoryFilter: ['!challenges'], - fileFilter: entry => { - // path without extension: - const filePath = entry.path.replace(/\.json$/, ''); - // The directory contains super block files and other curriculum-related files. - // We're only interested in super block ones. - const superBlocks = Object.values(SuperBlocks); - return superBlocks.includes(filePath); - } - }) - ).map(file => file.path); - - expect(superBlockFiles.length).toBeGreaterThan(0); - - superBlockFiles.forEach(file => { - const fileContentJson = fs.readFileSync( - `${clientStaticPath}/curriculum-data/${VERSION}/${file}`, - 'utf-8' - ); - - const fileContent = JSON.parse( - fileContentJson - ) as Curriculum; - - const superBlock = Object.keys(fileContent)[0] as SuperBlocks; - - // Randomly pick a block to check its data. - const blocks = Object.keys(fileContent[superBlock].blocks); - const randomBlockIndex = Math.floor(Math.random() * blocks.length); - const randomBlock = blocks[randomBlockIndex]; - - expect(fileContent[superBlock].intro).toEqual(intros[superBlock].intro); - expect(fileContent[superBlock].blocks[randomBlock]?.desc).toEqual( - ( - intros[superBlock].blocks as unknown as Record< - string, - { intro: unknown } - > - )[randomBlock].intro - ); - }); - }); - - test('All public SuperBlocks should be present in the SuperBlock object', () => { - const dashedNames = orderedSuperBlockInfo.map( - ({ dashedName }) => dashedName - ); - - const publicSuperBlockNames = Object.entries(superBlockStages) - .filter(([key]) => { - const stage = Number(key) as SuperBlockStage; - return ( - stage !== SuperBlockStage.Next && - stage !== SuperBlockStage.Upcoming && - stage !== SuperBlockStage.Catalog && - stage !== SuperBlockStage.Core && - stage !== SuperBlockStage.Spanish && - stage !== SuperBlockStage.Chinese - ); - }) - .flatMap(([, superBlocks]) => superBlocks); - - expect(dashedNames).toEqual(expect.arrayContaining(publicSuperBlockNames)); - expect(Object.keys(orderedSuperBlockInfo)).toHaveLength( - publicSuperBlockNames.length - ); - }); - - test('challenge files should be created and in the correct directory', () => { - expect( - fs.existsSync(`${clientStaticPath}/curriculum-data/${VERSION}/challenges`) - ).toBe(true); - - expect( - fs.readdirSync( - `${clientStaticPath}/curriculum-data/${VERSION}/challenges` - ).length - ).toBeGreaterThan(0); - }); -}); diff --git a/client/tools/external-curriculum/build-external-curricula-data-v1.ts b/client/tools/external-curriculum/build-external-curricula-data-v1.ts deleted file mode 100644 index da83012d01b..00000000000 --- a/client/tools/external-curriculum/build-external-curricula-data-v1.ts +++ /dev/null @@ -1,161 +0,0 @@ -import { mkdirSync, writeFileSync, readFileSync } from 'fs'; -import { resolve, dirname } from 'path'; -import { omit } from 'lodash'; -import { submitTypes } from '@freecodecamp/shared/config/challenge-types'; -import { SuperBlocks } from '@freecodecamp/shared/config/curriculum'; -import { patchBlock } from './patches'; - -export type CurriculumIntros = { - [keyValue in SuperBlocks]: { - title: string; - intro: string[]; - blocks: Record; - }; -}; - -export type Curriculum = { - [keyValue in SuperBlocks]: T extends CurriculumProps - ? CurriculumProps - : GeneratedCurriculumProps; -}; - -export interface CurriculumProps { - intro: string[]; - blocks: Record>; -} - -export interface GeneratedCurriculumProps { - intro: string[]; - blocks: Record>>; -} - -interface Block { - desc: string[]; - intro: string[]; - challenges: T; - meta: Record; -} - -const ver = 'v1'; - -// NOTE: Please don't add new superblocks to this list as this version is being deprecated. -// New superblocks should be added to v2 of the external curriculum data at -// tools/scripts/build/build-external-curricula-data-v2.ts -export const orderedSuperBlockInfo = [ - { dashedName: SuperBlocks.RespWebDesignNew, public: true }, - { dashedName: SuperBlocks.DataAnalysisPy, public: true }, - { dashedName: SuperBlocks.MachineLearningPy, public: true }, - { dashedName: SuperBlocks.CollegeAlgebraPy, public: true }, - { dashedName: SuperBlocks.A2English, public: true }, - { dashedName: SuperBlocks.B1English, public: true }, - { dashedName: SuperBlocks.TheOdinProject, public: true }, - { dashedName: SuperBlocks.RespWebDesign, public: true }, - { dashedName: SuperBlocks.PythonForEverybody, public: true }, - { dashedName: SuperBlocks.JsAlgoDataStructNew, public: false }, - { dashedName: SuperBlocks.FrontEndDevLibs, public: false }, - { dashedName: SuperBlocks.DataVis, public: false }, - { dashedName: SuperBlocks.RelationalDb, public: false }, - { dashedName: SuperBlocks.BackEndDevApis, public: false }, - { dashedName: SuperBlocks.QualityAssurance, public: false }, - { dashedName: SuperBlocks.SciCompPy, public: false }, - { dashedName: SuperBlocks.InfoSec, public: false }, - { dashedName: SuperBlocks.FoundationalCSharp, public: false }, - { dashedName: SuperBlocks.CodingInterviewPrep, public: false }, - { dashedName: SuperBlocks.ProjectEuler, public: false }, - { dashedName: SuperBlocks.RosettaCode, public: false }, - { dashedName: SuperBlocks.JsAlgoDataStruct, public: false } -]; - -const dashedNames = orderedSuperBlockInfo.map(({ dashedName }) => dashedName); - -export function buildExtCurriculumDataV1( - curriculum: Curriculum -): void { - const staticFolderPath = resolve(__dirname, '../../../client/static'); - const dataPath = `${staticFolderPath}/curriculum-data/`; - const blockIntroPath = resolve( - __dirname, - '../../../client/i18n/locales/english/intro.json' - ); - const intros = JSON.parse( - readFileSync(blockIntroPath, 'utf-8') - ) as CurriculumIntros; - - mkdirSync(dataPath, { recursive: true }); - - parseCurriculumData(); - getSubmitTypes(); - - function parseCurriculumData() { - const superBlockKeys = Object.values(SuperBlocks).filter(x => - dashedNames.includes(x) - ); - - writeToFile('available-superblocks', { - superblocks: orderedSuperBlockInfo.map(x => ({ - ...x, - title: intros[x.dashedName].title - })) - }); - - for (const superBlockKey of superBlockKeys) { - const superBlock = >{}; - const blockNames = Object.keys(curriculum[superBlockKey].blocks); - - if (blockNames.length === 0) continue; - - superBlock[superBlockKey] = {}; - superBlock[superBlockKey].intro = intros[superBlockKey]['intro']; - superBlock[superBlockKey].blocks = {}; - - for (const blockName of blockNames) { - superBlock[superBlockKey]['blocks'][blockName] = < - Block> - >{}; - - const block = intros[superBlockKey]['blocks'][blockName]; - - if (!block) { - throw Error( - `Block ${blockName} not found in intros for ${superBlockKey}` - ); - } - - superBlock[superBlockKey]['blocks'][blockName]['desc'] = block['intro']; - - superBlock[superBlockKey]['blocks'][blockName]['challenges'] = - patchBlock( - omit(curriculum[superBlockKey]['blocks'][blockName]?.meta, [ - 'chapter', - 'module' - ]) - ); - - const blockChallenges = - curriculum[superBlockKey]['blocks'][blockName]?.challenges; - - for (const challenge of blockChallenges) { - const challengeId = challenge.id; - const challengePath = `challenges/${superBlockKey}/${blockName}/${challengeId}`; - - writeToFile(challengePath, challenge); - } - } - - writeToFile(superBlockKey, superBlock); - } - } - - function writeToFile(fileName: string, data: Record): void { - const filePath = `${dataPath}/${ver}/${fileName}.json`; - mkdirSync(dirname(filePath), { recursive: true }); - writeFileSync(filePath, JSON.stringify(data, null, 2)); - } - - function getSubmitTypes() { - writeFileSync( - `${dataPath}/submit-types.json`, - JSON.stringify(submitTypes, null, 2) - ); - } -} diff --git a/client/tools/external-curriculum/build.ts b/client/tools/external-curriculum/build.ts index 243b3183869..3a54294f153 100644 --- a/client/tools/external-curriculum/build.ts +++ b/client/tools/external-curriculum/build.ts @@ -8,11 +8,6 @@ const curriculum = JSON.parse( readFileSync(join(__dirname, CURRICULUM_PATH), 'utf-8') ); -import { - buildExtCurriculumDataV1, - Curriculum as CurriculumV1, - CurriculumProps as CurriculumPropsV1 -} from './build-external-curricula-data-v1'; import { buildExtCurriculumDataV2, Curriculum as CurriculumV2, @@ -29,6 +24,5 @@ if (isSelectiveBuild) { 'Skipping external curriculum build (selective build mode active)' ); } else { - buildExtCurriculumDataV1(curriculum as CurriculumV1); buildExtCurriculumDataV2(curriculum as CurriculumV2); } diff --git a/client/tools/external-curriculum/external-data-schema-v1.ts b/client/tools/external-curriculum/external-data-schema-v1.ts deleted file mode 100644 index c93be59251b..00000000000 --- a/client/tools/external-curriculum/external-data-schema-v1.ts +++ /dev/null @@ -1,93 +0,0 @@ -import Joi from 'joi'; -import { chapterBasedSuperBlocks } from '@freecodecamp/shared/config/curriculum'; - -const blockSchema = Joi.object({}).keys({ - desc: Joi.array().min(1), - challenges: Joi.object({}) - .keys({ - name: Joi.string().required(), - isUpcomingChange: Joi.bool().required(), - usesMultifileEditor: Joi.bool().optional(), - hasEditableBoundaries: Joi.bool().optional(), - dashedName: Joi.string().required(), - helpCategory: Joi.valid( - 'JavaScript', - 'HTML-CSS', - 'Python', - 'Backend Development', - 'C-Sharp', - 'English', - 'Odin', - 'Euler', - 'Rosetta' - ).required(), - order: Joi.number().required(), - template: Joi.string().allow(''), - required: Joi.array(), - superBlock: Joi.string().required(), - blockLayout: Joi.valid( - 'challenge-list', - 'challenge-grid', - 'dialogue-grid', - 'link', - 'project-list', - 'legacy-challenge-list', - 'legacy-link', - 'legacy-challenge-grid' - ).required(), - blockType: Joi.valid( - 'lecture', - 'workshop', - 'lab', - 'review', - 'quiz', - 'exam', - 'warm-up', - 'learn', - 'practice' - ).when('superBlock', { - is: chapterBasedSuperBlocks, - then: Joi.required(), - otherwise: Joi.optional() - }), - challengeOrder: Joi.array() - .items( - Joi.object({}).keys({ - id: Joi.string(), - title: Joi.string() - }) - ) - .min(1) - .required(), - disableLoopProtectTests: Joi.boolean(), - disableLoopProtectPreview: Joi.boolean(), - superOrder: Joi.number() - }) - .required() -}); - -const subSchema = Joi.object({}).keys({ - intro: Joi.array(), - blocks: Joi.object({}).pattern(Joi.string(), Joi.object().concat(blockSchema)) -}); - -const schema = Joi.object({}).pattern( - Joi.string(), - Joi.object().concat(subSchema) -); - -const availableSuperBlocksSchema = Joi.object({ - superblocks: Joi.array().items( - Joi.object({ - dashedName: Joi.string().required(), - title: Joi.string().required(), - public: Joi.bool().required() - }) - ) -}); - -export const superblockSchemaValidator = () => (superblock: unknown) => - schema.validate(superblock); - -export const availableSuperBlocksValidator = () => (data: unknown) => - availableSuperBlocksSchema.validate(data); diff --git a/client/tools/external-curriculum/patches.ts b/client/tools/external-curriculum/patches.ts deleted file mode 100644 index 742f3fe1fdf..00000000000 --- a/client/tools/external-curriculum/patches.ts +++ /dev/null @@ -1,7 +0,0 @@ -export const patchBlock = (meta: Record) => { - const { blockLabel, ...rest } = meta; - return { - ...rest, - blockType: blockLabel - }; -};