mirror of
https://github.com/freeCodeCamp/freeCodeCamp.git
synced 2026-05-28 18:26:54 +00:00
refactor(curriculum): remove block name metadata and source titles from intro (#66415)
Co-authored-by: Oliver Eyton-Williams <ojeytonwilliams@gmail.com>
This commit is contained in:
@@ -31,6 +31,8 @@ import {
|
||||
updateChapterModuleSuperblockStructure
|
||||
} from './helpers/create-project.js';
|
||||
import { getLangFromSuperBlock } from './helpers/get-lang-from-superblock.js';
|
||||
import { parseIntroJson } from './helpers/parse-json.js';
|
||||
import { withTrace } from './helpers/utils.js';
|
||||
|
||||
const langToHelpCategory: Record<ChallengeLang, string> = {
|
||||
[ChallengeLang.English]: 'English',
|
||||
@@ -38,20 +40,6 @@ const langToHelpCategory: Record<ChallengeLang, string> = {
|
||||
[ChallengeLang.Spanish]: 'Spanish Curriculum'
|
||||
};
|
||||
|
||||
type BlockInfo = {
|
||||
title: string;
|
||||
intro: string[];
|
||||
};
|
||||
|
||||
type SuperBlockInfo = {
|
||||
blocks: Record<string, BlockInfo>;
|
||||
chapters?: Record<string, string>;
|
||||
modules?: Record<string, string>;
|
||||
'module-intros'?: Record<string, { intro: string[]; note: string }>;
|
||||
};
|
||||
|
||||
type IntroJson = Record<SuperBlocks, SuperBlockInfo>;
|
||||
|
||||
interface CreateBlockArgs {
|
||||
superBlock: SuperBlocks;
|
||||
block: string;
|
||||
@@ -164,7 +152,7 @@ async function updateIntroJson({
|
||||
__dirname,
|
||||
'../../client/i18n/locales/english/intro.json'
|
||||
);
|
||||
const newIntro = await parseJson<IntroJson>(introJsonPath);
|
||||
const newIntro = await parseIntroJson(introJsonPath);
|
||||
|
||||
newIntro[superBlock].blocks[block] = {
|
||||
title,
|
||||
@@ -215,7 +203,6 @@ async function createMetaJson(
|
||||
blockLayout?: string
|
||||
) {
|
||||
const newMeta = getBaseMeta('Language');
|
||||
newMeta.name = title;
|
||||
newMeta.dashedName = block;
|
||||
newMeta.helpCategory = helpCategory;
|
||||
|
||||
@@ -275,26 +262,6 @@ async function createQuizChallenge(
|
||||
});
|
||||
}
|
||||
|
||||
function parseJson<JsonSchema>(filePath: string) {
|
||||
return withTrace(fs.readFile, filePath, 'utf8').then(
|
||||
// unfortunately, withTrace does not correctly infer that the third argument
|
||||
// is a string, so it uses the (path, options?) overload and we have to cast
|
||||
// result to string.
|
||||
result => JSON.parse(result as string) as JsonSchema
|
||||
);
|
||||
}
|
||||
|
||||
// fs Promise functions return errors, but no stack trace. This adds back in
|
||||
// the stack trace.
|
||||
function withTrace<Args extends unknown[], Result>(
|
||||
fn: (...x: Args) => Promise<Result>,
|
||||
...args: Args
|
||||
): Promise<Result> {
|
||||
return fn(...args).catch((reason: Error) => {
|
||||
throw Error(reason.message);
|
||||
});
|
||||
}
|
||||
|
||||
function getBlockPrefix(
|
||||
superBlock: SuperBlocks,
|
||||
blockLabel?: BlockLabel
|
||||
|
||||
@@ -21,7 +21,7 @@ import {
|
||||
getAllBlocks
|
||||
} from './utils.js';
|
||||
import { getBaseMeta } from './helpers/get-base-meta.js';
|
||||
import { IntroJson, parseJson } from './helpers/parse-json.js';
|
||||
import { parseIntroJson } from './helpers/parse-json.js';
|
||||
import {
|
||||
ChapterModuleSuperblockStructure,
|
||||
updateChapterModuleSuperblockStructure,
|
||||
@@ -152,7 +152,7 @@ async function updateIntroJson(
|
||||
__dirname,
|
||||
'../../client/i18n/locales/english/intro.json'
|
||||
);
|
||||
const newIntro = await parseJson<IntroJson>(introJsonPath);
|
||||
const newIntro = await parseIntroJson(introJsonPath);
|
||||
newIntro[superBlock].blocks[block] = {
|
||||
title,
|
||||
intro: [title, '']
|
||||
@@ -186,7 +186,6 @@ async function createMetaJson(
|
||||
newMeta = getBaseMeta('Step');
|
||||
newMeta.order = order;
|
||||
}
|
||||
newMeta.name = title;
|
||||
newMeta.dashedName = block;
|
||||
newMeta.helpCategory = helpCategory;
|
||||
|
||||
|
||||
@@ -13,6 +13,8 @@ import { superBlockToFilename } from '@freecodecamp/curriculum/build-curriculum'
|
||||
import { createQuizFile, getAllBlocks, validateBlockName } from './utils.js';
|
||||
import { getBaseMeta } from './helpers/get-base-meta.js';
|
||||
import { updateSimpleSuperblockStructure } from './helpers/create-project.js';
|
||||
import { parseIntroJson } from './helpers/parse-json.js';
|
||||
import { withTrace } from './helpers/utils.js';
|
||||
|
||||
const helpCategories = [
|
||||
'HTML-CSS',
|
||||
@@ -21,17 +23,6 @@ const helpCategories = [
|
||||
'Python'
|
||||
] as const;
|
||||
|
||||
type BlockInfo = {
|
||||
title: string;
|
||||
intro: string[];
|
||||
};
|
||||
|
||||
type SuperBlockInfo = {
|
||||
blocks: Record<string, BlockInfo>;
|
||||
};
|
||||
|
||||
type IntroJson = Record<SuperBlocks, SuperBlockInfo>;
|
||||
|
||||
async function createQuiz(
|
||||
superBlock: SuperBlocks,
|
||||
block: string,
|
||||
@@ -62,7 +53,7 @@ async function updateIntroJson(
|
||||
__dirname,
|
||||
'../../client/i18n/locales/english/intro.json'
|
||||
);
|
||||
const newIntro = await parseJson<IntroJson>(introJsonPath);
|
||||
const newIntro = await parseIntroJson(introJsonPath);
|
||||
newIntro[superBlock].blocks[block] = {
|
||||
title,
|
||||
intro: ['', '']
|
||||
@@ -81,7 +72,6 @@ async function createMetaJson(
|
||||
challengeId: ObjectId
|
||||
) {
|
||||
const newMeta = getBaseMeta('Quiz');
|
||||
newMeta.name = title;
|
||||
newMeta.dashedName = block;
|
||||
newMeta.helpCategory = helpCategory;
|
||||
|
||||
@@ -109,26 +99,6 @@ async function createQuizChallenge({
|
||||
questionCount: questionCount
|
||||
});
|
||||
}
|
||||
function parseJson<JsonSchema>(filePath: string) {
|
||||
return withTrace(fs.readFile, filePath, 'utf8').then(
|
||||
// unfortunately, withTrace does not correctly infer that the third argument
|
||||
// is a string, so it uses the (path, options?) overload and we have to cast
|
||||
// result to string.
|
||||
result => JSON.parse(result as string) as JsonSchema
|
||||
);
|
||||
}
|
||||
|
||||
// fs Promise functions return errors, but no stack trace. This adds back in
|
||||
// the stack trace.
|
||||
function withTrace<Args extends unknown[], Result>(
|
||||
fn: (...x: Args) => Promise<Result>,
|
||||
...args: Args
|
||||
): Promise<Result> {
|
||||
return fn(...args).catch((reason: Error) => {
|
||||
throw Error(reason.message);
|
||||
});
|
||||
}
|
||||
|
||||
void getAllBlocks().then(async existingBlocks => {
|
||||
const superBlock = await select<SuperBlocks>({
|
||||
message: 'Which certification does this belong to?',
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
interface Meta {
|
||||
name: string;
|
||||
isUpcomingChange: boolean;
|
||||
dashedName: string;
|
||||
helpCategory: string;
|
||||
@@ -15,7 +14,6 @@ interface Meta {
|
||||
}
|
||||
|
||||
const baseMeta: Meta = {
|
||||
name: '',
|
||||
isUpcomingChange: true,
|
||||
dashedName: '',
|
||||
helpCategory: '',
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import fs from 'fs/promises';
|
||||
|
||||
import { SuperBlocks } from '@freecodecamp/shared/config/curriculum';
|
||||
import { withTrace } from './utils.js';
|
||||
|
||||
export type BlockInfo = {
|
||||
@@ -10,15 +9,25 @@ export type BlockInfo = {
|
||||
|
||||
export type SuperBlockInfo = {
|
||||
blocks: Record<string, BlockInfo>;
|
||||
chapters?: Record<string, string>;
|
||||
modules?: Record<string, string>;
|
||||
'module-intros'?: Record<
|
||||
string,
|
||||
{
|
||||
intro: string[];
|
||||
note?: string;
|
||||
title?: string;
|
||||
}
|
||||
>;
|
||||
};
|
||||
|
||||
export type IntroJson = Record<SuperBlocks, SuperBlockInfo>;
|
||||
export type IntroJson = Record<string, SuperBlockInfo>;
|
||||
|
||||
export function parseJson<JsonSchema>(filePath: string) {
|
||||
export function parseIntroJson(filePath: string) {
|
||||
return withTrace(fs.readFile, filePath, 'utf8').then(
|
||||
// unfortunately, withTrace does not correctly infer that the third argument
|
||||
// is a string, so it uses the (path, options?) overload and we have to cast
|
||||
// result to string.
|
||||
result => JSON.parse(result as string) as JsonSchema
|
||||
result => JSON.parse(result as string) as IntroJson
|
||||
);
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@ import type { BlockLabel } from '@freecodecamp/shared/config/blocks';
|
||||
import { getProjectPath } from './get-project-info.js';
|
||||
|
||||
export type Meta = {
|
||||
name: string;
|
||||
blockLayout: string;
|
||||
blockLabel?: BlockLabel;
|
||||
isUpcomingChange: boolean;
|
||||
|
||||
@@ -3,7 +3,7 @@ import path, { join } from 'path';
|
||||
import { input } from '@inquirer/prompts';
|
||||
import { format } from 'prettier';
|
||||
|
||||
import { IntroJson, parseJson } from './helpers/parse-json';
|
||||
import { IntroJson, parseIntroJson } from './helpers/parse-json';
|
||||
import { withTrace } from './helpers/utils';
|
||||
import { getAllBlocks, validateBlockName } from './utils';
|
||||
import {
|
||||
@@ -22,11 +22,87 @@ interface RenameBlockArgs {
|
||||
newName: string;
|
||||
}
|
||||
|
||||
const introJsonPath = path.resolve(
|
||||
__dirname,
|
||||
'../../client/i18n/locales/english/intro.json'
|
||||
);
|
||||
|
||||
function getBlockTitleFromIntro(intro: IntroJson, block: string) {
|
||||
for (const superBlockInfo of Object.values(intro)) {
|
||||
const blockInfo = superBlockInfo.blocks[block];
|
||||
if (blockInfo?.title) return blockInfo.title;
|
||||
}
|
||||
}
|
||||
|
||||
function renameBlockInSimpleStructure(
|
||||
blocks: string[] | undefined,
|
||||
oldBlock: string,
|
||||
newBlock: string
|
||||
) {
|
||||
if (!blocks) return false;
|
||||
const blockIndex = blocks.findIndex(block => block === oldBlock);
|
||||
if (blockIndex === -1) return false;
|
||||
blocks[blockIndex] = newBlock;
|
||||
return true;
|
||||
}
|
||||
|
||||
function renameBlockInChapterStructure(
|
||||
chapters:
|
||||
| {
|
||||
modules: {
|
||||
blocks: string[];
|
||||
}[];
|
||||
}[]
|
||||
| undefined,
|
||||
oldBlock: string,
|
||||
newBlock: string
|
||||
) {
|
||||
if (!chapters) return false;
|
||||
let updated = false;
|
||||
for (const chapter of chapters) {
|
||||
for (const module of chapter.modules) {
|
||||
const blockIndex = module.blocks.findIndex(block => block === oldBlock);
|
||||
if (blockIndex !== -1) {
|
||||
module.blocks[blockIndex] = newBlock;
|
||||
updated = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return updated;
|
||||
}
|
||||
|
||||
function renameBlockInIntro(
|
||||
intro: IntroJson,
|
||||
superblock: string,
|
||||
oldBlock: string,
|
||||
newBlock: string,
|
||||
newName: string
|
||||
) {
|
||||
const superBlockIntro = intro[superblock];
|
||||
if (!superBlockIntro) return false;
|
||||
|
||||
const introBlocks = Object.entries(superBlockIntro.blocks);
|
||||
const blockIntroIndex = introBlocks.findIndex(
|
||||
([block]) => block === oldBlock
|
||||
);
|
||||
if (blockIntroIndex === -1) return false;
|
||||
|
||||
const currentBlockInfo = introBlocks[blockIntroIndex]?.[1];
|
||||
if (!currentBlockInfo) return false;
|
||||
|
||||
introBlocks[blockIntroIndex] = [
|
||||
newBlock,
|
||||
{ ...currentBlockInfo, title: newName }
|
||||
];
|
||||
superBlockIntro.blocks = Object.fromEntries(introBlocks);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
async function renameBlock({ newBlock, newName, oldBlock }: RenameBlockArgs) {
|
||||
const blockStructure = getBlockStructure(oldBlock);
|
||||
const blockStructurePath = getBlockStructurePath(oldBlock);
|
||||
blockStructure.dashedName = newBlock;
|
||||
blockStructure.name = newName;
|
||||
await writeBlockStructure(newBlock, blockStructure);
|
||||
await fs.rm(blockStructurePath);
|
||||
console.log('New block structure .json written.');
|
||||
@@ -37,50 +113,48 @@ async function renameBlock({ newBlock, newName, oldBlock }: RenameBlockArgs) {
|
||||
await fs.rename(oldBlockContentDir, newBlockContentDir);
|
||||
console.log('Block challenges moved to new directory.');
|
||||
|
||||
const newIntro = await parseIntroJson(introJsonPath);
|
||||
let didUpdateIntro = false;
|
||||
|
||||
const { superblocks } = getCurriculumStructure();
|
||||
console.log('Updating superblocks containing renamed block.');
|
||||
for (const superblock of superblocks) {
|
||||
const superblockStructure = getSuperblockStructure(superblock);
|
||||
const { chapters = [] } = superblockStructure;
|
||||
for (const chapter of chapters) {
|
||||
for (const module of chapter.modules) {
|
||||
const { blocks } = module;
|
||||
const blockIndex = blocks.findIndex(block => block === oldBlock);
|
||||
if (blockIndex !== -1) {
|
||||
module.blocks[blockIndex] = newBlock;
|
||||
await writeSuperblockStructure(superblock, superblockStructure);
|
||||
console.log(
|
||||
`Updated superblock .json file written for ${superblock}.`
|
||||
);
|
||||
const didUpdateSuperblock =
|
||||
renameBlockInSimpleStructure(
|
||||
superblockStructure.blocks,
|
||||
oldBlock,
|
||||
newBlock
|
||||
) ||
|
||||
renameBlockInChapterStructure(
|
||||
superblockStructure.chapters,
|
||||
oldBlock,
|
||||
newBlock
|
||||
);
|
||||
|
||||
const introJsonPath = path.resolve(
|
||||
__dirname,
|
||||
`../../client/i18n/locales/english/intro.json`
|
||||
);
|
||||
const newIntro = await parseJson<IntroJson>(introJsonPath);
|
||||
const introBlocks = Object.entries(newIntro[superblock].blocks);
|
||||
const blockIntroIndex = introBlocks.findIndex(
|
||||
([block]) => block === oldBlock
|
||||
);
|
||||
introBlocks[blockIntroIndex] = [
|
||||
newBlock,
|
||||
{ ...introBlocks[blockIntroIndex][1], title: newName }
|
||||
];
|
||||
newIntro[superblock].blocks = Object.fromEntries(introBlocks);
|
||||
if (didUpdateSuperblock) {
|
||||
await writeSuperblockStructure(superblock, superblockStructure);
|
||||
console.log(`Updated superblock .json file written for ${superblock}.`);
|
||||
|
||||
await withTrace(
|
||||
fs.writeFile,
|
||||
introJsonPath,
|
||||
await format(JSON.stringify(newIntro), { parser: 'json' })
|
||||
);
|
||||
console.log('Updated locale intro.json file written.');
|
||||
}
|
||||
}
|
||||
didUpdateIntro =
|
||||
renameBlockInIntro(newIntro, superblock, oldBlock, newBlock, newName) ||
|
||||
didUpdateIntro;
|
||||
}
|
||||
}
|
||||
|
||||
if (didUpdateIntro) {
|
||||
await withTrace(
|
||||
fs.writeFile,
|
||||
introJsonPath,
|
||||
await format(JSON.stringify(newIntro), { parser: 'json' })
|
||||
);
|
||||
console.log('Updated locale intro.json file written.');
|
||||
}
|
||||
}
|
||||
|
||||
void getAllBlocks().then(async existingBlocks => {
|
||||
const intro = await parseIntroJson(introJsonPath);
|
||||
|
||||
const oldBlock = await input({
|
||||
message: 'What is the dashed name of block to rename?',
|
||||
validate: (block: string) =>
|
||||
@@ -89,7 +163,7 @@ void getAllBlocks().then(async existingBlocks => {
|
||||
|
||||
const newName = await input({
|
||||
message: 'What is the new name?',
|
||||
default: getBlockStructure(oldBlock).name
|
||||
default: getBlockTitleFromIntro(intro, oldBlock) ?? oldBlock
|
||||
});
|
||||
|
||||
const newBlock = await input({
|
||||
|
||||
Reference in New Issue
Block a user