mirror of
https://github.com/freeCodeCamp/freeCodeCamp.git
synced 2026-09-24 23:01:18 +08:00
fix: make superblock data paths match normal paths (#46406)
This commit is contained in:
Generated
+8
-3
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
}
|
||||
@@ -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<string, unknown>;
|
||||
}
|
||||
|
||||
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 = <Curriculum>{};
|
||||
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<string, unknown>): 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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
@@ -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');
|
||||
});
|
||||
}
|
||||
@@ -20,6 +20,7 @@
|
||||
"main": "none",
|
||||
"devDependencies": {
|
||||
"debug": "4.3.2",
|
||||
"dotenv": "10.0.0"
|
||||
"dotenv": "10.0.0",
|
||||
"readdirp": "^3.6.0"
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user