diff options
| author | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-07-22 14:58:26 +0300 |
|---|---|---|
| committer | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-07-22 14:59:09 +0300 |
| commit | c234f221f9be698bc0501c60459e1ae8e67df6c3 (patch) | |
| tree | 944b4dec10fada8d93456dc5a85e6bfdb36a8b8b /website/scripts/prepare-docs.mjs | |
| parent | ef85fa0c7ebb48bb59fe7593af5a67f3e02ff3d4 (diff) | |
feat(docs): publish versioned release documentation
Diffstat (limited to 'website/scripts/prepare-docs.mjs')
| -rw-r--r-- | website/scripts/prepare-docs.mjs | 173 |
1 files changed, 154 insertions, 19 deletions
diff --git a/website/scripts/prepare-docs.mjs b/website/scripts/prepare-docs.mjs index 2c6fd5f4..c7b3e9d8 100644 --- a/website/scripts/prepare-docs.mjs +++ b/website/scripts/prepare-docs.mjs @@ -1,5 +1,5 @@ -import { cp, mkdir, readdir, readFile, rm, writeFile } from 'node:fs/promises'; -import { dirname, join, relative, resolve } from 'node:path'; +import { cp, mkdir, readFile, readdir, rm, writeFile } from 'node:fs/promises'; +import { dirname, extname, join, relative, resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; import process from 'node:process'; @@ -9,6 +9,10 @@ const publicDir = resolve(repoRoot, 'website/public'); const stableDocsDir = resolve(repoRoot, 'website/src/content/docs'); const previewDocsSourceDir = resolve(repoRoot, 'docs/next/website/src/content/docs'); const previewDocsDir = resolve(stableDocsDir, 'preview'); +const generatedVersionsDocsDir = resolve(stableDocsDir, '_versions'); +const versionsDir = resolve(repoRoot, 'docs/versions'); +const versionsManifestPath = resolve(versionsDir, 'manifest.json'); +const generatedVersionsDataPath = resolve(repoRoot, 'website/src/data/docs-versions.json'); const previewConfigReferenceSource = resolve( repoRoot, 'docs/next/website/src/data/config-reference.json', @@ -17,14 +21,29 @@ const previewConfigReferenceDestination = resolve( repoRoot, 'website/src/data/config-reference-preview.json', ); +const generatedVersionReferencesPath = resolve( + repoRoot, + 'website/src/data/config-reference-versions.json', +); if (process.argv[2] === '--rewrite-preview-doc-fixture') { const chunks = []; for await (const chunk of process.stdin) chunks.push(chunk); process.stdout.write(rewritePreviewDocContent(Buffer.concat(chunks).toString('utf8'))); +} else if (process.argv[2] === '--rewrite-version-doc-fixture') { + const chunks = []; + for await (const chunk of process.stdin) chunks.push(chunk); + process.stdout.write( + rewriteVersionDocContent(Buffer.concat(chunks).toString('utf8'), { + version: process.argv[3] ?? '0.7.5', + tag: `v${process.argv[3] ?? '0.7.5'}`, + sourceRoot: 'docs/next/website/src/content/docs', + relativePath: 'index.mdx', + }), + ); } else { await preparePublicAssets(); - await preparePreviewDocs(); + await prepareDocs(); } async function preparePublicAssets() { @@ -56,49 +75,165 @@ async function preparePublicAssets() { } } -async function preparePreviewDocs() { +async function prepareDocs() { await rm(previewDocsDir, { recursive: true, force: true }); - await copyPreviewDocs(previewDocsSourceDir, previewDocsDir); + await rm(generatedVersionsDocsDir, { recursive: true, force: true }); + + await copyPreparedDocs(previewDocsSourceDir, previewDocsDir, (content, relativePath) => + rewritePreviewDocContent(content, relativePath), + ); await cp(previewConfigReferenceSource, previewConfigReferenceDestination); + + const manifest = JSON.parse(await readFile(versionsManifestPath, 'utf8')); + if (manifest.schema_version !== 1 || typeof manifest.current !== 'string') { + throw new Error(`${versionsManifestPath} has an unsupported schema`); + } + + const scopes = { + stable: await collectDocsScope(stableDocsDir, new Set(['preview', '_versions'])), + preview: await collectDocsScope(previewDocsSourceDir), + }; + const configReferences = {}; + + for (const entry of manifest.versions) { + const version = entry.version; + const snapshotDocsRoot = resolve(versionsDir, version, 'website/src/content/docs'); + const destinationRoot = resolve(generatedVersionsDocsDir, version); + await copyPreparedDocs(snapshotDocsRoot, destinationRoot, (content, relativePath) => + rewriteVersionDocContent(content, { + version, + tag: entry.tag, + sourceRoot: entry.source, + relativePath, + }), + ); + scopes[version] = await collectDocsScope(snapshotDocsRoot); + + const referencePath = resolve( + versionsDir, + version, + 'website/src/data/config-reference.json', + ); + try { + configReferences[version] = JSON.parse(await readFile(referencePath, 'utf8')); + } catch (error) { + if (error.code !== 'ENOENT') throw error; + } + } + + await writeFile( + generatedVersionsDataPath, + `${JSON.stringify({ ...manifest, scopes }, null, 2)}\n`, + 'utf8', + ); + await writeFile( + generatedVersionReferencesPath, + `${JSON.stringify(configReferences, null, 2)}\n`, + 'utf8', + ); } -async function copyPreviewDocs(sourceDir, destinationDir) { +async function copyPreparedDocs(sourceDir, destinationDir, rewrite, pathPrefix = '') { await mkdir(destinationDir, { recursive: true }); for (const entry of await readdir(sourceDir, { withFileTypes: true })) { const source = join(sourceDir, entry.name); const destination = join(destinationDir, entry.name); + const relativePath = pathPrefix ? `${pathPrefix}/${entry.name}` : entry.name; if (entry.isDirectory()) { - await copyPreviewDocs(source, destination); + await copyPreparedDocs(source, destination, rewrite, relativePath); continue; } if (!entry.isFile()) continue; + if (!['.md', '.mdx'].includes(extname(entry.name).toLowerCase())) { + await cp(source, destination); + continue; + } const content = await readFile(source, 'utf8'); - const relativePath = relative(previewDocsSourceDir, source); - await writeFile(destination, rewritePreviewDocContent(content, relativePath), 'utf8'); + await writeFile(destination, rewrite(content, relativePath), 'utf8'); } } +async function collectDocsScope(sourceDir, excludedDirectories = new Set()) { + const locales = { root: [], ja: [], 'zh-cn': [] }; + + async function walk(directory, prefix = '') { + for (const entry of await readdir(directory, { withFileTypes: true })) { + if (entry.isDirectory() && excludedDirectories.has(entry.name)) continue; + const path = join(directory, entry.name); + const relativePath = prefix ? `${prefix}/${entry.name}` : entry.name; + if (entry.isDirectory()) { + await walk(path, relativePath); + continue; + } + if (!entry.isFile() || !['.md', '.mdx'].includes(extname(entry.name))) continue; + + const segments = relativePath.replace(/\.(md|mdx)$/i, '').split('/'); + const locale = segments[0] === 'ja' || segments[0] === 'zh-cn' ? segments.shift() : 'root'; + const page = segments.join('/').replace(/(^|\/)index$/, '').replace(/\/$/, ''); + locales[locale].push(page); + } + } + + await walk(sourceDir); + for (const pages of Object.values(locales)) pages.sort(); + for (const locale of ['ja', 'zh-cn']) { + if (locales[locale].length === 0) locales[locale] = [...locales.root]; + } + return { locales }; +} + export function rewritePreviewDocContent(content, relativePath = '') { - const rewritten = content - .replaceAll('/docs/', '/docs/preview/') - .replaceAll('../../../public/', '../../../../public/') - // Preview docs live one directory deeper than stable docs, so component - // imports need one more parent segment regardless of locale depth. Only - // MDX import lines are rewritten; prose mentioning relative paths is not. - .replace(/^(import .*from\s+['"])(?=(?:\.\.\/)+components\/)/gm, '$1../'); - return insertPreviewNotice(rewritten, relativePath); + const rewritten = rewriteRelativeDocPaths( + content.replaceAll('/docs/', '/docs/preview/'), + 1, + ); + const withEditLink = setGeneratedEditUrl( + rewritten, + `https://github.com/ogulcancelik/herdr/edit/master/docs/next/website/src/content/docs/${relativePath}`, + ); + return insertPreviewNotice(withEditLink, relativePath); +} + +export function rewriteVersionDocContent(content, { version, tag, sourceRoot, relativePath }) { + const taggedContent = content + .replaceAll('/docs/', `/docs/${version}/`) + .replaceAll( + 'https://github.com/ogulcancelik/herdr/blob/master/', + `https://github.com/ogulcancelik/herdr/blob/${tag}/`, + ) + .replaceAll( + 'https://raw.githubusercontent.com/ogulcancelik/herdr/master/', + `https://raw.githubusercontent.com/ogulcancelik/herdr/${tag}/`, + ); + const rewritten = rewriteRelativeDocPaths(taggedContent, 2); + return setGeneratedEditUrl( + rewritten, + `https://github.com/ogulcancelik/herdr/blob/${tag}/${sourceRoot}/${relativePath}`, + ); +} + +function rewriteRelativeDocPaths(content, extraDepth) { + const parents = '../'.repeat(extraDepth); + return content + .replace(/((?:\.\.\/)+)(?=public\/)/g, `$1${parents}`) + .replace(/^(import .*from\s+['"])(?=(?:\.\.\/)+components\/)/gm, `$1${parents}`); +} + +function setGeneratedEditUrl(content, editUrl) { + if (!content.startsWith('---\n') || /^editUrl:/m.test(content)) return content; + return content.replace(/^---\n/, `---\neditUrl: ${editUrl}\n`); } function insertPreviewNotice(content, relativePath) { const notice = [ - '> Preview docs describe unreleased preview builds. Stable docs remain at [/docs/](/docs/).', + '> Next docs describe unreleased work from `master`. Stable docs remain at [/docs/](/docs/).', '', '', ].join('\n'); const indexPrefix = relativePath === 'index.mdx' - ? content.replace('title: Herdr documentation', 'title: Herdr preview documentation') + ? content.replace('title: Herdr documentation', 'title: Herdr next documentation') : content; const frontmatter = indexPrefix.match(/^---\n[\s\S]*?\n---\n/); if (!frontmatter) { |
