89 lines
3.7 KiB
TypeScript
89 lines
3.7 KiB
TypeScript
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<string, LoadedApiOperationContract[]>();
|
|
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<string, unknown>;
|
|
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.`);
|