From 232b6d232adf1d4eed184872cf993c450f58bc75 Mon Sep 17 00:00:00 2001 From: Oliver Eyton-Williams Date: Fri, 24 Jun 2022 15:12:16 +0200 Subject: [PATCH] fix: make superblock data paths match normal paths (#46406) --- package-lock.json | 11 ++- .../build-external-curricula-data.test.ts | 97 +++++++++++++++++++ .../build/build-external-curricula-data.ts | 65 ++++++------- ...obileSchema.js => external-data-schema.js} | 18 +++- tools/scripts/build/mobile-curriculum.test.ts | 81 ---------------- tools/scripts/build/package.json | 3 +- 6 files changed, 153 insertions(+), 122 deletions(-) create mode 100644 tools/scripts/build/build-external-curricula-data.test.ts rename tools/scripts/build/{mobileSchema.js => external-data-schema.js} (65%) delete mode 100644 tools/scripts/build/mobile-curriculum.test.ts diff --git a/package-lock.json b/package-lock.json index d0d58f9de4d..b36dc1b7c37 100644 --- a/package-lock.json +++ b/package-lock.json @@ -44287,7 +44287,8 @@ }, "node_modules/readdirp": { "version": "3.6.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", "dependencies": { "picomatch": "^2.2.1" }, @@ -53744,7 +53745,8 @@ "license": "BSD-3-Clause", "devDependencies": { "debug": "4.3.2", - "dotenv": "10.0.0" + "dotenv": "10.0.0", + "readdirp": "^3.6.0" }, "engines": { "node": ">=16", @@ -56128,7 +56130,8 @@ "version": "file:tools/scripts/build", "requires": { "debug": "4.3.2", - "dotenv": "10.0.0" + "dotenv": "10.0.0", + "readdirp": "*" }, "dependencies": { "debug": { @@ -83946,6 +83949,8 @@ }, "readdirp": { "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", "requires": { "picomatch": "^2.2.1" } diff --git a/tools/scripts/build/build-external-curricula-data.test.ts b/tools/scripts/build/build-external-curricula-data.test.ts new file mode 100644 index 00000000000..04688b21cda --- /dev/null +++ b/tools/scripts/build/build-external-curricula-data.test.ts @@ -0,0 +1,97 @@ +import path from 'path'; +import fs from 'fs'; + +import readdirp from 'readdirp'; +import { AssertionError } from 'chai'; +import envData from '../../../config/env.json'; +import { SuperBlocks } from '../../../config/certification-settings'; +import { + superblockSchemaValidator, + availableSuperBlocksValidator +} from './external-data-schema'; +import { orderedSuperBlockInfo } from './build-external-curricula-data'; + +if (envData.clientLocale == 'english' && !envData.showUpcomingChanges) { + 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('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); + + if (result.error) { + throw new AssertionError( + result.error.toString(), + `file: available-superblocks.json` + ); + } + }); + + test('the files generated should have the correct schema', async () => { + const fileArray = ( + await readdirp.promise(`${clientStaticPath}/curriculum-data/${VERSION}`) + ).map(file => file.path); + + fileArray + .filter(fileInArray => fileInArray !== 'available-superblocks.json') + .forEach(fileInArray => { + const fileContent = fs.readFileSync( + `${clientStaticPath}/curriculum-data/${VERSION}/${fileInArray}`, + 'utf-8' + ); + + const result = validateSuperBlock(JSON.parse(fileContent)); + + if (result.error) { + throw new AssertionError( + result.error.toString(), + `file: ${fileInArray}` + ); + } + }); + }); + + test('All public SuperBlocks should be present in the SuperBlock object', () => { + const dashedNames = orderedSuperBlockInfo.map( + ({ dashedName }) => dashedName + ); + // TODO: this is a hack, we should have a single source of truth for the + // list of superblocks that are available. + const publicSuperBlockNames = Object.values(SuperBlocks).filter( + x => x !== '2022/javascript-algorithms-and-data-structures' + ); + + expect(dashedNames).toEqual( + expect.arrayContaining(publicSuperBlockNames) + ); + expect(Object.keys(orderedSuperBlockInfo)).toHaveLength( + publicSuperBlockNames.length + ); + }); + }); +} else { + describe.skip('External curriculum data is localized', () => { + test.todo('localized tests'); + }); +} diff --git a/tools/scripts/build/build-external-curricula-data.ts b/tools/scripts/build/build-external-curricula-data.ts index 7afb4367d8b..a6c718220d4 100644 --- a/tools/scripts/build/build-external-curricula-data.ts +++ b/tools/scripts/build/build-external-curricula-data.ts @@ -1,11 +1,9 @@ import { mkdirSync, writeFileSync, readFileSync } from 'fs'; -import { resolve } from 'path'; +import { resolve, dirname } from 'path'; import { SuperBlocks } from '../../../config/certification-settings'; type Intro = { [keyValue in SuperBlocks]: IntroProps }; export type Curriculum = { [keyValue in SuperBlocks]: CurriculumProps }; -type SuperBlockKeys = keyof typeof SuperBlocks; -type SuperBlockValues = typeof SuperBlocks[SuperBlockKeys]; interface IntroProps extends CurriculumProps { title: string; @@ -23,22 +21,24 @@ interface Block { meta: Record; } -export const superBlockMobileAppOrder = [ - { dashedName: '2022/responsive-web-design', public: false }, - { dashedName: 'responsive-web-design', public: true }, - { dashedName: 'javascript-algorithms-and-data-structures', public: true }, - { dashedName: 'front-end-development-libraries', public: false }, - { dashedName: 'data-visualization', public: false }, - { dashedName: 'back-end-development-and-apis', public: false }, - { dashedName: 'quality-assurance', public: false }, - { dashedName: 'scientific-computing-with-python', public: false }, - { dashedName: 'data-analysis-with-python', public: false }, - { dashedName: 'information-security', public: false }, - { dashedName: 'machine-learning-with-python', public: false }, - { dashedName: 'coding-interview-prep', public: false }, - { dashedName: 'relational-database', public: false } +export const orderedSuperBlockInfo = [ + { dashedName: SuperBlocks.RespWebDesignNew, public: false }, + { dashedName: SuperBlocks.RespWebDesign, public: true }, + { dashedName: SuperBlocks.JsAlgoDataStruct, public: true }, + { dashedName: SuperBlocks.FrontEndDevLibs, public: false }, + { dashedName: SuperBlocks.DataVis, public: false }, + { dashedName: SuperBlocks.BackEndDevApis, public: false }, + { dashedName: SuperBlocks.QualityAssurance, public: false }, + { dashedName: SuperBlocks.SciCompPy, public: false }, + { dashedName: SuperBlocks.DataAnalysisPy, public: false }, + { dashedName: SuperBlocks.InfoSec, public: false }, + { dashedName: SuperBlocks.MachineLearningPy, public: false }, + { dashedName: SuperBlocks.CodingInterviewPrep, public: false }, + { dashedName: SuperBlocks.RelationalDb, public: false } ]; +const dashedNames = orderedSuperBlockInfo.map(({ dashedName }) => dashedName); + export function buildExtCurriculumData( ver: string, curriculum: Curriculum @@ -55,21 +55,20 @@ export function buildExtCurriculumData( parseCurriculumData(); function parseCurriculumData() { - const superBlockKeys = Object.values(SuperBlocks); + const superBlockKeys = Object.values(SuperBlocks).filter(x => + dashedNames.includes(x) + ); writeToFile('available-superblocks', { - superblocks: [ - superBlockMobileAppOrder, - Object.values(SuperBlocks).map(superblock => - getSuperBlockName(superblock) - ) - ] + superblocks: orderedSuperBlockInfo.map(x => ({ + ...x, + title: getSuperBlockTitle(x.dashedName) + })) }); - for (let i = 0; i < superBlockKeys.length; i++) { + for (const superBlockKey of superBlockKeys) { const superBlock = {}; - const superBlockKey = Object.values(SuperBlocks)[i]; - const blockNames = Object.keys(curriculum[superBlockKeys[i]].blocks); + const blockNames = Object.keys(curriculum[superBlockKey].blocks); if (blockNames.length === 0) continue; @@ -86,20 +85,18 @@ export function buildExtCurriculumData( curriculum[superBlockKey]['blocks'][blockNames[j]]['meta']; } - writeToFile(superBlockKeys[i].replace(/\//, '-'), superBlock); + writeToFile(superBlockKey, superBlock); } } function writeToFile(fileName: string, data: Record): void { - mkdirSync(versionPath, { recursive: true }); - const filePath = `${versionPath}/${fileName}.json`; - + mkdirSync(dirname(filePath), { recursive: true }); writeFileSync(filePath, JSON.stringify(data, null, 2)); } function getBlockDescription( - superBlockKeys: SuperBlockValues, + superBlockKeys: SuperBlocks, blockKey: string ): string[] { const intros = JSON.parse(readFileSync(blockIntroPath, 'utf-8')) as Intro; @@ -107,11 +104,11 @@ export function buildExtCurriculumData( return intros[superBlockKeys]['blocks'][blockKey]['intro']; } - function getSuperBlockName(superBlockKeys: SuperBlockValues): string { + function getSuperBlockTitle(superBlock: SuperBlocks): string { const superBlocks = JSON.parse( readFileSync(blockIntroPath, 'utf-8') ) as Intro; - return superBlocks[superBlockKeys].title; + return superBlocks[superBlock].title; } } diff --git a/tools/scripts/build/mobileSchema.js b/tools/scripts/build/external-data-schema.js similarity index 65% rename from tools/scripts/build/mobileSchema.js rename to tools/scripts/build/external-data-schema.js index d51eae977ee..5072214e6f7 100644 --- a/tools/scripts/build/mobileSchema.js +++ b/tools/scripts/build/external-data-schema.js @@ -27,6 +27,18 @@ const schema = Joi.object({}).pattern( Joi.object().concat(subSchema) ); -exports.mobileSchemaValidator = () => { - return superblock => schema.validate(superblock); -}; +const availableSuperBlocksSchema = Joi.object({ + superblocks: Joi.array().items( + Joi.object({ + dashedName: Joi.string().required(), + title: Joi.string().required(), + public: Joi.bool().required() + }) + ) +}); + +exports.superblockSchemaValidator = () => superblock => + schema.validate(superblock); + +exports.availableSuperBlocksValidator = () => data => + availableSuperBlocksSchema.validate(data); diff --git a/tools/scripts/build/mobile-curriculum.test.ts b/tools/scripts/build/mobile-curriculum.test.ts deleted file mode 100644 index 1427777c796..00000000000 --- a/tools/scripts/build/mobile-curriculum.test.ts +++ /dev/null @@ -1,81 +0,0 @@ -import path from 'path'; -import fs from 'fs'; -import { AssertionError } from 'chai'; -import envData from '../../../config/env.json'; -import { SuperBlocks } from '../../../config/certification-settings'; -import { mobileSchemaValidator } from './mobileSchema'; -import { superBlockMobileAppOrder } from './build-external-curricula-data'; - -if (envData.clientLocale == 'english' && !envData.showUpcomingChanges) { - const VERSION = 'v1'; - - describe('mobile curriculum build', () => { - const mobileStaticPath = path.resolve(__dirname, '../../../client/static'); - const blockIntroPath = path.resolve( - __dirname, - '../../../client/i18n/locales/english/intro.json' - ); - - const validateMobileSuperBlock = mobileSchemaValidator(); - - test('the mobile curriculum should have a static folder with multiple files', () => { - expect( - fs.existsSync(`${mobileStaticPath}/curriculum-data/${VERSION}`) - ).toBe(true); - - expect( - fs.readdirSync(`${mobileStaticPath}/curriculum-data/${VERSION}`).length - ).toBeGreaterThan(0); - }); - - test('the mobile curriculum should have access to the intro.json file', () => { - expect(fs.existsSync(blockIntroPath)).toBe(true); - }); - - test('the files generated should have the correct schema', () => { - const fileArray = fs.readdirSync( - `${mobileStaticPath}/curriculum-data/${VERSION}` - ); - - fileArray - .filter(fileInArray => fileInArray !== 'available-superblocks.json') - .forEach(fileInArray => { - const fileContent = fs.readFileSync( - `${mobileStaticPath}/curriculum-data/${VERSION}/${fileInArray}`, - 'utf-8' - ); - - const result = validateMobileSuperBlock(JSON.parse(fileContent)); - - if (result.error) { - throw new AssertionError( - result.error.toString(), - `file: ${fileInArray}` - ); - } - }); - }); - - test('All SuperBlocks should be present in the mobile SuperBlock object', () => { - const dashedNames = superBlockMobileAppOrder.map( - ({ dashedName }) => dashedName - ); - // TODO: this is a hack, we should have a single source of truth for the - // list of superblocks that are available. - const publicSuperBlockNames = Object.values(SuperBlocks).filter( - x => x !== '2022/javascript-algorithms-and-data-structures' - ); - - expect(dashedNames).toEqual( - expect.arrayContaining(publicSuperBlockNames) - ); - expect(Object.keys(superBlockMobileAppOrder)).toHaveLength( - publicSuperBlockNames.length - ); - }); - }); -} else { - describe.skip('Mobile curriculum is not localized', () => { - test.todo('localized tests'); - }); -} diff --git a/tools/scripts/build/package.json b/tools/scripts/build/package.json index f795c1b2841..e12f76c777b 100644 --- a/tools/scripts/build/package.json +++ b/tools/scripts/build/package.json @@ -20,6 +20,7 @@ "main": "none", "devDependencies": { "debug": "4.3.2", - "dotenv": "10.0.0" + "dotenv": "10.0.0", + "readdirp": "^3.6.0" } }