import fs from 'node:fs';
import path from 'node:path';
import process from 'node:process';
import https from 'node:https';
import http from 'node:http';
const OPENAPI_INPUT = process.argv[2] || process.env.OPENAPI_URL || 'http://localhost:9001/api/openapi.json';
const scriptDir = path.dirname(new URL(import.meta.url).pathname);
const projectRoot = path.resolve(scriptDir, '..');
const outputFile = path.join(projectRoot, 'src', 'api', 'endpoints.ts');
function fetchJsonFromHttp(url) {
return new Promise((resolve, reject) => {
const client = url.startsWith('https') ? https : http;
client
.get(url, (res) => {
if (res.statusCode !== 200) {
reject(new Error(`Failed to fetch ${url}: ${res.statusCode}`));
return;
}
let data = '';
res.on('data', (chunk) => (data += chunk));
res.on('end', () => {
try {
resolve(JSON.parse(data));
} catch (e) {
reject(new Error(`Failed to parse JSON from ${url}: ${e.message}`));
}
});
})
.on('error', (err) => reject(new Error(`Failed to fetch ${url}: ${err.message}`)));
});
}
function loadSpec(input) {
if (input.startsWith('http://') || input.startsWith('https://')) {
return fetchJsonFromHttp(input);
}
const p = input.startsWith('file://') ? input.slice('file://'.length) : input;
const abs = path.isAbsolute(p) ? p : path.join(process.cwd(), p);
if (!fs.existsSync(abs)) {
const hint = 'Set OPENAPI_URL to a local openapi.json file (for example ./target/openapi.json).';
throw new Error(`OpenAPI spec not found at ${abs}. ${hint}`);
}
try {
const raw = fs.readFileSync(abs, 'utf8');
return Promise.resolve(JSON.parse(raw));
} catch (e) {
throw new Error(`Failed to load OpenAPI spec from ${abs}: ${e.message}`);
}
}
function emit(filePath, content) {
fs.mkdirSync(path.dirname(filePath), { recursive: true });
fs.writeFileSync(filePath, content, 'utf8');
}
function header(sourceHint) {
return `// AUTO-GENERATED by scripts/generate-endpoints.js from ${sourceHint}\n// Do not edit manually. Run: npm run generate:endpoints\n\n`;
}
function toConstCase(name) {
return String(name)
.replace(/([a-z0-9])([A-Z])/g, '$1_$2')
.replace(/[^a-zA-Z0-9]+/g, '_')
.replace(/^_+|_+$/g, '')
.toUpperCase();
}
function pathToBuilder(pathStr) {
const withoutApi = pathStr.replace(/^\/api/, '');
const params = Array.from(withoutApi.matchAll(/\{(\w+)\}/g)).map((m) => m[1]);
if (params.length === 0) {
return { kind: 'static', code: `'${withoutApi}'` };
}
const args = params.join(', ');
const template = withoutApi.replace(/\{(\w+)\}/g, (_m, p1) => '${' + p1 + '}');
return { kind: 'param', code: `(${args}) => \`${template}\`` };
}
function buildTypedParamSignature(pathStr, method, params) {
return params
.map((p) => `${p}: PathParams<'${pathStr}', '${method}'>['${p}']`)
.join(', ');
}
function generateEndpointsFromSpec(spec) {
const paths = spec.paths || {};
const methodPriority = ['get', 'post', 'put', 'patch', 'delete'];
const pathInfo = new Map();
for (const [p, methods] of Object.entries(paths)) {
let chosen = null;
for (const m of methodPriority) {
if (methods[m]) {
chosen = { method: m, op: methods[m] };
break;
}
}
if (!chosen) {
const firstMethod = Object.keys(methods)[0];
if (firstMethod) chosen = { method: firstMethod, op: methods[firstMethod] };
}
if (chosen) {
const opId = chosen.op.operationId || `${chosen.method}_${p.replace(/\{|\}/g, '')}`;
pathInfo.set(p, { opId, method: chosen.method });
}
}
const derivedLines = [];
derivedLines.push("import type { paths } from '../types/openapi'\n");
derivedLines.push('type PathParams<P extends keyof paths, M extends keyof paths[P]> =');
derivedLines.push(" paths[P][M] extends { parameters: { path: infer T } } ? T : Record<string, string>\n");
derivedLines.push('export const API_ENDPOINTS_DERIVED = {');
for (const [p, info] of pathInfo.entries()) {
const key = toConstCase(info.opId);
const builder = pathToBuilder(p);
if (builder.kind === 'param') {
const params = Array.from(p.matchAll(/\{(\w+)\}/g)).map((m) => m[1]);
const typedSig = buildTypedParamSignature(p, info.method, params);
const code = builder.code.replace(/^\(([^)]*)\)/, `(${typedSig})`);
derivedLines.push(` ${key}: ${code},`);
} else {
derivedLines.push(` ${key}: ${builder.code},`);
}
}
derivedLines.push('} as const;');
const baseSegments = new Set();
for (const p of Object.keys(paths)) {
const m = p.match(/^\/api\/?([^\/]+)/);
if (m && m[1]) baseSegments.add(m[1]);
}
const baseUrlsLines = [];
baseUrlsLines.push('// Base URL prefixes derived from OpenAPI paths');
baseUrlsLines.push('export const API_BASE_URLS = {');
baseUrlsLines.push(" ROOT: '/api',");
Array.from(baseSegments)
.sort()
.forEach((seg) => {
const key = toConstCase(seg);
baseUrlsLines.push(` ${key}: '/api/${seg}',`);
});
baseUrlsLines.push('} as const;');
const lines = [];
lines.push(...derivedLines);
lines.push('');
lines.push(...baseUrlsLines);
lines.push('');
lines.push('// Export generated endpoints directly (no hardcoded aliases)');
lines.push('export const API_ENDPOINTS = API_ENDPOINTS_DERIVED;');
lines.push('export type ApiEndpoint = typeof API_ENDPOINTS[keyof typeof API_ENDPOINTS];');
const content = header(OPENAPI_INPUT) + lines.join('\n') + '\n';
return { content, missing: [] };
}
async function main() {
try {
const spec = await loadSpec(OPENAPI_INPUT);
const { content, missing } = generateEndpointsFromSpec(spec);
if (missing.length) {
console.warn('[generate-endpoints] Missing in OpenAPI spec:', missing.join(', '));
}
emit(outputFile, content);
console.log(`[generate-endpoints] Wrote ${outputFile}`);
} catch (e) {
console.error('[generate-endpoints] Failed:', e.message);
console.error('[generate-endpoints] Provide OPENAPI_URL to a local spec file (e.g. ./target/openapi.json).');
process.exit(1);
}
}
main();