trickle-backend 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/dist/db/connection.d.ts +3 -0
  2. package/dist/db/connection.js +16 -0
  3. package/dist/db/migrations.d.ts +2 -0
  4. package/dist/db/migrations.js +51 -0
  5. package/dist/db/queries.d.ts +70 -0
  6. package/dist/db/queries.js +186 -0
  7. package/dist/index.d.ts +1 -0
  8. package/dist/index.js +10 -0
  9. package/dist/routes/audit.d.ts +2 -0
  10. package/dist/routes/audit.js +251 -0
  11. package/dist/routes/codegen.d.ts +2 -0
  12. package/dist/routes/codegen.js +224 -0
  13. package/dist/routes/coverage.d.ts +2 -0
  14. package/dist/routes/coverage.js +98 -0
  15. package/dist/routes/dashboard.d.ts +2 -0
  16. package/dist/routes/dashboard.js +433 -0
  17. package/dist/routes/diff.d.ts +2 -0
  18. package/dist/routes/diff.js +181 -0
  19. package/dist/routes/errors.d.ts +2 -0
  20. package/dist/routes/errors.js +86 -0
  21. package/dist/routes/functions.d.ts +2 -0
  22. package/dist/routes/functions.js +69 -0
  23. package/dist/routes/ingest.d.ts +2 -0
  24. package/dist/routes/ingest.js +111 -0
  25. package/dist/routes/mock.d.ts +2 -0
  26. package/dist/routes/mock.js +57 -0
  27. package/dist/routes/search.d.ts +2 -0
  28. package/dist/routes/search.js +136 -0
  29. package/dist/routes/tail.d.ts +2 -0
  30. package/dist/routes/tail.js +11 -0
  31. package/dist/routes/types.d.ts +2 -0
  32. package/dist/routes/types.js +97 -0
  33. package/dist/server.d.ts +2 -0
  34. package/dist/server.js +40 -0
  35. package/dist/services/sse-broker.d.ts +10 -0
  36. package/dist/services/sse-broker.js +39 -0
  37. package/dist/services/type-differ.d.ts +2 -0
  38. package/dist/services/type-differ.js +126 -0
  39. package/dist/services/type-generator.d.ts +319 -0
  40. package/dist/services/type-generator.js +3207 -0
  41. package/dist/types.d.ts +56 -0
  42. package/dist/types.js +2 -0
  43. package/package.json +22 -0
  44. package/src/db/connection.ts +16 -0
  45. package/src/db/migrations.ts +50 -0
  46. package/src/db/queries.ts +260 -0
  47. package/src/index.ts +11 -0
  48. package/src/routes/audit.ts +283 -0
  49. package/src/routes/codegen.ts +237 -0
  50. package/src/routes/coverage.ts +120 -0
  51. package/src/routes/dashboard.ts +435 -0
  52. package/src/routes/diff.ts +215 -0
  53. package/src/routes/errors.ts +91 -0
  54. package/src/routes/functions.ts +75 -0
  55. package/src/routes/ingest.ts +139 -0
  56. package/src/routes/mock.ts +66 -0
  57. package/src/routes/search.ts +169 -0
  58. package/src/routes/tail.ts +12 -0
  59. package/src/routes/types.ts +106 -0
  60. package/src/server.ts +40 -0
  61. package/src/services/sse-broker.ts +51 -0
  62. package/src/services/type-differ.ts +141 -0
  63. package/src/services/type-generator.ts +3853 -0
  64. package/src/types.ts +37 -0
  65. package/tsconfig.json +8 -0
