Amp-Thread-ID: https://ampcode.com/threads/T-019f9d91-77c1-7206-a60f-ed6554ce92ab Co-authored-by: Heaust Azure <heaust.azure@gmail.com>
84 lines
3.9 KiB
TypeScript
84 lines
3.9 KiB
TypeScript
import assert from "node:assert/strict";
|
|
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
import { join } from "node:path";
|
|
|
|
const root = new URL("..", import.meta.url).pathname;
|
|
const docs = join(root, "docs");
|
|
const output = join(root, "docs/reference/generated");
|
|
const files = readdirSync(output, { recursive: true }).map(String);
|
|
const sidebarFile = files.find((file) => /sidebar.*\.json$/i.test(file));
|
|
assert(sidebarFile, "VitePress sidebar JSON was not generated");
|
|
const sidebar: unknown = JSON.parse(readFileSync(join(output, sidebarFile), "utf8"));
|
|
assert(Array.isArray(sidebar), "sidebar root must be the observed VitePress array structure");
|
|
|
|
type SidebarItem = { text?: unknown; link?: unknown; items?: unknown };
|
|
const sidebarItems: SidebarItem[] = [];
|
|
const visit = (items: unknown): void => {
|
|
assert(Array.isArray(items), "sidebar items must be arrays");
|
|
for (const item of items) {
|
|
assert(item && typeof item === "object", "sidebar entries must be objects");
|
|
const typed = item as SidebarItem;
|
|
sidebarItems.push(typed);
|
|
if (typed.items !== undefined) visit(typed.items);
|
|
}
|
|
};
|
|
visit(sidebar);
|
|
for (const item of sidebarItems) {
|
|
if (item.link === undefined) continue;
|
|
assert(typeof item.link === "string", "sidebar links must be strings");
|
|
const link = item.link;
|
|
const relative = link.replace(/^\/reference\/generated\//, "");
|
|
assert(relative !== link, `sidebar link is outside generated reference: ${link}`);
|
|
const target = relative.endsWith("/") ? `${relative}index.md` : relative;
|
|
assert(existsSync(join(output, target)), `broken sidebar link: ${link}`);
|
|
}
|
|
const packageModules = sidebarItems
|
|
.filter(
|
|
(item): item is SidebarItem & { text: string; link: string } =>
|
|
typeof item.text === "string" && typeof item.link === "string" && item.link.endsWith("/"),
|
|
)
|
|
.map(({ text, link }) => ({ text, link }));
|
|
assert.deepEqual(packageModules, [
|
|
{ text: "fxnode", link: "/reference/generated/fxnode/" },
|
|
{ text: "headless", link: "/reference/generated/fxnode/headless/" },
|
|
{ text: "color-ramp", link: "/reference/generated/fxnode/widgets/color-ramp/" },
|
|
]);
|
|
|
|
const text = files
|
|
.filter((file) => file.endsWith(".md") || file.endsWith(".json"))
|
|
.map((file) => readFileSync(join(output, file), "utf8"))
|
|
.join("\n");
|
|
for (const moduleName of ["fxnode", "fxnode/headless", "fxnode/widgets/color-ramp"])
|
|
assert(text.includes(moduleName), `missing documented package module: ${moduleName}`);
|
|
for (const denied of ["BoundEngine", "BoundDocument", "bindEngine", "bindDocument", "@lib/"])
|
|
assert(!text.includes(denied), `implementation name leaked into docs: ${denied}`);
|
|
for (const line of text.split("\n").filter((line) => line.startsWith("Defined in:")))
|
|
assert.match(
|
|
line,
|
|
/^Defined in: \[[^\]]+:\d+\]\(https:\/\/github\.com\/Heaust-ops\/fxnode\/blob\/main\/[A-Za-z0-9._/-]+#L\d+\)$/,
|
|
`invalid source location: ${line}`,
|
|
);
|
|
for (const denied of ["/home/", "file:", "node\\_modules/", "node_modules/"])
|
|
assert(!text.includes(denied), `local or dependency path leaked into docs: ${denied}`);
|
|
|
|
// VitePress treats a relative link outside docs as a page link and can fail to
|
|
// report a missing repository source file. Repository TypeScript links must be
|
|
// durable GitHub links; relative image links deliberately remain supported.
|
|
const markdownFiles = readdirSync(docs, { recursive: true })
|
|
.map(String)
|
|
.filter((file) => file.endsWith(".md") && !file.startsWith("reference/generated/"));
|
|
for (const file of markdownFiles) {
|
|
const markdown = readFileSync(join(docs, file), "utf8");
|
|
for (const match of markdown.matchAll(/(?<!!)\[[^\]]*\]\(([^)]+\.ts(?:#[^)]*)?)\)/g)) {
|
|
const target = match[1]!;
|
|
assert(
|
|
/^https?:\/\//.test(target),
|
|
`${file}: repository TypeScript source link must be an absolute URL: ${target}`,
|
|
);
|
|
}
|
|
}
|
|
|
|
console.log(
|
|
`Docs passed (${files.filter((file) => file.endsWith(".md")).length} generated API pages, ${markdownFiles.length} authored pages; ${sidebarFile})`,
|
|
);
|