mirror of
https://github.com/freeCodeCamp/freeCodeCamp.git
synced 2025-12-19 18:18:27 -05:00
feat(tools): rename-block helper script (#64201)
This commit is contained in:
@@ -22,11 +22,13 @@ import {
|
||||
} from './utils.js';
|
||||
import { getBaseMeta } from './helpers/get-base-meta.js';
|
||||
import { createIntroMD } from './helpers/create-intro.js';
|
||||
import { IntroJson, parseJson } from './helpers/parse-json.js';
|
||||
import {
|
||||
ChapterModuleSuperblockStructure,
|
||||
updateChapterModuleSuperblockStructure,
|
||||
updateSimpleSuperblockStructure
|
||||
} from './helpers/create-project.js';
|
||||
import { withTrace } from './helpers/utils.js';
|
||||
|
||||
const helpCategories = [
|
||||
'HTML-CSS',
|
||||
@@ -39,17 +41,6 @@ const helpCategories = [
|
||||
'Rosetta'
|
||||
] as const;
|
||||
|
||||
type BlockInfo = {
|
||||
title: string;
|
||||
intro: string[];
|
||||
};
|
||||
|
||||
type SuperBlockInfo = {
|
||||
blocks: Record<string, BlockInfo>;
|
||||
};
|
||||
|
||||
type IntroJson = Record<SuperBlocks, SuperBlockInfo>;
|
||||
|
||||
interface CreateProjectArgs {
|
||||
superBlock: SuperBlocks;
|
||||
block: string;
|
||||
@@ -241,26 +232,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);
|
||||
});
|
||||
}
|
||||
|
||||
async function getChapters(superBlock: string) {
|
||||
const blockMetaFile = await fs.readFile(
|
||||
'../../curriculum/structure/superblocks/' + superBlock + '.json',
|
||||
|
||||
24
tools/challenge-helper-scripts/helpers/parse-json.ts
Normal file
24
tools/challenge-helper-scripts/helpers/parse-json.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import fs from 'fs/promises';
|
||||
|
||||
import { SuperBlocks } from '../../../shared-dist/config/curriculum.js';
|
||||
import { withTrace } from './utils.js';
|
||||
|
||||
export type BlockInfo = {
|
||||
title: string;
|
||||
intro: string[];
|
||||
};
|
||||
|
||||
export type SuperBlockInfo = {
|
||||
blocks: Record<string, BlockInfo>;
|
||||
};
|
||||
|
||||
export type IntroJson = Record<SuperBlocks, SuperBlockInfo>;
|
||||
|
||||
export 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
|
||||
);
|
||||
}
|
||||
@@ -6,3 +6,14 @@ export function insertInto<T>(arr: T[], index: number, elem: T): T[] {
|
||||
return id === index ? [elem, x] : x;
|
||||
});
|
||||
}
|
||||
|
||||
// fs Promise functions return errors, but no stack trace. This adds back in
|
||||
// the stack trace.
|
||||
export 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);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
"create-project": "tsx create-project",
|
||||
"create-language-block": "tsx create-language-block",
|
||||
"create-quiz": "tsx create-quiz",
|
||||
"rename-block": "tsx rename-block",
|
||||
"lint": "eslint --max-warnings 0",
|
||||
"test": "vitest"
|
||||
},
|
||||
|
||||
136
tools/challenge-helper-scripts/rename-block.ts
Normal file
136
tools/challenge-helper-scripts/rename-block.ts
Normal file
@@ -0,0 +1,136 @@
|
||||
import fs from 'fs/promises';
|
||||
import path, { join } from 'path';
|
||||
import { prompt } from 'inquirer';
|
||||
import { format } from 'prettier';
|
||||
|
||||
import { IntroJson, parseJson } from './helpers/parse-json';
|
||||
import { withTrace } from './helpers/utils';
|
||||
import { getAllBlocks, validateBlockName } from './utils';
|
||||
import {
|
||||
getBlockStructure,
|
||||
getBlockStructurePath,
|
||||
getSuperblockStructure,
|
||||
writeBlockStructure,
|
||||
writeSuperblockStructure,
|
||||
getContentConfig,
|
||||
getCurriculumStructure
|
||||
} from '../../curriculum/src/file-handler';
|
||||
import matter from 'gray-matter';
|
||||
|
||||
interface RenameBlockArgs {
|
||||
newBlock: string;
|
||||
oldBlock: string;
|
||||
newName: string;
|
||||
}
|
||||
|
||||
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.');
|
||||
|
||||
const { blockContentDir } = getContentConfig('english');
|
||||
const oldBlockContentDir = join(blockContentDir, oldBlock);
|
||||
const newBlockContentDir = join(blockContentDir, newBlock);
|
||||
await fs.rename(oldBlockContentDir, newBlockContentDir);
|
||||
console.log('Block challenges moved to new directory.');
|
||||
|
||||
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 superblockPagesDir = path.resolve(
|
||||
__dirname,
|
||||
`../../client/src/pages/learn/${superblock}/`
|
||||
);
|
||||
const blockPagesDir = join(superblockPagesDir, oldBlock);
|
||||
const indexMdPath = join(blockPagesDir, 'index.md');
|
||||
const frontMatter = matter.read(indexMdPath);
|
||||
const newData = {
|
||||
...frontMatter.data,
|
||||
block: newBlock
|
||||
};
|
||||
|
||||
await fs.writeFile(
|
||||
indexMdPath,
|
||||
matter.stringify(frontMatter.content, newData)
|
||||
);
|
||||
const newBlockClientDir = join(superblockPagesDir, newBlock);
|
||||
await fs.rename(blockPagesDir, newBlockClientDir);
|
||||
console.log("Updated block's index.md file written.");
|
||||
|
||||
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);
|
||||
|
||||
await withTrace(
|
||||
fs.writeFile,
|
||||
introJsonPath,
|
||||
await format(JSON.stringify(newIntro), { parser: 'json' })
|
||||
);
|
||||
console.log('Updated locale intro.json file written.');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void getAllBlocks()
|
||||
.then(existingBlocks =>
|
||||
prompt([
|
||||
{
|
||||
name: 'oldBlock',
|
||||
message: 'What is the dashed name of block to rename?',
|
||||
type: 'input',
|
||||
validate: (block: string) => existingBlocks.includes(block)
|
||||
},
|
||||
{
|
||||
name: 'newName',
|
||||
message: 'What is the new name?',
|
||||
type: 'input',
|
||||
default: ({ oldBlock }: RenameBlockArgs) =>
|
||||
getBlockStructure(oldBlock).name
|
||||
},
|
||||
{
|
||||
name: 'newBlock',
|
||||
message: 'What is the new dashed name (in kebab-case)?',
|
||||
validate: (newBlock: string) =>
|
||||
validateBlockName(newBlock, existingBlocks)
|
||||
}
|
||||
])
|
||||
)
|
||||
.then(
|
||||
async ({ newBlock, newName, oldBlock }: RenameBlockArgs) =>
|
||||
await renameBlock({ newBlock, newName, oldBlock })
|
||||
)
|
||||
.then(() =>
|
||||
console.log(
|
||||
'All set. Now use pnpm run clean:client in the root and it should be good to go'
|
||||
)
|
||||
);
|
||||
Reference in New Issue
Block a user