/** * Emits TypeScript declarations for the RPC wire bundle. * * The output describes the wire shape: a field with a decoder default may be * absent, so it is optional here. `test/rpc-wire/conformance.types.ts` checks * the output against the server's hand-written `rpc-types.ts` and the internal * types behind it. */ import type { WireDef, WireModel, WireType } from "./model"; const IDENTIFIER = /^[A-Za-z_$][A-Za-z0-9_$]*$/; function render(type: WireType): string { switch (type.kind) { case "string": case "boolean": case "null": case "unknown": return type.kind; case "integer": case "number": return "number"; case "literal": return JSON.stringify(type.value); case "enum": return type.values.map(value => JSON.stringify(value)).join(" | "); case "ref": return type.name; case "array": { const items = render(type.items); return type.items.kind === "union" || type.items.kind === "enum" ? `(${items})[]` : `${items}[]`; } case "record": return `Record`; case "union": return type.members.map(render).join(" | "); } } function docComment(text: string | undefined, indent: string): string { if (!text) return ""; const lines = text.split("\n"); if (lines.length === 1) return `${indent}/** ${text} */\n`; return `${indent}/**\n${lines.map(line => `${indent} * ${line}`).join("\n")}\n${indent} */\n`; } function renderDef(def: WireDef): string { const head = docComment(def.doc, ""); if (def.kind === "alias") return `${head}export type ${def.name} = ${render(def.type)};\n`; const fields = def.fields .map(field => { const key = IDENTIFIER.test(field.key) ? field.key : JSON.stringify(field.key); const optional = field.required ? "" : "?"; return `${docComment(field.doc, "\t")}\t${key}${optional}: ${render(field.type)};\n`; }) .join(""); return `${head}export interface ${def.name} {\n${fields}}\n`; } /** Renders the generated TypeScript module. */ export function emitTypeScript(model: WireModel): string { const parts = [ "// Generated by `bun run gen:rpc` from src/modes/rpc/wire. Do not edit.\n", ...[...model.defs.values()].map(renderDef), ]; const commands = model.commands .map(command => { const params = command.params ?? "undefined"; const result = command.result === null ? "undefined" : command.nullable ? `${command.result} | null` : command.result; return `\t${command.name}: { params: ${params}; result: ${result} };\n`; }) .join(""); parts.push( `/** Every RPC command's parameters and successful response \`data\`. */\nexport interface RpcWireCommands {\n${commands}}\n`, ); return parts.join("\n"); }