aboutsummaryrefslogtreecommitdiff
path: root/website/scripts/prepare-docs.mjs
diff options
context:
space:
mode:
authorOgulcan Celik <ogulcancelik@gmail.com>2026-07-22 14:58:26 +0300
committerOgulcan Celik <ogulcancelik@gmail.com>2026-07-22 14:59:09 +0300
commitc234f221f9be698bc0501c60459e1ae8e67df6c3 (patch)
tree944b4dec10fada8d93456dc5a85e6bfdb36a8b8b /website/scripts/prepare-docs.mjs
parentef85fa0c7ebb48bb59fe7593af5a67f3e02ff3d4 (diff)
feat(docs): publish versioned release documentation
Diffstat (limited to 'website/scripts/prepare-docs.mjs')
-rw-r--r--website/scripts/prepare-docs.mjs173
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) {