import { mkdir, readFile, writeFile } from "node:fs/promises"; import path from "node:path"; import { fileURLToPath } from "node:url"; const scriptDirectory = path.dirname(fileURLToPath(import.meta.url)); const repoRoot = path.resolve(scriptDirectory, ".."); const openApiPath = path.join(repoRoot, "openapi", "RpgRoller.json"); const outputPath = path.join(repoRoot, "RpgRoller", "wwwroot", "generated", "api-client.js"); function escapePathSegment(segment) { return segment.replaceAll("\\", "\\\\").replaceAll("\"", "\\\""); } function collectOperations(document) { const operations = []; for (const [pathKey, pathItem] of Object.entries(document.paths ?? {})) { for (const [method, operation] of Object.entries(pathItem ?? {})) { if (operation === null || typeof operation !== "object") { continue; } if (typeof operation.operationId !== "string" || operation.operationId.length === 0) { throw new Error(`Missing operationId for ${method.toUpperCase()} ${pathKey}`); } operations.push({ operationId: operation.operationId, method: method.toUpperCase(), path: pathKey }); } } if (operations.length === 0) { throw new Error("OpenAPI document does not define any operations."); } return operations.sort((left, right) => left.operationId.localeCompare(right.operationId)); } function buildClientSource(operations) { const operationEntries = operations .map((operation) => ` ${operation.operationId}: { method: "${operation.method}", path: "${escapePathSegment(operation.path)}" }`) .join(",\n"); const helper = `const apiOperations = {\n${operationEntries}\n};\n`; const sendFunction = ` async function send(operation, pathParams = {}) { let resolvedPath = operation.path; for (const [key, value] of Object.entries(pathParams)) { resolvedPath = resolvedPath.replace(\`{\${key}}\`, encodeURIComponent(String(value))); } const response = await fetch(resolvedPath, { method: operation.method, headers: { "Accept": "application/json" } }); if (!response.ok) { const errorPayload = await response.json().catch(() => ({ error: "Unknown API error." })); throw new Error(errorPayload.error ?? \`Request failed with status \${response.status}\`); } return response.json(); } `.trim(); const exports = operations .map((operation) => { const params = [...operation.path.matchAll(/\{([^}]+)\}/g)].map((match) => match[1]); const signature = params.length === 0 ? "" : params.join(", "); const pathParams = params.length === 0 ? "{}" : `{ ${params.map((name) => `${name}: ${name}`).join(", ")} }`; return `export async function ${operation.operationId}(${signature}) {\n return send(apiOperations.${operation.operationId}, ${pathParams});\n}`; }) .join("\n\n"); return `/* This file is generated by scripts/generate-api-client.mjs. */\n\nexport { apiOperations };\n\n${helper}\n${sendFunction}\n\n${exports}\n`; } const openApiText = await readFile(openApiPath, "utf8"); const document = JSON.parse(openApiText); const operations = collectOperations(document); const clientSource = buildClientSource(operations); await mkdir(path.dirname(outputPath), { recursive: true }); await writeFile(outputPath, clientSource, "utf8"); console.log(`Generated API client: ${path.relative(repoRoot, outputPath)}`);