diff --git a/website/package.json b/website/package.json index 6c206c45492f..c2233f1de1ef 100644 --- a/website/package.json +++ b/website/package.json @@ -8,6 +8,7 @@ "build": "rm -rf .next && rm -rf out && next build", "fetch-remote-filepaths": "tsx scripts/fetch-remote-filepaths.ts", "fix-pages-structure": "tsx scripts/fix-pages-structure.ts", + "move-pages": "tsx scripts/move-pages.ts", "predev": "pnpm fetch-remote-filepaths", "prebuild": "pnpm fetch-remote-filepaths && pnpm fix-pages-structure", "postbuild": "next-sitemap --config next-sitemap.config.mjs && node scripts/sitemap-ci.js", diff --git a/website/scripts/fix-pages-structure.ts b/website/scripts/fix-pages-structure.ts index e134b6bb958a..2b9ae955e70b 100644 --- a/website/scripts/fix-pages-structure.ts +++ b/website/scripts/fix-pages-structure.ts @@ -20,8 +20,8 @@ * referencing a non-existent directory in the meta file */ -import fs from 'fs/promises' -import path from 'path' +import fs from 'node:fs/promises' +import path from 'node:path' const FORCE_META = process.argv.includes('--force-meta') @@ -31,7 +31,7 @@ const META_FILENAME = '_meta.js' const CATCH_ALL_PREFIX = '[[...' const HIDDEN_FILE_PREFIX = '.' -async function fileExists(filepath: string) { +async function fileExists(filepath: string): Promise { try { await fs.access(filepath) return true @@ -153,7 +153,12 @@ async function main() { for (const locale of [SOURCE_LOCALE, ...translatedLocales]) { const { directories } = await getPagesStructure(locale) for (const directory of directories) { - if (!sourceStructure.contentDirectories.has(directory)) { + // Delete directory if it has no content files in source language + // AND none of its subdirectories have content files + const existsInSource = + sourceStructure.contentDirectories.has(directory) || + Array.from(sourceStructure.contentDirectories).some((sourceDir) => sourceDir.startsWith(directory + '/')) + if (!existsInSource) { console.log(`Removing directory ${path.join(locale, directory)}`) await fs.rm(path.join(PAGES_DIRECTORY, locale, directory), { recursive: true, force: true }) } @@ -161,4 +166,7 @@ async function main() { } } -main().catch(console.error) +main().catch((error) => { + console.error(error.message) + process.exit(1) +}) diff --git a/website/scripts/move-pages.ts b/website/scripts/move-pages.ts new file mode 100644 index 000000000000..543fc7a84d88 --- /dev/null +++ b/website/scripts/move-pages.ts @@ -0,0 +1,178 @@ +/** + * This script moves and/or renames pages or directories of pages. + * It performs these operations in order: + * + * 1. Prepares the move using English locale: + * - Gets list of files to move (scanning directory if needed) + * - Errors if files would be overwritten (except _meta.js) + * + * 2. Moves files in all locales: + * - Creates destination directories as needed + * - Skips files that don't exist or would be overwritten + * + * 3. Runs `fix-pages-structure` to: + * - Clean up any orphaned directories + * - Create missing meta files + * - Ensure directory structure is consistent + */ + +import { execFile } from 'node:child_process' +import fs from 'node:fs/promises' +import path from 'node:path' +import { promisify } from 'node:util' + +const execFileAsync = promisify(execFile) + +const PAGES_DIRECTORY = path.join(process.cwd(), 'pages') +const SOURCE_LOCALE = 'en' +const META_FILENAME = '_meta.js' + +type FileToMove = { + sourcePath: string // Relative to locale directory + destinationPath: string // Relative to locale directory +} + +async function fileExists(filepath: string): Promise { + try { + await fs.access(filepath) + return true + } catch { + return false + } +} + +async function getSourceFiles(sourcePath: string): Promise { + const sourceDirectory = path.join(PAGES_DIRECTORY, SOURCE_LOCALE) + const fullPath = path.join(sourceDirectory, sourcePath) + + if (!(await fileExists(fullPath))) { + throw new Error(`Source path '${sourcePath}' does not exist in source locale (${SOURCE_LOCALE})`) + } + + const fileStat = await fs.stat(fullPath) + if (fileStat.isDirectory()) { + const files: string[] = [] + + async function scan(directory: string) { + const items = await fs.readdir(directory, { withFileTypes: true }) + for (const item of items) { + const itemPath = path.join(directory, item.name) + if (item.isDirectory()) { + await scan(itemPath) + } else { + files.push(path.relative(sourceDirectory, itemPath)) + } + } + } + + await scan(fullPath) + return files + } + + return [sourcePath] +} + +async function main() { + const [sourcePath, destinationPath] = process.argv.slice(2) + if (!sourcePath || !destinationPath) { + throw new Error( + 'Usage: pnpm run move-pages \n' + + 'Examples:\n' + + ' pnpm run move-pages page.mdx new-directory Move page (keeping name)\n' + + ' pnpm run move-pages page.mdx new-name.mdx Rename page\n' + + ' pnpm run move-pages page.mdx new-dir/new-name.mdx Move and rename page\n' + + ' pnpm run move-pages developing subgraphs/developing Move directory\n' + + ' pnpm run move-pages developing subgraphs Rename directory\n', + ) + } + + // Normalize paths + const normalizedSource = path.normalize(sourcePath).replace(/^[/\\]|[/\\]$/g, '') + const normalizedDestination = path.normalize(destinationPath).replace(/^[/\\]|[/\\]$/g, '') + + // Get list of files to move from source locale + const sourceFiles = await getSourceFiles(normalizedSource) + + // Build destination paths + const isFile = (await fs.stat(path.join(PAGES_DIRECTORY, SOURCE_LOCALE, normalizedSource))).isFile() + const filesToMove = sourceFiles.map((file) => { + if (isFile) { + // When moving a single file: + // - If destination has extension, use it as the new filename + // - Otherwise treat destination as directory and keep original filename + const destinationHasExtension = path.extname(normalizedDestination) !== '' + const newPath = destinationHasExtension + ? normalizedDestination + : path.join(normalizedDestination, path.basename(file)) + + return { + sourcePath: file, + destinationPath: newPath, + } + } else { + // When moving a directory, preserve the relative paths + const relativePath = path.relative(normalizedSource, file) + return { + sourcePath: file, + destinationPath: path.join(normalizedDestination, relativePath), + } + } + }) + + // Validate moves in English locale + const sourceDirectory = path.join(PAGES_DIRECTORY, SOURCE_LOCALE) + for (const { sourcePath, destinationPath } of filesToMove) { + // Allow _meta.js files to exist in destination since we skip them during move if they exist + if (path.basename(destinationPath) === META_FILENAME) { + continue + } + + // Don't allow overwriting existing files + const destinationFile = path.join(sourceDirectory, destinationPath) + if (await fileExists(destinationFile)) { + throw new Error(`Destination path '${destinationPath}' already exists in source locale (${SOURCE_LOCALE})`) + } + } + + // Get all locales + const locales = (await fs.readdir(PAGES_DIRECTORY)).filter((directory) => /^[a-z]{2}$/.test(directory)) + + // Move files in each locale + for (const locale of locales) { + const localeDirectory = path.join(PAGES_DIRECTORY, locale) + + // First create all necessary destination directories + const destinationDirs = new Set(filesToMove.map(({ destinationPath }) => path.dirname(destinationPath))) + for (const dir of destinationDirs) { + await fs.mkdir(path.join(localeDirectory, dir), { recursive: true }) + } + + // Then move the files + for (const { sourcePath, destinationPath } of filesToMove) { + const sourceFile = path.join(localeDirectory, sourcePath) + const destinationFile = path.join(localeDirectory, destinationPath) + + // Skip if source doesn't exist or would overwrite an existing _meta.js + if ( + !(await fileExists(sourceFile)) || + (path.basename(destinationPath) === META_FILENAME && (await fileExists(destinationFile))) + ) { + continue + } + + console.log(`Moving ${path.join(locale, sourcePath)}`) + console.log(` to ${path.join(locale, destinationPath)}`) + await fs.rename(sourceFile, destinationFile) + } + console.log() // Add blank line between locales + } + + // Run fix-pages-structure to clean up and update meta files + console.log('\nRunning fix-pages-structure...') + await execFileAsync('tsx', ['scripts/fix-pages-structure.ts']) +} + +main().catch((error) => { + console.error(error.message) + process.exit(1) +})