#!/usr/bin/env node /** * gen-openapi-module.mjs — build helper that reads docs/reference/openapi.yaml, * flattens the path/method matrix, and emits * src/app/docs/lib/openapi.generated.ts so the Api Explorer client can * iterate endpoints without parsing YAML at runtime. * * Runtime guarantees: * - No `any`. Everything is typed via an explicit `OpenApiEndpoint`. * - Endpoints are pre-sorted by (path, method) for stable output. * - Internal management endpoints (those under /api/ but NOT /api/v1) are * filtered out by default so the Api Explorer focuses on the public * OpenAI-compatible surface. Override with --include-management. * * Wired into `prebuild:docs` so `next build` always sees a fresh module. */ import { promises as fs } from "node:fs"; import path from "node:path"; import { fileURLToPath } from "node:url"; import yaml from "js-yaml"; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const ROOT = path.resolve(__dirname, "..", ".."); const OPENAPI_PATH = path.join(ROOT, "docs", "reference", "openapi.yaml"); const OUT_PATH = path.join(ROOT, "src", "app", "docs", "lib", "openapi.generated.ts"); const HTTP_METHODS = ["get", "post", "put", "delete", "patch", "options", "head"]; const args = process.argv.slice(2); const includeManagement = args.includes("--include-management"); function summarizeEndpoint(rawPath, method, op) { const tags = Array.isArray(op?.tags) && op.tags.length > 0 ? op.tags : ["Other"]; return { path: rawPath, method: method.toUpperCase(), summary: typeof op?.summary === "string" ? op.summary : "", description: typeof op?.description === "string" ? op.description : "", tag: typeof tags[0] === "string" ? tags[0] : "Other", tags, requiresAuth: Array.isArray(op?.security) && op.security.length > 0, hasRequestBody: Boolean(op?.requestBody), }; } function isPublicV1(rawPath) { // Anything starting with /api/v1 is the OpenAI-compatible surface; everything // else under /api/* is internal management. Routes that don't start with /api // (rare) are kept because they are typically root-level surfaces. return rawPath.startsWith("/api/v1") || !rawPath.startsWith("/api/"); } function quote(value) { if (value === undefined || value === null) return "undefined"; return JSON.stringify(value); } function tsArray(values) { if (!values || values.length === 0) return "[]"; return `[${values.map((v) => quote(v)).join(", ")}]`; } function renderEndpoint(ep) { return ` { path: ${quote(ep.path)}, method: ${quote(ep.method)}, summary: ${quote(ep.summary)}, description: ${quote(ep.description)}, tag: ${quote(ep.tag)}, tags: ${tsArray(ep.tags)}, requiresAuth: ${ep.requiresAuth ? "true" : "false"}, hasRequestBody: ${ep.hasRequestBody ? "true" : "false"}, }`; } async function main() { const yamlText = await fs.readFile(OPENAPI_PATH, "utf8"); const spec = yaml.load(yamlText); if (!spec || typeof spec !== "object" || !spec.paths || typeof spec.paths !== "object") { throw new Error("openapi.yaml has no `paths` map"); } const version = spec.info && typeof spec.info.version === "string" ? spec.info.version : "0.0.0"; const title = spec.info && typeof spec.info.title === "string" ? spec.info.title : "OmniRoute API"; const endpoints = []; for (const [rawPath, pathItem] of Object.entries(spec.paths)) { if (!pathItem || typeof pathItem !== "object") continue; if (!includeManagement && !isPublicV1(rawPath)) continue; for (const method of HTTP_METHODS) { const op = pathItem[method]; if (!op) continue; endpoints.push(summarizeEndpoint(rawPath, method, op)); } } endpoints.sort((a, b) => { if (a.path !== b.path) return a.path.localeCompare(b.path); return a.method.localeCompare(b.method); }); const totalManagement = Object.entries(spec.paths).filter(([p]) => !isPublicV1(p)).length; const header = `// AUTO-GENERATED by scripts/docs/gen-openapi-module.mjs — DO NOT EDIT MANUALLY // Regenerate with: node scripts/docs/gen-openapi-module.mjs // // Source of truth: docs/reference/openapi.yaml // // The Api Explorer consumes \`OPENAPI_ENDPOINTS\`; the spec metadata // (\`OPENAPI_VERSION\`, \`OPENAPI_TITLE\`) is surfaced in the page header. `; const body = ` export interface OpenApiEndpoint { /** Path template — may contain \`{param}\` placeholders. */ path: string; /** HTTP method in upper case (GET / POST / PUT / DELETE / PATCH / ...). */ method: string; /** Short one-line summary from the spec. */ summary: string; /** Long-form description (markdown is allowed). */ description: string; /** Primary tag — used for sidebar grouping in the Api Explorer. */ tag: string; /** All tags declared on the operation. */ tags: string[]; /** \`true\` when the operation declares a non-empty \`security\` array. */ requiresAuth: boolean; /** \`true\` when the operation declares a \`requestBody\`. */ hasRequestBody: boolean; } export const OPENAPI_VERSION = ${quote(version)}; export const OPENAPI_TITLE = ${quote(title)}; export const OPENAPI_ENDPOINTS: OpenApiEndpoint[] = [ ${endpoints.map(renderEndpoint).join(",\n")}${endpoints.length > 0 ? "," : ""} ]; export const OPENAPI_TAGS: string[] = Array.from( new Set(OPENAPI_ENDPOINTS.map((endpoint) => endpoint.tag)) ).sort(); `; await fs.mkdir(path.dirname(OUT_PATH), { recursive: true }); await fs.writeFile(OUT_PATH, `${header}${body}`, "utf8"); console.log( `[gen-openapi-module] wrote ${path.relative(ROOT, OUT_PATH)} (${endpoints.length} endpoints, v${version}, skipped ${totalManagement} management paths)` ); } main().catch((err) => { console.error(err); process.exit(1); });