import { mkdir, readFile, writeFile } from 'node:fs/promises'; import { dirname, resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; const scriptDir = dirname(fileURLToPath(import.meta.url)); const webRoot = resolve(scriptDir, '..'); const repoRoot = resolve(webRoot, '..'); const inputPath = resolve(repoRoot, 'ErsatzTV/wwwroot/openapi/v1.json'); const outputPath = resolve(webRoot, 'src/api/generated/v1.d.ts'); const document = JSON.parse(await readFile(inputPath, 'utf8')); const schemas = document.components?.schemas ?? {}; function schemaNameFromRef(ref) { return ref.replace('#/components/schemas/', ''); } function formatDescription(description, indent = '') { if (!description) { return ''; } return `${indent}/** ${String(description).replaceAll('*/', '* /')} */\n`; } function typeFromSchema(schema) { if (!schema) { return 'unknown'; } if (schema.$ref) { return `components["schemas"]["${schemaNameFromRef(schema.$ref)}"]`; } if (schema.oneOf || schema.anyOf) { return (schema.oneOf ?? schema.anyOf).map(typeFromSchema).join(' | '); } if (schema.allOf) { return schema.allOf.map(typeFromSchema).join(' & '); } if (Array.isArray(schema.enum)) { return schema.enum.map((value) => JSON.stringify(value)).join(' | ') || 'never'; } if (Array.isArray(schema.type)) { return schema.type.map((type) => typeFromSchema({ ...schema, type })).join(' | '); } switch (schema.type) { case 'array': return `Array<${typeFromSchema(schema.items)}>`; case 'boolean': return 'boolean'; case 'integer': case 'number': return 'number'; case 'null': return 'null'; case 'object': return objectTypeFromSchema(schema); case 'string': return 'string'; default: if (schema.properties || schema.additionalProperties) { return objectTypeFromSchema(schema); } return 'unknown'; } } function objectTypeFromSchema(schema) { const properties = schema.properties ?? {}; const required = new Set(schema.required ?? []); const lines = ['{']; for (const [name, propertySchema] of Object.entries(properties)) { const optional = required.has(name) ? '' : '?'; lines.push(`${formatDescription(propertySchema.description, ' ')} ${JSON.stringify(name)}${optional}: ${typeFromSchema(propertySchema)};`); } if (schema.additionalProperties) { const valueType = schema.additionalProperties === true ? 'unknown' : typeFromSchema(schema.additionalProperties); lines.push(` [key: string]: ${valueType};`); } lines.push(' }'); return lines.join('\n'); } function operationType(operation) { if (!operation) { return 'never'; } const responses = operation.responses ?? {}; const successResponse = responses['200'] ?? responses['201'] ?? responses['204']; const jsonContent = successResponse?.content?.['application/json'] ?? successResponse?.content?.['text/json'] ?? successResponse?.content?.['text/plain']; if (!jsonContent?.schema) { return 'void'; } return typeFromSchema(jsonContent.schema); } const lines = [ '// This file is generated by web/scripts/generate-openapi-types.mjs.', '// Do not edit by hand.', '', 'export interface components {', ' schemas: {' ]; for (const [name, schema] of Object.entries(schemas).sort(([a], [b]) => a.localeCompare(b))) { lines.push(`${formatDescription(schema.description, ' ')} ${JSON.stringify(name)}: ${typeFromSchema(schema)};`); } lines.push(' };'); lines.push('}'); lines.push(''); lines.push('export interface operations {'); for (const [path, methods] of Object.entries(document.paths ?? {}).sort(([a], [b]) => a.localeCompare(b))) { for (const [method, operation] of Object.entries(methods).sort(([a], [b]) => a.localeCompare(b))) { const name = operation.operationId ?? `${method.toUpperCase()} ${path}`; lines.push(` ${JSON.stringify(name)}: {`); lines.push(` response: ${operationType(operation)};`); lines.push(' };'); } } lines.push('}'); lines.push(''); await mkdir(dirname(outputPath), { recursive: true }); await writeFile(outputPath, `${lines.join('\n')}\n`);