From c2d8cca3b4a232e1ee6c8ba0ebc75076f29d7864 Mon Sep 17 00:00:00 2001 From: yusing Date: Wed, 18 Feb 2026 14:05:40 +0800 Subject: [PATCH] refactor(docs): migrate wiki generation to MDX format - Convert markdown output to fumadocs MDX - Add api-md2mdx.ts for markdown to MDX transformation - Remove sidebar auto-update functionality - Change output directory from src/impl to content/docs/impl - Update DOCS_DIR path in Makefile to local wiki directory - Copy swagger.json directly instead of generating markdown - Add argparse dependency for CLI argument parsing --- Makefile | 7 +- goutils | 2 +- scripts/update-wiki/api-md2mdx.ts | 114 ++++++ scripts/update-wiki/bun.lock | 16 +- scripts/update-wiki/main.ts | 571 ++++++++++++++---------------- scripts/update-wiki/package.json | 8 +- 6 files changed, 393 insertions(+), 325 deletions(-) create mode 100644 scripts/update-wiki/api-md2mdx.ts diff --git a/Makefile b/Makefile index 2195cb23..43d8df75 100755 --- a/Makefile +++ b/Makefile @@ -7,7 +7,7 @@ export GOOS = linux REPO_URL ?= https://github.com/yusing/godoxy WEBUI_DIR ?= ../godoxy-webui -DOCS_DIR ?= ${WEBUI_DIR}/wiki +DOCS_DIR ?= wiki ifneq ($(BRANCH), compat) GO_TAGS = sonic @@ -178,10 +178,7 @@ gen-swagger: python3 scripts/fix-swagger-json.py # we don't need this rm internal/api/v1/docs/docs.go - -gen-swagger-markdown: gen-swagger - # brew tap go-swagger/go-swagger && brew install go-swagger - swagger generate markdown -f internal/api/v1/docs/swagger.yaml --skip-validation --output ${DOCS_DIR}/src/API.md + cp internal/api/v1/docs/swagger.json ${DOCS_DIR}/public/api.json gen-api-types: gen-swagger # --disable-throw-on-error diff --git a/goutils b/goutils index 860d05e8..01cd6d40 160000 --- a/goutils +++ b/goutils @@ -1 +1 @@ -Subproject commit 860d05e804133d14bd0baa1c3229e9edfea9e65a +Subproject commit 01cd6d408cf66c6abb9bfbda54e8b6e8dd12bc1a diff --git a/scripts/update-wiki/api-md2mdx.ts b/scripts/update-wiki/api-md2mdx.ts new file mode 100644 index 00000000..3524d5dd --- /dev/null +++ b/scripts/update-wiki/api-md2mdx.ts @@ -0,0 +1,114 @@ +export function md2mdx(md: string) { + const indexFirstH2 = md.indexOf("## "); + if (indexFirstH2 === -1) { + console.error("## section not found in the file"); + process.exit(1); + } + + const h1 = md.slice(0, indexFirstH2); + const h1Lines = h1.split("\n"); + const keptH1Lines: string[] = []; + const callouts: string[] = []; + + for (let i = 0; i < h1Lines.length; i++) { + const line = h1Lines[i] ?? ""; + const calloutStart = line.match(/^>\s*\[!([a-z0-9_-]+)\]\s*$/i); + if (calloutStart) { + const rawCalloutType = (calloutStart[1] ?? "note").toLowerCase(); + const calloutType = + rawCalloutType === "note" + ? "info" + : rawCalloutType === "warning" + ? "warn" + : rawCalloutType; + const contentLines: string[] = []; + + i++; + for (; i < h1Lines.length; i++) { + const blockLine = h1Lines[i] ?? ""; + if (!blockLine.startsWith(">")) { + i--; + break; + } + contentLines.push(blockLine.replace(/^>\s?/, "")); + } + + while (contentLines[0] === "") { + contentLines.shift(); + } + while (contentLines[contentLines.length - 1] === "") { + contentLines.pop(); + } + + if (contentLines.length > 0) { + callouts.push( + `\n${contentLines.join("\n")}\n`, + ); + } + continue; + } + + keptH1Lines.push(line); + } + + const h1WithoutCallout = keptH1Lines.join("\n"); + const titleMatchResult = h1WithoutCallout.match( + new RegExp(/^\s*#\s+([^\n]+)/, "im"), + ); + const title = titleMatchResult?.[1]?.trim() ?? ""; + let description = h1WithoutCallout + .replace(new RegExp(/^\s*#\s+[^\n]+\n?/, "im"), "") + .replaceAll(new RegExp(/^\s*>.+$/, "gm"), "") + .trim(); + // remove trailing full stop + if (description.endsWith(".")) { + description = description.slice(0, -1); + } + + let header = `---\ntitle: ${title}`; + if (description) { + header += `\ndescription: ${description}`; + } + header += "\n---"; + + md = md.slice(indexFirstH2); + const calloutsBlock = callouts.join("\n\n"); + if (calloutsBlock) { + md = `${header}\n\n${calloutsBlock}\n\n${md}`; + } else { + md = `${header}\n\n${md}`; + } + + md = md.replaceAll("
", "
"); + md = md.replaceAll("<0", "\\<0"); + + return md; +} + +async function main() { + const Parser = await import("argparse").then((m) => m.ArgumentParser); + + const parser = new Parser({ + description: "Convert API markdown to VitePress MDX", + }); + parser.add_argument("-i", "--input", { + help: "Input markdown file", + required: true, + }); + parser.add_argument("-o", "--output", { + help: "Output VitePress MDX file", + required: true, + }); + + const args = parser.parse_args(); + const inMdFile = args.input; + const outMdxFile = args.output; + + const md = await Bun.file(inMdFile).text(); + const mdx = md2mdx(md); + await Bun.write(outMdxFile, mdx); +} + +if (import.meta.main) { + await main(); +} diff --git a/scripts/update-wiki/bun.lock b/scripts/update-wiki/bun.lock index f618b17a..5b5dbc0b 100644 --- a/scripts/update-wiki/bun.lock +++ b/scripts/update-wiki/bun.lock @@ -4,20 +4,28 @@ "workspaces": { "": { "name": "update-wiki", + "dependencies": { + "argparse": "^2.0.1", + }, "devDependencies": { - "@types/bun": "latest", + "@types/argparse": "^2.0.17", + "@types/bun": "^1.3.9", }, "peerDependencies": { - "typescript": "^5", + "typescript": "^5.9.3", }, }, }, "packages": { - "@types/bun": ["@types/bun@1.3.5", "", { "dependencies": { "bun-types": "1.3.5" } }, "sha512-RnygCqNrd3srIPEWBd5LFeUYG7plCoH2Yw9WaZGyNmdTEei+gWaHqydbaIRkIkcbXwhBT94q78QljxN0Sk838w=="], + "@types/argparse": ["@types/argparse@2.0.17", "", {}, "sha512-fueJssTf+4dW4HODshEGkIZbkLKHzgu1FvCI4cTc/MKum/534Euo3SrN+ilq8xgyHnOjtmg33/hee8iXLRg1XA=="], + + "@types/bun": ["@types/bun@1.3.9", "", { "dependencies": { "bun-types": "1.3.9" } }, "sha512-KQ571yULOdWJiMH+RIWIOZ7B2RXQGpL1YQrBtLIV3FqDcCu6FsbFUBwhdKUlCKUpS3PJDsHlJ1QKlpxoVR+xtw=="], "@types/node": ["@types/node@25.0.3", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA=="], - "bun-types": ["bun-types@1.3.5", "", { "dependencies": { "@types/node": "*" } }, "sha512-inmAYe2PFLs0SUbFOWSVD24sg1jFlMPxOjOSSCYqUgn4Hsc3rDc7dFvfVYjFPNHtov6kgUeulV4SxbuIV/stPw=="], + "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], + + "bun-types": ["bun-types@1.3.9", "", { "dependencies": { "@types/node": "*" } }, "sha512-+UBWWOakIP4Tswh0Bt0QD0alpTY8cb5hvgiYeWCMet9YukHbzuruIEeXC2D7nMJPB12kbh8C7XJykSexEqGKJg=="], "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], diff --git a/scripts/update-wiki/main.ts b/scripts/update-wiki/main.ts index 8e29452d..79bb2d26 100644 --- a/scripts/update-wiki/main.ts +++ b/scripts/update-wiki/main.ts @@ -1,393 +1,338 @@ import { mkdir, readdir, readFile, rm, writeFile } from "node:fs/promises"; import path from "node:path"; import { Glob } from "bun"; +import { md2mdx } from "./api-md2mdx"; type ImplDoc = { - /** Directory path relative to this repo, e.g. "internal/health/check" */ - pkgPath: string; - /** File name in wiki `src/impl/`, e.g. "internal-health-check.md" */ - docFileName: string; - /** VitePress route path (extensionless), e.g. "/impl/internal-health-check" */ - docRoute: string; - /** Absolute source README path */ - srcPathAbs: string; - /** Absolute destination doc path */ - dstPathAbs: string; + /** Directory path relative to this repo, e.g. "internal/health/check" */ + pkgPath: string; + /** File name in wiki `src/impl/`, e.g. "internal-health-check.md" */ + docFileName: string; + /** VitePress route path (extensionless), e.g. "/impl/internal-health-check" */ + docRoute: string; + /** Absolute source README path */ + srcPathAbs: string; + /** Absolute destination doc path */ + dstPathAbs: string; }; -const START_MARKER = "// GENERATED-IMPL-SIDEBAR-START"; -const END_MARKER = "// GENERATED-IMPL-SIDEBAR-END"; - const skipSubmodules = [ - "internal/go-oidc/", - "internal/gopsutil/", - "internal/go-proxmox/", + "internal/go-oidc/", + "internal/gopsutil/", + "internal/go-proxmox/", ]; -function escapeRegex(s: string) { - return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); -} - -function escapeSingleQuotedTs(s: string) { - return s.replace(/\\/g, "\\\\").replace(/'/g, "\\'"); -} - function normalizeRepoUrl(raw: string) { - let url = (raw ?? "").trim(); - if (!url) return ""; - // Common typo: "https://https://github.com/..." - url = url.replace(/^https?:\/\/https?:\/\//i, "https://"); - if (!/^https?:\/\//i.test(url)) url = `https://${url}`; - url = url.replace(/\/+$/, ""); - return url; + let url = (raw ?? "").trim(); + if (!url) return ""; + // Common typo: "https://https://github.com/..." + url = url.replace(/^https?:\/\/https?:\/\//i, "https://"); + if (!/^https?:\/\//i.test(url)) url = `https://${url}`; + url = url.replace(/\/+$/, ""); + return url; } function sanitizeFileStemFromPkgPath(pkgPath: string) { - // Convert a package path into a stable filename. - // Example: "internal/go-oidc/example" -> "internal-go-oidc-example" - // Keep it readable and unique (uses full path). - const parts = pkgPath - .split("/") - .filter(Boolean) - .map((p) => p.replace(/[^A-Za-z0-9._-]+/g, "-")); - const joined = parts.join("-"); - return joined.replace(/-+/g, "-").replace(/^-|-$/g, ""); + // Convert a package path into a stable filename. + // Example: "internal/go-oidc/example" -> "internal-go-oidc-example" + // Keep it readable and unique (uses full path). + const parts = pkgPath + .split("/") + .filter(Boolean) + .map((p) => p.replace(/[^A-Za-z0-9._-]+/g, "-")); + const joined = parts.join("-"); + return joined.replace(/-+/g, "-").replace(/^-|-$/g, ""); } function splitUrlAndFragment(url: string): { - urlNoFragment: string; - fragment: string; + urlNoFragment: string; + fragment: string; } { - const i = url.indexOf("#"); - if (i === -1) return { urlNoFragment: url, fragment: "" }; - return { urlNoFragment: url.slice(0, i), fragment: url.slice(i) }; + const i = url.indexOf("#"); + if (i === -1) return { urlNoFragment: url, fragment: "" }; + return { urlNoFragment: url.slice(0, i), fragment: url.slice(i) }; } function isExternalOrAbsoluteUrl(url: string) { - // - absolute site links: "/foo" - // - pure fragments: "#bar" - // - external schemes: "https:", "mailto:", "vscode:", etc. - // IMPORTANT: don't treat "config.go:29" as a scheme. - if (url.startsWith("/") || url.startsWith("#")) return true; - if (url.includes("://")) return true; - return /^(https?|mailto|tel|vscode|file|data|ssh|git):/i.test(url); + // - absolute site links: "/foo" + // - pure fragments: "#bar" + // - external schemes: "https:", "mailto:", "vscode:", etc. + // IMPORTANT: don't treat "config.go:29" as a scheme. + if (url.startsWith("/") || url.startsWith("#")) return true; + if (url.includes("://")) return true; + return /^(https?|mailto|tel|vscode|file|data|ssh|git):/i.test(url); } function isRepoSourceFilePath(filePath: string) { - // Conservative allow-list: avoid rewriting .md (non-README) which may be VitePress docs. - return /\.(go|ts|tsx|js|jsx|py|sh|yml|yaml|json|toml|env|css|html|txt)$/i.test( - filePath, - ); + // Conservative allow-list: avoid rewriting .md (non-README) which may be VitePress docs. + return /\.(go|ts|tsx|js|jsx|py|sh|yml|yaml|json|toml|env|css|html|txt)$/i.test( + filePath, + ); } function parseFileLineSuffix(urlNoFragment: string): { - filePath: string; - line?: string; + filePath: string; + line?: string; } { - // Match "file.ext:123" (line suffix), while leaving "file.ext" untouched. - const m = urlNoFragment.match(/^(.*?):(\d+)$/); - if (!m) return { filePath: urlNoFragment }; - return { filePath: m[1] ?? urlNoFragment, line: m[2] }; + // Match "file.ext:123" (line suffix), while leaving "file.ext" untouched. + const m = urlNoFragment.match(/^(.*?):(\d+)$/); + if (!m) return { filePath: urlNoFragment }; + return { filePath: m[1] ?? urlNoFragment, line: m[2] }; } function rewriteMarkdownLinksOutsideFences( - md: string, - rewriteInline: (url: string) => string, + md: string, + rewriteInline: (url: string) => string, ) { - const lines = md.split("\n"); - let inFence = false; + const lines = md.split("\n"); + let inFence = false; - for (let i = 0; i < lines.length; i++) { - const line = lines[i] ?? ""; - const trimmed = line.trimStart(); - if (trimmed.startsWith("```")) { - inFence = !inFence; - continue; - } - if (inFence) continue; + for (let i = 0; i < lines.length; i++) { + const line = lines[i] ?? ""; + const trimmed = line.trimStart(); + if (trimmed.startsWith("```")) { + inFence = !inFence; + continue; + } + if (inFence) continue; - // Inline markdown links/images: [text](url "title") / ![alt](url) - lines[i] = line.replace( - /\]\(([^)\s]+)(\s+"[^"]*")?\)/g, - (_full, urlRaw: string, maybeTitle: string | undefined) => { - const rewritten = rewriteInline(urlRaw); - return `](${rewritten}${maybeTitle ?? ""})`; - }, - ); - } + // Inline markdown links/images: [text](url "title") / ![alt](url) + lines[i] = line.replace( + /\]\(([^)\s]+)(\s+"[^"]*")?\)/g, + (_full, urlRaw: string, maybeTitle: string | undefined) => { + const rewritten = rewriteInline(urlRaw); + return `](${rewritten}${maybeTitle ?? ""})`; + }, + ); + } - return lines.join("\n"); + return lines.join("\n"); } function rewriteImplMarkdown(params: { - md: string; - pkgPath: string; - readmeRelToDocRoute: Map; - dirPathToDocRoute: Map; - repoUrl: string; + md: string; + pkgPath: string; + readmeRelToDocRoute: Map; + dirPathToDocRoute: Map; + repoUrl: string; }) { - const { md, pkgPath, readmeRelToDocRoute, dirPathToDocRoute, repoUrl } = - params; + const { md, pkgPath, readmeRelToDocRoute, dirPathToDocRoute, repoUrl } = + params; - return rewriteMarkdownLinksOutsideFences(md, (urlRaw) => { - // Handle angle-bracketed destinations: (<./foo/README.md>) - const angleWrapped = - urlRaw.startsWith("<") && urlRaw.endsWith(">") - ? urlRaw.slice(1, -1) - : urlRaw; + return rewriteMarkdownLinksOutsideFences(md, (urlRaw) => { + // Handle angle-bracketed destinations: (<./foo/README.md>) + const angleWrapped = + urlRaw.startsWith("<") && urlRaw.endsWith(">") + ? urlRaw.slice(1, -1) + : urlRaw; - const { urlNoFragment, fragment } = splitUrlAndFragment(angleWrapped); - if (!urlNoFragment) return urlRaw; - if (isExternalOrAbsoluteUrl(urlNoFragment)) return urlRaw; + const { urlNoFragment, fragment } = splitUrlAndFragment(angleWrapped); + if (!urlNoFragment) return urlRaw; + if (isExternalOrAbsoluteUrl(urlNoFragment)) return urlRaw; - // 1) Directory links like "common" or "common/" that have a README - const dirPathNormalized = urlNoFragment.replace(/\/+$/, ""); - let rewritten: string | undefined; - // First try exact match - if (dirPathToDocRoute.has(dirPathNormalized)) { - rewritten = `${dirPathToDocRoute.get(dirPathNormalized)}${fragment}`; - } else { - // Fallback: check parent directories for a README - // This handles paths like "internal/watcher/events" where only the parent has a README - let parentPath = dirPathNormalized; - while (parentPath.includes("/")) { - parentPath = parentPath.slice(0, parentPath.lastIndexOf("/")); - if (dirPathToDocRoute.has(parentPath)) { - rewritten = `${dirPathToDocRoute.get(parentPath)}${fragment}`; - break; - } - } - } - if (rewritten) { - return angleWrapped === urlRaw ? rewritten : `<${rewritten}>`; - } + // 1) Directory links like "common" or "common/" that have a README + const dirPathNormalized = urlNoFragment.replace(/\/+$/, ""); + let rewritten: string | undefined; + // First try exact match + if (dirPathToDocRoute.has(dirPathNormalized)) { + rewritten = `${dirPathToDocRoute.get(dirPathNormalized)}${fragment}`; + } else { + // Fallback: check parent directories for a README + // This handles paths like "internal/watcher/events" where only the parent has a README + let parentPath = dirPathNormalized; + while (parentPath.includes("/")) { + parentPath = parentPath.slice(0, parentPath.lastIndexOf("/")); + if (dirPathToDocRoute.has(parentPath)) { + rewritten = `${dirPathToDocRoute.get(parentPath)}${fragment}`; + break; + } + } + } + if (rewritten) { + return angleWrapped === urlRaw ? rewritten : `<${rewritten}>`; + } - // 2) Intra-repo README links -> VitePress impl routes - if (/(^|\/)README\.md$/.test(urlNoFragment)) { - const targetReadmeRel = path.posix.normalize( - path.posix.join(pkgPath, urlNoFragment), - ); - const route = readmeRelToDocRoute.get(targetReadmeRel); - if (route) { - const rewritten = `${route}${fragment}`; - return angleWrapped === urlRaw ? rewritten : `<${rewritten}>`; - } - return urlRaw; - } + // 2) Intra-repo README links -> VitePress impl routes + if (/(^|\/)README\.md$/.test(urlNoFragment)) { + const targetReadmeRel = path.posix.normalize( + path.posix.join(pkgPath, urlNoFragment), + ); + const route = readmeRelToDocRoute.get(targetReadmeRel); + if (route) { + const rewritten = `${route}${fragment}`; + return angleWrapped === urlRaw ? rewritten : `<${rewritten}>`; + } + return urlRaw; + } - // 3) Local source-file references like "config.go:29" -> GitHub blob link - if (repoUrl) { - const { filePath, line } = parseFileLineSuffix(urlNoFragment); - if (isRepoSourceFilePath(filePath)) { - const repoRel = path.posix.normalize( - path.posix.join(pkgPath, filePath), - ); - const githubUrl = `${repoUrl}/blob/main/${repoRel}${ - line ? `#L${line}` : "" - }`; - const rewritten = `${githubUrl}${fragment}`; - return angleWrapped === urlRaw ? rewritten : `<${rewritten}>`; - } - } + // 3) Local source-file references like "config.go:29" -> GitHub blob link + if (repoUrl) { + const { filePath, line } = parseFileLineSuffix(urlNoFragment); + if (isRepoSourceFilePath(filePath)) { + const repoRel = path.posix.normalize( + path.posix.join(pkgPath, filePath), + ); + const githubUrl = `${repoUrl}/blob/main/${repoRel}${ + line ? `#L${line}` : "" + }`; + const rewritten = `${githubUrl}${fragment}`; + return angleWrapped === urlRaw ? rewritten : `<${rewritten}>`; + } + } - return urlRaw; - }); + return urlRaw; + }); } async function listRepoReadmes(repoRootAbs: string): Promise { - const glob = new Glob("**/README.md"); - const readmes: string[] = []; + const glob = new Glob("**/README.md"); + const readmes: string[] = []; - for await (const rel of glob.scan({ - cwd: repoRootAbs, - onlyFiles: true, - dot: false, - })) { - // Bun returns POSIX-style rel paths. - if (rel === "README.md") continue; // exclude root README - if (rel.startsWith(".git/") || rel.includes("/.git/")) continue; - if (rel.startsWith("node_modules/") || rel.includes("/node_modules/")) - continue; - let skip = false; - for (const submodule of skipSubmodules) { - if (rel.startsWith(submodule)) { - skip = true; - break; - } - } - if (skip) continue; - readmes.push(rel); - } + for await (const rel of glob.scan({ + cwd: repoRootAbs, + onlyFiles: true, + dot: false, + })) { + // Bun returns POSIX-style rel paths. + if (rel === "README.md") continue; // exclude root README + if (rel.startsWith(".git/") || rel.includes("/.git/")) continue; + if (rel.startsWith("node_modules/") || rel.includes("/node_modules/")) + continue; + let skip = false; + for (const submodule of skipSubmodules) { + if (rel.startsWith(submodule)) { + skip = true; + break; + } + } + if (skip) continue; + readmes.push(rel); + } - // Deterministic order. - readmes.sort((a, b) => a.localeCompare(b)); - return readmes; + // Deterministic order. + readmes.sort((a, b) => a.localeCompare(b)); + return readmes; } async function writeImplDocCopy(params: { - srcAbs: string; - dstAbs: string; - pkgPath: string; - readmeRelToDocRoute: Map; - dirPathToDocRoute: Map; - repoUrl: string; + srcAbs: string; + dstAbs: string; + pkgPath: string; + readmeRelToDocRoute: Map; + dirPathToDocRoute: Map; + repoUrl: string; }) { - const { - srcAbs, - dstAbs, - pkgPath, - readmeRelToDocRoute, - dirPathToDocRoute, - repoUrl, - } = params; - await mkdir(path.dirname(dstAbs), { recursive: true }); - await rm(dstAbs, { force: true }); + const { + srcAbs, + dstAbs, + pkgPath, + readmeRelToDocRoute, + dirPathToDocRoute, + repoUrl, + } = params; + await mkdir(path.dirname(dstAbs), { recursive: true }); + await rm(dstAbs, { force: true }); - const original = await readFile(srcAbs, "utf8"); - const rewritten = rewriteImplMarkdown({ - md: original, - pkgPath, - readmeRelToDocRoute, - dirPathToDocRoute, - repoUrl, - }); - await writeFile(dstAbs, rewritten); + const original = await readFile(srcAbs, "utf8"); + const rewritten = rewriteImplMarkdown({ + md: original, + pkgPath, + readmeRelToDocRoute, + dirPathToDocRoute, + repoUrl, + }); + await writeFile(dstAbs, md2mdx(rewritten)); } async function syncImplDocs( - repoRootAbs: string, - wikiRootAbs: string, + repoRootAbs: string, + wikiRootAbs: string, ): Promise { - const implDirAbs = path.join(wikiRootAbs, "src", "impl"); - await mkdir(implDirAbs, { recursive: true }); + const implDirAbs = path.join(wikiRootAbs, "content", "docs", "impl"); + await mkdir(implDirAbs, { recursive: true }); - const readmes = await listRepoReadmes(repoRootAbs); - const docs: ImplDoc[] = []; - const expectedFileNames = new Set(); - expectedFileNames.add("introduction.md"); + const readmes = await listRepoReadmes(repoRootAbs); + const docs: ImplDoc[] = []; + const expectedFileNames = new Set(); + expectedFileNames.add("index.mdx"); + expectedFileNames.add("meta.json"); - const repoUrl = normalizeRepoUrl( - Bun.env.REPO_URL ?? "https://github.com/yusing/godoxy", - ); + const repoUrl = normalizeRepoUrl( + Bun.env.REPO_URL ?? "https://github.com/yusing/godoxy", + ); - // Precompute mapping from repo-relative README path -> VitePress route. - // This lets us rewrite intra-repo README links when copying content. - const readmeRelToDocRoute = new Map(); + // Precompute mapping from repo-relative README path -> VitePress route. + // This lets us rewrite intra-repo README links when copying content. + const readmeRelToDocRoute = new Map(); - // Also precompute mapping from directory path -> VitePress route. - // This handles links like "[`common/`](common)" that point to directories with READMEs. - const dirPathToDocRoute = new Map(); + // Also precompute mapping from directory path -> VitePress route. + // This handles links like "[`common/`](common)" that point to directories with READMEs. + const dirPathToDocRoute = new Map(); - for (const readmeRel of readmes) { - const pkgPath = path.posix.dirname(readmeRel); - if (!pkgPath || pkgPath === ".") continue; + for (const readmeRel of readmes) { + const pkgPath = path.posix.dirname(readmeRel); + if (!pkgPath || pkgPath === ".") continue; - const docStem = sanitizeFileStemFromPkgPath(pkgPath); - if (!docStem) continue; - const route = `/impl/${docStem}`; - readmeRelToDocRoute.set(readmeRel, route); - dirPathToDocRoute.set(pkgPath, route); - } + const docStem = sanitizeFileStemFromPkgPath(pkgPath); + if (!docStem) continue; + const route = `/impl/${docStem}`; + readmeRelToDocRoute.set(readmeRel, route); + dirPathToDocRoute.set(pkgPath, route); + } - for (const readmeRel of readmes) { - const pkgPath = path.posix.dirname(readmeRel); - if (!pkgPath || pkgPath === ".") continue; + for (const readmeRel of readmes) { + const pkgPath = path.posix.dirname(readmeRel); + if (!pkgPath || pkgPath === ".") continue; - const docStem = sanitizeFileStemFromPkgPath(pkgPath); - if (!docStem) continue; - const docFileName = `${docStem}.md`; - const docRoute = `/impl/${docStem}`; + const docStem = sanitizeFileStemFromPkgPath(pkgPath); + if (!docStem) continue; + const docFileName = `${docStem}.mdx`; + const docRoute = `/impl/${docStem}`; - const srcPathAbs = path.join(repoRootAbs, readmeRel); - const dstPathAbs = path.join(implDirAbs, docFileName); + const srcPathAbs = path.join(repoRootAbs, readmeRel); + const dstPathAbs = path.join(implDirAbs, docFileName); - await writeImplDocCopy({ - srcAbs: srcPathAbs, - dstAbs: dstPathAbs, - pkgPath, - readmeRelToDocRoute, - dirPathToDocRoute, - repoUrl, - }); + await writeImplDocCopy({ + srcAbs: srcPathAbs, + dstAbs: dstPathAbs, + pkgPath, + readmeRelToDocRoute, + dirPathToDocRoute, + repoUrl, + }); - docs.push({ pkgPath, docFileName, docRoute, srcPathAbs, dstPathAbs }); - expectedFileNames.add(docFileName); - } + docs.push({ pkgPath, docFileName, docRoute, srcPathAbs, dstPathAbs }); + expectedFileNames.add(docFileName); + } - // Clean orphaned impl docs. - const existing = await readdir(implDirAbs, { withFileTypes: true }); - for (const ent of existing) { - if (!ent.isFile()) continue; - if (!ent.name.endsWith(".md")) continue; - if (expectedFileNames.has(ent.name)) continue; - await rm(path.join(implDirAbs, ent.name), { force: true }); - } + // Clean orphaned impl docs. + const existing = await readdir(implDirAbs, { withFileTypes: true }); + for (const ent of existing) { + if (!ent.isFile()) continue; + if (!ent.name.endsWith(".md")) continue; + if (expectedFileNames.has(ent.name)) continue; + await rm(path.join(implDirAbs, ent.name), { force: true }); + } - // Deterministic for sidebar. - docs.sort((a, b) => a.pkgPath.localeCompare(b.pkgPath)); - return docs; -} - -function renderSidebarItems(docs: ImplDoc[], indent: string) { - // link: '/impl/' (extensionless) because VitePress `srcDir = "src"`. - if (docs.length === 0) return ""; - return ( - docs - .map((d) => { - const text = escapeSingleQuotedTs(d.pkgPath); - const link = escapeSingleQuotedTs(d.docRoute); - return `${indent}{ text: '${text}', link: '${link}' },`; - }) - .join("\n") + "\n" - ); -} - -async function updateVitepressSidebar(wikiRootAbs: string, docs: ImplDoc[]) { - const configPathAbs = path.join(wikiRootAbs, ".vitepress", "config.mts"); - if (!(await Bun.file(configPathAbs).exists())) { - throw new Error(`vitepress config not found: ${configPathAbs}`); - } - - const original = await readFile(configPathAbs, "utf8"); - - // Replace between markers with generated items. - // We keep indentation based on the marker line. - const markerRe = new RegExp( - `(^[\\t ]*)${escapeRegex(START_MARKER)}[\\s\\S]*?\\n\\1${escapeRegex( - END_MARKER, - )}`, - "m", - ); - - const m = original.match(markerRe); - if (!m) { - throw new Error( - `sidebar markers not found in ${configPathAbs}. Expected lines: ${START_MARKER} ... ${END_MARKER}`, - ); - } - const indent = m[1] ?? ""; - const generated = `${indent}${START_MARKER}\n${renderSidebarItems( - docs, - indent, - )}${indent}${END_MARKER}`; - - const updated = original.replace(markerRe, generated); - if (updated !== original) { - await writeFile(configPathAbs, updated); - } + // Deterministic for sidebar. + docs.sort((a, b) => a.pkgPath.localeCompare(b.pkgPath)); + return docs; } async function main() { - // This script lives in `scripts/update-wiki/`, so repo root is two levels up. - const repoRootAbs = path.resolve(import.meta.dir, "../.."); + // This script lives in `scripts/update-wiki/`, so repo root is two levels up. + const repoRootAbs = path.resolve(import.meta.dir); - // Required by task, but allow overriding via env for convenience. - const wikiRootAbs = Bun.env.DOCS_DIR - ? path.resolve(repoRootAbs, Bun.env.DOCS_DIR) - : path.resolve(repoRootAbs, "..", "godoxy-webui", "wiki"); + // Required by task, but allow overriding via env for convenience. + const wikiRootAbs = Bun.env.DOCS_DIR + ? path.resolve(repoRootAbs, Bun.env.DOCS_DIR) + : undefined; - const docs = await syncImplDocs(repoRootAbs, wikiRootAbs); - await updateVitepressSidebar(wikiRootAbs, docs); + if (!wikiRootAbs) { + throw new Error("DOCS_DIR is not set"); + } + + await syncImplDocs(repoRootAbs, wikiRootAbs); } await main(); diff --git a/scripts/update-wiki/package.json b/scripts/update-wiki/package.json index b5aec0d0..bbffb4fd 100644 --- a/scripts/update-wiki/package.json +++ b/scripts/update-wiki/package.json @@ -2,9 +2,13 @@ "name": "update-wiki", "private": true, "devDependencies": { - "@types/bun": "latest" + "@types/argparse": "^2.0.17", + "@types/bun": "^1.3.9" }, "peerDependencies": { - "typescript": "^5" + "typescript": "^5.9.3" + }, + "dependencies": { + "argparse": "^2.0.1" } }