diff --git a/.gitignore b/.gitignore index c7d58b52a5f..c5ec8a11980 100644 --- a/.gitignore +++ b/.gitignore @@ -205,6 +205,7 @@ curriculum/dist curriculum/build client/static/_redirects client/static/mobile +client/static/curriculum-data ### UI Components ### tools/ui-components/dist diff --git a/tools/scripts/build/build-curriculum.js b/tools/scripts/build/build-curriculum.ts similarity index 53% rename from tools/scripts/build/build-curriculum.js rename to tools/scripts/build/build-curriculum.ts index a5d56f2a6e6..562d44275f4 100644 --- a/tools/scripts/build/build-curriculum.js +++ b/tools/scripts/build/build-curriculum.ts @@ -1,8 +1,11 @@ -const fs = require('fs'); -const path = require('path'); +import fs from 'fs'; +import path from 'path'; -const { getChallengesForLang } = require('../../../curriculum/getChallenges'); -const { buildMobileCurriculum } = require('./build-mobile-curriculum'); +import { getChallengesForLang } from '../../../curriculum/getChallenges'; +import { + buildExtCurriculumData, + Curriculum +} from './build-external-curricula-data'; const { CURRICULUM_LOCALE } = process.env; @@ -10,10 +13,10 @@ const globalConfigPath = path.resolve(__dirname, '../../../config'); // We are defaulting to English because the ids for the challenges are same // across all languages. -getChallengesForLang('english') - .then(result => { +void getChallengesForLang('english') + .then((result: Record) => { if (CURRICULUM_LOCALE === 'english') { - buildMobileCurriculum(result); + buildExtCurriculumData('v1.0.0', result as Curriculum); } return result; }) diff --git a/tools/scripts/build/build-external-curricula-data.ts b/tools/scripts/build/build-external-curricula-data.ts new file mode 100644 index 00000000000..5e2bf2b1f54 --- /dev/null +++ b/tools/scripts/build/build-external-curricula-data.ts @@ -0,0 +1,118 @@ +import { mkdirSync, writeFileSync, readFileSync } from 'fs'; +import { resolve } 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; + intro: string[]; +} + +interface CurriculumProps { + blocks: Record; +} + +interface Block { + desc: string[]; + intro: string[]; + challenges: Record; + meta: Record; +} + +export const superBlockMobileAppOrder = { + 'responsive-web-design': { public: true }, + '2022/responsive-web-design': { public: false }, + 'javascript-algorithms-and-data-structures': { public: true }, + '2022/javascript-algorithms-and-data-structures': { public: false }, + 'front-end-development-libraries': { public: false }, + 'data-visualization': { public: false }, + 'back-end-development-and-apis': { public: false }, + 'quality-assurance': { public: false }, + 'scientific-computing-with-python': { public: false }, + 'data-analysis-with-python': { public: false }, + 'information-security': { public: false }, + 'machine-learning-with-python': { public: false }, + 'coding-interview-prep': { public: false }, + 'relational-database': { public: false } +}; + +export function buildExtCurriculumData( + ver: string, + curriculum: Curriculum +): void { + const staticFolderPath = resolve(__dirname, '../../../client/static'); + const versionPath = `${staticFolderPath}/curriculum-data/${ver}`; + const blockIntroPath = resolve( + __dirname, + '../../../client/i18n/locales/english/intro.json' + ); + + mkdirSync(versionPath, { recursive: true }); + + parseCurriculumData(); + + function parseCurriculumData() { + const superBlockKeys = Object.values(SuperBlocks); + + writeToFile('availableSuperblocks', { + superblocks: [ + superBlockMobileAppOrder, + Object.values(SuperBlocks).map(superblock => + getSuperBlockName(superblock) + ) + ] + }); + + for (let i = 0; i < superBlockKeys.length; i++) { + const superBlock = {}; + const superBlockKey = Object.values(SuperBlocks)[i]; + const blockNames = Object.keys(curriculum[superBlockKeys[i]].blocks); + + if (blockNames.length === 0) continue; + + superBlock[superBlockKey] = {}; + superBlock[superBlockKey]['blocks'] = {}; + + for (let j = 0; j < blockNames.length; j++) { + superBlock[superBlockKey]['blocks'][blockNames[j]] = {}; + + superBlock[superBlockKey]['blocks'][blockNames[j]]['desc'] = + getBlockDescription(superBlockKey, blockNames[j]); + + superBlock[superBlockKey]['blocks'][blockNames[j]]['challenges'] = + curriculum[superBlockKey]['blocks'][blockNames[j]]['meta']; + } + + writeToFile(superBlockKeys[i].replace(/\//, '-'), superBlock); + } + } + + function writeToFile(fileName: string, data: Record): void { + mkdirSync(versionPath, { recursive: true }); + + const filePath = `${versionPath}/${fileName}.json`; + + writeFileSync(filePath, JSON.stringify(data, null, 2)); + } + + function getBlockDescription( + superBlockKeys: SuperBlockValues, + blockKey: string + ): string[] { + const intros = JSON.parse(readFileSync(blockIntroPath, 'utf-8')) as Intro; + + return intros[superBlockKeys]['blocks'][blockKey]['intro']; + } + + function getSuperBlockName(superBlockKeys: SuperBlockValues): string { + const superBlocks = JSON.parse( + readFileSync(blockIntroPath, 'utf-8') + ) as Intro; + + return superBlocks[superBlockKeys].title; + } +} diff --git a/tools/scripts/build/build-mobile-curriculum.js b/tools/scripts/build/build-mobile-curriculum.js deleted file mode 100644 index bdacb71e5fd..00000000000 --- a/tools/scripts/build/build-mobile-curriculum.js +++ /dev/null @@ -1,68 +0,0 @@ -const fs = require('fs'); -const path = require('path'); - -exports.buildMobileCurriculum = function buildMobileCurriculum(json) { - const mobileStaticPath = path.resolve(__dirname, '../../../client/static'); - const blockIntroPath = path.resolve( - __dirname, - '../../../client/i18n/locales/english/intro.json' - ); - - fs.mkdirSync(`${mobileStaticPath}/mobile`, { recursive: true }); - writeAndParseCurriculumJson(json); - - function writeAndParseCurriculumJson(curriculum) { - const superBlockKeys = Object.keys(curriculum).filter( - key => key !== 'certifications' - ); - - writeToFile('availableSuperblocks', { - // removing "/" as it will create an extra sub-path when accessed via an endpoint - - superblocks: [ - superBlockKeys.map(key => key.replace(/\//, '-')), - getSuperBlockNames(superBlockKeys) - ] - }); - - for (let i = 0; i < superBlockKeys.length; i++) { - const superBlock = {}; - const blockNames = Object.keys(curriculum[superBlockKeys[i]].blocks); - - if (blockNames.length === 0) continue; - - superBlock[superBlockKeys[i]] = {}; - superBlock[superBlockKeys[i]]['blocks'] = {}; - - for (let j = 0; j < blockNames.length; j++) { - superBlock[superBlockKeys[i]]['blocks'][blockNames[j]] = {}; - - superBlock[superBlockKeys[i]]['blocks'][blockNames[j]]['desc'] = - getBlockDescription(superBlockKeys[i], blockNames[j]); - - superBlock[superBlockKeys[i]]['blocks'][blockNames[j]]['challenges'] = - curriculum[superBlockKeys[i]]['blocks'][blockNames[j]]['meta']; - } - - writeToFile(superBlockKeys[i].replace(/\//, '-'), superBlock); - } - } - - function getBlockDescription(superBlockKey, blockKey) { - const intros = JSON.parse(fs.readFileSync(blockIntroPath)); - - return intros[superBlockKey]['blocks'][blockKey]['intro']; - } - - function getSuperBlockNames(superBlockKeys) { - const superBlocks = JSON.parse(fs.readFileSync(blockIntroPath)); - - return superBlockKeys.map(key => superBlocks[key].title); - } - - function writeToFile(fileName, json) { - const fullPath = `${mobileStaticPath}/mobile/${fileName}.json`; - fs.mkdirSync(path.dirname(fullPath), { recursive: true }); - fs.writeFileSync(fullPath, JSON.stringify(json, null, 2)); - } -}; diff --git a/tools/scripts/build/mobile-curriculum.test.js b/tools/scripts/build/mobile-curriculum.test.js deleted file mode 100644 index 7c4e8ee41ed..00000000000 --- a/tools/scripts/build/mobile-curriculum.test.js +++ /dev/null @@ -1,51 +0,0 @@ -const path = require('path'); -const fs = require('fs'); -const { AssertionError } = require('chai'); -const envData = require('../../../config/env.json'); -const { mobileSchemaValidator } = require('./mobileSchema'); - -if (envData.clientLocale == 'english' && !envData.showUpcomingChanges) { - 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}/mobile`)).toBe(true); - - expect( - fs.readdirSync(`${mobileStaticPath}/mobile`).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}/mobile`); - - fileArray - .filter(fileInArray => fileInArray !== 'availableSuperblocks.json') - .forEach(fileInArray => { - const fileContent = fs.readFileSync( - `${mobileStaticPath}/mobile/${fileInArray}` - ); - - const result = validateMobileSuperBlock(JSON.parse(fileContent)); - - if (result.error) { - throw new AssertionError(result.error, `file: ${fileInArray}`); - } - }); - }); - }); -} else { - describe.skip('Mobile curriculum is not localized', () => { - test.todo('localized tests'); - }); -} diff --git a/tools/scripts/build/mobile-curriculum.test.ts b/tools/scripts/build/mobile-curriculum.test.ts new file mode 100644 index 00000000000..f2f905999eb --- /dev/null +++ b/tools/scripts/build/mobile-curriculum.test.ts @@ -0,0 +1,72 @@ +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.0.0'; + + 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 !== 'availableSuperblocks.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', () => { + expect(Object.keys(superBlockMobileAppOrder)).toEqual( + expect.arrayContaining(Object.values(SuperBlocks)) + ); + expect(Object.keys(superBlockMobileAppOrder)).toHaveLength( + Object.values(SuperBlocks).length + ); + }); + }); +} else { + describe.skip('Mobile curriculum is not localized', () => { + test.todo('localized tests'); + }); +}