#!/usr/bin/env node // Fetches the arcadia OpenAPI spec and regenerates ../src/generated/openapi.d.ts. // // Usage (from a consuming app, with arcadia reachable): // node ../lib-arcadia-core-client/scripts/sync-spec.mjs // // Configurable via env: // ARCADIA_OPENAPI_URL default: http://localhost:4000/api/openapi // ARCADIA_BEARER_TOKEN optional, sent as Authorization if the spec route // is gated in your deployment // // Requires `openapi-typescript` to be available (in the consuming app's // node_modules, or globally). Run from a consuming app so the dep resolves: // npm i -D openapi-typescript import { writeFile, mkdir } from "node:fs/promises"; import { fileURLToPath, pathToFileURL } from "node:url"; import { dirname, resolve } from "node:path"; import { createRequire } from "node:module"; const here = dirname(fileURLToPath(import.meta.url)); const outDir = resolve(here, "..", "src", "generated"); const outFile = resolve(outDir, "openapi.d.ts"); const specUrl = process.env.ARCADIA_OPENAPI_URL ?? "http://localhost:4000/api/openapi"; async function main() { console.log(`→ fetching ${specUrl}`); const headers = {}; if (process.env.ARCADIA_BEARER_TOKEN) { headers["Authorization"] = `Bearer ${process.env.ARCADIA_BEARER_TOKEN}`; } const res = await fetch(specUrl, { headers }); if (!res.ok) { console.error(`spec fetch failed: ${res.status} ${res.statusText}`); process.exit(1); } const spec = await res.json(); // Resolve openapi-typescript from the consuming app's node_modules, since // the lib has no package.json of its own. let openapiTs; let astToString; try { const requireFromCwd = createRequire(resolve(process.cwd(), "package.json")); const resolved = requireFromCwd.resolve("openapi-typescript"); const mod = await import(pathToFileURL(resolved).href); // Handle both ESM (mod.default is the function) and CJS-via-import // (mod.default is the namespace, mod.default.default is the function). openapiTs = typeof mod.default === "function" ? mod.default : mod.default?.default; astToString = mod.astToString ?? mod.default?.astToString; } catch (err) { console.error( `openapi-typescript could not be resolved from ${process.cwd()}.\n` + `Run from your consuming app's directory after installing it:\n` + ` cd && npm i -D openapi-typescript\n` + `Original error: ${err.message}`, ); process.exit(1); } // Scrub malformed operation entries. Arcadia's spec contains some // endpoints whose operation value is the bare string "ok" (a controller // placeholder that never got replaced with a real OperationObject). // openapi-typescript can't transform them — strip them and report so // the generation succeeds for the rest. Fix is on the arcadia side. const skipped = []; if (spec.paths) { for (const [path, item] of Object.entries(spec.paths)) { if (!item || typeof item !== "object") continue; for (const [verb, op] of Object.entries(item)) { if (typeof op !== "object" || op === null) { skipped.push(`${verb.toUpperCase()} ${path}`); delete item[verb]; } } } } if (skipped.length) { console.warn(`⚠ skipped ${skipped.length} malformed operations (arcadia spec bug):`); for (const s of skipped.slice(0, 10)) console.warn(` - ${s}`); if (skipped.length > 10) console.warn(` …and ${skipped.length - 10} more`); } console.log(`→ generating types`); const ast = await openapiTs(spec); const dts = astToString(ast); await mkdir(outDir, { recursive: true }); const banner = "// AUTO-GENERATED — do not edit by hand. Regenerate with sync-spec.mjs.\n" + `// Source: ${specUrl}\n` + `// Generated: ${new Date().toISOString()}\n\n`; await writeFile(outFile, banner + dts, "utf8"); console.log(`✓ wrote ${outFile}`); } main().catch((err) => { console.error(err); process.exit(1); });