import { execFile } from "node:child_process"; import { createHash } from "node:crypto"; import { mkdtemp, readFile, rm } from "node:fs/promises"; import { tmpdir } from "node:os"; import { basename, join } from "node:path"; import { fileURLToPath } from "node:url"; import { promisify } from "node:util"; const manifest = JSON.parse( await readFile(new URL("../contracts/manifest.json", import.meta.url), "utf8"), ); const run = promisify(execFile); const projectRoot = fileURLToPath(new URL("../", import.meta.url)); const generator = join( projectRoot, "node_modules", "openapi-typescript", "bin", "cli.js", ); const temporaryDirectory = await mkdtemp( join(tmpdir(), "tjwater-api-contracts-"), ); try { for (const [name, contract] of Object.entries(manifest.contracts)) { const contractPath = fileURLToPath( new URL(`../contracts/${contract.file}`, import.meta.url), ); const payload = await readFile(contractPath); const actual = createHash("sha256").update(payload).digest("hex"); if (actual !== contract.sha256) { throw new Error( `${name} contract hash mismatch: expected ${contract.sha256}, got ${actual}`, ); } const generatedName = `${name}Api.ts`; const temporaryOutput = join(temporaryDirectory, generatedName); await run(process.execPath, [generator, contractPath, "-o", temporaryOutput]); const expectedOutput = await readFile( new URL(`../src/generated/${generatedName}`, import.meta.url), ); const generatedOutput = await readFile(temporaryOutput); if (!expectedOutput.equals(generatedOutput)) { throw new Error( `${basename(contract.file)} generated type is stale: run \`npm run api:generate\``, ); } } } finally { await rm(temporaryDirectory, { recursive: true, force: true }); } console.log( `validated API contract mirrors and generated types for ${manifest.contract_version}`, );