@donkeylabs/server 0.3.0 → 0.4.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 (49) hide show
  1. package/LICENSE +1 -1
  2. package/docs/api-client.md +7 -7
  3. package/docs/cache.md +1 -74
  4. package/docs/core-services.md +4 -116
  5. package/docs/cron.md +1 -1
  6. package/docs/errors.md +2 -2
  7. package/docs/events.md +3 -98
  8. package/docs/handlers.md +13 -48
  9. package/docs/logger.md +3 -58
  10. package/docs/middleware.md +2 -2
  11. package/docs/plugins.md +13 -64
  12. package/docs/project-structure.md +4 -142
  13. package/docs/rate-limiter.md +4 -136
  14. package/docs/router.md +6 -14
  15. package/docs/sse.md +1 -99
  16. package/docs/sveltekit-adapter.md +420 -0
  17. package/package.json +8 -11
  18. package/registry.d.ts +15 -14
  19. package/src/core/cache.ts +0 -75
  20. package/src/core/cron.ts +3 -96
  21. package/src/core/errors.ts +78 -11
  22. package/src/core/events.ts +1 -47
  23. package/src/core/index.ts +0 -4
  24. package/src/core/jobs.ts +0 -112
  25. package/src/core/logger.ts +12 -79
  26. package/src/core/rate-limiter.ts +29 -108
  27. package/src/core/sse.ts +1 -84
  28. package/src/core.ts +13 -104
  29. package/src/generator/index.ts +566 -0
  30. package/src/generator/zod-to-ts.ts +114 -0
  31. package/src/handlers.ts +14 -110
  32. package/src/index.ts +30 -24
  33. package/src/middleware.ts +2 -5
  34. package/src/registry.ts +4 -0
  35. package/src/router.ts +47 -1
  36. package/src/server.ts +618 -332
  37. package/README.md +0 -254
  38. package/cli/commands/dev.ts +0 -134
  39. package/cli/commands/generate.ts +0 -605
  40. package/cli/commands/init.ts +0 -205
  41. package/cli/commands/interactive.ts +0 -417
  42. package/cli/commands/plugin.ts +0 -192
  43. package/cli/commands/route.ts +0 -195
  44. package/cli/donkeylabs +0 -2
  45. package/cli/index.ts +0 -114
  46. package/docs/svelte-frontend.md +0 -324
  47. package/docs/testing.md +0 -438
  48. package/mcp/donkeylabs-mcp +0 -3238
  49. package/mcp/server.ts +0 -3238
