mirror of
https://github.com/yusing/godoxy.git
synced 2026-04-24 01:38:50 +02:00
feat(scriptsi): add script to sync implementation docs with wiki
- Introduced a new `update-wiki` script to automate the synchronization of implementation documentation from the repository to the wiki. - Added necessary configuration files including `package.json`, `tsconfig.json`, and `.gitignore` for the new script. - Updated the Makefile to include a target for running the `update-wiki` script.
This commit is contained in:
6
Makefile
6
Makefile
@@ -171,4 +171,8 @@ gen-api-types: gen-swagger
|
|||||||
# --disable-throw-on-error
|
# --disable-throw-on-error
|
||||||
bunx --bun swagger-typescript-api generate --sort-types --generate-union-enums --axios --add-readonly --route-types \
|
bunx --bun swagger-typescript-api generate --sort-types --generate-union-enums --axios --add-readonly --route-types \
|
||||||
--responses -o ${WEBUI_DIR}/lib -n api.ts -p internal/api/v1/docs/swagger.json
|
--responses -o ${WEBUI_DIR}/lib -n api.ts -p internal/api/v1/docs/swagger.json
|
||||||
bunx --bun prettier --config ${WEBUI_DIR}/.prettierrc --write ${WEBUI_DIR}/lib/api.ts
|
bunx --bun prettier --config ${WEBUI_DIR}/.prettierrc --write ${WEBUI_DIR}/lib/api.ts
|
||||||
|
|
||||||
|
.PHONY: update-wiki
|
||||||
|
update-wiki:
|
||||||
|
DOCS_DIR=${DOCS_DIR} bun --bun scripts/update-wiki/main.ts
|
||||||
|
|||||||
1
scripts/update-wiki/.gitignore
vendored
Normal file
1
scripts/update-wiki/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
node_modules
|
||||||
26
scripts/update-wiki/bun.lock
Normal file
26
scripts/update-wiki/bun.lock
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"lockfileVersion": 1,
|
||||||
|
"configVersion": 1,
|
||||||
|
"workspaces": {
|
||||||
|
"": {
|
||||||
|
"name": "update-wiki",
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/bun": "latest",
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"typescript": "^5",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"packages": {
|
||||||
|
"@types/bun": ["@types/bun@1.3.5", "", { "dependencies": { "bun-types": "1.3.5" } }, "sha512-RnygCqNrd3srIPEWBd5LFeUYG7plCoH2Yw9WaZGyNmdTEei+gWaHqydbaIRkIkcbXwhBT94q78QljxN0Sk838w=="],
|
||||||
|
|
||||||
|
"@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=="],
|
||||||
|
|
||||||
|
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
||||||
|
|
||||||
|
"undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
|
||||||
|
}
|
||||||
|
}
|
||||||
175
scripts/update-wiki/main.ts
Normal file
175
scripts/update-wiki/main.ts
Normal file
@@ -0,0 +1,175 @@
|
|||||||
|
import { Glob } from "bun";
|
||||||
|
import { linkSync } from "fs";
|
||||||
|
import { mkdir, readdir, readFile, rm, writeFile } from "fs/promises";
|
||||||
|
import path from "path";
|
||||||
|
|
||||||
|
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;
|
||||||
|
/** 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";
|
||||||
|
|
||||||
|
function escapeRegex(s: string) {
|
||||||
|
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||||
|
}
|
||||||
|
|
||||||
|
function escapeSingleQuotedTs(s: string) {
|
||||||
|
return s.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
|
||||||
|
}
|
||||||
|
|
||||||
|
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, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
async function listRepoReadmes(repoRootAbs: string): Promise<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;
|
||||||
|
if (rel.startsWith("internal/go-oidc/")) continue;
|
||||||
|
if (rel.startsWith("internal/gopsutil/")) continue;
|
||||||
|
readmes.push(rel);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deterministic order.
|
||||||
|
readmes.sort((a, b) => a.localeCompare(b));
|
||||||
|
return readmes;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function ensureHardLink(srcAbs: string, dstAbs: string) {
|
||||||
|
await mkdir(path.dirname(dstAbs), { recursive: true });
|
||||||
|
await rm(dstAbs, { force: true });
|
||||||
|
// Prefer sync for better error surfaces in Bun on some platforms.
|
||||||
|
linkSync(srcAbs, dstAbs);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function syncImplDocs(
|
||||||
|
repoRootAbs: string,
|
||||||
|
wikiRootAbs: string
|
||||||
|
): Promise<ImplDoc[]> {
|
||||||
|
const implDirAbs = path.join(wikiRootAbs, "src", "impl");
|
||||||
|
await mkdir(implDirAbs, { recursive: true });
|
||||||
|
|
||||||
|
const readmes = await listRepoReadmes(repoRootAbs);
|
||||||
|
const docs: ImplDoc[] = [];
|
||||||
|
const expectedFileNames = new Set<string>();
|
||||||
|
|
||||||
|
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 srcPathAbs = path.join(repoRootAbs, readmeRel);
|
||||||
|
const dstPathAbs = path.join(implDirAbs, docFileName);
|
||||||
|
|
||||||
|
await ensureHardLink(srcPathAbs, dstPathAbs);
|
||||||
|
|
||||||
|
docs.push({ pkgPath, docFileName, 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 });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deterministic for sidebar.
|
||||||
|
docs.sort((a, b) => a.pkgPath.localeCompare(b.pkgPath));
|
||||||
|
return docs;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderSidebarItems(docs: ImplDoc[], indent: string) {
|
||||||
|
// link: '/impl/<file>.md' because VitePress `srcDir = "src"`.
|
||||||
|
if (docs.length === 0) return "";
|
||||||
|
return (
|
||||||
|
docs
|
||||||
|
.map((d) => {
|
||||||
|
const text = escapeSingleQuotedTs(d.pkgPath);
|
||||||
|
const link = escapeSingleQuotedTs(`/impl/${d.docFileName}`);
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
// 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");
|
||||||
|
|
||||||
|
const docs = await syncImplDocs(repoRootAbs, wikiRootAbs);
|
||||||
|
await updateVitepressSidebar(wikiRootAbs, docs);
|
||||||
|
}
|
||||||
|
|
||||||
|
await main();
|
||||||
10
scripts/update-wiki/package.json
Normal file
10
scripts/update-wiki/package.json
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"name": "update-wiki",
|
||||||
|
"private": true,
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/bun": "latest"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"typescript": "^5"
|
||||||
|
}
|
||||||
|
}
|
||||||
29
scripts/update-wiki/tsconfig.json
Normal file
29
scripts/update-wiki/tsconfig.json
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
// Environment setup & latest features
|
||||||
|
"lib": ["ESNext"],
|
||||||
|
"target": "ESNext",
|
||||||
|
"module": "Preserve",
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"allowJs": true,
|
||||||
|
|
||||||
|
// Bundler mode
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"verbatimModuleSyntax": true,
|
||||||
|
"noEmit": true,
|
||||||
|
|
||||||
|
// Best practices
|
||||||
|
"strict": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"noUncheckedIndexedAccess": true,
|
||||||
|
"noImplicitOverride": true,
|
||||||
|
|
||||||
|
// Some stricter flags (disabled by default)
|
||||||
|
"noUnusedLocals": false,
|
||||||
|
"noUnusedParameters": false,
|
||||||
|
"noPropertyAccessFromIndexSignature": false
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user