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") / 
- 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") / 
+ 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"
}
}