@@ -0,0 +1,3207 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.generateFunctionTypes = generateFunctionTypes;
4
+ exports.generateAllTypes = generateAllTypes;
5
+ exports.generatePythonTypes = generatePythonTypes;
6
+ exports.generateApiClient = generateApiClient;
7
+ exports.generateOpenApiSpec = generateOpenApiSpec;
8
+ exports.generateHandlerTypes = generateHandlerTypes;
9
+ exports.generateZodSchemas = generateZodSchemas;
10
+ exports.generateReactQueryHooks = generateReactQueryHooks;
11
+ exports.generateTypeGuards = generateTypeGuards;
12
+ exports.generateMiddleware = generateMiddleware;
13
+ exports.generateMswHandlers = generateMswHandlers;
14
+ exports.generateJsonSchemas = generateJsonSchemas;
15
+ exports.generateSwrHooks = generateSwrHooks;
16
+ exports.generatePydanticModels = generatePydanticModels;
17
+ exports.generateClassValidatorDtos = generateClassValidatorDtos;
18
+ exports.generateGraphqlSchema = generateGraphqlSchema;
19
+ exports.generateTrpcRouter = generateTrpcRouter;
20
+ exports.generateAxiosClient = generateAxiosClient;
21
+ exports.generateInlineAnnotations = generateInlineAnnotations;
22
+ exports.typeNodeToTSPublic = typeNodeToTS;
23
+ // ── Naming helpers ──
24
+ function toPascalCase(name) {
25
+ // Sanitize route-style names like "GET /api/users/:id" → "GetApiUsersId"
26
+ // Also handles camelCase/PascalCase input by splitting on boundaries
27
+ return name
28
+ .replace(/[^a-zA-Z0-9]+/g, " ") // replace non-alphanumeric with spaces
29
+ .replace(/([a-z])([A-Z])/g, "$1 $2") // split camelCase: "userId" → "user Id"
30
+ .replace(/([A-Z]+)([A-Z][a-z])/g, "$1 $2") // split acronyms: "XMLParser" → "XML Parser"
31
+ .trim()
32
+ .split(/\s+/)
33
+ .map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
34
+ .join("");
35
+ }
36
+ function toSnakeCase(name) {
37
+ return name
38
+ .replace(/([a-z])([A-Z])/g, "$1_$2")
39
+ .replace(/[-\s]+/g, "_")
40
+ .toLowerCase();
41
+ }
42
+ function formatTimeAgo(isoDate) {
43
+ try {
44
+ const date = new Date(isoDate);
45
+ const now = new Date();
46
+ const diffMs = now.getTime() - date.getTime();
47
+ const diffSec = Math.floor(diffMs / 1000);
48
+ if (diffSec < 60)
49
+ return `${diffSec}s ago`;
50
+ const diffMin = Math.floor(diffSec / 60);
51
+ if (diffMin < 60)
52
+ return `${diffMin}m ago`;
53
+ const diffHr = Math.floor(diffMin / 60);
54
+ if (diffHr < 24)
55
+ return `${diffHr}h ago`;
56
+ const diffDay = Math.floor(diffHr / 24);
57
+ return `${diffDay}d ago`;
58
+ }
59
+ catch {
60
+ return isoDate;
61
+ }
62
+ }
63
+ /**
64
+ * Convert a TypeNode to a TypeScript type string.
65
+ *
66
+ * Large nested objects (>2 properties) are extracted into named interfaces
67
+ * so the output stays readable.
68
+ */
69
+ function typeNodeToTS(node, extracted, parentName, propName, indent) {
70
+ switch (node.kind) {
71
+ case "primitive":
72
+ return node.name;
73
+ case "unknown":
74
+ return "unknown";
75
+ case "array": {
76
+ const inner = typeNodeToTS(node.element, extracted, parentName, propName, indent);
77
+ if (node.element.kind === "union" || node.element.kind === "function") {
78
+ return `Array<${inner}>`;
79
+ }
80
+ return `${inner}[]`;
81
+ }
82
+ case "tuple": {
83
+ const elements = node.elements.map((el, i) => typeNodeToTS(el, extracted, parentName, `${propName || "el"}${i}`, indent));
84
+ return `[${elements.join(", ")}]`;
85
+ }
86
+ case "union": {
87
+ const members = node.members.map((m) => typeNodeToTS(m, extracted, parentName, propName, indent));
88
+ return members.join(" | ");
89
+ }
90
+ case "map": {
91
+ const k = typeNodeToTS(node.key, extracted, parentName, "key", indent);
92
+ const v = typeNodeToTS(node.value, extracted, parentName, "value", indent);
93
+ return `Map<${k}, ${v}>`;
94
+ }
95
+ case "set": {
96
+ const inner = typeNodeToTS(node.element, extracted, parentName, propName, indent);
97
+ return `Set<${inner}>`;
98
+ }
99
+ case "promise": {
100
+ const inner = typeNodeToTS(node.resolved, extracted, parentName, propName, indent);
101
+ return `Promise<${inner}>`;
102
+ }
103
+ case "function": {
104
+ const params = node.params.map((p, i) => `arg${i}: ${typeNodeToTS(p, extracted, parentName, `param${i}`, indent)}`);
105
+ const ret = typeNodeToTS(node.returnType, extracted, parentName, "return", indent);
106
+ return `(${params.join(", ")}) => ${ret}`;
107
+ }
108
+ case "object": {
109
+ const keys = Object.keys(node.properties);
110
+ if (keys.length === 0)
111
+ return "Record<string, never>";
112
+ // Extract large nested objects into named interfaces for readability
113
+ if (keys.length > 2 && propName) {
114
+ const ifaceName = toPascalCase(parentName) + toPascalCase(propName);
115
+ // Avoid duplicate extractions
116
+ if (!extracted.some((e) => e.name === ifaceName)) {
117
+ extracted.push({ name: ifaceName, node });
118
+ }
119
+ return ifaceName;
120
+ }
121
+ // Inline small objects
122
+ const pad = " ".repeat(indent + 1);
123
+ const closePad = " ".repeat(indent);
124
+ const entries = keys.map((key) => {
125
+ const val = typeNodeToTS(node.properties[key], extracted, parentName, key, indent + 1);
126
+ return `${pad}${key}: ${val};`;
127
+ });
128
+ return `{\n${entries.join("\n")}\n${closePad}}`;
129
+ }
130
+ }
131
+ }
132
+ /**
133
+ * Render a named interface from an extracted object TypeNode.
134
+ * Recursively extracts any further nested objects.
135
+ */
136
+ function renderInterface(name, node, allExtracted) {
137
+ const keys = Object.keys(node.properties);
138
+ const lines = [];
139
+ lines.push(`export interface ${name} {`);
140
+ for (const key of keys) {
141
+ const val = typeNodeToTS(node.properties[key], allExtracted, name, key, 1);
142
+ lines.push(` ${key}: ${val};`);
143
+ }
144
+ lines.push(`}`);
145
+ return lines.join("\n");
146
+ }
147
+ /**
148
+ * Generate TypeScript definitions for a single function.
149
+ *
150
+ * For a function `processOrder(order)` that takes an object and returns an object:
151
+ * ```ts
152
+ * export interface ProcessOrderInput { id: string; customer: Customer; ... }
153
+ * export interface ProcessOrderOutput { orderId: string; total: number; ... }
154
+ * export declare function processOrder(order: ProcessOrderInput): ProcessOrderOutput;
155
+ * ```
156
+ */
157
+ function generateFunctionTypes(functionName, argsType, returnType, meta) {
158
+ const baseName = toPascalCase(functionName);
159
+ const extracted = [];
160
+ const lines = [];
161
+ // Metadata comment
162
+ const metaParts = [];
163
+ if (meta?.module)
164
+ metaParts.push(`${meta.module} module`);
165
+ if (meta?.env)
166
+ metaParts.push(`observed in ${meta.env}`);
167
+ if (meta?.observedAt)
168
+ metaParts.push(formatTimeAgo(meta.observedAt));
169
+ const metaStr = metaParts.length > 0 ? ` — ${metaParts.join(", ")}` : "";
170
+ // ── Determine argument structure ──
171
+ // argsType is typically a tuple of the function's positional args.
172
+ // For a single-object arg, we use that object directly as the "Input" interface.
173
+ // For multiple args, we generate separate types and a function declaration with individual params.
174
+ let argEntries = [];
175
+ if (argsType.kind === "tuple") {
176
+ argEntries = argsType.elements.map((el, i) => ({
177
+ paramName: `arg${i}`,
178
+ typeNode: el,
179
+ }));
180
+ }
181
+ else if (argsType.kind === "object") {
182
+ // Named params from Python kwargs
183
+ for (const key of Object.keys(argsType.properties)) {
184
+ argEntries.push({ paramName: key, typeNode: argsType.properties[key] });
185
+ }
186
+ }
187
+ else {
188
+ argEntries = [{ paramName: "input", typeNode: argsType }];
189
+ }
190
+ // ── Single-object shortcut ──
191
+ // If there's exactly one arg and it's an object, promote it to a named interface.
192
+ const singleObjectArg = argEntries.length === 1 && argEntries[0].typeNode.kind === "object";
193
+ // ── Generate input type(s) ──
194
+ if (singleObjectArg) {
195
+ const inputName = `${baseName}Input`;
196
+ const objNode = argEntries[0].typeNode;
197
+ // Extract nested objects from within the input
198
+ const inputBody = renderInterface(inputName, objNode, extracted);
199
+ lines.push(`/**`);
200
+ lines.push(` * Input type for \`${functionName}\`${metaStr}`);
201
+ lines.push(` */`);
202
+ lines.push(inputBody);
203
+ lines.push("");
204
+ }
205
+ else if (argEntries.length > 1) {
206
+ // Multiple args — generate a type for each non-primitive arg
207
+ for (let i = 0; i < argEntries.length; i++) {
208
+ const entry = argEntries[i];
209
+ if (entry.typeNode.kind === "object" && Object.keys(entry.typeNode.properties).length > 0) {
210
+ const typeName = `${baseName}${toPascalCase(entry.paramName)}`;
211
+ const body = renderInterface(typeName, entry.typeNode, extracted);
212
+ lines.push(body);
213
+ lines.push("");
214
+ }
215
+ }
216
+ }
217
+ // ── Generate output type ──
218
+ const outputName = `${baseName}Output`;
219
+ if (returnType.kind === "object" && Object.keys(returnType.properties).length > 0) {
220
+ const outputBody = renderInterface(outputName, returnType, extracted);
221
+ lines.push(`/**`);
222
+ lines.push(` * Output type for \`${functionName}\`${metaStr}`);
223
+ lines.push(` */`);
224
+ lines.push(outputBody);
225
+ lines.push("");
226
+ }
227
+ else {
228
+ const retStr = typeNodeToTS(returnType, extracted, baseName, undefined, 0);
229
+ lines.push(`/**`);
230
+ lines.push(` * Output type for \`${functionName}\`${metaStr}`);
231
+ lines.push(` */`);
232
+ lines.push(`export type ${outputName} = ${retStr};`);
233
+ lines.push("");
234
+ }
235
+ // ── Emit extracted interfaces (dependencies) ──
236
+ // Process in order: render each, which may add more extractions.
237
+ const emitted = new Set();
238
+ const extractedLines = [];
239
+ let cursor = 0;
240
+ while (cursor < extracted.length) {
241
+ const iface = extracted[cursor];
242
+ cursor++;
243
+ if (emitted.has(iface.name))
244
+ continue;
245
+ emitted.add(iface.name);
246
+ extractedLines.push(renderInterface(iface.name, iface.node, extracted));
247
+ extractedLines.push("");
248
+ }
249
+ // ── Build function declaration ──
250
+ // Use camelCase version of baseName for function declaration identifier
251
+ const funcIdent = baseName.charAt(0).toLowerCase() + baseName.slice(1);
252
+ let funcDecl;
253
+ if (singleObjectArg) {
254
+ const inputName = `${baseName}Input`;
255
+ funcDecl = `export declare function ${funcIdent}(input: ${inputName}): ${outputName};`;
256
+ }
257
+ else {
258
+ // Build param list with proper types
259
+ const params = argEntries.map((entry) => {
260
+ if (entry.typeNode.kind === "object" && Object.keys(entry.typeNode.properties).length > 0) {
261
+ return `${entry.paramName}: ${baseName}${toPascalCase(entry.paramName)}`;
262
+ }
263
+ return `${entry.paramName}: ${typeNodeToTS(entry.typeNode, extracted, baseName, entry.paramName, 0)}`;
264
+ });
265
+ funcDecl = `export declare function ${funcIdent}(${params.join(", ")}): ${outputName};`;
266
+ }
267
+ // ── Assemble output ──
268
+ // Order: extracted interfaces → input → output → function declaration
269
+ const result = [];
270
+ if (extractedLines.length > 0) {
271
+ result.push(...extractedLines);
272
+ }
273
+ result.push(...lines);
274
+ result.push(funcDecl);
275
+ return result.join("\n");
276
+ }
277
+ /**
278
+ * Generate a complete TypeScript declarations file for all functions.
279
+ */
280
+ function generateAllTypes(functions) {
281
+ const sections = [];
282
+ sections.push("// Auto-generated by trickle from runtime type observations");
283
+ sections.push(`// Generated at ${new Date().toISOString()}`);
284
+ sections.push("// Do not edit manually — re-run `trickle codegen` to update");
285
+ sections.push("");
286
+ for (const fn of functions) {
287
+ sections.push(generateFunctionTypes(fn.name, fn.argsType, fn.returnType, {
288
+ module: fn.module,
289
+ env: fn.env,
290
+ observedAt: fn.observedAt,
291
+ }));
292
+ sections.push("");
293
+ }
294
+ return sections.join("\n").trimEnd() + "\n";
295
+ }
296
+ // ── Python stub generation ──
297
+ function typeNodeToPython(node, extracted, parentName, propName, depth) {
298
+ switch (node.kind) {
299
+ case "primitive":
300
+ switch (node.name) {
301
+ case "string": return "str";
302
+ case "number": return "float";
303
+ case "boolean": return "bool";
304
+ case "null": return "None";
305
+ case "undefined": return "None";
306
+ case "bigint": return "int";
307
+ case "symbol": return "str";
308
+ default: return "Any";
309
+ }
310
+ case "unknown":
311
+ return "Any";
312
+ case "array": {
313
+ const inner = typeNodeToPython(node.element, extracted, parentName, propName, depth + 1);
314
+ return `List[${inner}]`;
315
+ }
316
+ case "tuple": {
317
+ const elements = node.elements.map((el, i) => typeNodeToPython(el, extracted, parentName, `el${i}`, depth + 1));
318
+ return `Tuple[${elements.join(", ")}]`;
319
+ }
320
+ case "union": {
321
+ const members = node.members.map((m) => typeNodeToPython(m, extracted, parentName, propName, depth + 1));
322
+ if (members.length === 2 && members.includes("None")) {
323
+ const nonNone = members.find((m) => m !== "None");
324
+ return `Optional[${nonNone}]`;
325
+ }
326
+ return `Union[${members.join(", ")}]`;
327
+ }
328
+ case "map": {
329
+ const k = typeNodeToPython(node.key, extracted, parentName, "key", depth + 1);
330
+ const v = typeNodeToPython(node.value, extracted, parentName, "value", depth + 1);
331
+ return `Dict[${k}, ${v}]`;
332
+ }
333
+ case "set": {
334
+ const inner = typeNodeToPython(node.element, extracted, parentName, propName, depth + 1);
335
+ return `Set[${inner}]`;
336
+ }
337
+ case "promise": {
338
+ const inner = typeNodeToPython(node.resolved, extracted, parentName, propName, depth + 1);
339
+ return `Awaitable[${inner}]`;
340
+ }
341
+ case "function": {
342
+ const params = node.params.map((p) => typeNodeToPython(p, extracted, parentName, undefined, depth + 1));
343
+ const ret = typeNodeToPython(node.returnType, extracted, parentName, "return", depth + 1);
344
+ return `Callable[[${params.join(", ")}], ${ret}]`;
345
+ }
346
+ case "object": {
347
+ const keys = Object.keys(node.properties);
348
+ if (keys.length === 0)
349
+ return "Dict[str, Any]";
350
+ // Always extract objects as named TypedDicts
351
+ if (propName) {
352
+ const className = toPascalCase(parentName) + toPascalCase(propName);
353
+ if (!extracted.some((e) => e.name === className)) {
354
+ extracted.push({ name: className, node });
355
+ }
356
+ return className;
357
+ }
358
+ return "Dict[str, Any]";
359
+ }
360
+ }
361
+ }
362
+ function renderPythonTypedDict(name, node, extracted) {
363
+ const keys = Object.keys(node.properties);
364
+ const innerExtracted = [];
365
+ const lines = [];
366
+ const entries = keys.map((key) => {
367
+ const pyType = typeNodeToPython(node.properties[key], innerExtracted, name, key, 1);
368
+ return ` ${toSnakeCase(key)}: ${pyType}`;
369
+ });
370
+ // Emit nested TypedDicts first
371
+ for (const iface of innerExtracted) {
372
+ lines.push(renderPythonTypedDict(iface.name, iface.node, innerExtracted));
373
+ lines.push("");
374
+ lines.push("");
375
+ }
376
+ lines.push(`class ${name}(TypedDict):`);
377
+ if (entries.length === 0) {
378
+ lines.push(" pass");
379
+ }
380
+ else {
381
+ lines.push(...entries);
382
+ }
383
+ return lines.join("\n");
384
+ }
385
+ function generatePythonTypes(functions) {
386
+ const sections = [];
387
+ sections.push("# Auto-generated by trickle from runtime type observations");
388
+ sections.push(`# Generated at ${new Date().toISOString()}`);
389
+ sections.push("# Do not edit manually — re-run `trickle codegen --python` to update");
390
+ sections.push("");
391
+ sections.push("from typing import Any, Awaitable, Callable, Dict, List, Optional, Set, Tuple, TypedDict, Union");
392
+ sections.push("");
393
+ sections.push("");
394
+ for (const fn of functions) {
395
+ const baseName = toPascalCase(fn.name);
396
+ const extracted = [];
397
+ // Meta comment
398
+ const metaParts = [];
399
+ if (fn.module)
400
+ metaParts.push(`${fn.module} module`);
401
+ if (fn.env)
402
+ metaParts.push(`observed in ${fn.env}`);
403
+ if (fn.observedAt)
404
+ metaParts.push(formatTimeAgo(fn.observedAt));
405
+ if (metaParts.length > 0) {
406
+ sections.push(`# ${baseName} — ${metaParts.join(", ")}`);
407
+ }
408
+ // Input type
409
+ if (fn.argsType.kind === "tuple" && fn.argsType.elements.length === 1 && fn.argsType.elements[0].kind === "object") {
410
+ // Single-object arg: use it directly
411
+ sections.push(renderPythonTypedDict(`${baseName}Input`, fn.argsType.elements[0], extracted));
412
+ }
413
+ else if (fn.argsType.kind === "object") {
414
+ sections.push(renderPythonTypedDict(`${baseName}Input`, fn.argsType, extracted));
415
+ }
416
+ else if (fn.argsType.kind === "tuple") {
417
+ // Multiple args — create a TypedDict with param names
418
+ const fakeObj = { kind: "object", properties: {} };
419
+ fn.argsType.elements.forEach((el, i) => {
420
+ fakeObj.properties[`arg${i}`] = el;
421
+ });
422
+ sections.push(renderPythonTypedDict(`${baseName}Args`, fakeObj, extracted));
423
+ }
424
+ else {
425
+ const pyType = typeNodeToPython(fn.argsType, extracted, baseName, undefined, 0);
426
+ sections.push(`${baseName}Input = ${pyType}`);
427
+ }
428
+ sections.push("");
429
+ sections.push("");
430
+ // Output type
431
+ if (fn.returnType.kind === "object" && Object.keys(fn.returnType.properties).length > 0) {
432
+ sections.push(renderPythonTypedDict(`${baseName}Output`, fn.returnType, extracted));
433
+ }
434
+ else {
435
+ const pyType = typeNodeToPython(fn.returnType, extracted, baseName, undefined, 0);
436
+ sections.push(`${baseName}Output = ${pyType}`);
437
+ }
438
+ // Emit extracted TypedDicts that were accumulated
439
+ const emitted = new Set();
440
+ const pendingExtracted = [];
441
+ for (const iface of extracted) {
442
+ if (emitted.has(iface.name))
443
+ continue;
444
+ emitted.add(iface.name);
445
+ pendingExtracted.push("");
446
+ pendingExtracted.push("");
447
+ pendingExtracted.push(renderPythonTypedDict(iface.name, iface.node, extracted));
448
+ }
449
+ // Insert extracted defs before the last output (they need to be defined first)
450
+ // Actually, since Python TypedDicts reference forward, just append
451
+ if (pendingExtracted.length > 0) {
452
+ // Re-order: put extracted before the Input/Output that reference them
453
+ // For simplicity, just include them — Python TypedDict forward refs work with __future__.annotations
454
+ }
455
+ sections.push("");
456
+ sections.push("");
457
+ }
458
+ return sections.join("\n").trimEnd() + "\n";
459
+ }
460
+ function parseRouteName(name) {
461
+ const match = name.match(/^(GET|POST|PUT|DELETE|PATCH)\s+(.+)$/i);
462
+ if (!match)
463
+ return null;
464
+ const method = match[1].toUpperCase();
465
+ const path = match[2];
466
+ // Extract path params like :id, :userId
467
+ const pathParams = [];
468
+ path.replace(/:(\w+)/g, (_, param) => {
469
+ pathParams.push(param);
470
+ return _;
471
+ });
472
+ const typeName = toPascalCase(name);
473
+ // For camelCase: lowercase the HTTP method prefix (e.g., GETApiUsers → getApiUsers)
474
+ const methodLower = method.charAt(0).toUpperCase() + method.slice(1).toLowerCase();
475
+ const pathPart = toPascalCase(path);
476
+ const funcName = method.toLowerCase() + pathPart;
477
+ return { method, path, pathParams, funcName, typeName };
478
+ }
479
+ function toCamelCase(name) {
480
+ const pascal = toPascalCase(name);
481
+ return pascal.charAt(0).toLowerCase() + pascal.slice(1);
482
+ }
483
+ /**
484
+ * Generate a fully-typed fetch-based API client from runtime-observed routes.
485
+ *
486
+ * Output is a single TypeScript file with:
487
+ * - All request/response interfaces
488
+ * - A `createTrickleClient(baseUrl)` factory that returns typed fetch wrappers
489
+ * - Proper path parameter substitution
490
+ * - Request body typing for POST/PUT/PATCH
491
+ */
492
+ function generateApiClient(functions) {
493
+ // Filter to route-style functions only
494
+ const routes = [];
495
+ for (const fn of functions) {
496
+ const parsed = parseRouteName(fn.name);
497
+ if (parsed) {
498
+ routes.push({ parsed, fn });
499
+ }
500
+ }
501
+ if (routes.length === 0) {
502
+ return "// No API routes found. Instrument your Express/FastAPI app to generate a typed client.\n";
503
+ }
504
+ const sections = [];
505
+ sections.push("// Auto-generated typed API client by trickle");
506
+ sections.push(`// Generated at ${new Date().toISOString()}`);
507
+ sections.push("// Do not edit manually — re-run `trickle codegen --client` to update");
508
+ sections.push("");
509
+ // Generate interfaces for each route
510
+ const extracted = [];
511
+ for (const { parsed, fn } of routes) {
512
+ const baseName = parsed.typeName;
513
+ // --- Input types (only for methods with body) ---
514
+ if (["POST", "PUT", "PATCH"].includes(parsed.method)) {
515
+ // argsType is typically an object with { body, params, query } from Express instrumentation
516
+ if (fn.argsType.kind === "object") {
517
+ const bodyNode = fn.argsType.properties["body"];
518
+ if (bodyNode && bodyNode.kind === "object" && Object.keys(bodyNode.properties).length > 0) {
519
+ const inputName = `${baseName}Input`;
520
+ sections.push(renderInterface(inputName, bodyNode, extracted));
521
+ sections.push("");
522
+ }
523
+ }
524
+ else if (fn.argsType.kind === "tuple" && fn.argsType.elements.length === 1) {
525
+ const el = fn.argsType.elements[0];
526
+ if (el.kind === "object") {
527
+ const bodyNode = el.properties["body"];
528
+ if (bodyNode && bodyNode.kind === "object" && Object.keys(bodyNode.properties).length > 0) {
529
+ const inputName = `${baseName}Input`;
530
+ sections.push(renderInterface(inputName, bodyNode, extracted));
531
+ sections.push("");
532
+ }
533
+ }
534
+ }
535
+ }
536
+ // --- Path params type (if route has :params) ---
537
+ if (parsed.pathParams.length > 0) {
538
+ // Try to extract from argsType.properties.params
539
+ let paramsNode;
540
+ if (fn.argsType.kind === "object" && fn.argsType.properties["params"]) {
541
+ paramsNode = fn.argsType.properties["params"];
542
+ }
543
+ // Only generate if we have object-typed params
544
+ if (paramsNode && paramsNode.kind === "object" && Object.keys(paramsNode.properties).length > 0) {
545
+ const paramsName = `${baseName}Params`;
546
+ sections.push(renderInterface(paramsName, paramsNode, extracted));
547
+ sections.push("");
548
+ }
549
+ }
550
+ // --- Query params type ---
551
+ if (fn.argsType.kind === "object" && fn.argsType.properties["query"]) {
552
+ const queryNode = fn.argsType.properties["query"];
553
+ if (queryNode.kind === "object" && Object.keys(queryNode.properties).length > 0) {
554
+ const queryName = `${baseName}Query`;
555
+ sections.push(renderInterface(queryName, queryNode, extracted));
556
+ sections.push("");
557
+ }
558
+ }
559
+ // --- Output type ---
560
+ if (fn.returnType.kind === "object" && Object.keys(fn.returnType.properties).length > 0) {
561
+ const outputName = `${baseName}Output`;
562
+ sections.push(renderInterface(outputName, fn.returnType, extracted));
563
+ sections.push("");
564
+ }
565
+ else {
566
+ const outputName = `${baseName}Output`;
567
+ const retStr = typeNodeToTS(fn.returnType, extracted, baseName, undefined, 0);
568
+ sections.push(`export type ${outputName} = ${retStr};`);
569
+ sections.push("");
570
+ }
571
+ }
572
+ // Emit extracted sub-interfaces
573
+ const emitted = new Set();
574
+ const extractedLines = [];
575
+ let cursor = 0;
576
+ while (cursor < extracted.length) {
577
+ const iface = extracted[cursor];
578
+ cursor++;
579
+ if (emitted.has(iface.name))
580
+ continue;
581
+ emitted.add(iface.name);
582
+ extractedLines.push(renderInterface(iface.name, iface.node, extracted));
583
+ extractedLines.push("");
584
+ }
585
+ if (extractedLines.length > 0) {
586
+ // Insert extracted interfaces before the main interfaces
587
+ sections.splice(4, 0, ...extractedLines);
588
+ }
589
+ // --- Generate the client factory ---
590
+ sections.push("// ── API Client ──");
591
+ sections.push("");
592
+ sections.push("export function createTrickleClient(baseUrl: string) {");
593
+ sections.push(" async function request<T>(method: string, path: string, body?: unknown, query?: Record<string, string>): Promise<T> {");
594
+ sections.push(" const url = new URL(path, baseUrl);");
595
+ sections.push(" if (query) { for (const [k, v] of Object.entries(query)) { if (v !== undefined) url.searchParams.set(k, v); } }");
596
+ sections.push(" const opts: RequestInit = { method, headers: { 'Content-Type': 'application/json' } };");
597
+ sections.push(" if (body !== undefined) opts.body = JSON.stringify(body);");
598
+ sections.push(" const res = await fetch(url.toString(), opts);");
599
+ sections.push(" if (!res.ok) throw new Error(`${method} ${path}: HTTP ${res.status}`);");
600
+ sections.push(" return res.json() as Promise<T>;");
601
+ sections.push(" }");
602
+ sections.push("");
603
+ sections.push(" return {");
604
+ for (const { parsed, fn } of routes) {
605
+ const baseName = parsed.typeName;
606
+ const outputType = `${baseName}Output`;
607
+ const hasBody = ["POST", "PUT", "PATCH"].includes(parsed.method);
608
+ // Determine if we have a typed input
609
+ let hasInputType = false;
610
+ if (hasBody && fn.argsType.kind === "object") {
611
+ const bodyNode = fn.argsType.properties["body"];
612
+ hasInputType = !!(bodyNode && bodyNode.kind === "object" && Object.keys(bodyNode.properties).length > 0);
613
+ }
614
+ // Build path expression with template literals for path params
615
+ let pathExpr;
616
+ if (parsed.pathParams.length > 0) {
617
+ pathExpr = "`" + parsed.path.replace(/:(\w+)/g, (_, param) => `\${${toCamelCase(param)}}`) + "`";
618
+ }
619
+ else {
620
+ pathExpr = `"${parsed.path}"`;
621
+ }
622
+ // Build method signature
623
+ const params = [];
624
+ if (parsed.pathParams.length > 0) {
625
+ for (const p of parsed.pathParams) {
626
+ params.push(`${toCamelCase(p)}: string`);
627
+ }
628
+ }
629
+ if (hasInputType) {
630
+ params.push(`input: ${baseName}Input`);
631
+ }
632
+ const bodyArg = hasInputType ? "input" : "undefined";
633
+ const paramStr = params.length > 0 ? params.join(", ") : "";
634
+ // Generate JSDoc comment
635
+ sections.push(` /** ${parsed.method} ${parsed.path} */`);
636
+ sections.push(` ${parsed.funcName}: (${paramStr}): Promise<${outputType}> =>`);
637
+ sections.push(` request<${outputType}>("${parsed.method}", ${pathExpr}, ${bodyArg}),`);
638
+ sections.push("");
639
+ }
640
+ sections.push(" };");
641
+ sections.push("}");
642
+ sections.push("");
643
+ sections.push("export type TrickleClient = ReturnType<typeof createTrickleClient>;");
644
+ return sections.join("\n").trimEnd() + "\n";
645
+ }
646
+ // ── OpenAPI spec generation ──
647
+ /**
648
+ * Convert a TypeNode to a JSON Schema object.
649
+ */
650
+ function typeNodeToJsonSchema(node, defs, parentName, propName) {
651
+ switch (node.kind) {
652
+ case "primitive":
653
+ switch (node.name) {
654
+ case "string": return { type: "string" };
655
+ case "number": return { type: "number" };
656
+ case "boolean": return { type: "boolean" };
657
+ case "null": return { type: "string", nullable: true };
658
+ case "undefined": return { type: "string", nullable: true };
659
+ case "bigint": return { type: "integer", format: "int64" };
660
+ case "symbol": return { type: "string" };
661
+ default: return {};
662
+ }
663
+ case "unknown":
664
+ return {};
665
+ case "array":
666
+ return {
667
+ type: "array",
668
+ items: typeNodeToJsonSchema(node.element, defs, parentName, propName),
669
+ };
670
+ case "tuple":
671
+ return {
672
+ type: "array",
673
+ items: node.elements.length > 0
674
+ ? typeNodeToJsonSchema(node.elements[0], defs, parentName, propName)
675
+ : {},
676
+ minItems: node.elements.length,
677
+ maxItems: node.elements.length,
678
+ };
679
+ case "union": {
680
+ const schemas = node.members.map((m) => typeNodeToJsonSchema(m, defs, parentName, propName));
681
+ // Simplify nullable unions: { type: "string" } | null → nullable string
682
+ const nonNull = schemas.filter((s) => !("nullable" in s && s.nullable === true));
683
+ if (nonNull.length === 1 && nonNull.length < schemas.length) {
684
+ return { ...nonNull[0], nullable: true };
685
+ }
686
+ return { oneOf: schemas };
687
+ }
688
+ case "map":
689
+ return {
690
+ type: "object",
691
+ additionalProperties: typeNodeToJsonSchema(node.value, defs, parentName, propName),
692
+ };
693
+ case "set":
694
+ return {
695
+ type: "array",
696
+ items: typeNodeToJsonSchema(node.element, defs, parentName, propName),
697
+ uniqueItems: true,
698
+ };
699
+ case "promise":
700
+ return typeNodeToJsonSchema(node.resolved, defs, parentName, propName);
701
+ case "function":
702
+ return { type: "object", description: "function" };
703
+ case "object": {
704
+ const keys = Object.keys(node.properties);
705
+ if (keys.length === 0)
706
+ return { type: "object" };
707
+ // Extract complex objects as $ref to keep schemas readable
708
+ if (keys.length > 2 && propName) {
709
+ const schemaName = toPascalCase(parentName) + toPascalCase(propName);
710
+ if (!defs[schemaName]) {
711
+ // Placeholder to prevent infinite recursion
712
+ defs[schemaName] = {};
713
+ const properties = {};
714
+ const required = [];
715
+ for (const key of keys) {
716
+ properties[key] = typeNodeToJsonSchema(node.properties[key], defs, schemaName, key);
717
+ required.push(key);
718
+ }
719
+ defs[schemaName] = { type: "object", properties, required };
720
+ }
721
+ return { $ref: `#/components/schemas/${schemaName}` };
722
+ }
723
+ const properties = {};
724
+ const required = [];
725
+ for (const key of keys) {
726
+ properties[key] = typeNodeToJsonSchema(node.properties[key], defs, parentName, key);
727
+ required.push(key);
728
+ }
729
+ return { type: "object", properties, required };
730
+ }
731
+ }
732
+ }
733
+ /**
734
+ * Generate an OpenAPI 3.0 specification from runtime-observed route types.
735
+ */
736
+ function generateOpenApiSpec(functions, options) {
737
+ const title = options?.title || "API";
738
+ const version = options?.version || "1.0.0";
739
+ const paths = {};
740
+ const schemas = {};
741
+ for (const fn of functions) {
742
+ const parsed = parseRouteName(fn.name);
743
+ if (!parsed)
744
+ continue;
745
+ const method = parsed.method.toLowerCase();
746
+ const baseName = parsed.typeName;
747
+ // Convert Express :param to OpenAPI {param}
748
+ const openApiPath = parsed.path.replace(/:(\w+)/g, "{$1}");
749
+ // Build response schema
750
+ const responseDefs = {};
751
+ const responseSchema = typeNodeToJsonSchema(fn.returnType, responseDefs, baseName + "Output", undefined);
752
+ Object.assign(schemas, responseDefs);
753
+ // If response is a complex object, extract to a named schema
754
+ if (fn.returnType.kind === "object" && Object.keys(fn.returnType.properties).length > 0) {
755
+ const responseSchemaName = `${baseName}Response`;
756
+ schemas[responseSchemaName] = responseSchema;
757
+ }
758
+ const responseRef = fn.returnType.kind === "object" && Object.keys(fn.returnType.properties).length > 0
759
+ ? { $ref: `#/components/schemas/${baseName}Response` }
760
+ : responseSchema;
761
+ // Build operation object
762
+ const operation = {
763
+ operationId: parsed.funcName,
764
+ summary: `${parsed.method} ${parsed.path}`,
765
+ responses: {
766
+ "200": {
767
+ description: "Successful response",
768
+ content: {
769
+ "application/json": {
770
+ schema: responseRef,
771
+ },
772
+ },
773
+ },
774
+ },
775
+ };
776
+ // Add path parameters
777
+ if (parsed.pathParams.length > 0) {
778
+ operation.parameters = parsed.pathParams.map((param) => ({
779
+ name: param,
780
+ in: "path",
781
+ required: true,
782
+ schema: { type: "string" },
783
+ }));
784
+ }
785
+ // Add query parameters from argsType
786
+ if (fn.argsType.kind === "object" && fn.argsType.properties["query"]) {
787
+ const queryNode = fn.argsType.properties["query"];
788
+ if (queryNode.kind === "object") {
789
+ const queryParams = Object.keys(queryNode.properties).map((param) => {
790
+ const paramSchema = typeNodeToJsonSchema(queryNode.properties[param], schemas, baseName, param);
791
+ return {
792
+ name: param,
793
+ in: "query",
794
+ required: false,
795
+ schema: paramSchema,
796
+ };
797
+ });
798
+ const existing = operation.parameters || [];
799
+ operation.parameters = [...existing, ...queryParams];
800
+ }
801
+ }
802
+ // Add request body for POST/PUT/PATCH
803
+ if (["POST", "PUT", "PATCH"].includes(parsed.method)) {
804
+ let bodyNode;
805
+ if (fn.argsType.kind === "object" && fn.argsType.properties["body"]) {
806
+ bodyNode = fn.argsType.properties["body"];
807
+ }
808
+ else if (fn.argsType.kind === "tuple" && fn.argsType.elements.length === 1) {
809
+ const el = fn.argsType.elements[0];
810
+ if (el.kind === "object" && el.properties["body"]) {
811
+ bodyNode = el.properties["body"];
812
+ }
813
+ }
814
+ if (bodyNode && bodyNode.kind === "object" && Object.keys(bodyNode.properties).length > 0) {
815
+ const bodyDefs = {};
816
+ const bodySchema = typeNodeToJsonSchema(bodyNode, bodyDefs, baseName + "Request", undefined);
817
+ Object.assign(schemas, bodyDefs);
818
+ const requestSchemaName = `${baseName}Request`;
819
+ schemas[requestSchemaName] = bodySchema;
820
+ operation.requestBody = {
821
+ required: true,
822
+ content: {
823
+ "application/json": {
824
+ schema: { $ref: `#/components/schemas/${requestSchemaName}` },
825
+ },
826
+ },
827
+ };
828
+ }
829
+ }
830
+ // Add tags based on path prefix
831
+ const pathParts = parsed.path.split("/").filter(Boolean);
832
+ if (pathParts.length >= 2) {
833
+ operation.tags = [pathParts[1]]; // e.g., /api/users → "users" tag would be pathParts[1]
834
+ // But /api is the first meaningful segment, so use the one after /api
835
+ if (pathParts[0] === "api" && pathParts.length >= 2) {
836
+ operation.tags = [pathParts[1]];
837
+ }
838
+ }
839
+ if (!paths[openApiPath]) {
840
+ paths[openApiPath] = {};
841
+ }
842
+ paths[openApiPath][method] = operation;
843
+ }
844
+ const spec = {
845
+ openapi: "3.0.3",
846
+ info: { title, version },
847
+ paths,
848
+ };
849
+ if (Object.keys(schemas).length > 0) {
850
+ spec.components = { schemas };
851
+ }
852
+ if (options?.serverUrl) {
853
+ spec.servers = [{ url: options.serverUrl }];
854
+ }
855
+ return spec;
856
+ }
857
+ // ── Express handler type generation ──
858
+ /**
859
+ * Generate typed Express handler type aliases from runtime-observed routes.
860
+ *
861
+ * For each route like `GET /api/users/:id`, produces:
862
+ * - `GetApiUsersIdHandler` — a fully typed `RequestHandler` with
863
+ * `Request<Params, ResBody, ReqBody, Query>` and `Response<ResBody>`
864
+ *
865
+ * Developers can use these to type their route handlers:
866
+ * app.get('/api/users/:id', ((req, res) => { ... }) as GetApiUsersIdHandler);
867
+ */
868
+ function generateHandlerTypes(functions) {
869
+ // Filter to route-style functions only
870
+ const routes = [];
871
+ for (const fn of functions) {
872
+ const parsed = parseRouteName(fn.name);
873
+ if (parsed) {
874
+ routes.push({ parsed, fn });
875
+ }
876
+ }
877
+ if (routes.length === 0) {
878
+ return "// No API routes found. Instrument your Express app to generate handler types.\n";
879
+ }
880
+ const sections = [];
881
+ sections.push("// Auto-generated Express handler types by trickle");
882
+ sections.push(`// Generated at ${new Date().toISOString()}`);
883
+ sections.push("// Do not edit manually — re-run `trickle codegen --handlers` to update");
884
+ sections.push("");
885
+ sections.push('import { Request, Response, NextFunction } from "express";');
886
+ sections.push("");
887
+ const extracted = [];
888
+ for (const { parsed, fn } of routes) {
889
+ const baseName = parsed.typeName;
890
+ // --- Params type ---
891
+ let paramsType = "Record<string, string>";
892
+ if (parsed.pathParams.length > 0) {
893
+ const paramsName = `${baseName}Params`;
894
+ // Check if we have observed param types
895
+ let paramsNode;
896
+ if (fn.argsType.kind === "object" && fn.argsType.properties["params"]) {
897
+ paramsNode = fn.argsType.properties["params"];
898
+ }
899
+ if (paramsNode && paramsNode.kind === "object" && Object.keys(paramsNode.properties).length > 0) {
900
+ sections.push(renderInterface(paramsName, paramsNode, extracted));
901
+ sections.push("");
902
+ }
903
+ else {
904
+ // Generate from path params — all strings
905
+ const props = parsed.pathParams.map(p => ` ${p}: string;`);
906
+ sections.push(`export interface ${paramsName} {`);
907
+ sections.push(...props);
908
+ sections.push("}");
909
+ sections.push("");
910
+ }
911
+ paramsType = paramsName;
912
+ }
913
+ // --- Response body type ---
914
+ let resBodyType = "unknown";
915
+ const resBodyName = `${baseName}ResBody`;
916
+ if (fn.returnType.kind === "object" && Object.keys(fn.returnType.properties).length > 0) {
917
+ sections.push(renderInterface(resBodyName, fn.returnType, extracted));
918
+ sections.push("");
919
+ resBodyType = resBodyName;
920
+ }
921
+ else {
922
+ const retStr = typeNodeToTS(fn.returnType, extracted, baseName, undefined, 0);
923
+ sections.push(`export type ${resBodyName} = ${retStr};`);
924
+ sections.push("");
925
+ resBodyType = resBodyName;
926
+ }
927
+ // --- Request body type ---
928
+ let reqBodyType = "unknown";
929
+ if (["POST", "PUT", "PATCH"].includes(parsed.method)) {
930
+ let bodyNode;
931
+ if (fn.argsType.kind === "object" && fn.argsType.properties["body"]) {
932
+ bodyNode = fn.argsType.properties["body"];
933
+ }
934
+ else if (fn.argsType.kind === "tuple" && fn.argsType.elements.length === 1) {
935
+ const el = fn.argsType.elements[0];
936
+ if (el.kind === "object" && el.properties["body"]) {
937
+ bodyNode = el.properties["body"];
938
+ }
939
+ }
940
+ if (bodyNode && bodyNode.kind === "object" && Object.keys(bodyNode.properties).length > 0) {
941
+ const reqBodyName = `${baseName}ReqBody`;
942
+ sections.push(renderInterface(reqBodyName, bodyNode, extracted));
943
+ sections.push("");
944
+ reqBodyType = reqBodyName;
945
+ }
946
+ }
947
+ // --- Query type ---
948
+ let queryType = "qs.ParsedQs";
949
+ if (fn.argsType.kind === "object" && fn.argsType.properties["query"]) {
950
+ const queryNode = fn.argsType.properties["query"];
951
+ if (queryNode.kind === "object" && Object.keys(queryNode.properties).length > 0) {
952
+ const queryName = `${baseName}Query`;
953
+ sections.push(renderInterface(queryName, queryNode, extracted));
954
+ sections.push("");
955
+ queryType = queryName;
956
+ }
957
+ }
958
+ // --- Handler type alias ---
959
+ sections.push(`/** ${parsed.method} ${parsed.path} */`);
960
+ sections.push(`export type ${baseName}Handler = (` +
961
+ `req: Request<${paramsType}, ${resBodyType}, ${reqBodyType}, ${queryType}>, ` +
962
+ `res: Response<${resBodyType}>, ` +
963
+ `next: NextFunction` +
964
+ `) => void;`);
965
+ sections.push("");
966
+ }
967
+ // Emit extracted sub-interfaces
968
+ const emitted = new Set();
969
+ const extractedLines = [];
970
+ let cursor = 0;
971
+ while (cursor < extracted.length) {
972
+ const iface = extracted[cursor];
973
+ cursor++;
974
+ if (emitted.has(iface.name))
975
+ continue;
976
+ emitted.add(iface.name);
977
+ extractedLines.push(renderInterface(iface.name, iface.node, extracted));
978
+ extractedLines.push("");
979
+ }
980
+ if (extractedLines.length > 0) {
981
+ // Insert after the import line (index 4 = after the blank line after import)
982
+ sections.splice(5, 0, ...extractedLines);
983
+ }
984
+ return sections.join("\n").trimEnd() + "\n";
985
+ }
986
+ // ── Zod schema generation ──
987
+ /**
988
+ * Convert a TypeNode to a Zod schema expression string.
989
+ */
990
+ function typeNodeToZod(node, indent) {
991
+ const pad = " ".repeat(indent);
992
+ switch (node.kind) {
993
+ case "primitive":
994
+ switch (node.name) {
995
+ case "string": return "z.string()";
996
+ case "number": return "z.number()";
997
+ case "boolean": return "z.boolean()";
998
+ case "null": return "z.null()";
999
+ case "undefined": return "z.undefined()";
1000
+ case "bigint": return "z.bigint()";
1001
+ case "symbol": return "z.symbol()";
1002
+ default: return "z.unknown()";
1003
+ }
1004
+ case "unknown":
1005
+ return "z.unknown()";
1006
+ case "array":
1007
+ return `z.array(${typeNodeToZod(node.element, indent)})`;
1008
+ case "tuple": {
1009
+ if (node.elements.length === 0)
1010
+ return "z.tuple([])";
1011
+ const els = node.elements.map((el) => typeNodeToZod(el, indent));
1012
+ return `z.tuple([${els.join(", ")}])`;
1013
+ }
1014
+ case "union": {
1015
+ const members = node.members.map((m) => typeNodeToZod(m, indent));
1016
+ if (members.length === 1)
1017
+ return members[0];
1018
+ return `z.union([${members.join(", ")}])`;
1019
+ }
1020
+ case "map": {
1021
+ return `z.map(${typeNodeToZod(node.key, indent)}, ${typeNodeToZod(node.value, indent)})`;
1022
+ }
1023
+ case "set": {
1024
+ return `z.set(${typeNodeToZod(node.element, indent)})`;
1025
+ }
1026
+ case "promise": {
1027
+ return `z.promise(${typeNodeToZod(node.resolved, indent)})`;
1028
+ }
1029
+ case "function": {
1030
+ return "z.function()";
1031
+ }
1032
+ case "object": {
1033
+ const keys = Object.keys(node.properties);
1034
+ if (keys.length === 0)
1035
+ return "z.object({})";
1036
+ const innerPad = " ".repeat(indent + 1);
1037
+ const entries = keys.map((key) => {
1038
+ const val = typeNodeToZod(node.properties[key], indent + 1);
1039
+ return `${innerPad}${key}: ${val},`;
1040
+ });
1041
+ return `z.object({\n${entries.join("\n")}\n${pad}})`;
1042
+ }
1043
+ }
1044
+ }
1045
+ /**
1046
+ * Generate Zod validation schemas from runtime-observed types.
1047
+ *
1048
+ * For each function/route, generates a named Zod schema that can be used for:
1049
+ * - Runtime validation of API inputs/outputs
1050
+ * - TypeScript type inference via `z.infer<typeof schema>`
1051
+ * - Form validation, config parsing, etc.
1052
+ */
1053
+ function generateZodSchemas(functions) {
1054
+ if (functions.length === 0) {
1055
+ return "// No functions found. Instrument your app to generate Zod schemas.\n";
1056
+ }
1057
+ const sections = [];
1058
+ sections.push("// Auto-generated Zod schemas by trickle");
1059
+ sections.push(`// Generated at ${new Date().toISOString()}`);
1060
+ sections.push("// Do not edit manually — re-run `trickle codegen --zod` to update");
1061
+ sections.push("");
1062
+ sections.push('import { z } from "zod";');
1063
+ sections.push("");
1064
+ // Check if any are route-style functions
1065
+ const hasRoutes = functions.some((fn) => parseRouteName(fn.name) !== null);
1066
+ for (const fn of functions) {
1067
+ const parsed = parseRouteName(fn.name);
1068
+ if (parsed) {
1069
+ // Route-style: generate Input/Output schemas
1070
+ const baseName = toCamelCase(parsed.typeName);
1071
+ const BaseNamePascal = parsed.typeName;
1072
+ // --- Response schema ---
1073
+ sections.push(`/** ${parsed.method} ${parsed.path} — response */`);
1074
+ sections.push(`export const ${baseName}ResponseSchema = ${typeNodeToZod(fn.returnType, 0)};`);
1075
+ sections.push(`export type ${BaseNamePascal}Response = z.infer<typeof ${baseName}ResponseSchema>;`);
1076
+ sections.push("");
1077
+ // --- Request body schema (for POST/PUT/PATCH) ---
1078
+ if (["POST", "PUT", "PATCH"].includes(parsed.method)) {
1079
+ let bodyNode;
1080
+ if (fn.argsType.kind === "object" && fn.argsType.properties["body"]) {
1081
+ bodyNode = fn.argsType.properties["body"];
1082
+ }
1083
+ else if (fn.argsType.kind === "tuple" && fn.argsType.elements.length === 1) {
1084
+ const el = fn.argsType.elements[0];
1085
+ if (el.kind === "object" && el.properties["body"]) {
1086
+ bodyNode = el.properties["body"];
1087
+ }
1088
+ }
1089
+ if (bodyNode && bodyNode.kind === "object" && Object.keys(bodyNode.properties).length > 0) {
1090
+ sections.push(`/** ${parsed.method} ${parsed.path} — request body */`);
1091
+ sections.push(`export const ${baseName}RequestSchema = ${typeNodeToZod(bodyNode, 0)};`);
1092
+ sections.push(`export type ${BaseNamePascal}Request = z.infer<typeof ${baseName}RequestSchema>;`);
1093
+ sections.push("");
1094
+ }
1095
+ }
1096
+ // --- Path params schema (if route has :params) ---
1097
+ if (parsed.pathParams.length > 0) {
1098
+ let paramsNode;
1099
+ if (fn.argsType.kind === "object" && fn.argsType.properties["params"]) {
1100
+ paramsNode = fn.argsType.properties["params"];
1101
+ }
1102
+ if (paramsNode && paramsNode.kind === "object" && Object.keys(paramsNode.properties).length > 0) {
1103
+ sections.push(`/** ${parsed.method} ${parsed.path} — path params */`);
1104
+ sections.push(`export const ${baseName}ParamsSchema = ${typeNodeToZod(paramsNode, 0)};`);
1105
+ sections.push(`export type ${BaseNamePascal}Params = z.infer<typeof ${baseName}ParamsSchema>;`);
1106
+ sections.push("");
1107
+ }
1108
+ }
1109
+ // --- Query params schema ---
1110
+ if (fn.argsType.kind === "object" && fn.argsType.properties["query"]) {
1111
+ const queryNode = fn.argsType.properties["query"];
1112
+ if (queryNode.kind === "object" && Object.keys(queryNode.properties).length > 0) {
1113
+ sections.push(`/** ${parsed.method} ${parsed.path} — query params */`);
1114
+ sections.push(`export const ${baseName}QuerySchema = ${typeNodeToZod(queryNode, 0)};`);
1115
+ sections.push(`export type ${BaseNamePascal}Query = z.infer<typeof ${baseName}QuerySchema>;`);
1116
+ sections.push("");
1117
+ }
1118
+ }
1119
+ }
1120
+ else {
1121
+ // Non-route function: generate Input/Output schemas
1122
+ const baseName = toCamelCase(toPascalCase(fn.name));
1123
+ const BaseNamePascal = toPascalCase(fn.name);
1124
+ // Input schema
1125
+ if (fn.argsType.kind !== "unknown") {
1126
+ sections.push(`/** ${fn.name} — input */`);
1127
+ if (fn.argsType.kind === "tuple" && fn.argsType.elements.length === 1) {
1128
+ sections.push(`export const ${baseName}InputSchema = ${typeNodeToZod(fn.argsType.elements[0], 0)};`);
1129
+ }
1130
+ else {
1131
+ sections.push(`export const ${baseName}InputSchema = ${typeNodeToZod(fn.argsType, 0)};`);
1132
+ }
1133
+ sections.push(`export type ${BaseNamePascal}Input = z.infer<typeof ${baseName}InputSchema>;`);
1134
+ sections.push("");
1135
+ }
1136
+ // Output schema
1137
+ sections.push(`/** ${fn.name} — output */`);
1138
+ sections.push(`export const ${baseName}OutputSchema = ${typeNodeToZod(fn.returnType, 0)};`);
1139
+ sections.push(`export type ${BaseNamePascal}Output = z.infer<typeof ${baseName}OutputSchema>;`);
1140
+ sections.push("");
1141
+ }
1142
+ }
1143
+ return sections.join("\n").trimEnd() + "\n";
1144
+ }
1145
+ // ── React Query hooks generation ──
1146
+ /**
1147
+ * Generate fully-typed TanStack Query (React Query) hooks from runtime-observed routes.
1148
+ *
1149
+ * For each route:
1150
+ * - GET → useQuery hook with typed response and query keys
1151
+ * - POST/PUT/PATCH/DELETE → useMutation hook with typed input/output
1152
+ * - Query key factory for cache invalidation
1153
+ * - All request/response interfaces included
1154
+ */
1155
+ function generateReactQueryHooks(functions) {
1156
+ // Filter to route-style functions only
1157
+ const routes = [];
1158
+ for (const fn of functions) {
1159
+ const parsed = parseRouteName(fn.name);
1160
+ if (parsed) {
1161
+ routes.push({ parsed, fn });
1162
+ }
1163
+ }
1164
+ if (routes.length === 0) {
1165
+ return "// No API routes found. Instrument your Express/FastAPI app to generate React Query hooks.\n";
1166
+ }
1167
+ const sections = [];
1168
+ sections.push("// Auto-generated React Query hooks by trickle");
1169
+ sections.push(`// Generated at ${new Date().toISOString()}`);
1170
+ sections.push("// Do not edit manually — re-run `trickle codegen --react-query` to update");
1171
+ sections.push("");
1172
+ sections.push('import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";');
1173
+ sections.push('import type { UseQueryOptions, UseMutationOptions } from "@tanstack/react-query";');
1174
+ sections.push("");
1175
+ // --- Generate interfaces ---
1176
+ const extracted = [];
1177
+ for (const { parsed, fn } of routes) {
1178
+ const baseName = parsed.typeName;
1179
+ // Response type
1180
+ if (fn.returnType.kind === "object" && Object.keys(fn.returnType.properties).length > 0) {
1181
+ sections.push(renderInterface(`${baseName}Response`, fn.returnType, extracted));
1182
+ sections.push("");
1183
+ }
1184
+ else {
1185
+ const retStr = typeNodeToTS(fn.returnType, extracted, baseName, undefined, 0);
1186
+ sections.push(`export type ${baseName}Response = ${retStr};`);
1187
+ sections.push("");
1188
+ }
1189
+ // Request body type (POST/PUT/PATCH)
1190
+ if (["POST", "PUT", "PATCH"].includes(parsed.method)) {
1191
+ let bodyNode;
1192
+ if (fn.argsType.kind === "object" && fn.argsType.properties["body"]) {
1193
+ bodyNode = fn.argsType.properties["body"];
1194
+ }
1195
+ else if (fn.argsType.kind === "tuple" && fn.argsType.elements.length === 1) {
1196
+ const el = fn.argsType.elements[0];
1197
+ if (el.kind === "object" && el.properties["body"]) {
1198
+ bodyNode = el.properties["body"];
1199
+ }
1200
+ }
1201
+ if (bodyNode && bodyNode.kind === "object" && Object.keys(bodyNode.properties).length > 0) {
1202
+ sections.push(renderInterface(`${baseName}Input`, bodyNode, extracted));
1203
+ sections.push("");
1204
+ }
1205
+ }
1206
+ }
1207
+ // Emit extracted sub-interfaces
1208
+ const emitted = new Set();
1209
+ const extractedLines = [];
1210
+ let cursor = 0;
1211
+ while (cursor < extracted.length) {
1212
+ const iface = extracted[cursor];
1213
+ cursor++;
1214
+ if (emitted.has(iface.name))
1215
+ continue;
1216
+ emitted.add(iface.name);
1217
+ extractedLines.push(renderInterface(iface.name, iface.node, extracted));
1218
+ extractedLines.push("");
1219
+ }
1220
+ if (extractedLines.length > 0) {
1221
+ sections.push(...extractedLines);
1222
+ }
1223
+ // --- Internal fetch helper ---
1224
+ sections.push("// ── Internal fetch helper ──");
1225
+ sections.push("");
1226
+ sections.push("let _baseUrl = \"\";");
1227
+ sections.push("");
1228
+ sections.push("/** Set the base URL for all API requests. Call once at app startup. */");
1229
+ sections.push("export function configureTrickleHooks(baseUrl: string) {");
1230
+ sections.push(" _baseUrl = baseUrl;");
1231
+ sections.push("}");
1232
+ sections.push("");
1233
+ sections.push("async function _fetch<T>(method: string, path: string, body?: unknown): Promise<T> {");
1234
+ sections.push(" const opts: RequestInit = { method, headers: { \"Content-Type\": \"application/json\" } };");
1235
+ sections.push(" if (body !== undefined) opts.body = JSON.stringify(body);");
1236
+ sections.push(" const res = await fetch(`${_baseUrl}${path}`, opts);");
1237
+ sections.push(" if (!res.ok) throw new Error(`${method} ${path}: HTTP ${res.status}`);");
1238
+ sections.push(" return res.json() as Promise<T>;");
1239
+ sections.push("}");
1240
+ sections.push("");
1241
+ // --- Query key factory ---
1242
+ sections.push("// ── Query Keys ──");
1243
+ sections.push("");
1244
+ sections.push("export const queryKeys = {");
1245
+ // Group routes by resource (first path segment after /api/)
1246
+ const resources = new Set();
1247
+ for (const { parsed } of routes) {
1248
+ const parts = parsed.path.split("/").filter(Boolean);
1249
+ // /api/users → "users", /products → "products"
1250
+ const resource = parts[0] === "api" && parts.length >= 2 ? parts[1] : parts[0];
1251
+ resources.add(resource);
1252
+ }
1253
+ for (const resource of resources) {
1254
+ sections.push(` ${resource}: {`);
1255
+ sections.push(` all: ["${resource}"] as const,`);
1256
+ // Add specific keys for routes in this resource
1257
+ const resourceRoutes = routes.filter(({ parsed }) => {
1258
+ const parts = parsed.path.split("/").filter(Boolean);
1259
+ const r = parts[0] === "api" && parts.length >= 2 ? parts[1] : parts[0];
1260
+ return r === resource;
1261
+ });
1262
+ for (const { parsed } of resourceRoutes) {
1263
+ if (parsed.method === "GET") {
1264
+ if (parsed.pathParams.length > 0) {
1265
+ const paramArgs = parsed.pathParams.map((p) => `${p}: string`).join(", ");
1266
+ const paramKeys = parsed.pathParams.map((p) => p).join(", ");
1267
+ sections.push(` detail: (${paramArgs}) => ["${resource}", ${paramKeys}] as const,`);
1268
+ }
1269
+ else {
1270
+ sections.push(` list: () => ["${resource}", "list"] as const,`);
1271
+ }
1272
+ }
1273
+ }
1274
+ sections.push(" },");
1275
+ }
1276
+ sections.push("} as const;");
1277
+ sections.push("");
1278
+ // --- Generate hooks ---
1279
+ sections.push("// ── Hooks ──");
1280
+ sections.push("");
1281
+ for (const { parsed, fn } of routes) {
1282
+ const baseName = parsed.typeName;
1283
+ const hookName = `use${baseName}`;
1284
+ const responseType = `${baseName}Response`;
1285
+ // Determine resource for query keys
1286
+ const parts = parsed.path.split("/").filter(Boolean);
1287
+ const resource = parts[0] === "api" && parts.length >= 2 ? parts[1] : parts[0];
1288
+ if (parsed.method === "GET") {
1289
+ // --- useQuery hook ---
1290
+ const hasPathParams = parsed.pathParams.length > 0;
1291
+ // Build params
1292
+ const fnParams = [];
1293
+ if (hasPathParams) {
1294
+ for (const p of parsed.pathParams) {
1295
+ fnParams.push(`${p}: string`);
1296
+ }
1297
+ }
1298
+ fnParams.push(`options?: Omit<UseQueryOptions<${responseType}, Error>, "queryKey" | "queryFn">`);
1299
+ // Build path expression
1300
+ let pathExpr;
1301
+ if (hasPathParams) {
1302
+ pathExpr = "`" + parsed.path.replace(/:(\w+)/g, (_, param) => `\${${param}}`) + "`";
1303
+ }
1304
+ else {
1305
+ pathExpr = `"${parsed.path}"`;
1306
+ }
1307
+ // Build query key
1308
+ let queryKeyExpr;
1309
+ if (hasPathParams) {
1310
+ queryKeyExpr = `queryKeys.${resource}.detail(${parsed.pathParams.join(", ")})`;
1311
+ }
1312
+ else {
1313
+ queryKeyExpr = `queryKeys.${resource}.list()`;
1314
+ }
1315
+ sections.push(`/** ${parsed.method} ${parsed.path} */`);
1316
+ sections.push(`export function ${hookName}(${fnParams.join(", ")}) {`);
1317
+ sections.push(` return useQuery({`);
1318
+ sections.push(` queryKey: ${queryKeyExpr},`);
1319
+ sections.push(` queryFn: () => _fetch<${responseType}>("GET", ${pathExpr}),`);
1320
+ sections.push(` ...options,`);
1321
+ sections.push(` });`);
1322
+ sections.push(`}`);
1323
+ sections.push("");
1324
+ }
1325
+ else {
1326
+ // --- useMutation hook ---
1327
+ const hasBody = ["POST", "PUT", "PATCH"].includes(parsed.method);
1328
+ let inputType = "void";
1329
+ // Check if we have a typed input
1330
+ if (hasBody) {
1331
+ let bodyNode;
1332
+ if (fn.argsType.kind === "object") {
1333
+ bodyNode = fn.argsType.properties["body"];
1334
+ }
1335
+ else if (fn.argsType.kind === "tuple" && fn.argsType.elements.length === 1) {
1336
+ const el = fn.argsType.elements[0];
1337
+ if (el.kind === "object")
1338
+ bodyNode = el.properties["body"];
1339
+ }
1340
+ if (bodyNode && bodyNode.kind === "object" && Object.keys(bodyNode.properties).length > 0) {
1341
+ inputType = `${baseName}Input`;
1342
+ }
1343
+ }
1344
+ // For mutations with path params, create a combined variables type
1345
+ const hasPathParams = parsed.pathParams.length > 0;
1346
+ let variablesType;
1347
+ if (hasPathParams && inputType !== "void") {
1348
+ variablesType = `{ ${parsed.pathParams.map((p) => `${p}: string`).join("; ")}; input: ${inputType} }`;
1349
+ }
1350
+ else if (hasPathParams) {
1351
+ variablesType = `{ ${parsed.pathParams.map((p) => `${p}: string`).join("; ")} }`;
1352
+ }
1353
+ else {
1354
+ variablesType = inputType;
1355
+ }
1356
+ // Build path expression
1357
+ let pathExpr;
1358
+ if (hasPathParams) {
1359
+ pathExpr = "`" + parsed.path.replace(/:(\w+)/g, (_, param) => `\${vars.${param}}`) + "`";
1360
+ }
1361
+ else {
1362
+ pathExpr = `"${parsed.path}"`;
1363
+ }
1364
+ // Build body expression
1365
+ let bodyExpr;
1366
+ if (hasPathParams && inputType !== "void") {
1367
+ bodyExpr = "vars.input";
1368
+ }
1369
+ else if (inputType !== "void") {
1370
+ bodyExpr = "vars";
1371
+ }
1372
+ else {
1373
+ bodyExpr = "undefined";
1374
+ }
1375
+ const optionsType = `UseMutationOptions<${responseType}, Error, ${variablesType}>`;
1376
+ sections.push(`/** ${parsed.method} ${parsed.path} */`);
1377
+ sections.push(`export function ${hookName}(options?: Omit<${optionsType}, "mutationFn">) {`);
1378
+ sections.push(` const queryClient = useQueryClient();`);
1379
+ sections.push(` return useMutation({`);
1380
+ if (variablesType === "void") {
1381
+ sections.push(` mutationFn: () => _fetch<${responseType}>("${parsed.method}", ${pathExpr}, ${bodyExpr}),`);
1382
+ }
1383
+ else {
1384
+ sections.push(` mutationFn: (vars: ${variablesType}) => _fetch<${responseType}>("${parsed.method}", ${pathExpr}, ${bodyExpr}),`);
1385
+ }
1386
+ sections.push(` onSuccess: (...args) => {`);
1387
+ sections.push(` queryClient.invalidateQueries({ queryKey: queryKeys.${resource}.all });`);
1388
+ sections.push(` options?.onSuccess?.(...args);`);
1389
+ sections.push(` },`);
1390
+ sections.push(` ...options,`);
1391
+ sections.push(` });`);
1392
+ sections.push(`}`);
1393
+ sections.push("");
1394
+ }
1395
+ }
1396
+ return sections.join("\n").trimEnd() + "\n";
1397
+ }
1398
+ // ── Type guard generation ──
1399
+ /**
1400
+ * Generate a runtime type guard check expression for a TypeNode.
1401
+ * Returns a string that evaluates to boolean when `varName` is the variable.
1402
+ */
1403
+ function typeNodeToGuardCheck(node, varName, depth = 0) {
1404
+ if (depth > 4)
1405
+ return "true"; // Limit recursion depth
1406
+ switch (node.kind) {
1407
+ case "primitive": {
1408
+ if (node.name === "null")
1409
+ return `${varName} === null`;
1410
+ if (node.name === "undefined")
1411
+ return `${varName} === undefined`;
1412
+ return `typeof ${varName} === "${node.name}"`;
1413
+ }
1414
+ case "unknown":
1415
+ return "true";
1416
+ case "array": {
1417
+ const elemCheck = typeNodeToGuardCheck(node.element, `${varName}[0]`, depth + 1);
1418
+ if (elemCheck === "true") {
1419
+ return `Array.isArray(${varName})`;
1420
+ }
1421
+ return `(Array.isArray(${varName}) && (${varName}.length === 0 || ${elemCheck}))`;
1422
+ }
1423
+ case "tuple": {
1424
+ const checks = [`Array.isArray(${varName})`];
1425
+ checks.push(`${varName}.length === ${node.elements.length}`);
1426
+ node.elements.forEach((el, i) => {
1427
+ const c = typeNodeToGuardCheck(el, `${varName}[${i}]`, depth + 1);
1428
+ if (c !== "true")
1429
+ checks.push(c);
1430
+ });
1431
+ return checks.join(" && ");
1432
+ }
1433
+ case "union": {
1434
+ const memberChecks = node.members.map((m) => typeNodeToGuardCheck(m, varName, depth + 1));
1435
+ return `(${memberChecks.join(" || ")})`;
1436
+ }
1437
+ case "object": {
1438
+ const keys = Object.keys(node.properties);
1439
+ if (keys.length === 0) {
1440
+ return `(typeof ${varName} === "object" && ${varName} !== null)`;
1441
+ }
1442
+ const checks = [
1443
+ `typeof ${varName} === "object"`,
1444
+ `${varName} !== null`,
1445
+ ];
1446
+ for (const key of keys) {
1447
+ checks.push(`"${key}" in ${varName}`);
1448
+ if (depth < 3) {
1449
+ const propCheck = typeNodeToGuardCheck(node.properties[key], `(${varName} as any).${key}`, depth + 1);
1450
+ if (propCheck !== "true") {
1451
+ checks.push(propCheck);
1452
+ }
1453
+ }
1454
+ }
1455
+ return checks.join(" && ");
1456
+ }
1457
+ case "map":
1458
+ return `${varName} instanceof Map`;
1459
+ case "set":
1460
+ return `${varName} instanceof Set`;
1461
+ case "promise":
1462
+ return `${varName} instanceof Promise`;
1463
+ case "function":
1464
+ return `typeof ${varName} === "function"`;
1465
+ }
1466
+ }
1467
+ /**
1468
+ * Generate TypeScript type guard functions from runtime-observed types.
1469
+ *
1470
+ * For each route:
1471
+ * - `isGetApiUsersResponse(value): value is GetApiUsersResponse`
1472
+ * - `isPostApiUsersRequest(value): value is PostApiUsersRequest`
1473
+ *
1474
+ * Type guards perform structural checks: verify typeof, key existence,
1475
+ * array shapes, and nested object structure.
1476
+ */
1477
+ function generateTypeGuards(functions) {
1478
+ if (functions.length === 0) {
1479
+ return "// No functions found. Instrument your app to generate type guards.\n";
1480
+ }
1481
+ const sections = [];
1482
+ sections.push("// Auto-generated type guards by trickle");
1483
+ sections.push(`// Generated at ${new Date().toISOString()}`);
1484
+ sections.push("// Do not edit manually — re-run `trickle codegen --guards` to update");
1485
+ sections.push("");
1486
+ // First, collect all interfaces we'll need to reference
1487
+ const interfaces = [];
1488
+ const guards = [];
1489
+ for (const fn of functions) {
1490
+ const parsed = parseRouteName(fn.name);
1491
+ if (parsed) {
1492
+ const basePascal = parsed.typeName;
1493
+ const baseCamel = toCamelCase(basePascal);
1494
+ // Response type + guard
1495
+ const responseTypeName = `${basePascal}Response`;
1496
+ const responseInterface = generateGuardInterface(responseTypeName, fn.returnType);
1497
+ if (responseInterface)
1498
+ interfaces.push(responseInterface);
1499
+ const responseCheck = typeNodeToGuardCheck(fn.returnType, "value");
1500
+ guards.push(`/** Type guard for ${parsed.method} ${parsed.path} response */`);
1501
+ guards.push(`export function is${responseTypeName}(value: unknown): value is ${responseTypeName} {`);
1502
+ guards.push(` return ${responseCheck};`);
1503
+ guards.push(`}`);
1504
+ guards.push("");
1505
+ // Request body type + guard (for POST/PUT/PATCH)
1506
+ if (["POST", "PUT", "PATCH"].includes(parsed.method)) {
1507
+ let bodyNode;
1508
+ if (fn.argsType.kind === "object" && fn.argsType.properties["body"]) {
1509
+ bodyNode = fn.argsType.properties["body"];
1510
+ }
1511
+ if (bodyNode && bodyNode.kind === "object" && Object.keys(bodyNode.properties).length > 0) {
1512
+ const requestTypeName = `${basePascal}Request`;
1513
+ const requestInterface = generateGuardInterface(requestTypeName, bodyNode);
1514
+ if (requestInterface)
1515
+ interfaces.push(requestInterface);
1516
+ const requestCheck = typeNodeToGuardCheck(bodyNode, "value");
1517
+ guards.push(`/** Type guard for ${parsed.method} ${parsed.path} request body */`);
1518
+ guards.push(`export function is${requestTypeName}(value: unknown): value is ${requestTypeName} {`);
1519
+ guards.push(` return ${requestCheck};`);
1520
+ guards.push(`}`);
1521
+ guards.push("");
1522
+ }
1523
+ }
1524
+ }
1525
+ else {
1526
+ // Non-route function
1527
+ const basePascal = toPascalCase(fn.name);
1528
+ // Output guard
1529
+ const outputTypeName = `${basePascal}Output`;
1530
+ const outputInterface = generateGuardInterface(outputTypeName, fn.returnType);
1531
+ if (outputInterface)
1532
+ interfaces.push(outputInterface);
1533
+ const outputCheck = typeNodeToGuardCheck(fn.returnType, "value");
1534
+ guards.push(`/** Type guard for ${fn.name} output */`);
1535
+ guards.push(`export function is${outputTypeName}(value: unknown): value is ${outputTypeName} {`);
1536
+ guards.push(` return ${outputCheck};`);
1537
+ guards.push(`}`);
1538
+ guards.push("");
1539
+ // Input guard (if non-trivial)
1540
+ if (fn.argsType.kind !== "unknown" && fn.argsType.kind !== "object" || (fn.argsType.kind === "object" && Object.keys(fn.argsType.properties).length > 0)) {
1541
+ const inputTypeName = `${basePascal}Input`;
1542
+ const inputNode = fn.argsType.kind === "tuple" && fn.argsType.elements.length === 1
1543
+ ? fn.argsType.elements[0]
1544
+ : fn.argsType;
1545
+ const inputInterface = generateGuardInterface(inputTypeName, inputNode);
1546
+ if (inputInterface)
1547
+ interfaces.push(inputInterface);
1548
+ const inputCheck = typeNodeToGuardCheck(inputNode, "value");
1549
+ guards.push(`/** Type guard for ${fn.name} input */`);
1550
+ guards.push(`export function is${inputTypeName}(value: unknown): value is ${inputTypeName} {`);
1551
+ guards.push(` return ${inputCheck};`);
1552
+ guards.push(`}`);
1553
+ guards.push("");
1554
+ }
1555
+ }
1556
+ }
1557
+ // Output: interfaces first, then guards
1558
+ sections.push("// ── Type Interfaces ──");
1559
+ sections.push("");
1560
+ sections.push(...interfaces);
1561
+ sections.push("// ── Type Guards ──");
1562
+ sections.push("");
1563
+ sections.push(...guards);
1564
+ return sections.join("\n").trimEnd() + "\n";
1565
+ }
1566
+ /**
1567
+ * Generate a simple interface declaration for use with type guards.
1568
+ */
1569
+ function generateGuardInterface(name, node) {
1570
+ if (node.kind !== "object") {
1571
+ // For non-object types, generate a type alias
1572
+ const extracted = [];
1573
+ const tsType = typeNodeToTS(node, extracted, name, undefined, 0);
1574
+ return `export type ${name} = ${tsType};\n`;
1575
+ }
1576
+ const keys = Object.keys(node.properties);
1577
+ if (keys.length === 0)
1578
+ return `export type ${name} = Record<string, unknown>;\n`;
1579
+ const extracted = [];
1580
+ const lines = [];
1581
+ lines.push(`export interface ${name} {`);
1582
+ for (const key of keys) {
1583
+ const val = typeNodeToTS(node.properties[key], extracted, name, key, 1);
1584
+ lines.push(` ${key}: ${val};`);
1585
+ }
1586
+ lines.push(`}`);
1587
+ // Include any extracted sub-interfaces
1588
+ const subInterfaces = [];
1589
+ for (const ext of extracted) {
1590
+ if (ext.node.kind === "object") {
1591
+ subInterfaces.push(renderInterface(ext.name, ext.node, extracted));
1592
+ }
1593
+ }
1594
+ return [...subInterfaces, lines.join("\n")].join("\n\n") + "\n";
1595
+ }
1596
+ // ── Express validation middleware generation ──
1597
+ /**
1598
+ * Generate a validation expression that returns an array of error strings.
1599
+ * `varName` is the variable being checked.
1600
+ */
1601
+ function typeNodeToValidation(node, varName, path, depth = 0) {
1602
+ if (depth > 3)
1603
+ return [];
1604
+ const checks = [];
1605
+ switch (node.kind) {
1606
+ case "primitive": {
1607
+ if (node.name === "null") {
1608
+ checks.push(`if (${varName} !== null) errors.push(\`${path} must be null\`);`);
1609
+ }
1610
+ else if (node.name === "undefined") {
1611
+ // Don't validate undefined — field just shouldn't be required
1612
+ }
1613
+ else {
1614
+ checks.push(`if (typeof ${varName} !== "${node.name}") errors.push(\`${path} must be a ${node.name}\`);`);
1615
+ }
1616
+ break;
1617
+ }
1618
+ case "array": {
1619
+ checks.push(`if (!Array.isArray(${varName})) errors.push(\`${path} must be an array\`);`);
1620
+ if (node.element.kind !== "unknown" && depth < 3) {
1621
+ checks.push(`else if (${varName}.length > 0) {`);
1622
+ checks.push(...typeNodeToValidation(node.element, `${varName}[0]`, `${path}[0]`, depth + 1).map(l => " " + l));
1623
+ checks.push(`}`);
1624
+ }
1625
+ break;
1626
+ }
1627
+ case "object": {
1628
+ const keys = Object.keys(node.properties);
1629
+ checks.push(`if (typeof ${varName} !== "object" || ${varName} === null) errors.push(\`${path} must be an object\`);`);
1630
+ if (keys.length > 0 && depth < 3) {
1631
+ checks.push(`else {`);
1632
+ for (const key of keys) {
1633
+ const propPath = path ? `${path}.${key}` : key;
1634
+ const propAccess = `${varName}["${key}"]`;
1635
+ // Check key existence
1636
+ checks.push(` if (!("${key}" in ${varName})) errors.push(\`${propPath} is required\`);`);
1637
+ // Type check if present
1638
+ const innerChecks = typeNodeToValidation(node.properties[key], propAccess, propPath, depth + 1);
1639
+ if (innerChecks.length > 0) {
1640
+ checks.push(` else {`);
1641
+ checks.push(...innerChecks.map(l => " " + l));
1642
+ checks.push(` }`);
1643
+ }
1644
+ }
1645
+ checks.push(`}`);
1646
+ }
1647
+ break;
1648
+ }
1649
+ case "union": {
1650
+ // For unions, value must match at least one member — skip detailed validation
1651
+ break;
1652
+ }
1653
+ default:
1654
+ break;
1655
+ }
1656
+ return checks;
1657
+ }
1658
+ /**
1659
+ * Generate Express validation middleware from runtime-observed types.
1660
+ *
1661
+ * For each POST/PUT/PATCH route, generates middleware that:
1662
+ * - Validates request body structure and types
1663
+ * - Returns 400 with structured errors on failure
1664
+ * - Passes through to next() on success
1665
+ *
1666
+ * Self-contained — no external validation library required.
1667
+ */
1668
+ function generateMiddleware(functions) {
1669
+ // Filter to routes with request bodies
1670
+ const routes = [];
1671
+ for (const fn of functions) {
1672
+ const parsed = parseRouteName(fn.name);
1673
+ if (!parsed)
1674
+ continue;
1675
+ if (!["POST", "PUT", "PATCH"].includes(parsed.method))
1676
+ continue;
1677
+ let bodyNode;
1678
+ if (fn.argsType.kind === "object" && fn.argsType.properties["body"]) {
1679
+ bodyNode = fn.argsType.properties["body"];
1680
+ }
1681
+ if (!bodyNode || bodyNode.kind === "unknown")
1682
+ continue;
1683
+ if (bodyNode.kind === "object" && Object.keys(bodyNode.properties).length === 0)
1684
+ continue;
1685
+ routes.push({ parsed, fn, bodyNode });
1686
+ }
1687
+ if (routes.length === 0) {
1688
+ return "// No POST/PUT/PATCH routes with request bodies found.\n// Instrument your app and make some requests first.\n";
1689
+ }
1690
+ const sections = [];
1691
+ sections.push("// Auto-generated Express validation middleware by trickle");
1692
+ sections.push(`// Generated at ${new Date().toISOString()}`);
1693
+ sections.push("// Do not edit manually — re-run `trickle codegen --middleware` to update");
1694
+ sections.push("");
1695
+ sections.push('import { Request, Response, NextFunction } from "express";');
1696
+ sections.push("");
1697
+ // Generate interfaces for request body types
1698
+ const extracted = [];
1699
+ for (const { parsed, bodyNode } of routes) {
1700
+ const typeName = `${parsed.typeName}Body`;
1701
+ if (bodyNode.kind === "object") {
1702
+ sections.push(renderInterface(typeName, bodyNode, extracted));
1703
+ }
1704
+ else {
1705
+ const tsType = typeNodeToTS(bodyNode, extracted, typeName, undefined, 0);
1706
+ sections.push(`export type ${typeName} = ${tsType};`);
1707
+ }
1708
+ sections.push("");
1709
+ }
1710
+ // Render any extracted sub-interfaces
1711
+ for (const ext of extracted) {
1712
+ if (ext.node.kind === "object") {
1713
+ sections.push(renderInterface(ext.name, ext.node, extracted));
1714
+ sections.push("");
1715
+ }
1716
+ }
1717
+ // Generate validation middleware for each route
1718
+ for (const { parsed, bodyNode } of routes) {
1719
+ const middlewareName = `validate${parsed.typeName}`;
1720
+ const typeName = `${parsed.typeName}Body`;
1721
+ sections.push(`/**`);
1722
+ sections.push(` * Validates request body for ${parsed.method} ${parsed.path}`);
1723
+ sections.push(` * Returns 400 with structured errors if validation fails.`);
1724
+ sections.push(` */`);
1725
+ sections.push(`export function ${middlewareName}(req: Request, res: Response, next: NextFunction): void {`);
1726
+ sections.push(` const errors: string[] = [];`);
1727
+ sections.push(` const body = req.body;`);
1728
+ sections.push("");
1729
+ // Null/undefined check
1730
+ sections.push(` if (body === null || body === undefined || typeof body !== "object") {`);
1731
+ sections.push(` res.status(400).json({ error: "Request body is required", errors: ["body must be an object"] });`);
1732
+ sections.push(` return;`);
1733
+ sections.push(` }`);
1734
+ sections.push("");
1735
+ // Generate field validations
1736
+ if (bodyNode.kind === "object") {
1737
+ const keys = Object.keys(bodyNode.properties);
1738
+ for (const key of keys) {
1739
+ const propNode = bodyNode.properties[key];
1740
+ sections.push(` // Validate ${key}`);
1741
+ sections.push(` if (!("${key}" in body)) {`);
1742
+ sections.push(` errors.push("${key} is required");`);
1743
+ sections.push(` }`);
1744
+ const innerChecks = typeNodeToValidation(propNode, `body["${key}"]`, key, 1);
1745
+ if (innerChecks.length > 0) {
1746
+ sections.push(` else {`);
1747
+ for (const check of innerChecks) {
1748
+ sections.push(` ${check}`);
1749
+ }
1750
+ sections.push(` }`);
1751
+ }
1752
+ sections.push("");
1753
+ }
1754
+ }
1755
+ sections.push(` if (errors.length > 0) {`);
1756
+ sections.push(` res.status(400).json({ error: "Validation failed", errors });`);
1757
+ sections.push(` return;`);
1758
+ sections.push(` }`);
1759
+ sections.push("");
1760
+ sections.push(` next();`);
1761
+ sections.push(`}`);
1762
+ sections.push("");
1763
+ }
1764
+ // Export a combined middleware map for convenience
1765
+ sections.push("/** Map of route patterns to their validation middleware */");
1766
+ sections.push("export const validators = {");
1767
+ for (const { parsed } of routes) {
1768
+ const middlewareName = `validate${parsed.typeName}`;
1769
+ sections.push(` "${parsed.method} ${parsed.path}": ${middlewareName},`);
1770
+ }
1771
+ sections.push("} as const;");
1772
+ sections.push("");
1773
+ return sections.join("\n").trimEnd() + "\n";
1774
+ }
1775
+ // ── MSW Handler Generation ──
1776
+ /**
1777
+ * Generate a sample value literal string from a TypeNode.
1778
+ * Used to create realistic mock responses in MSW handlers.
1779
+ */
1780
+ function typeNodeToSampleLiteral(node, indent = 0) {
1781
+ const pad = " ".repeat(indent);
1782
+ const innerPad = " ".repeat(indent + 1);
1783
+ switch (node.kind) {
1784
+ case "primitive":
1785
+ switch (node.name) {
1786
+ case "string": return '""';
1787
+ case "number": return "0";
1788
+ case "boolean": return "true";
1789
+ case "null": return "null";
1790
+ case "undefined": return "undefined";
1791
+ case "bigint": return "0";
1792
+ case "symbol": return '"symbol"';
1793
+ default: return "null";
1794
+ }
1795
+ case "array":
1796
+ return `[${typeNodeToSampleLiteral(node.element, indent)}]`;
1797
+ case "tuple": {
1798
+ const elements = node.elements.map((el) => typeNodeToSampleLiteral(el, indent));
1799
+ return `[${elements.join(", ")}]`;
1800
+ }
1801
+ case "object": {
1802
+ const keys = Object.keys(node.properties);
1803
+ if (keys.length === 0)
1804
+ return "{}";
1805
+ const entries = keys.map((key) => `${innerPad}${key}: ${typeNodeToSampleLiteral(node.properties[key], indent + 1)}`);
1806
+ return `{\n${entries.join(",\n")}\n${pad}}`;
1807
+ }
1808
+ case "union":
1809
+ // Use the first non-null/undefined member
1810
+ for (const m of node.members) {
1811
+ if (m.kind === "primitive" && (m.name === "null" || m.name === "undefined"))
1812
+ continue;
1813
+ return typeNodeToSampleLiteral(m, indent);
1814
+ }
1815
+ return "null";
1816
+ case "map":
1817
+ return "new Map()";
1818
+ case "set":
1819
+ return "new Set()";
1820
+ case "promise":
1821
+ return typeNodeToSampleLiteral(node.resolved, indent);
1822
+ case "function":
1823
+ return "() => {}";
1824
+ case "unknown":
1825
+ return "null";
1826
+ default:
1827
+ return "null";
1828
+ }
1829
+ }
1830
+ /**
1831
+ * Generate Mock Service Worker (MSW) request handlers from observed API routes.
1832
+ *
1833
+ * Output:
1834
+ * - Import from 'msw'
1835
+ * - Response type interfaces for each route
1836
+ * - Individual handler exports (e.g. getApiUsersHandler)
1837
+ * - A combined `handlers` array for setupServer/setupWorker
1838
+ */
1839
+ function generateMswHandlers(functions) {
1840
+ const routes = [];
1841
+ for (const fn of functions) {
1842
+ const parsed = parseRouteName(fn.name);
1843
+ if (parsed) {
1844
+ routes.push({ parsed, fn });
1845
+ }
1846
+ }
1847
+ if (routes.length === 0) {
1848
+ return "// No API routes found. Instrument your Express app to generate MSW handlers.\n";
1849
+ }
1850
+ const sections = [];
1851
+ sections.push("// Auto-generated MSW request handlers by trickle");
1852
+ sections.push(`// Generated at ${new Date().toISOString()}`);
1853
+ sections.push("// Do not edit manually — re-run `trickle codegen --msw` to update");
1854
+ sections.push("");
1855
+ sections.push('import { http, HttpResponse } from "msw";');
1856
+ sections.push("");
1857
+ // Generate response type interfaces
1858
+ const extracted = [];
1859
+ for (const { parsed, fn } of routes) {
1860
+ const responseName = `${parsed.typeName}Response`;
1861
+ if (fn.returnType.kind === "object" && Object.keys(fn.returnType.properties).length > 0) {
1862
+ sections.push(renderInterface(responseName, fn.returnType, extracted));
1863
+ }
1864
+ else {
1865
+ const tsType = typeNodeToTS(fn.returnType, extracted, responseName, undefined, 0);
1866
+ sections.push(`export type ${responseName} = ${tsType};`);
1867
+ }
1868
+ sections.push("");
1869
+ }
1870
+ // Render extracted sub-interfaces
1871
+ for (const ext of extracted) {
1872
+ if (ext.node.kind === "object") {
1873
+ sections.push(renderInterface(ext.name, ext.node, extracted));
1874
+ sections.push("");
1875
+ }
1876
+ }
1877
+ // Generate individual handler exports
1878
+ const handlerNames = [];
1879
+ for (const { parsed, fn } of routes) {
1880
+ const handlerName = `${parsed.funcName}Handler`;
1881
+ handlerNames.push(handlerName);
1882
+ const method = parsed.method.toLowerCase();
1883
+ // Convert Express-style :param to MSW-style :param (same format, already compatible)
1884
+ const mswPath = parsed.path;
1885
+ // Generate sample response from returnType
1886
+ const sampleResponse = typeNodeToSampleLiteral(fn.returnType, 1);
1887
+ sections.push(`/**`);
1888
+ sections.push(` * Mock handler for ${parsed.method} ${parsed.path}`);
1889
+ sections.push(` */`);
1890
+ sections.push(`export const ${handlerName} = http.${method}("${mswPath}", () => {`);
1891
+ sections.push(` return HttpResponse.json(${sampleResponse} satisfies ${parsed.typeName}Response);`);
1892
+ sections.push(`});`);
1893
+ sections.push("");
1894
+ }
1895
+ // Export combined handlers array
1896
+ sections.push("/** All mock handlers — use with setupServer(...handlers) or setupWorker(...handlers) */");
1897
+ sections.push("export const handlers = [");
1898
+ for (const name of handlerNames) {
1899
+ sections.push(` ${name},`);
1900
+ }
1901
+ sections.push("];");
1902
+ sections.push("");
1903
+ return sections.join("\n").trimEnd() + "\n";
1904
+ }
1905
+ // ── JSON Schema Generation ──
1906
+ /**
1907
+ * Convert a TypeNode to a standalone JSON Schema (Draft 2020-12).
1908
+ * Unlike the OpenAPI variant, this produces proper JSON Schema with
1909
+ * $defs, oneOf, prefixItems, and nullable via type arrays.
1910
+ */
1911
+ function typeNodeToStandaloneSchema(node) {
1912
+ switch (node.kind) {
1913
+ case "primitive":
1914
+ switch (node.name) {
1915
+ case "string": return { type: "string" };
1916
+ case "number": return { type: "number" };
1917
+ case "boolean": return { type: "boolean" };
1918
+ case "null": return { type: "null" };
1919
+ case "undefined": return { type: "null" };
1920
+ case "bigint": return { type: "integer" };
1921
+ case "symbol": return { type: "string" };
1922
+ default: return {};
1923
+ }
1924
+ case "array":
1925
+ return { type: "array", items: typeNodeToStandaloneSchema(node.element) };
1926
+ case "tuple":
1927
+ return {
1928
+ type: "array",
1929
+ prefixItems: node.elements.map(typeNodeToStandaloneSchema),
1930
+ minItems: node.elements.length,
1931
+ maxItems: node.elements.length,
1932
+ };
1933
+ case "object": {
1934
+ const properties = {};
1935
+ const required = [];
1936
+ for (const [key, val] of Object.entries(node.properties)) {
1937
+ properties[key] = typeNodeToStandaloneSchema(val);
1938
+ required.push(key);
1939
+ }
1940
+ const schema = { type: "object", properties };
1941
+ if (required.length > 0)
1942
+ schema.required = required;
1943
+ return schema;
1944
+ }
1945
+ case "union": {
1946
+ const nonNull = node.members.filter((m) => !(m.kind === "primitive" && (m.name === "null" || m.name === "undefined")));
1947
+ const hasNull = nonNull.length < node.members.length;
1948
+ if (nonNull.length === 1) {
1949
+ const inner = typeNodeToStandaloneSchema(nonNull[0]);
1950
+ if (hasNull) {
1951
+ if (typeof inner.type === "string") {
1952
+ return { ...inner, type: [inner.type, "null"] };
1953
+ }
1954
+ return { oneOf: [inner, { type: "null" }] };
1955
+ }
1956
+ return inner;
1957
+ }
1958
+ const schemas = nonNull.map(typeNodeToStandaloneSchema);
1959
+ if (hasNull)
1960
+ schemas.push({ type: "null" });
1961
+ return { oneOf: schemas };
1962
+ }
1963
+ case "map":
1964
+ return {
1965
+ type: "object",
1966
+ additionalProperties: typeNodeToStandaloneSchema(node.value),
1967
+ };
1968
+ case "set":
1969
+ return {
1970
+ type: "array",
1971
+ uniqueItems: true,
1972
+ items: typeNodeToStandaloneSchema(node.element),
1973
+ };
1974
+ case "promise":
1975
+ return typeNodeToStandaloneSchema(node.resolved);
1976
+ case "function":
1977
+ return {};
1978
+ case "unknown":
1979
+ return {};
1980
+ default:
1981
+ return {};
1982
+ }
1983
+ }
1984
+ /**
1985
+ * Generate JSON Schema definitions from observed runtime types.
1986
+ *
1987
+ * Output is a single JSON object with $defs for each route/function's
1988
+ * request and response types, suitable for use with ajv, joi, or any
1989
+ * JSON Schema-compatible validator.
1990
+ */
1991
+ function generateJsonSchemas(functions) {
1992
+ const defs = {};
1993
+ for (const fn of functions) {
1994
+ const parsed = parseRouteName(fn.name);
1995
+ const baseName = parsed ? parsed.typeName : toPascalCase(fn.name);
1996
+ // For routes, generate request body + response schemas
1997
+ if (parsed) {
1998
+ // Request body (only for methods with body)
1999
+ if (["POST", "PUT", "PATCH"].includes(parsed.method)) {
2000
+ let bodyNode;
2001
+ if (fn.argsType.kind === "object" && fn.argsType.properties["body"]) {
2002
+ bodyNode = fn.argsType.properties["body"];
2003
+ }
2004
+ if (bodyNode && bodyNode.kind !== "unknown") {
2005
+ const bodySchema = typeNodeToStandaloneSchema(bodyNode);
2006
+ defs[`${baseName}Request`] = {
2007
+ description: `Request body for ${parsed.method} ${parsed.path}`,
2008
+ ...bodySchema,
2009
+ };
2010
+ }
2011
+ }
2012
+ // Response
2013
+ if (fn.returnType.kind !== "unknown") {
2014
+ const responseSchema = typeNodeToStandaloneSchema(fn.returnType);
2015
+ defs[`${baseName}Response`] = {
2016
+ description: `Response for ${parsed.method} ${parsed.path}`,
2017
+ ...responseSchema,
2018
+ };
2019
+ }
2020
+ }
2021
+ else {
2022
+ // Non-route function: generate input/output schemas
2023
+ if (fn.argsType.kind !== "unknown") {
2024
+ defs[`${baseName}Input`] = typeNodeToStandaloneSchema(fn.argsType);
2025
+ }
2026
+ if (fn.returnType.kind !== "unknown") {
2027
+ defs[`${baseName}Output`] = typeNodeToStandaloneSchema(fn.returnType);
2028
+ }
2029
+ }
2030
+ }
2031
+ const schema = {
2032
+ $schema: "https://json-schema.org/draft/2020-12/schema",
2033
+ title: "API Schemas",
2034
+ description: "Auto-generated JSON Schema definitions by trickle",
2035
+ $defs: defs,
2036
+ };
2037
+ return JSON.stringify(schema, null, 2) + "\n";
2038
+ }
2039
+ // ── SWR Hook Generation ──
2040
+ /**
2041
+ * Generate typed SWR hooks from observed API routes.
2042
+ *
2043
+ * Output:
2044
+ * - Import from 'swr' and 'swr/mutation'
2045
+ * - Response/input type interfaces
2046
+ * - A configurable fetcher
2047
+ * - useSWR hooks for GET routes
2048
+ * - useSWRMutation hooks for POST/PUT/PATCH/DELETE routes
2049
+ */
2050
+ function generateSwrHooks(functions) {
2051
+ const routes = [];
2052
+ for (const fn of functions) {
2053
+ const parsed = parseRouteName(fn.name);
2054
+ if (parsed) {
2055
+ routes.push({ parsed, fn });
2056
+ }
2057
+ }
2058
+ if (routes.length === 0) {
2059
+ return "// No API routes found. Instrument your Express/FastAPI app to generate SWR hooks.\n";
2060
+ }
2061
+ const sections = [];
2062
+ sections.push("// Auto-generated SWR hooks by trickle");
2063
+ sections.push(`// Generated at ${new Date().toISOString()}`);
2064
+ sections.push("// Do not edit manually — re-run `trickle codegen --swr` to update");
2065
+ sections.push("");
2066
+ sections.push('import useSWR from "swr";');
2067
+ sections.push('import useSWRMutation from "swr/mutation";');
2068
+ sections.push('import type { SWRConfiguration, SWRResponse } from "swr";');
2069
+ sections.push('import type { SWRMutationConfiguration, SWRMutationResponse } from "swr/mutation";');
2070
+ sections.push("");
2071
+ // Generate interfaces
2072
+ const extracted = [];
2073
+ for (const { parsed, fn } of routes) {
2074
+ const baseName = parsed.typeName;
2075
+ // Response type
2076
+ if (fn.returnType.kind === "object" && Object.keys(fn.returnType.properties).length > 0) {
2077
+ sections.push(renderInterface(`${baseName}Response`, fn.returnType, extracted));
2078
+ }
2079
+ else {
2080
+ const retStr = typeNodeToTS(fn.returnType, extracted, baseName, undefined, 0);
2081
+ sections.push(`export type ${baseName}Response = ${retStr};`);
2082
+ }
2083
+ sections.push("");
2084
+ // Request body type (POST/PUT/PATCH)
2085
+ if (["POST", "PUT", "PATCH"].includes(parsed.method)) {
2086
+ let bodyNode;
2087
+ if (fn.argsType.kind === "object" && fn.argsType.properties["body"]) {
2088
+ bodyNode = fn.argsType.properties["body"];
2089
+ }
2090
+ if (bodyNode && bodyNode.kind === "object" && Object.keys(bodyNode.properties).length > 0) {
2091
+ sections.push(renderInterface(`${baseName}Input`, bodyNode, extracted));
2092
+ sections.push("");
2093
+ }
2094
+ }
2095
+ }
2096
+ // Emit extracted sub-interfaces
2097
+ const emitted = new Set();
2098
+ let cursor = 0;
2099
+ while (cursor < extracted.length) {
2100
+ const iface = extracted[cursor];
2101
+ cursor++;
2102
+ if (emitted.has(iface.name))
2103
+ continue;
2104
+ emitted.add(iface.name);
2105
+ sections.push(renderInterface(iface.name, iface.node, extracted));
2106
+ sections.push("");
2107
+ }
2108
+ // Fetcher setup
2109
+ sections.push("// ── Fetcher ──");
2110
+ sections.push("");
2111
+ sections.push("let _baseUrl = \"\";");
2112
+ sections.push("");
2113
+ sections.push("/** Set the base URL for all API requests. Call once at app startup. */");
2114
+ sections.push("export function configureSwrHooks(baseUrl: string) {");
2115
+ sections.push(" _baseUrl = baseUrl;");
2116
+ sections.push("}");
2117
+ sections.push("");
2118
+ sections.push("const fetcher = <T>(path: string): Promise<T> =>");
2119
+ sections.push(" fetch(`${_baseUrl}${path}`).then((res) => {");
2120
+ sections.push(" if (!res.ok) throw new Error(`GET ${path}: HTTP ${res.status}`);");
2121
+ sections.push(" return res.json() as Promise<T>;");
2122
+ sections.push(" });");
2123
+ sections.push("");
2124
+ sections.push("async function mutationFetcher<T>(");
2125
+ sections.push(" url: string,");
2126
+ sections.push(" { arg }: { arg: { method: string; body?: unknown } },");
2127
+ sections.push("): Promise<T> {");
2128
+ sections.push(" const opts: RequestInit = {");
2129
+ sections.push(" method: arg.method,");
2130
+ sections.push(' headers: { "Content-Type": "application/json" },');
2131
+ sections.push(" };");
2132
+ sections.push(" if (arg.body !== undefined) opts.body = JSON.stringify(arg.body);");
2133
+ sections.push(" const res = await fetch(`${_baseUrl}${url}`, opts);");
2134
+ sections.push(" if (!res.ok) throw new Error(`${arg.method} ${url}: HTTP ${res.status}`);");
2135
+ sections.push(" return res.json() as Promise<T>;");
2136
+ sections.push("}");
2137
+ sections.push("");
2138
+ // Generate hooks
2139
+ sections.push("// ── Hooks ──");
2140
+ sections.push("");
2141
+ for (const { parsed, fn } of routes) {
2142
+ const baseName = parsed.typeName;
2143
+ const hookName = `use${baseName}`;
2144
+ const responseType = `${baseName}Response`;
2145
+ if (parsed.method === "GET") {
2146
+ // useSWR hook
2147
+ const hasPathParams = parsed.pathParams.length > 0;
2148
+ const fnParams = [];
2149
+ if (hasPathParams) {
2150
+ for (const p of parsed.pathParams) {
2151
+ fnParams.push(`${p}: string`);
2152
+ }
2153
+ }
2154
+ fnParams.push(`config?: SWRConfiguration<${responseType}, Error>`);
2155
+ let pathExpr;
2156
+ if (hasPathParams) {
2157
+ pathExpr = "`" + parsed.path.replace(/:(\w+)/g, (_, param) => `\${${param}}`) + "`";
2158
+ }
2159
+ else {
2160
+ pathExpr = `"${parsed.path}"`;
2161
+ }
2162
+ sections.push(`/** ${parsed.method} ${parsed.path} */`);
2163
+ sections.push(`export function ${hookName}(${fnParams.join(", ")}): SWRResponse<${responseType}, Error> {`);
2164
+ sections.push(` return useSWR<${responseType}, Error>(${pathExpr}, fetcher<${responseType}>, config);`);
2165
+ sections.push("}");
2166
+ sections.push("");
2167
+ }
2168
+ else {
2169
+ // useSWRMutation hook for POST/PUT/PATCH/DELETE
2170
+ const method = parsed.method;
2171
+ const hasPathParams = parsed.pathParams.length > 0;
2172
+ const fnParams = [];
2173
+ if (hasPathParams) {
2174
+ for (const p of parsed.pathParams) {
2175
+ fnParams.push(`${p}: string`);
2176
+ }
2177
+ }
2178
+ // Check if route has input body
2179
+ let inputType;
2180
+ if (["POST", "PUT", "PATCH"].includes(method)) {
2181
+ let bodyNode;
2182
+ if (fn.argsType.kind === "object" && fn.argsType.properties["body"]) {
2183
+ bodyNode = fn.argsType.properties["body"];
2184
+ }
2185
+ if (bodyNode && bodyNode.kind === "object" && Object.keys(bodyNode.properties).length > 0) {
2186
+ inputType = `${baseName}Input`;
2187
+ }
2188
+ }
2189
+ const triggerArgType = inputType || "void";
2190
+ fnParams.push(`config?: SWRMutationConfiguration<${responseType}, Error, string, ${triggerArgType}>`);
2191
+ let pathExpr;
2192
+ if (hasPathParams) {
2193
+ pathExpr = "`" + parsed.path.replace(/:(\w+)/g, (_, param) => `\${${param}}`) + "`";
2194
+ }
2195
+ else {
2196
+ pathExpr = `"${parsed.path}"`;
2197
+ }
2198
+ sections.push(`/** ${method} ${parsed.path} */`);
2199
+ sections.push(`export function ${hookName}(${fnParams.join(", ")}): SWRMutationResponse<${responseType}, Error, string, ${triggerArgType}> {`);
2200
+ sections.push(` return useSWRMutation<${responseType}, Error, string, ${triggerArgType}>(`);
2201
+ sections.push(` ${pathExpr},`);
2202
+ if (inputType) {
2203
+ sections.push(` (url, { arg }) => mutationFetcher<${responseType}>(url, { arg: { method: "${method}", body: arg } }),`);
2204
+ }
2205
+ else {
2206
+ sections.push(` (url) => mutationFetcher<${responseType}>(url, { arg: { method: "${method}" } }),`);
2207
+ }
2208
+ sections.push(" config,");
2209
+ sections.push(" );");
2210
+ sections.push("}");
2211
+ sections.push("");
2212
+ }
2213
+ }
2214
+ return sections.join("\n").trimEnd() + "\n";
2215
+ }
2216
+ // ── Pydantic Model Generation ──
2217
+ /**
2218
+ * Convert a TypeNode to a Pydantic-compatible Python type string.
2219
+ */
2220
+ function typeNodeToPydantic(node, extracted, parentName, propName) {
2221
+ switch (node.kind) {
2222
+ case "primitive":
2223
+ switch (node.name) {
2224
+ case "string": return "str";
2225
+ case "number": return "float";
2226
+ case "boolean": return "bool";
2227
+ case "null": return "None";
2228
+ case "undefined": return "None";
2229
+ case "bigint": return "int";
2230
+ case "symbol": return "str";
2231
+ default: return "Any";
2232
+ }
2233
+ case "unknown":
2234
+ return "Any";
2235
+ case "array": {
2236
+ const inner = typeNodeToPydantic(node.element, extracted, parentName, propName);
2237
+ return `List[${inner}]`;
2238
+ }
2239
+ case "tuple": {
2240
+ const elements = node.elements.map((el, i) => typeNodeToPydantic(el, extracted, parentName, `el${i}`));
2241
+ return `Tuple[${elements.join(", ")}]`;
2242
+ }
2243
+ case "union": {
2244
+ const members = node.members.map((m) => typeNodeToPydantic(m, extracted, parentName, propName));
2245
+ if (members.length === 2 && members.includes("None")) {
2246
+ const nonNone = members.find((m) => m !== "None");
2247
+ return `Optional[${nonNone}]`;
2248
+ }
2249
+ return `Union[${members.join(", ")}]`;
2250
+ }
2251
+ case "map": {
2252
+ const v = typeNodeToPydantic(node.value, extracted, parentName, "value");
2253
+ return `Dict[str, ${v}]`;
2254
+ }
2255
+ case "set": {
2256
+ const inner = typeNodeToPydantic(node.element, extracted, parentName, propName);
2257
+ return `Set[${inner}]`;
2258
+ }
2259
+ case "promise":
2260
+ return typeNodeToPydantic(node.resolved, extracted, parentName, propName);
2261
+ case "function":
2262
+ return "Any";
2263
+ case "object": {
2264
+ const keys = Object.keys(node.properties);
2265
+ if (keys.length === 0)
2266
+ return "Dict[str, Any]";
2267
+ if (propName) {
2268
+ const className = toPascalCase(parentName) + toPascalCase(propName);
2269
+ if (!extracted.some((e) => e.name === className)) {
2270
+ extracted.push({ name: className, node });
2271
+ }
2272
+ return className;
2273
+ }
2274
+ return "Dict[str, Any]";
2275
+ }
2276
+ }
2277
+ }
2278
+ /**
2279
+ * Render a Pydantic BaseModel class from an object TypeNode.
2280
+ */
2281
+ function renderPydanticModel(name, node, extracted) {
2282
+ const keys = Object.keys(node.properties);
2283
+ const innerExtracted = [];
2284
+ const lines = [];
2285
+ const entries = keys.map((key) => {
2286
+ const pyType = typeNodeToPydantic(node.properties[key], innerExtracted, name, key);
2287
+ return ` ${toSnakeCase(key)}: ${pyType}`;
2288
+ });
2289
+ // Emit nested models first (they must be defined before use)
2290
+ for (const iface of innerExtracted) {
2291
+ lines.push(renderPydanticModel(iface.name, iface.node, innerExtracted));
2292
+ lines.push("");
2293
+ lines.push("");
2294
+ }
2295
+ lines.push(`class ${name}(BaseModel):`);
2296
+ if (entries.length === 0) {
2297
+ lines.push(" pass");
2298
+ }
2299
+ else {
2300
+ lines.push(...entries);
2301
+ }
2302
+ return lines.join("\n");
2303
+ }
2304
+ /**
2305
+ * Generate Pydantic BaseModel classes from observed runtime types.
2306
+ *
2307
+ * Unlike --python (TypedDict), Pydantic models provide:
2308
+ * - Runtime validation (model_validate)
2309
+ * - JSON serialization (model_dump_json)
2310
+ * - JSON Schema generation (model_json_schema)
2311
+ * - Direct use as FastAPI request/response models
2312
+ */
2313
+ function generatePydanticModels(functions) {
2314
+ const sections = [];
2315
+ sections.push("# Auto-generated Pydantic models by trickle");
2316
+ sections.push(`# Generated at ${new Date().toISOString()}`);
2317
+ sections.push("# Do not edit manually — re-run `trickle codegen --pydantic` to update");
2318
+ sections.push("");
2319
+ sections.push("from __future__ import annotations");
2320
+ sections.push("");
2321
+ sections.push("from typing import Any, Dict, List, Optional, Set, Tuple, Union");
2322
+ sections.push("");
2323
+ sections.push("from pydantic import BaseModel");
2324
+ sections.push("");
2325
+ sections.push("");
2326
+ for (const fn of functions) {
2327
+ const parsed = parseRouteName(fn.name);
2328
+ const baseName = parsed ? parsed.typeName : toPascalCase(fn.name);
2329
+ const metaParts = [];
2330
+ if (fn.module)
2331
+ metaParts.push(`${fn.module} module`);
2332
+ if (fn.env)
2333
+ metaParts.push(`observed in ${fn.env}`);
2334
+ if (fn.observedAt)
2335
+ metaParts.push(formatTimeAgo(fn.observedAt));
2336
+ if (metaParts.length > 0) {
2337
+ sections.push(`# ${baseName} — ${metaParts.join(", ")}`);
2338
+ }
2339
+ if (parsed) {
2340
+ // Route-style: generate Request (body) and Response models
2341
+ // Request body (only for methods with body)
2342
+ if (["POST", "PUT", "PATCH"].includes(parsed.method)) {
2343
+ let bodyNode;
2344
+ if (fn.argsType.kind === "object" && fn.argsType.properties["body"]) {
2345
+ bodyNode = fn.argsType.properties["body"];
2346
+ }
2347
+ if (bodyNode && bodyNode.kind === "object" && Object.keys(bodyNode.properties).length > 0) {
2348
+ const extracted = [];
2349
+ sections.push(renderPydanticModel(`${baseName}Request`, bodyNode, extracted));
2350
+ sections.push("");
2351
+ sections.push("");
2352
+ }
2353
+ }
2354
+ // Response model
2355
+ if (fn.returnType.kind === "object" && Object.keys(fn.returnType.properties).length > 0) {
2356
+ const extracted = [];
2357
+ sections.push(renderPydanticModel(`${baseName}Response`, fn.returnType, extracted));
2358
+ }
2359
+ else if (fn.returnType.kind !== "unknown") {
2360
+ const extracted = [];
2361
+ const pyType = typeNodeToPydantic(fn.returnType, extracted, baseName, undefined);
2362
+ sections.push(`${baseName}Response = ${pyType}`);
2363
+ }
2364
+ }
2365
+ else {
2366
+ // Non-route: generate Input/Output models
2367
+ if (fn.argsType.kind === "object" && Object.keys(fn.argsType.properties).length > 0) {
2368
+ const extracted = [];
2369
+ sections.push(renderPydanticModel(`${baseName}Input`, fn.argsType, extracted));
2370
+ sections.push("");
2371
+ sections.push("");
2372
+ }
2373
+ if (fn.returnType.kind === "object" && Object.keys(fn.returnType.properties).length > 0) {
2374
+ const extracted = [];
2375
+ sections.push(renderPydanticModel(`${baseName}Output`, fn.returnType, extracted));
2376
+ }
2377
+ else if (fn.returnType.kind !== "unknown") {
2378
+ const extracted = [];
2379
+ const pyType = typeNodeToPydantic(fn.returnType, extracted, baseName, undefined);
2380
+ sections.push(`${baseName}Output = ${pyType}`);
2381
+ }
2382
+ }
2383
+ sections.push("");
2384
+ sections.push("");
2385
+ }
2386
+ return sections.join("\n").trimEnd() + "\n";
2387
+ }
2388
+ // ── class-validator DTO Generation (NestJS) ──
2389
+ /**
2390
+ * Get the class-validator decorator for a TypeNode.
2391
+ * Returns the decorator string(s) and the TypeScript type.
2392
+ */
2393
+ function classValidatorField(node, propName, parentName, nestedClasses) {
2394
+ switch (node.kind) {
2395
+ case "primitive":
2396
+ switch (node.name) {
2397
+ case "string":
2398
+ return { decorators: ["@IsString()"], tsType: "string" };
2399
+ case "number":
2400
+ return { decorators: ["@IsNumber()"], tsType: "number" };
2401
+ case "boolean":
2402
+ return { decorators: ["@IsBoolean()"], tsType: "boolean" };
2403
+ default:
2404
+ return { decorators: [], tsType: "any" };
2405
+ }
2406
+ case "array": {
2407
+ const innerDecorators = ["@IsArray()"];
2408
+ if (node.element.kind === "object" && Object.keys(node.element.properties).length > 0) {
2409
+ const nestedName = parentName + toPascalCase(propName) + "Item";
2410
+ if (!nestedClasses.some((c) => c.name === nestedName)) {
2411
+ nestedClasses.push({ name: nestedName, node: node.element });
2412
+ }
2413
+ innerDecorators.push("@ValidateNested({ each: true })");
2414
+ innerDecorators.push(`@Type(() => ${nestedName})`);
2415
+ return { decorators: innerDecorators, tsType: `${nestedName}[]` };
2416
+ }
2417
+ const inner = classValidatorField(node.element, propName, parentName, nestedClasses);
2418
+ return { decorators: innerDecorators, tsType: `${inner.tsType}[]` };
2419
+ }
2420
+ case "object": {
2421
+ const keys = Object.keys(node.properties);
2422
+ if (keys.length === 0) {
2423
+ return { decorators: ["@IsObject()"], tsType: "Record<string, any>" };
2424
+ }
2425
+ const nestedName = parentName + toPascalCase(propName);
2426
+ if (!nestedClasses.some((c) => c.name === nestedName)) {
2427
+ nestedClasses.push({ name: nestedName, node });
2428
+ }
2429
+ return {
2430
+ decorators: ["@ValidateNested()", `@Type(() => ${nestedName})`],
2431
+ tsType: nestedName,
2432
+ };
2433
+ }
2434
+ case "union": {
2435
+ // Check for nullable (T | null)
2436
+ const nonNull = node.members.filter((m) => !(m.kind === "primitive" && (m.name === "null" || m.name === "undefined")));
2437
+ const isNullable = nonNull.length < node.members.length;
2438
+ if (nonNull.length === 1) {
2439
+ const inner = classValidatorField(nonNull[0], propName, parentName, nestedClasses);
2440
+ if (isNullable) {
2441
+ return {
2442
+ decorators: ["@IsOptional()", ...inner.decorators],
2443
+ tsType: `${inner.tsType} | null`,
2444
+ };
2445
+ }
2446
+ return inner;
2447
+ }
2448
+ // Complex union — use basic validation
2449
+ return { decorators: [], tsType: "any" };
2450
+ }
2451
+ case "unknown":
2452
+ return { decorators: [], tsType: "any" };
2453
+ default:
2454
+ return { decorators: [], tsType: "any" };
2455
+ }
2456
+ }
2457
+ /**
2458
+ * Render a class-validator DTO class.
2459
+ */
2460
+ function renderValidatorClass(name, node, nestedClasses) {
2461
+ const lines = [];
2462
+ const keys = Object.keys(node.properties);
2463
+ lines.push(`export class ${name} {`);
2464
+ for (let i = 0; i < keys.length; i++) {
2465
+ const key = keys[i];
2466
+ const propNode = node.properties[key];
2467
+ const { decorators, tsType } = classValidatorField(propNode, key, name, nestedClasses);
2468
+ for (const dec of decorators) {
2469
+ lines.push(` ${dec}`);
2470
+ }
2471
+ lines.push(` ${key}: ${tsType};`);
2472
+ if (i < keys.length - 1)
2473
+ lines.push("");
2474
+ }
2475
+ lines.push("}");
2476
+ return lines.join("\n");
2477
+ }
2478
+ /**
2479
+ * Generate class-validator DTO classes from observed runtime types.
2480
+ *
2481
+ * Output: NestJS-ready DTOs with class-validator decorators for
2482
+ * request validation and class-transformer for nested object support.
2483
+ */
2484
+ function generateClassValidatorDtos(functions) {
2485
+ const routes = [];
2486
+ for (const fn of functions) {
2487
+ const parsed = parseRouteName(fn.name);
2488
+ if (parsed) {
2489
+ routes.push({ parsed, fn });
2490
+ }
2491
+ }
2492
+ if (routes.length === 0) {
2493
+ return "// No API routes found. Instrument your Express/NestJS app to generate DTOs.\n";
2494
+ }
2495
+ const sections = [];
2496
+ sections.push("// Auto-generated class-validator DTOs by trickle");
2497
+ sections.push(`// Generated at ${new Date().toISOString()}`);
2498
+ sections.push("// Do not edit manually — re-run `trickle codegen --class-validator` to update");
2499
+ sections.push("");
2500
+ // Collect which decorators are needed
2501
+ const usedDecorators = new Set();
2502
+ const needsType = { value: false };
2503
+ // Pre-scan to collect all classes and nested classes
2504
+ const allClasses = [];
2505
+ const nestedClasses = [];
2506
+ for (const { parsed, fn } of routes) {
2507
+ // Request body DTO (POST/PUT/PATCH only)
2508
+ if (["POST", "PUT", "PATCH"].includes(parsed.method)) {
2509
+ let bodyNode;
2510
+ if (fn.argsType.kind === "object" && fn.argsType.properties["body"]) {
2511
+ bodyNode = fn.argsType.properties["body"];
2512
+ }
2513
+ if (bodyNode && bodyNode.kind === "object" && Object.keys(bodyNode.properties).length > 0) {
2514
+ allClasses.push({
2515
+ name: `${parsed.typeName}Body`,
2516
+ node: bodyNode,
2517
+ comment: `/** Request body for ${parsed.method} ${parsed.path} */`,
2518
+ });
2519
+ }
2520
+ }
2521
+ // Response DTO
2522
+ if (fn.returnType.kind === "object" && Object.keys(fn.returnType.properties).length > 0) {
2523
+ allClasses.push({
2524
+ name: `${parsed.typeName}Response`,
2525
+ node: fn.returnType,
2526
+ comment: `/** Response for ${parsed.method} ${parsed.path} */`,
2527
+ });
2528
+ }
2529
+ }
2530
+ // Render all classes, collecting nested ones as we go
2531
+ const renderedClasses = [];
2532
+ const rendered = new Set();
2533
+ for (const cls of allClasses) {
2534
+ if (rendered.has(cls.name))
2535
+ continue;
2536
+ rendered.add(cls.name);
2537
+ const code = renderValidatorClass(cls.name, cls.node, nestedClasses);
2538
+ renderedClasses.push({ name: cls.name, code, comment: cls.comment });
2539
+ }
2540
+ // Render nested classes (may add more to nestedClasses as we process)
2541
+ let cursor = 0;
2542
+ while (cursor < nestedClasses.length) {
2543
+ const nested = nestedClasses[cursor];
2544
+ cursor++;
2545
+ if (rendered.has(nested.name))
2546
+ continue;
2547
+ rendered.add(nested.name);
2548
+ const code = renderValidatorClass(nested.name, nested.node, nestedClasses);
2549
+ renderedClasses.unshift({ name: nested.name, code, comment: "" });
2550
+ }
2551
+ // Scan for used decorators
2552
+ const allCode = renderedClasses.map((c) => c.code).join("\n");
2553
+ const decoratorNames = ["IsString", "IsNumber", "IsBoolean", "IsArray", "IsObject", "IsOptional", "ValidateNested", "IsNotEmpty"];
2554
+ for (const name of decoratorNames) {
2555
+ if (allCode.includes(`@${name}`))
2556
+ usedDecorators.add(name);
2557
+ }
2558
+ if (allCode.includes("@Type("))
2559
+ needsType.value = true;
2560
+ // Emit imports
2561
+ if (usedDecorators.size > 0) {
2562
+ const sorted = Array.from(usedDecorators).sort();
2563
+ sections.push(`import { ${sorted.join(", ")} } from "class-validator";`);
2564
+ }
2565
+ if (needsType.value) {
2566
+ sections.push('import { Type } from "class-transformer";');
2567
+ }
2568
+ sections.push("");
2569
+ // Emit classes
2570
+ for (const cls of renderedClasses) {
2571
+ if (cls.comment) {
2572
+ sections.push(cls.comment);
2573
+ }
2574
+ sections.push(cls.code);
2575
+ sections.push("");
2576
+ }
2577
+ return sections.join("\n").trimEnd() + "\n";
2578
+ }
2579
+ // ── GraphQL SDL Generation ──
2580
+ /**
2581
+ * Convert a TypeNode to a GraphQL type string, accumulating named types.
2582
+ */
2583
+ function typeNodeToGraphQL(node, namedTypes, parentName, propName) {
2584
+ switch (node.kind) {
2585
+ case "primitive":
2586
+ switch (node.name) {
2587
+ case "string": return "String";
2588
+ case "number": return "Float";
2589
+ case "boolean": return "Boolean";
2590
+ case "bigint": return "String";
2591
+ case "null": return "String";
2592
+ case "undefined": return "String";
2593
+ case "symbol": return "String";
2594
+ default: return "String";
2595
+ }
2596
+ case "array": {
2597
+ const elementType = typeNodeToGraphQL(node.element, namedTypes, parentName, propName);
2598
+ return `[${elementType}]`;
2599
+ }
2600
+ case "object": {
2601
+ const typeName = propName
2602
+ ? parentName + toPascalCase(propName)
2603
+ : parentName;
2604
+ const fields = [];
2605
+ for (const [key, val] of Object.entries(node.properties)) {
2606
+ const fieldType = typeNodeToGraphQL(val, namedTypes, typeName, key);
2607
+ fields.push(` ${key}: ${fieldType}`);
2608
+ }
2609
+ if (fields.length === 0) {
2610
+ return "String";
2611
+ }
2612
+ const body = `type ${typeName} {\n${fields.join("\n")}\n}`;
2613
+ namedTypes.set(typeName, body);
2614
+ return typeName;
2615
+ }
2616
+ case "union": {
2617
+ // GraphQL unions only work for object types; for primitives, pick the first non-null
2618
+ const nonNull = node.members.filter((m) => !(m.kind === "primitive" && (m.name === "null" || m.name === "undefined")));
2619
+ if (nonNull.length === 0)
2620
+ return "String";
2621
+ return typeNodeToGraphQL(nonNull[0], namedTypes, parentName, propName);
2622
+ }
2623
+ case "promise":
2624
+ return typeNodeToGraphQL(node.resolved, namedTypes, parentName, propName);
2625
+ case "map":
2626
+ return "String"; // JSON scalar
2627
+ case "set": {
2628
+ const elementType = typeNodeToGraphQL(node.element, namedTypes, parentName, propName);
2629
+ return `[${elementType}]`;
2630
+ }
2631
+ case "tuple": {
2632
+ if (node.elements.length === 0)
2633
+ return "[String]";
2634
+ const elementType = typeNodeToGraphQL(node.elements[0], namedTypes, parentName, propName);
2635
+ return `[${elementType}]`;
2636
+ }
2637
+ case "function":
2638
+ case "unknown":
2639
+ default:
2640
+ return "String";
2641
+ }
2642
+ }
2643
+ /**
2644
+ * Generate a GraphQL SDL schema from runtime-observed API routes.
2645
+ *
2646
+ * Converts REST routes into Query (GET) and Mutation (POST/PUT/PATCH/DELETE)
2647
+ * fields with properly typed inputs and outputs.
2648
+ */
2649
+ function generateGraphqlSchema(functions) {
2650
+ const namedTypes = new Map();
2651
+ const queryFields = [];
2652
+ const mutationFields = [];
2653
+ const inputTypes = [];
2654
+ for (const fn of functions) {
2655
+ const route = parseRouteName(fn.name);
2656
+ if (!route)
2657
+ continue;
2658
+ const typeName = route.typeName;
2659
+ const fieldName = toCamelCase(fn.name);
2660
+ // Generate return type
2661
+ const returnTypeName = typeName + "Response";
2662
+ typeNodeToGraphQL(fn.returnType, namedTypes, returnTypeName);
2663
+ // Build field arguments from argsType
2664
+ const args = [];
2665
+ if (fn.argsType.kind === "object") {
2666
+ const props = fn.argsType.properties;
2667
+ // Path params
2668
+ for (const param of route.pathParams) {
2669
+ args.push(`${param}: String!`);
2670
+ }
2671
+ // Query params
2672
+ if (props.query && props.query.kind === "object") {
2673
+ for (const [key, val] of Object.entries(props.query.properties)) {
2674
+ const gqlType = typeNodeToGraphQL(val, namedTypes, typeName, key);
2675
+ args.push(`${key}: ${gqlType}`);
2676
+ }
2677
+ }
2678
+ // Body → input type for mutations
2679
+ if (props.body && props.body.kind === "object") {
2680
+ const inputName = typeName + "Input";
2681
+ const inputFields = [];
2682
+ for (const [key, val] of Object.entries(props.body.properties)) {
2683
+ const fieldType = typeNodeToGraphQLInput(val, namedTypes, inputName, key);
2684
+ inputFields.push(` ${key}: ${fieldType}`);
2685
+ }
2686
+ if (inputFields.length > 0) {
2687
+ inputTypes.push(`input ${inputName} {\n${inputFields.join("\n")}\n}`);
2688
+ args.push(`input: ${inputName}!`);
2689
+ }
2690
+ }
2691
+ }
2692
+ const argsStr = args.length > 0 ? `(${args.join(", ")})` : "";
2693
+ // Determine the return type name — use the named type if it was created
2694
+ const resolvedReturnType = namedTypes.has(returnTypeName) ? returnTypeName : typeNodeToGraphQL(fn.returnType, namedTypes, returnTypeName);
2695
+ if (route.method === "GET") {
2696
+ queryFields.push(` ${fieldName}${argsStr}: ${resolvedReturnType}`);
2697
+ }
2698
+ else {
2699
+ mutationFields.push(` ${fieldName}${argsStr}: ${resolvedReturnType}`);
2700
+ }
2701
+ }
2702
+ if (queryFields.length === 0 && mutationFields.length === 0) {
2703
+ return "# No API routes found.\n";
2704
+ }
2705
+ const sections = [];
2706
+ sections.push("# Auto-generated GraphQL schema from runtime-observed types");
2707
+ sections.push("# Generated by trickle — https://github.com/yiheinchai/trickle");
2708
+ sections.push("");
2709
+ // Emit named types (skip duplicates, emit in order)
2710
+ const emittedTypes = new Set();
2711
+ for (const [name, body] of namedTypes) {
2712
+ if (!emittedTypes.has(name)) {
2713
+ emittedTypes.add(name);
2714
+ sections.push(body);
2715
+ sections.push("");
2716
+ }
2717
+ }
2718
+ // Emit input types
2719
+ for (const input of inputTypes) {
2720
+ sections.push(input);
2721
+ sections.push("");
2722
+ }
2723
+ // Emit Query
2724
+ if (queryFields.length > 0) {
2725
+ sections.push(`type Query {`);
2726
+ for (const f of queryFields) {
2727
+ sections.push(f);
2728
+ }
2729
+ sections.push("}");
2730
+ sections.push("");
2731
+ }
2732
+ // Emit Mutation
2733
+ if (mutationFields.length > 0) {
2734
+ sections.push(`type Mutation {`);
2735
+ for (const f of mutationFields) {
2736
+ sections.push(f);
2737
+ }
2738
+ sections.push("}");
2739
+ sections.push("");
2740
+ }
2741
+ return sections.join("\n").trimEnd() + "\n";
2742
+ }
2743
+ /**
2744
+ * Convert TypeNode to GraphQL input type string (uses `input` instead of `type` for nested objects).
2745
+ */
2746
+ function typeNodeToGraphQLInput(node, namedTypes, parentName, propName) {
2747
+ switch (node.kind) {
2748
+ case "primitive":
2749
+ switch (node.name) {
2750
+ case "string": return "String";
2751
+ case "number": return "Float";
2752
+ case "boolean": return "Boolean";
2753
+ default: return "String";
2754
+ }
2755
+ case "array": {
2756
+ const elementType = typeNodeToGraphQLInput(node.element, namedTypes, parentName, propName);
2757
+ return `[${elementType}]`;
2758
+ }
2759
+ case "object": {
2760
+ const typeName = propName
2761
+ ? parentName + toPascalCase(propName)
2762
+ : parentName;
2763
+ // Don't emit as named type — just inline or skip
2764
+ // For nested input objects, the parent handles them
2765
+ return "String"; // Flatten deep nested inputs to JSON string
2766
+ }
2767
+ case "union": {
2768
+ const nonNull = node.members.filter((m) => !(m.kind === "primitive" && (m.name === "null" || m.name === "undefined")));
2769
+ if (nonNull.length === 0)
2770
+ return "String";
2771
+ return typeNodeToGraphQLInput(nonNull[0], namedTypes, parentName, propName);
2772
+ }
2773
+ default:
2774
+ return "String";
2775
+ }
2776
+ }
2777
+ // ── tRPC Router Generation ──
2778
+ /**
2779
+ * Generate a fully-typed tRPC router from runtime-observed API routes.
2780
+ *
2781
+ * - GET routes → t.procedure.query()
2782
+ * - POST/PUT/PATCH/DELETE routes → t.procedure.input(zodSchema).mutation()
2783
+ * - Includes Zod input schemas for request body validation
2784
+ * - Includes TypeScript return type annotations from observed responses
2785
+ * - Exports AppRouter type for client-side type inference
2786
+ */
2787
+ function generateTrpcRouter(functions) {
2788
+ if (functions.length === 0) {
2789
+ return "// No API routes found. Instrument your app to generate a tRPC router.\n";
2790
+ }
2791
+ const sections = [];
2792
+ sections.push("// Auto-generated tRPC router by trickle");
2793
+ sections.push(`// Generated at ${new Date().toISOString()}`);
2794
+ sections.push("// Do not edit manually — re-run `trickle codegen --trpc` to update");
2795
+ sections.push("");
2796
+ sections.push('import { initTRPC } from "@trpc/server";');
2797
+ sections.push('import { z } from "zod";');
2798
+ sections.push("");
2799
+ sections.push("const t = initTRPC.create();");
2800
+ sections.push("");
2801
+ // Collect route procedures
2802
+ const inputSchemas = [];
2803
+ const procedures = [];
2804
+ const returnTypes = [];
2805
+ for (const fn of functions) {
2806
+ const route = parseRouteName(fn.name);
2807
+ if (!route)
2808
+ continue;
2809
+ const procedureName = toCamelCase(route.typeName);
2810
+ const typeName = route.typeName;
2811
+ // Generate return type interface
2812
+ const extracted = [];
2813
+ const returnTypeStr = typeNodeToTS(fn.returnType, extracted, typeName + "Response", undefined, 0);
2814
+ returnTypes.push(`export interface ${typeName}Response ${returnTypeStr.startsWith("{") ? returnTypeStr : `{ data: ${returnTypeStr} }`}`);
2815
+ // Also include any extracted nested interfaces
2816
+ for (const ext of extracted) {
2817
+ returnTypes.push(renderInterface(ext.name, ext.node, []));
2818
+ }
2819
+ // Check for body input (POST/PUT/PATCH/DELETE)
2820
+ let hasInput = false;
2821
+ let bodyNode;
2822
+ if (["POST", "PUT", "PATCH", "DELETE"].includes(route.method)) {
2823
+ if (fn.argsType.kind === "object" && fn.argsType.properties["body"]) {
2824
+ bodyNode = fn.argsType.properties["body"];
2825
+ }
2826
+ if (bodyNode && bodyNode.kind === "object" && Object.keys(bodyNode.properties).length > 0) {
2827
+ hasInput = true;
2828
+ const schemaName = `${procedureName}Input`;
2829
+ inputSchemas.push(`const ${schemaName} = ${typeNodeToZod(bodyNode, 0)};`);
2830
+ }
2831
+ }
2832
+ // Check for query params input (GET)
2833
+ let hasQueryInput = false;
2834
+ let queryNode;
2835
+ if (fn.argsType.kind === "object" && fn.argsType.properties["query"]) {
2836
+ queryNode = fn.argsType.properties["query"];
2837
+ if (queryNode.kind === "object" && Object.keys(queryNode.properties).length > 0) {
2838
+ hasQueryInput = true;
2839
+ const schemaName = `${procedureName}Input`;
2840
+ if (!hasInput) {
2841
+ inputSchemas.push(`const ${schemaName} = ${typeNodeToZod(queryNode, 0)};`);
2842
+ }
2843
+ }
2844
+ }
2845
+ // Check for path params
2846
+ const hasPathParams = route.pathParams.length > 0;
2847
+ let pathParamSchema = "";
2848
+ if (hasPathParams && !hasInput && !hasQueryInput) {
2849
+ const pathProps = {};
2850
+ for (const param of route.pathParams) {
2851
+ pathProps[param] = { kind: "primitive", name: "string" };
2852
+ }
2853
+ const pathNode = { kind: "object", properties: pathProps };
2854
+ const schemaName = `${procedureName}Input`;
2855
+ inputSchemas.push(`const ${schemaName} = ${typeNodeToZod(pathNode, 0)};`);
2856
+ pathParamSchema = schemaName;
2857
+ }
2858
+ const inputName = `${procedureName}Input`;
2859
+ const hasAnyInput = hasInput || hasQueryInput || (hasPathParams && pathParamSchema);
2860
+ // Build procedure
2861
+ const isQuery = route.method === "GET";
2862
+ let proc = ` ${procedureName}: t.procedure`;
2863
+ if (hasAnyInput) {
2864
+ proc += `\n .input(${inputName})`;
2865
+ }
2866
+ if (isQuery) {
2867
+ proc += `\n .query(async (${hasAnyInput ? "{ input }" : ""}) => {`;
2868
+ proc += `\n // ${route.method} ${route.path}`;
2869
+ proc += `\n // Return type: ${typeName}Response`;
2870
+ proc += `\n throw new Error("Not implemented — replace with your logic");`;
2871
+ proc += `\n })`;
2872
+ }
2873
+ else {
2874
+ proc += `\n .mutation(async (${hasAnyInput ? "{ input }" : ""}) => {`;
2875
+ proc += `\n // ${route.method} ${route.path}`;
2876
+ proc += `\n // Return type: ${typeName}Response`;
2877
+ proc += `\n throw new Error("Not implemented — replace with your logic");`;
2878
+ proc += `\n })`;
2879
+ }
2880
+ procedures.push(proc);
2881
+ }
2882
+ if (procedures.length === 0) {
2883
+ return "// No API routes found. Instrument your app to generate a tRPC router.\n";
2884
+ }
2885
+ // Emit return type interfaces
2886
+ sections.push("// ── Response types ──");
2887
+ sections.push("");
2888
+ for (const rt of returnTypes) {
2889
+ sections.push(rt);
2890
+ sections.push("");
2891
+ }
2892
+ // Emit input schemas
2893
+ if (inputSchemas.length > 0) {
2894
+ sections.push("// ── Input validation schemas ──");
2895
+ sections.push("");
2896
+ for (const schema of inputSchemas) {
2897
+ sections.push(schema);
2898
+ sections.push("");
2899
+ }
2900
+ }
2901
+ // Emit router
2902
+ sections.push("// ── Router ──");
2903
+ sections.push("");
2904
+ sections.push("export const appRouter = t.router({");
2905
+ sections.push(procedures.join(",\n\n"));
2906
+ sections.push("});");
2907
+ sections.push("");
2908
+ sections.push("export type AppRouter = typeof appRouter;");
2909
+ return sections.join("\n").trimEnd() + "\n";
2910
+ }
2911
+ // ── Axios Client Generation ──
2912
+ /**
2913
+ * Generate a typed Axios client from runtime-observed API routes.
2914
+ *
2915
+ * For each route:
2916
+ * - Request/response interfaces
2917
+ * - Typed function using axios.get/post/put/patch/delete
2918
+ * - Path parameter interpolation
2919
+ * - Query parameter support
2920
+ * - Configurable base URL and axios instance
2921
+ */
2922
+ function generateAxiosClient(functions) {
2923
+ const routes = [];
2924
+ for (const fn of functions) {
2925
+ const parsed = parseRouteName(fn.name);
2926
+ if (parsed) {
2927
+ routes.push({ parsed, fn });
2928
+ }
2929
+ }
2930
+ if (routes.length === 0) {
2931
+ return "// No API routes found. Instrument your app to generate a typed Axios client.\n";
2932
+ }
2933
+ const sections = [];
2934
+ sections.push("// Auto-generated typed Axios client by trickle");
2935
+ sections.push(`// Generated at ${new Date().toISOString()}`);
2936
+ sections.push("// Do not edit manually — re-run `trickle codegen --axios` to update");
2937
+ sections.push("");
2938
+ sections.push('import axios, { AxiosInstance, AxiosRequestConfig } from "axios";');
2939
+ sections.push("");
2940
+ // Generate interfaces
2941
+ const extracted = [];
2942
+ const interfaceSections = [];
2943
+ for (const { parsed, fn } of routes) {
2944
+ const baseName = parsed.typeName;
2945
+ // Body input type (POST/PUT/PATCH)
2946
+ if (["POST", "PUT", "PATCH"].includes(parsed.method)) {
2947
+ if (fn.argsType.kind === "object") {
2948
+ const bodyNode = fn.argsType.properties["body"];
2949
+ if (bodyNode && bodyNode.kind === "object" && Object.keys(bodyNode.properties).length > 0) {
2950
+ interfaceSections.push(renderInterface(`${baseName}Body`, bodyNode, extracted));
2951
+ interfaceSections.push("");
2952
+ }
2953
+ }
2954
+ }
2955
+ // Query params type
2956
+ if (fn.argsType.kind === "object" && fn.argsType.properties["query"]) {
2957
+ const queryNode = fn.argsType.properties["query"];
2958
+ if (queryNode.kind === "object" && Object.keys(queryNode.properties).length > 0) {
2959
+ interfaceSections.push(renderInterface(`${baseName}Query`, queryNode, extracted));
2960
+ interfaceSections.push("");
2961
+ }
2962
+ }
2963
+ // Response type
2964
+ if (fn.returnType.kind === "object" && Object.keys(fn.returnType.properties).length > 0) {
2965
+ interfaceSections.push(renderInterface(`${baseName}Response`, fn.returnType, extracted));
2966
+ interfaceSections.push("");
2967
+ }
2968
+ else {
2969
+ const retStr = typeNodeToTS(fn.returnType, extracted, baseName, undefined, 0);
2970
+ interfaceSections.push(`export type ${baseName}Response = ${retStr};`);
2971
+ interfaceSections.push("");
2972
+ }
2973
+ }
2974
+ // Emit extracted sub-interfaces first
2975
+ const emitted = new Set();
2976
+ let cursor = 0;
2977
+ while (cursor < extracted.length) {
2978
+ const iface = extracted[cursor];
2979
+ cursor++;
2980
+ if (emitted.has(iface.name))
2981
+ continue;
2982
+ emitted.add(iface.name);
2983
+ sections.push(renderInterface(iface.name, iface.node, extracted));
2984
+ sections.push("");
2985
+ }
2986
+ // Emit main interfaces
2987
+ sections.push(...interfaceSections);
2988
+ // Client setup
2989
+ sections.push("// ── Axios Client ──");
2990
+ sections.push("");
2991
+ sections.push("let _instance: AxiosInstance = axios.create();");
2992
+ sections.push("");
2993
+ sections.push("/**");
2994
+ sections.push(" * Configure the Axios client instance.");
2995
+ sections.push(" * Call this once at app startup with your base URL.");
2996
+ sections.push(" */");
2997
+ sections.push('export function configureAxiosClient(baseURL: string, instance?: AxiosInstance): void {');
2998
+ sections.push(" if (instance) {");
2999
+ sections.push(" _instance = instance;");
3000
+ sections.push(" } else {");
3001
+ sections.push(" _instance = axios.create({ baseURL });");
3002
+ sections.push(" }");
3003
+ sections.push("}");
3004
+ sections.push("");
3005
+ // Generate typed functions
3006
+ sections.push("// ── Typed API Functions ──");
3007
+ sections.push("");
3008
+ for (const { parsed, fn } of routes) {
3009
+ const baseName = parsed.typeName;
3010
+ const funcName = toCamelCase(baseName);
3011
+ const responseName = `${baseName}Response`;
3012
+ // Determine if we have body, query, path params
3013
+ const hasBody = ["POST", "PUT", "PATCH"].includes(parsed.method)
3014
+ && fn.argsType.kind === "object"
3015
+ && fn.argsType.properties["body"]
3016
+ && fn.argsType.properties["body"].kind === "object"
3017
+ && Object.keys(fn.argsType.properties["body"].properties).length > 0;
3018
+ const hasQuery = fn.argsType.kind === "object"
3019
+ && fn.argsType.properties["query"]
3020
+ && fn.argsType.properties["query"].kind === "object"
3021
+ && Object.keys(fn.argsType.properties["query"].properties).length > 0;
3022
+ const hasPathParams = parsed.pathParams.length > 0;
3023
+ // Build function parameters
3024
+ const params = [];
3025
+ if (hasPathParams) {
3026
+ for (const p of parsed.pathParams) {
3027
+ params.push(`${p}: string`);
3028
+ }
3029
+ }
3030
+ if (hasBody) {
3031
+ params.push(`body: ${baseName}Body`);
3032
+ }
3033
+ if (hasQuery) {
3034
+ params.push(`query?: ${baseName}Query`);
3035
+ }
3036
+ params.push("config?: AxiosRequestConfig");
3037
+ // Build URL path with interpolation
3038
+ let urlPath = parsed.path;
3039
+ for (const p of parsed.pathParams) {
3040
+ urlPath = urlPath.replace(`:${p}`, `\${${p}}`);
3041
+ }
3042
+ const urlExpr = hasPathParams ? `\`${urlPath}\`` : `"${parsed.path}"`;
3043
+ // Build function body
3044
+ const method = parsed.method.toLowerCase();
3045
+ sections.push(`/** ${parsed.method} ${parsed.path} */`);
3046
+ sections.push(`export async function ${funcName}(${params.join(", ")}): Promise<${responseName}> {`);
3047
+ if (hasQuery) {
3048
+ sections.push(" const requestConfig: AxiosRequestConfig = { ...config, params: query };");
3049
+ }
3050
+ const configArg = hasQuery ? "requestConfig" : "config";
3051
+ if (hasBody) {
3052
+ sections.push(` const { data } = await _instance.${method}<${responseName}>(${urlExpr}, body, ${configArg});`);
3053
+ }
3054
+ else if (method === "delete") {
3055
+ sections.push(` const { data } = await _instance.${method}<${responseName}>(${urlExpr}, ${configArg});`);
3056
+ }
3057
+ else {
3058
+ sections.push(` const { data } = await _instance.${method}<${responseName}>(${urlExpr}, ${configArg});`);
3059
+ }
3060
+ sections.push(" return data;");
3061
+ sections.push("}");
3062
+ sections.push("");
3063
+ }
3064
+ return sections.join("\n").trimEnd() + "\n";
3065
+ }
3066
+ // ── Inline annotation generation ──
3067
+ /**
3068
+ * Generate compact inline type annotations for annotating source code.
3069
+ * Unlike generateFunctionTypes which emits full interfaces, this produces
3070
+ * minimal inline annotations suitable for inserting into source files.
3071
+ */
3072
+ function generateInlineAnnotations(functions, language = "typescript") {
3073
+ const result = {};
3074
+ for (const fn of functions) {
3075
+ const extracted = [];
3076
+ const pyExtracted = [];
3077
+ // Build param types
3078
+ const params = [];
3079
+ if (fn.argsType.kind === "tuple") {
3080
+ for (let i = 0; i < fn.argsType.elements.length; i++) {
3081
+ const el = fn.argsType.elements[i];
3082
+ const typeStr = language === "python"
3083
+ ? typeNodeToPythonInline(el)
3084
+ : typeNodeToTSInline(el);
3085
+ params.push({ name: `arg${i}`, type: typeStr });
3086
+ }
3087
+ }
3088
+ else if (fn.argsType.kind === "object") {
3089
+ for (const key of Object.keys(fn.argsType.properties)) {
3090
+ const typeStr = language === "python"
3091
+ ? typeNodeToPythonInline(fn.argsType.properties[key])
3092
+ : typeNodeToTSInline(fn.argsType.properties[key]);
3093
+ params.push({ name: key, type: typeStr });
3094
+ }
3095
+ }
3096
+ // Build return type
3097
+ const returnTypeStr = language === "python"
3098
+ ? typeNodeToPythonInline(fn.returnType)
3099
+ : typeNodeToTSInline(fn.returnType);
3100
+ result[fn.name] = { params, returnType: returnTypeStr };
3101
+ }
3102
+ return result;
3103
+ }
3104
+ /**
3105
+ * Convert a TypeNode to an inline TypeScript type string (no extraction, no interfaces).
3106
+ */
3107
+ function typeNodeToTSInline(node, depth = 0) {
3108
+ if (depth > 5)
3109
+ return "any";
3110
+ switch (node.kind) {
3111
+ case "primitive":
3112
+ return node.name;
3113
+ case "unknown":
3114
+ return "unknown";
3115
+ case "array": {
3116
+ const inner = typeNodeToTSInline(node.element, depth + 1);
3117
+ return node.element.kind === "union" || node.element.kind === "function"
3118
+ ? `Array<${inner}>`
3119
+ : `${inner}[]`;
3120
+ }
3121
+ case "tuple": {
3122
+ const elements = node.elements.map(el => typeNodeToTSInline(el, depth + 1));
3123
+ return `[${elements.join(", ")}]`;
3124
+ }
3125
+ case "union": {
3126
+ const members = node.members.map(m => typeNodeToTSInline(m, depth + 1));
3127
+ return members.join(" | ");
3128
+ }
3129
+ case "map": {
3130
+ const k = typeNodeToTSInline(node.key, depth + 1);
3131
+ const v = typeNodeToTSInline(node.value, depth + 1);
3132
+ return `Map<${k}, ${v}>`;
3133
+ }
3134
+ case "set":
3135
+ return `Set<${typeNodeToTSInline(node.element, depth + 1)}>`;
3136
+ case "promise":
3137
+ return `Promise<${typeNodeToTSInline(node.resolved, depth + 1)}>`;
3138
+ case "function": {
3139
+ const params = node.params.map((p, i) => `arg${i}: ${typeNodeToTSInline(p, depth + 1)}`);
3140
+ return `(${params.join(", ")}) => ${typeNodeToTSInline(node.returnType, depth + 1)}`;
3141
+ }
3142
+ case "object": {
3143
+ const keys = Object.keys(node.properties);
3144
+ if (keys.length === 0)
3145
+ return "Record<string, never>";
3146
+ const entries = keys.map(key => `${key}: ${typeNodeToTSInline(node.properties[key], depth + 1)}`);
3147
+ return `{ ${entries.join("; ")} }`;
3148
+ }
3149
+ }
3150
+ }
3151
+ /**
3152
+ * Convert a TypeNode to an inline Python type string.
3153
+ */
3154
+ function typeNodeToPythonInline(node, depth = 0) {
3155
+ if (depth > 5)
3156
+ return "Any";
3157
+ switch (node.kind) {
3158
+ case "primitive":
3159
+ switch (node.name) {
3160
+ case "string": return "str";
3161
+ case "number": return "float";
3162
+ case "boolean": return "bool";
3163
+ case "null": return "None";
3164
+ case "undefined": return "None";
3165
+ case "bigint": return "int";
3166
+ case "symbol": return "str";
3167
+ default: return "Any";
3168
+ }
3169
+ case "unknown":
3170
+ return "Any";
3171
+ case "array":
3172
+ return `list[${typeNodeToPythonInline(node.element, depth + 1)}]`;
3173
+ case "tuple": {
3174
+ const elements = node.elements.map(el => typeNodeToPythonInline(el, depth + 1));
3175
+ return `tuple[${elements.join(", ")}]`;
3176
+ }
3177
+ case "union": {
3178
+ const members = node.members.map(m => typeNodeToPythonInline(m, depth + 1));
3179
+ if (members.length === 2 && members.includes("None")) {
3180
+ const nonNone = members.find(m => m !== "None");
3181
+ return `${nonNone} | None`;
3182
+ }
3183
+ return members.join(" | ");
3184
+ }
3185
+ case "map": {
3186
+ const k = typeNodeToPythonInline(node.key, depth + 1);
3187
+ const v = typeNodeToPythonInline(node.value, depth + 1);
3188
+ return `dict[${k}, ${v}]`;
3189
+ }
3190
+ case "set":
3191
+ return `set[${typeNodeToPythonInline(node.element, depth + 1)}]`;
3192
+ case "promise":
3193
+ return typeNodeToPythonInline(node.resolved, depth + 1);
3194
+ case "function": {
3195
+ const params = node.params.map(p => typeNodeToPythonInline(p, depth + 1));
3196
+ const ret = typeNodeToPythonInline(node.returnType, depth + 1);
3197
+ return `Callable[[${params.join(", ")}], ${ret}]`;
3198
+ }
3199
+ case "object": {
3200
+ const keys = Object.keys(node.properties);
3201
+ if (keys.length === 0)
3202
+ return "dict[str, Any]";
3203
+ const entries = keys.map(key => `"${key}": ${typeNodeToPythonInline(node.properties[key], depth + 1)}`);
3204
+ return `TypedDict("_", {${entries.join(", ")}})`;
3205
+ }
3206
+ }
3207
+ }