/** Keep one SDK/REST operation catalog on each resource page. */
import { readFileSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import { fileURLToPath } from "node:url";
import { docsDirectory, root, sdkSurface, httpSurface } from "./docs-surface.mjs";
const start = "{/* api-operations:start */}";
const end = "{/* api-operations:end */}";
const cell = (value) => value.replaceAll("|", "\\|").replaceAll("\n", " ");
const code = (value) => "`" + cell(value) + "`";
export function renderApiReference() {
const manifest = JSON.parse(readFileSync(join(root, "scripts/docs-api-reference.json"), "utf8"));
const methods = new Map(
sdkSurface().flatMap(({ group, methods }) =>
methods.map((method) => [`${group}.${method.name}`, { ...method, group }]),
),
);
const routes = new Map();
for (const route of httpSurface()) {
const key = `${route.method} ${route.path}`;
const variants = routes.get(key) ?? [];
variants.push(route);
routes.set(key, variants);
}
const pages = new Map();
const coveredRoutes = new Set();
function add(entry, sdk, route) {
if (!entry.page || !entry.description)
throw new Error(`Missing documentation for ${sdk ?? route}`);
if (route && !routes.has(route)) throw new Error(`Documented route no longer exists: ${route}`);
if (route && coveredRoutes.has(route)) throw new Error(`Route documented twice: ${route}`);
if (route) coveredRoutes.add(route);
const rows = pages.get(entry.page) ?? [];
rows.push({ ...entry, sdk, route });
pages.set(entry.page, rows);
}
for (const [key, entry] of Object.entries(manifest.methods)) {
const method = methods.get(key);
if (!method) throw new Error(`Documented SDK method no longer exists: ${key}`);
add(
entry,
method.calls.map((call) => code(`${method.group}.${call}`)).join("
") +
(method.surfaces.includes("native") ? "" : "
Remote client"),
entry.route,
);
methods.delete(key);
}
if (methods.size)
throw new Error(`SDK methods need documentation: ${[...methods.keys()].join(", ")}`);
for (const [route, entry] of Object.entries(manifest.http))
add(entry, entry.sdk ? code(entry.sdk) : undefined, route);
const missingRoutes = [...routes.keys()].filter((key) => !coveredRoutes.has(key));
if (missingRoutes.length)
throw new Error(`HTTP routes need documentation: ${missingRoutes.join(", ")}`);
const rendered = new Map();
for (const [page, rows] of pages) {
const sections = new Map();
for (const row of rows) {
const section = row.section ?? "HTTP endpoints";
const sectionRows = sections.get(section) ?? [];
sectionRows.push(row);
sections.set(section, sectionRows);
}
const onlyHttp = rows.every((row) => !row.sdk);
const content = [];
for (const [section, sectionRows] of sections) {
if (sections.size > 1)
content.push(`### ${section === "Operations" ? "Resource methods" : section}\n`);
content.push(
onlyHttp
? "| Operation | REST API |\n| --- | --- |"
: "| Operation | SDK | REST API |\n| --- | --- | --- |",
);
for (const row of sectionRows) {
const variants = row.route ? routes.get(row.route) : [];
const access = [...new Set(variants.map((variant) => variant.access))]
.map(code)
.join(" / ");
const availability =
variants.length && variants.every((variant) => variant.localOnly) ? " ยท Self-hosted" : "";
const rest = row.route
? `${code(row.route)}
${access}${availability}`
: `[Upload workflow](${row.workflow})`;
content.push(
onlyHttp
? `| ${cell(row.description)} | ${rest} |`
: `| ${cell(row.description)} | ${row.sdk ?? "HTTP only"} | ${rest} |`,
);
}
content.push("");
}
rendered.set(`api/${page}.mdx`, content.join("\n").trim());
}
return { rendered, methodCount: Object.keys(manifest.methods).length, routeCount: routes.size };
}
export function updateApiReference(write = false) {
const result = renderApiReference();
for (const [relative, block] of result.rendered) {
const file = join(docsDirectory, relative);
const previous = readFileSync(file, "utf8");
if (previous.split(start).length !== 2 || previous.split(end).length !== 2)
throw new Error(`Missing or repeated operation markers in ${relative}`);
const next =
previous.slice(0, previous.indexOf(start) + start.length) +
"\n\n" +
block +
"\n\n" +
previous.slice(previous.indexOf(end));
if (write) writeFileSync(file, next);
else if (next !== previous)
throw new Error(`Stale API catalog: ${relative}. Run bun run docs:reference.`);
}
return result;
}
if (process.argv[1] === fileURLToPath(import.meta.url)) {
const result = updateApiReference(process.argv.includes("--write"));
console.log(
`${result.methodCount} SDK methods and ${result.routeCount} HTTP routes across ${result.rendered.size} shared resource pages.`,
);
}