import { execFile } from 'node:child_process'; import { rm, writeFile } from 'node:fs/promises'; import path from 'node:path'; import { promisify } from 'node:util'; import { loadApiContracts } from '@/openapi/contracts'; import { discoverApiOperations, discoverContractFiles } from '@/openapi/discover'; import { buildOpenApiDocument } from '@/openapi/document'; import { inferApiContracts } from '@/openapi/infer'; import type { LoadedApiOperationContract } from '@/openapi/operation'; const projectRoot = process.cwd(); const generatedFileName = 'contract.generated.ts'; const generatedHeader = '// Generated by `pnpm openapi:contracts`. Do not edit directly.'; const execFileAsync = promisify(execFile); const existingGeneratedFiles = (await discoverContractFiles(projectRoot)).filter( file => path.basename(file) === generatedFileName, ); await Promise.all(existingGeneratedFiles.map(file => rm(file))); const [explicitContracts, discovered] = await Promise.all([ loadApiContracts(projectRoot), discoverApiOperations(projectRoot), ]); const inferredContracts = inferApiContracts(discovered, explicitContracts); const { document } = await buildOpenApiDocument('all', projectRoot); const documentPaths = document.paths ?? {}; const byRoute = new Map(); const generatedFiles: string[] = []; inferredContracts.forEach(contract => { byRoute.set(contract.source, [...(byRoute.get(contract.source) ?? []), contract]); }); function serialize(value: unknown) { return JSON.stringify(value, null, 2); } for (const [routeSource, contracts] of byRoute) { const routeFile = path.join(projectRoot, ...routeSource.split('/')); const outputFile = path.join(path.dirname(routeFile), generatedFileName); const operationDefinitions = contracts.map((contract, index) => { const generatedOperation = documentPaths[contract.path]?.[contract.method]; if (!generatedOperation || typeof generatedOperation !== 'object') { throw new Error( `Generated document is missing ${contract.method.toUpperCase()} ${contract.path}.`, ); } const operation = structuredClone(generatedOperation) as Record; delete operation.security; delete operation['x-umami-audience']; delete operation['x-umami-contract']; delete operation['x-umami-source']; if ( typeof operation.description === 'string' && operation.description.startsWith('Generated from') ) { delete operation.description; } return `const operation${index + 1} = defineOperation({\n method: ${serialize(contract.method)},\n path: ${serialize(contract.path)},\n audience: ${serialize(contract.audience)},\n auth: ${serialize(contract.auth)},\n operation: ${serialize(operation)} as ZodOpenApiOperationObject,\n});`; }); const exports = contracts.map((_, index) => `operation${index + 1}`).join(', '); const content = `${generatedHeader}\n\nimport type { ZodOpenApiOperationObject } from 'zod-openapi';\nimport { defineOperation } from '@/openapi/operation';\n\n${operationDefinitions.join('\n\n')}\n\nexport const operations = [${exports}] as const;\n`; await writeFile(outputFile, content, 'utf8'); generatedFiles.push(outputFile); } const formatter = path.join(projectRoot, 'node_modules', '@biomejs', 'biome', 'bin', 'biome'); for (let index = 0; index < generatedFiles.length; index += 20) { await execFileAsync(process.execPath, [ formatter, 'format', '--write', ...generatedFiles.slice(index, index + 20), ]); } const generatedCount = [...byRoute.values()].reduce( (total, contracts) => total + contracts.length, 0, ); console.log(`Generated ${generatedCount} explicit contracts in ${byRoute.size} route modules.`);