From 075375700f5f0c09e15439642510a89b4e031d4a Mon Sep 17 00:00:00 2001 From: "Krzysztof G." <60067306+gikf@users.noreply.github.com> Date: Wed, 10 Dec 2025 18:45:18 +0100 Subject: [PATCH] feat(tools): rename-block helper script (#64201) --- .../create-project.ts | 33 +---- .../helpers/parse-json.ts | 24 ++++ .../challenge-helper-scripts/helpers/utils.ts | 11 ++ tools/challenge-helper-scripts/package.json | 1 + .../challenge-helper-scripts/rename-block.ts | 136 ++++++++++++++++++ 5 files changed, 174 insertions(+), 31 deletions(-) create mode 100644 tools/challenge-helper-scripts/helpers/parse-json.ts create mode 100644 tools/challenge-helper-scripts/rename-block.ts diff --git a/tools/challenge-helper-scripts/create-project.ts b/tools/challenge-helper-scripts/create-project.ts index 2a54801cb43..5c4fe4cf1f6 100644 --- a/tools/challenge-helper-scripts/create-project.ts +++ b/tools/challenge-helper-scripts/create-project.ts @@ -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; -}; - -type IntroJson = Record; - interface CreateProjectArgs { superBlock: SuperBlocks; block: string; @@ -241,26 +232,6 @@ async function createQuizChallenge( }); } -function parseJson(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( - fn: (...x: Args) => Promise, - ...args: Args -): Promise { - 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', diff --git a/tools/challenge-helper-scripts/helpers/parse-json.ts b/tools/challenge-helper-scripts/helpers/parse-json.ts new file mode 100644 index 00000000000..0cefdeb2324 --- /dev/null +++ b/tools/challenge-helper-scripts/helpers/parse-json.ts @@ -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; +}; + +export type IntroJson = Record; + +export function parseJson(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 + ); +} diff --git a/tools/challenge-helper-scripts/helpers/utils.ts b/tools/challenge-helper-scripts/helpers/utils.ts index 96e222c6719..bddb5dcc211 100644 --- a/tools/challenge-helper-scripts/helpers/utils.ts +++ b/tools/challenge-helper-scripts/helpers/utils.ts @@ -6,3 +6,14 @@ export function insertInto(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( + fn: (...x: Args) => Promise, + ...args: Args +): Promise { + return fn(...args).catch((reason: Error) => { + throw Error(reason.message); + }); +} diff --git a/tools/challenge-helper-scripts/package.json b/tools/challenge-helper-scripts/package.json index 24faffe6ffc..7bce431a1de 100644 --- a/tools/challenge-helper-scripts/package.json +++ b/tools/challenge-helper-scripts/package.json @@ -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" }, diff --git a/tools/challenge-helper-scripts/rename-block.ts b/tools/challenge-helper-scripts/rename-block.ts new file mode 100644 index 00000000000..efcde974167 --- /dev/null +++ b/tools/challenge-helper-scripts/rename-block.ts @@ -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(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' + ) + );