@@ -0,0 +1,566 @@
1
+ /**
2
+ * Generator Building Blocks
3
+ *
4
+ * This module exports reusable functions for generating API clients.
5
+ * Adapters (like @donkeylabs/adapter-sveltekit) can import these functions
6
+ * and compose them with custom options to generate framework-specific clients.
7
+ */
8
+
9
+ // ==========================================
10
+ // Types
11
+ // ==========================================
12
+
13
+ export interface RouteInfo {
14
+ name: string;
15
+ prefix: string;
16
+ routeName: string;
17
+ handler: "typed" | "raw" | string;
18
+ inputSource?: string;
19
+ outputSource?: string;
20
+ }
21
+
22
+ export interface EventInfo {
23
+ name: string;
24
+ plugin: string;
25
+ schemaSource: string;
26
+ }
27
+
28
+ export interface ClientConfigInfo {
29
+ plugin: string;
30
+ credentials?: "include" | "same-origin" | "omit";
31
+ }
32
+
33
+ export interface GeneratorConfig {
34
+ routes: RouteInfo[] | ExtractedRoute[];
35
+ events?: EventInfo[];
36
+ clientConfigs?: ClientConfigInfo[];
37
+ }
38
+
39
+ export interface ExtractedRoute {
40
+ name: string;
41
+ handler: string;
42
+ }
43
+
44
+ export interface ClientGeneratorOptions {
45
+ /** Import statement for base class */
46
+ baseImport: string;
47
+ /** Base class name to extend */
48
+ baseClass: string;
49
+ /** Constructor parameters signature */
50
+ constructorSignature: string;
51
+ /** Constructor body implementation */
52
+ constructorBody: string;
53
+ /** Additional imports to include */
54
+ additionalImports?: string[];
55
+ /** Custom factory function (replaces default) */
56
+ factoryFunction?: string;
57
+ /** Additional class members to include */
58
+ additionalMembers?: string[];
59
+ }
60
+
61
+ /** Default options for standard HTTP-only client */
62
+ export const defaultGeneratorOptions: ClientGeneratorOptions = {
63
+ baseImport: 'import { ApiClientBase, type ApiClientOptions } from "@donkeylabs/server/client";',
64
+ baseClass: "ApiClientBase<{}>",
65
+ constructorSignature: "baseUrl: string, options?: ApiClientOptions",
66
+ constructorBody: "super(baseUrl, options);",
67
+ factoryFunction: `export function createApiClient(baseUrl: string, options?: ApiClientOptions) {
68
+ return new ApiClient(baseUrl, options);
69
+ }`,
70
+ };
71
+
72
+ // ==========================================
73
+ // Utility Functions
74
+ // ==========================================
75
+
76
+ export function toPascalCase(str: string): string {
77
+ return str
78
+ .split(/[-_.]/)
79
+ .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
80
+ .join("");
81
+ }
82
+
83
+ export function toCamelCase(str: string): string {
84
+ const pascal = toPascalCase(str);
85
+ return pascal.charAt(0).toLowerCase() + pascal.slice(1);
86
+ }
87
+
88
+ /**
89
+ * Split string by delimiter, respecting nested brackets
90
+ */
91
+ export function splitTopLevel(source: string, delimiter: string): string[] {
92
+ const result: string[] = [];
93
+ let current = "";
94
+ let depth = 0;
95
+
96
+ for (const char of source) {
97
+ if (char === "(" || char === "[" || char === "{") {
98
+ depth++;
99
+ current += char;
100
+ } else if (char === ")" || char === "]" || char === "}") {
101
+ depth--;
102
+ current += char;
103
+ } else if (char === delimiter && depth === 0) {
104
+ if (current.trim()) {
105
+ result.push(current.trim());
106
+ }
107
+ current = "";
108
+ } else {
109
+ current += char;
110
+ }
111
+ }
112
+
113
+ if (current.trim()) {
114
+ result.push(current.trim());
115
+ }
116
+
117
+ return result;
118
+ }
119
+
120
+ /**
121
+ * Extract content between balanced parentheses starting at a given position
122
+ */
123
+ export function extractParenContent(source: string, startPos: number): string {
124
+ let depth = 0;
125
+ let start = -1;
126
+ let end = -1;
127
+
128
+ for (let i = startPos; i < source.length; i++) {
129
+ if (source[i] === "(") {
130
+ if (depth === 0) start = i;
131
+ depth++;
132
+ } else if (source[i] === ")") {
133
+ depth--;
134
+ if (depth === 0) {
135
+ end = i;
136
+ break;
137
+ }
138
+ }
139
+ }
140
+
141
+ if (start !== -1 && end !== -1) {
142
+ return source.slice(start + 1, end);
143
+ }
144
+ return "";
145
+ }
146
+
147
+ // ==========================================
148
+ // Zod to TypeScript Conversion
149
+ // ==========================================
150
+
151
+ /**
152
+ * Parse object property definitions from Zod source
153
+ */
154
+ function parseObjectProps(
155
+ propsSource: string
156
+ ): { name: string; schema: string; optional: boolean }[] {
157
+ const props: { name: string; schema: string; optional: boolean }[] = [];
158
+ const entries = splitTopLevel(propsSource, ",");
159
+
160
+ for (const entry of entries) {
161
+ const colonIndex = entry.indexOf(":");
162
+ if (colonIndex === -1) continue;
163
+
164
+ const name = entry.slice(0, colonIndex).trim();
165
+ let schema = entry.slice(colonIndex + 1).trim();
166
+
167
+ const optional = schema.endsWith(".optional()");
168
+ if (optional) {
169
+ schema = schema.slice(0, -".optional()".length);
170
+ }
171
+
172
+ props.push({ name, schema, optional });
173
+ }
174
+
175
+ return props;
176
+ }
177
+
178
+ /**
179
+ * Convert Zod schema source to TypeScript type string
180
+ */
181
+ export function zodToTypeScript(zodSource: string | undefined): string {
182
+ if (!zodSource) return "unknown";
183
+
184
+ const typeMap: Record<string, string> = {
185
+ "z.string()": "string",
186
+ "z.number()": "number",
187
+ "z.boolean()": "boolean",
188
+ "z.null()": "null",
189
+ "z.undefined()": "undefined",
190
+ "z.void()": "void",
191
+ "z.any()": "any",
192
+ "z.unknown()": "unknown",
193
+ "z.never()": "never",
194
+ "z.date()": "Date",
195
+ "z.bigint()": "bigint",
196
+ };
197
+
198
+ if (typeMap[zodSource]) return typeMap[zodSource];
199
+
200
+ let source = zodSource;
201
+ let suffix = "";
202
+
203
+ if (source.endsWith(".optional()")) {
204
+ source = source.slice(0, -".optional()".length);
205
+ suffix = " | undefined";
206
+ } else if (source.endsWith(".nullable()")) {
207
+ source = source.slice(0, -".nullable()".length);
208
+ suffix = " | null";
209
+ }
210
+
211
+ // z.object({ ... })
212
+ if (source.startsWith("z.object(")) {
213
+ const innerContent = extractParenContent(source, 8);
214
+ const trimmed = innerContent.trim();
215
+ if (trimmed.startsWith("{") && trimmed.endsWith("}")) {
216
+ const propsSource = trimmed.slice(1, -1).trim();
217
+ const props = parseObjectProps(propsSource);
218
+ const typeProps = props
219
+ .map((p) => ` ${p.name}${p.optional ? "?" : ""}: ${zodToTypeScript(p.schema)};`)
220
+ .join("\n");
221
+ return `{\n${typeProps}\n}${suffix}`;
222
+ }
223
+ }
224
+
225
+ // z.array(...)
226
+ if (source.startsWith("z.array(")) {
227
+ const innerContent = extractParenContent(source, 7);
228
+ if (innerContent) {
229
+ return `${zodToTypeScript(innerContent.trim())}[]${suffix}`;
230
+ }
231
+ }
232
+
233
+ // z.enum([...])
234
+ const enumMatch = source.match(/z\.enum\s*\(\s*\[([^\]]+)\]\s*\)/);
235
+ if (enumMatch?.[1]) {
236
+ const values = enumMatch[1]
237
+ .split(",")
238
+ .map((v) => v.trim())
239
+ .filter(Boolean);
240
+ return values.join(" | ") + suffix;
241
+ }
242
+
243
+ // z.literal(...)
244
+ const literalMatch = source.match(/z\.literal\s*\(\s*([^)]+)\s*\)/);
245
+ if (literalMatch?.[1]) {
246
+ return literalMatch[1].trim() + suffix;
247
+ }
248
+
249
+ // z.union([...])
250
+ const unionMatch = source.match(/z\.union\s*\(\s*\[([^\]]+)\]\s*\)/);
251
+ if (unionMatch?.[1]) {
252
+ const schemas = splitTopLevel(unionMatch[1], ",");
253
+ return schemas.map((s) => zodToTypeScript(s.trim())).join(" | ") + suffix;
254
+ }
255
+
256
+ // z.record(...)
257
+ const recordMatch = source.match(/z\.record\s*\(\s*([^)]+)\s*\)/);
258
+ if (recordMatch?.[1]) {
259
+ const parts = splitTopLevel(recordMatch[1], ",");
260
+ if (parts.length === 2) {
261
+ return `Record<${zodToTypeScript(parts[0]?.trim())}, ${zodToTypeScript(parts[1]?.trim())}>${suffix}`;
262
+ }
263
+ return `Record<string, ${zodToTypeScript(recordMatch[1].trim())}>${suffix}`;
264
+ }
265
+
266
+ // z.tuple([...])
267
+ const tupleMatch = source.match(/z\.tuple\s*\(\s*\[([^\]]+)\]\s*\)/);
268
+ if (tupleMatch?.[1]) {
269
+ const schemas = splitTopLevel(tupleMatch[1], ",");
270
+ return `[${schemas.map((s) => zodToTypeScript(s.trim())).join(", ")}]${suffix}`;
271
+ }
272
+
273
+ // z.string().min/max/email/etc
274
+ if (source.startsWith("z.string()")) return "string" + suffix;
275
+ if (source.startsWith("z.number()")) return "number" + suffix;
276
+
277
+ return "unknown" + suffix;
278
+ }
279
+
280
+ // ==========================================
281
+ // Route Grouping
282
+ // ==========================================
283
+
284
+ /**
285
+ * Group routes by prefix for namespace organization
286
+ */
287
+ export function groupRoutesByPrefix(routes: RouteInfo[]): Map<string, RouteInfo[]> {
288
+ const groups = new Map<string, RouteInfo[]>();
289
+
290
+ for (const route of routes) {
291
+ const prefix = route.prefix || "_root";
292
+ if (!groups.has(prefix)) {
293
+ groups.set(prefix, []);
294
+ }
295
+ groups.get(prefix)!.push(route);
296
+ }
297
+
298
+ return groups;
299
+ }
300
+
301
+ // ==========================================
302
+ // Client Code Generation
303
+ // ==========================================
304
+
305
+ /**
306
+ * Generate client code from extracted routes (simple format)
307
+ * This is used by the CLI command which extracts routes by running the server
308
+ */
309
+ export function generateClientFromRoutes(
310
+ routes: ExtractedRoute[],
311
+ options: Partial<ClientGeneratorOptions> = {}
312
+ ): string {
313
+ const opts = { ...defaultGeneratorOptions, ...options };
314
+
315
+ // Check if all routes share a common prefix (e.g., "api.") - if so, skip it
316
+ // Common prefix stripping is disabled to respect explicit router nesting
317
+ const routesToProcess = routes;
318
+
319
+ // Group routes by namespace
320
+ const tree = new Map<string, Map<string, { method: string; fullName: string }[]>>();
321
+
322
+ for (const route of routesToProcess) {
323
+ // Find original route name for the actual request
324
+ const originalRoute = routes.find(r => r.name.endsWith(route.name));
325
+ const fullName = originalRoute?.name || route.name;
326
+
327
+ const parts = route.name.split(".");
328
+ if (parts.length < 2) {
329
+ const ns = "";
330
+ if (!tree.has(ns)) tree.set(ns, new Map());
331
+ const rootMethods = tree.get(ns)!;
332
+ if (!rootMethods.has("")) rootMethods.set("", []);
333
+ rootMethods.get("")!.push({ method: parts[0]!, fullName });
334
+ } else if (parts.length === 2) {
335
+ const [ns, method] = parts;
336
+ if (!tree.has(ns!)) tree.set(ns!, new Map());
337
+ const nsMethods = tree.get(ns!)!;
338
+ if (!nsMethods.has("")) nsMethods.set("", []);
339
+ nsMethods.get("")!.push({ method: method!, fullName });
340
+ } else {
341
+ const [ns, sub, ...rest] = parts;
342
+ const method = rest.join(".");
343
+ if (!tree.has(ns!)) tree.set(ns!, new Map());
344
+ const nsMethods = tree.get(ns!)!;
345
+ if (!nsMethods.has(sub!)) nsMethods.set(sub!, []);
346
+ nsMethods.get(sub!)!.push({ method: method || sub!, fullName });
347
+ }
348
+ }
349
+
350
+ // Generate method definitions
351
+ const namespaceBlocks: string[] = [];
352
+
353
+ for (const [namespace, subNamespaces] of tree) {
354
+ if (namespace === "") {
355
+ const rootMethods = subNamespaces.get("");
356
+ if (rootMethods && rootMethods.length > 0) {
357
+ for (const { method, fullName } of rootMethods) {
358
+ const methodName = method.replace(/-([a-z])/g, (_: string, c: string) => c.toUpperCase());
359
+ namespaceBlocks.push(` ${methodName} = (input: any) => this.request("${fullName}", input);`);
360
+ }
361
+ }
362
+ continue;
363
+ }
364
+
365
+ const subBlocks: string[] = [];
366
+ for (const [sub, methods] of subNamespaces) {
367
+ if (sub === "") {
368
+ for (const { method, fullName } of methods) {
369
+ const methodName = method.replace(/-([a-z])/g, (_: string, c: string) => c.toUpperCase());
370
+ subBlocks.push(` ${methodName}: (input: any) => this.request("${fullName}", input)`);
371
+ }
372
+ } else {
373
+ const subMethods = methods.map(({ method, fullName }) => {
374
+ const methodName = method.replace(/-([a-z])/g, (_: string, c: string) => c.toUpperCase());
375
+ return ` ${methodName}: (input: any) => this.request("${fullName}", input)`;
376
+ });
377
+ subBlocks.push(` ${sub}: {\n${subMethods.join(",\n")}\n }`);
378
+ }
379
+ }
380
+
381
+ namespaceBlocks.push(` ${namespace} = {\n${subBlocks.join(",\n")}\n };`);
382
+ }
383
+
384
+ // Build additional imports
385
+ const additionalImportsStr = opts.additionalImports?.length
386
+ ? "\n" + opts.additionalImports.join("\n")
387
+ : "";
388
+
389
+ // Build additional members
390
+ const additionalMembersStr = opts.additionalMembers?.length
391
+ ? "\n\n" + opts.additionalMembers.join("\n\n")
392
+ : "";
393
+
394
+ return `// Auto-generated by donkeylabs generate
395
+ // DO NOT EDIT MANUALLY
396
+
397
+ ${opts.baseImport}${additionalImportsStr}
398
+
399
+ export class ApiClient extends ${opts.baseClass} {
400
+ constructor(${opts.constructorSignature}) {
401
+ ${opts.constructorBody}
402
+ }
403
+
404
+ ${namespaceBlocks.join("\n\n") || " // No routes defined"}${additionalMembersStr}
405
+ }
406
+
407
+ ${opts.factoryFunction}
408
+ `;
409
+ }
410
+
411
+ /**
412
+ * Generate fully-typed client code with route types (advanced format)
413
+ * This is used by the standalone script which parses source files
414
+ */
415
+ export function generateClientCode(
416
+ ctx: GeneratorConfig,
417
+ options: Partial<ClientGeneratorOptions> = {}
418
+ ): string {
419
+ const { routes, events = [], clientConfigs = [] } = ctx;
420
+ const opts = { ...defaultGeneratorOptions, ...options };
421
+
422
+ // Check if routes are simple ExtractedRoute format
423
+ if (routes.length > 0 && !("prefix" in routes[0])) {
424
+ return generateClientFromRoutes(routes as ExtractedRoute[], options);
425
+ }
426
+
427
+ const routeInfos = routes as RouteInfo[];
428
+ const defaultCredentials =
429
+ clientConfigs.find((c) => c.credentials)?.credentials || "include";
430
+
431
+ const routeGroups = groupRoutesByPrefix(routeInfos);
432
+
433
+ // Generate route type definitions
434
+ const routeTypeBlocks: string[] = [];
435
+ const routeNamespaceBlocks: string[] = [];
436
+
437
+ for (const [prefix, prefixRoutes] of routeGroups) {
438
+ const namespaceName = prefix === "_root" ? "Root" : toPascalCase(prefix);
439
+ const methodName = prefix === "_root" ? "_root" : prefix;
440
+
441
+ const typeEntries = prefixRoutes
442
+ .filter((r) => r.handler === "typed")
443
+ .map((r) => {
444
+ const inputType = zodToTypeScript(r.inputSource);
445
+ const outputType = zodToTypeScript(r.outputSource);
446
+ const routeNs = toPascalCase(r.routeName);
447
+ return ` export namespace ${routeNs} {
448
+ export type Input = ${inputType};
449
+ export type Output = ${outputType};
450
+ }
451
+ export type ${routeNs} = { Input: ${routeNs}.Input; Output: ${routeNs}.Output };`;
452
+ });
453
+
454
+ if (typeEntries.length > 0) {
455
+ routeTypeBlocks.push(` export namespace ${namespaceName} {
456
+ ${typeEntries.join("\n\n")}
457
+ }`);
458
+ }
459
+
460
+ const methodEntries = prefixRoutes
461
+ .filter((r) => r.handler === "typed")
462
+ .map((r) => {
463
+ const inputType = `Routes.${namespaceName}.${toPascalCase(r.routeName)}.Input`;
464
+ const outputType = `Routes.${namespaceName}.${toPascalCase(r.routeName)}.Output`;
465
+ return ` ${toCamelCase(r.routeName)}: (input: ${inputType}, options?: RequestOptions): Promise<${outputType}> =>
466
+ this.request("${r.name}", input, options)`;
467
+ });
468
+
469
+ const rawMethodEntries = prefixRoutes
470
+ .filter((r) => r.handler === "raw")
471
+ .map((r) => {
472
+ return ` ${toCamelCase(r.routeName)}: (init?: RequestInit): Promise<Response> =>
473
+ this.rawRequest("${r.name}", init)`;
474
+ });
475
+
476
+ const allMethods = [...methodEntries, ...rawMethodEntries];
477
+
478
+ if (allMethods.length > 0) {
479
+ routeNamespaceBlocks.push(` ${methodName} = {
480
+ ${allMethods.join(",\n\n")}
481
+ };`);
482
+ }
483
+ }
484
+
485
+ // Generate event types
486
+ const eventTypeEntries = events.map((e) => {
487
+ const type = zodToTypeScript(e.schemaSource);
488
+ return ` "${e.name}": ${type};`;
489
+ });
490
+
491
+ const eventTypesBlock =
492
+ eventTypeEntries.length > 0
493
+ ? `export interface SSEEvents {
494
+ ${eventTypeEntries.join("\n")}
495
+ }`
496
+ : `export interface SSEEvents {}`;
497
+
498
+ // Build additional imports
499
+ const additionalImportsStr = opts.additionalImports?.length
500
+ ? "\n" + opts.additionalImports.join("\n")
501
+ : "";
502
+
503
+ return `// Auto-generated by scripts/generate-client.ts
504
+ // DO NOT EDIT MANUALLY
505
+
506
+ import {
507
+ ApiClientBase,
508
+ ApiError,
509
+ ValidationError,
510
+ type RequestOptions,
511
+ type ApiClientOptions,
512
+ type SSEOptions,
513
+ } from "./base";${additionalImportsStr}
514
+
515
+ // ============================================
516
+ // Route Types
517
+ // ============================================
518
+
519
+ export namespace Routes {
520
+ ${routeTypeBlocks.join("\n\n") || " // No typed routes found"}
521
+ }
522
+
523
+ // ============================================
524
+ // SSE Event Types
525
+ // ============================================
526
+
527
+ ${eventTypesBlock}
528
+
529
+ // ============================================
530
+ // API Client
531
+ // ============================================
532
+
533
+ export interface ApiClientConfig extends ApiClientOptions {
534
+ baseUrl: string;
535
+ }
536
+
537
+ export class ApiClient extends ApiClientBase<SSEEvents> {
538
+ constructor(config: ApiClientConfig) {
539
+ super(config.baseUrl, {
540
+ credentials: "${defaultCredentials}",
541
+ ...config,
542
+ });
543
+ }
544
+
545
+ // ==========================================
546
+ // Route Namespaces
547
+ // ==========================================
548
+
549
+ ${routeNamespaceBlocks.join("\n\n") || " // No routes defined"}
550
+ }
551
+
552
+ // ============================================
553
+ // Factory Function
554
+ // ============================================
555
+
556
+ export function createApiClient(config: ApiClientConfig): ApiClient {
557
+ return new ApiClient(config);
558
+ }
559
+
560
+ // Re-export base types for convenience
561
+ export { ApiError, ValidationError, type RequestOptions, type SSEOptions };
562
+ `;
563
+ }
564
+
565
+ // Re-export runtime Zod to TypeScript converter
566
+ export { zodSchemaToTs } from "./zod-to-ts";
@@ -0,0 +1,114 @@
1
+ import { z } from "zod";
2
+
3
+ /**
4
+ * Convert a Zod schema to TypeScript type string at runtime
5
+ * Uses Zod's internal _def to introspect the schema
6
+ */
7
+ export function zodSchemaToTs(schema: z.ZodType<any>): string {
8
+ return convertZodType(schema);
9
+ }
10
+
11
+ function convertZodType(schema: z.ZodType<any>): string {
12
+ const def = (schema as any)._def;
13
+ const typeName = def?.typeName;
14
+
15
+ switch (typeName) {
16
+ case "ZodString":
17
+ return "string";
18
+
19
+ case "ZodNumber":
20
+ return "number";
21
+
22
+ case "ZodBoolean":
23
+ return "boolean";
24
+
25
+ case "ZodDate":
26
+ return "Date";
27
+
28
+ case "ZodUndefined":
29
+ return "undefined";
30
+
31
+ case "ZodNull":
32
+ return "null";
33
+
34
+ case "ZodAny":
35
+ return "any";
36
+
37
+ case "ZodUnknown":
38
+ return "unknown";
39
+
40
+ case "ZodVoid":
41
+ return "void";
42
+
43
+ case "ZodNever":
44
+ return "never";
45
+
46
+ case "ZodLiteral":
47
+ const value = def.value;
48
+ return typeof value === "string" ? `"${value}"` : String(value);
49
+
50
+ case "ZodArray":
51
+ const itemType = convertZodType(def.type);
52
+ return `${itemType}[]`;
53
+
54
+ case "ZodObject":
55
+ const shape = def.shape();
56
+ const props = Object.entries(shape).map(([key, value]) => {
57
+ const propSchema = value as z.ZodType<any>;
58
+ const isOptional = (propSchema as any)._def?.typeName === "ZodOptional";
59
+ const innerType = isOptional
60
+ ? convertZodType((propSchema as any)._def.innerType)
61
+ : convertZodType(propSchema);
62
+ return ` ${key}${isOptional ? "?" : ""}: ${innerType};`;
63
+ });
64
+ return `{\n${props.join("\n")}\n}`;
65
+
66
+ case "ZodOptional":
67
+ return convertZodType(def.innerType);
68
+
69
+ case "ZodNullable":
70
+ return `${convertZodType(def.innerType)} | null`;
71
+
72
+ case "ZodDefault":
73
+ return convertZodType(def.innerType);
74
+
75
+ case "ZodUnion":
76
+ const options = def.options.map((opt: z.ZodType<any>) => convertZodType(opt));
77
+ return options.join(" | ");
78
+
79
+ case "ZodEnum":
80
+ return def.values.map((v: string) => `"${v}"`).join(" | ");
81
+
82
+ case "ZodNativeEnum":
83
+ return "number | string"; // Simplified
84
+
85
+ case "ZodRecord":
86
+ const keyType = def.keyType ? convertZodType(def.keyType) : "string";
87
+ const valueType = convertZodType(def.valueType);
88
+ return `Record<${keyType}, ${valueType}>`;
89
+
90
+ case "ZodTuple":
91
+ const items = def.items.map((item: z.ZodType<any>) => convertZodType(item));
92
+ return `[${items.join(", ")}]`;
93
+
94
+ case "ZodPromise":
95
+ return `Promise<${convertZodType(def.type)}>`;
96
+
97
+ case "ZodEffects":
98
+ // .transform(), .refine(), etc - use the inner schema
99
+ return convertZodType(def.schema);
100
+
101
+ case "ZodLazy":
102
+ // Lazy schemas - try to resolve
103
+ return convertZodType(def.getter());
104
+
105
+ case "ZodIntersection":
106
+ const left = convertZodType(def.left);
107
+ const right = convertZodType(def.right);
108
+ return `${left} & ${right}`;
109
+
110
+ default:
111
+ // Fallback for unknown types
112
+ return "unknown";
113
+ }
114
+ }