import { createHash, randomUUID } from "node:crypto"; import { readFile, writeFile, rename, rm } from "node:fs/promises"; import { dirname, resolve } from "node:path"; import { fileURLToPath } from "node:url"; import openapiTS, { astToString, COMMENT_HEADER } from "openapi-typescript"; const local = new URL("../openapi/openapi.json", import.meta.url); const manifest = new URL("../openapi/server-manifest.json", import.meta.url); const generated = new URL("../src/generated/server-api.d.ts", import.meta.url); const defaultUpstream = new URL("../../next-tjwater-server/contracts/server-v1.openapi.json", import.meta.url); export function assertContractDigest(raw: string, expected: string) { if (createHash("sha256").update(raw).digest("hex") !== expected) throw new Error("Server contract digest differs; run bun run sync:openapi"); } async function main() { const sync = process.argv.includes("--sync"); const upstreamIndex = process.argv.indexOf("--upstream"); if (upstreamIndex >= 0 && !process.argv[upstreamIndex + 1]) throw new Error("--upstream requires an OpenAPI file"); const upstream = upstreamIndex >= 0 ? resolve(process.argv[upstreamIndex + 1]) : defaultUpstream; let upstreamRaw: string | undefined; try { upstreamRaw = await readFile(upstream, "utf8"); } catch (error) { if (sync || upstreamIndex >= 0 || (error as NodeJS.ErrnoException).code !== "ENOENT") throw error; } if (sync) { const producerManifest = JSON.parse(await readFile(resolve( dirname(upstream instanceof URL ? fileURLToPath(upstream) : upstream), "manifest.json" ), "utf8")); assertContractDigest(upstreamRaw!, producerManifest.contracts.server.sha256); // Complete validation and generation before replacing any checked-in artifact. const types = COMMENT_HEADER + astToString(await openapiTS(JSON.parse(upstreamRaw!))); const artifacts = [ { target: local, content: upstreamRaw! }, { target: manifest, content: JSON.stringify(producerManifest, null, 2) + "\n" }, { target: generated, content: types }, ].map((artifact) => ({ ...artifact, temporary: new URL(`${artifact.target.href}.${randomUUID()}.tmp`) })); try { for (const artifact of artifacts) await writeFile(artifact.temporary, artifact.content); for (const artifact of artifacts) await rename(artifact.temporary, artifact.target); } finally { await Promise.all(artifacts.map((artifact) => rm(artifact.temporary, { force: true }))); } console.log("Server OpenAPI snapshot, producer digest and types synchronized."); return; } const raw = await readFile(local, "utf8"); const receipt = JSON.parse(await readFile(manifest, "utf8")); assertContractDigest(raw, receipt.contracts.server.sha256); if (upstreamRaw !== undefined && upstreamRaw !== raw) throw new Error("Upstream Server contract changed; run bun run sync:openapi"); if (await readFile(generated, "utf8") !== COMMENT_HEADER + astToString(await openapiTS(JSON.parse(raw)))) throw new Error("Generated Server types differ; run bun run generate:openapi"); console.log(upstreamRaw === undefined ? "Local Server contract digest and types checked; upstream unavailable in this checkout." : "Upstream Server contract, digest and generated types checked."); } if (import.meta.main) await main();