From 4081d85de0ef416e2e841e15fe6bc90a9be99576 Mon Sep 17 00:00:00 2001 From: Claude Code Date: Fri, 8 May 2026 14:24:53 -0700 Subject: [PATCH] chore(docs): freeze data imports when cutting a docs version MDX files can import JSON/YAML data from outside the section (e.g. admin_docs/configuration/country-map-tools.mdx imports ../../data/countries.json). Without intervention, the snapshot keeps reading the live data file, so the historical version's content silently changes whenever the data file is updated upstream. Add a freezeDataImports step to manage-versions.mjs that runs at cut time, before the depth-aware path rewriter: - Walks the freshly-snapshotted section dir - For each .md/.mdx file, finds escaping JSON/YAML imports (one or more ../) and resolves them against the file's original location - Copies the resolved file into /_versioned_data/, preserving its path relative to docs/ (the underscore prefix keeps Docusaurus from treating it as content) - Rewrites the import to point at the snapshot-local copy Verified end-to-end with a throwaway admin_docs cut: the snapshot's country-map-tools page renders all 201 countries from a frozen JSON, and a subsequent edit to the live docs/data/countries.json does not affect the snapshot's rendered output. --- docs/scripts/manage-versions.mjs | 85 ++++++++++++++++++++++++++++++++ 1 file changed, 85 insertions(+) diff --git a/docs/scripts/manage-versions.mjs b/docs/scripts/manage-versions.mjs index 6a23d6a06a0..39b888bac3c 100644 --- a/docs/scripts/manage-versions.mjs +++ b/docs/scripts/manage-versions.mjs @@ -43,6 +43,87 @@ function saveConfig(config) { fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2) + '\n'); } +function freezeDataImports(section, version) { + // MDX files can `import` JSON/YAML data from outside the section (e.g. + // country-map-tools.mdx imports `../../data/countries.json`). Without + // intervention, the snapshot keeps reading the live file, so the + // historical version's content silently changes whenever the data file + // is updated. Copy each escaping data import into a snapshot-local + // `_versioned_data/` dir and rewrite the import to point there. + const sectionRoot = section === 'docs' + ? path.join(__dirname, '..', 'docs') + : path.join(__dirname, '..', section); + const docsRoot = path.join(__dirname, '..'); + const versionedDocsDir = section === 'docs' + ? `versioned_docs/version-${version}` + : `${section}_versioned_docs/version-${version}`; + const versionedDocsPath = path.join(__dirname, '..', versionedDocsDir); + const frozenDataDir = path.join(versionedDocsPath, '_versioned_data'); + + if (!fs.existsSync(versionedDocsPath)) { + return; + } + + console.log(` Freezing data imports in ${versionedDocsDir}...`); + + // Matches `from '../../foo/bar.json'` and similar — only escaping paths + // (one or more `../`) targeting JSON/YAML files. + const dataImportRe = /(from\s+['"])((?:\.\.\/)+)([^'"\s]+\.(?:json|ya?ml))(['"])/g; + + function walk(dir, depth) { + for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { + const fullPath = path.join(dir, entry.name); + if (entry.isDirectory()) { + if (entry.name.startsWith('_')) continue; + walk(fullPath, depth + 1); + } else if (entry.isFile() && /\.(md|mdx)$/.test(entry.name)) { + const original = fs.readFileSync(fullPath, 'utf8'); + let inFence = false; + let mutated = false; + const updated = original.split('\n').map(line => { + if (/^\s*(```|~~~)/.test(line)) { + inFence = !inFence; + return line; + } + if (inFence) return line; + return line.replace(dataImportRe, (match, prefix, dots, importPath, suffix) => { + const upCount = dots.match(/\.\.\//g).length; + // Imports that stay inside the section are copied wholesale by + // Docusaurus, so they don't need freezing. + if (upCount <= depth) return match; + // Resolve the import against the file's *original* location to + // find the source file in the live tree. + const relativeFromVersioned = path.relative(versionedDocsPath, fullPath); + const originalDir = path.dirname(path.join(sectionRoot, relativeFromVersioned)); + const resolvedSource = path.resolve(originalDir, dots + importPath); + const relFromDocsRoot = path.relative(docsRoot, resolvedSource); + if (relFromDocsRoot.startsWith('..') || !fs.existsSync(resolvedSource)) { + return match; + } + const destPath = path.join(frozenDataDir, relFromDocsRoot); + fs.mkdirSync(path.dirname(destPath), { recursive: true }); + fs.copyFileSync(resolvedSource, destPath); + const rewritten = path + .relative(path.dirname(fullPath), destPath) + .split(path.sep) + .join('/'); + const finalImport = rewritten.startsWith('.') ? rewritten : `./${rewritten}`; + mutated = true; + return `${prefix}${finalImport}${suffix}`; + }); + }).join('\n'); + if (mutated) { + fs.writeFileSync(fullPath, updated); + const rel = path.relative(versionedDocsPath, fullPath); + console.log(` Froze data imports in ${rel}`); + } + } + } + } + + walk(versionedDocsPath, 0); +} + function fixVersionedImports(section, version) { // Versioned content lands one directory deeper than the source content, // so any `../../src/` or `../../data/` imports in .md/.mdx files need @@ -126,6 +207,10 @@ function addVersion(section, version) { process.exit(1); } + // Freeze data imports BEFORE adjusting paths, so the depth-aware rewriter + // doesn't process the now-local imports we just rewrote. + freezeDataImports(section, version); + // Fix relative imports in versioned content fixVersionedImports(section, version);