next-openapi-gen 0.10.5 → 1.0.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 (44) hide show
  1. package/dist/cli.d.ts +4 -0
  2. package/dist/cli.js +8599 -0
  3. package/dist/index.d.ts +18 -0
  4. package/dist/index.js +8645 -26
  5. package/dist/next/index.d.ts +1 -0
  6. package/dist/next/index.js +7965 -0
  7. package/dist/react-router/index.d.ts +1 -0
  8. package/dist/react-router/index.js +7134 -0
  9. package/dist/vite/index.d.ts +1 -0
  10. package/dist/vite/index.js +7134 -0
  11. package/package.json +102 -79
  12. package/{dist/components/rapidoc.js → templates/init/ui/nextjs/rapidoc.tsx} +16 -20
  13. package/templates/init/ui/nextjs/redoc.tsx +11 -0
  14. package/{dist/components/scalar.js → templates/init/ui/nextjs/scalar.tsx} +15 -21
  15. package/{dist/components/stoplight.js → templates/init/ui/nextjs/stoplight.tsx} +11 -17
  16. package/templates/init/ui/nextjs/swagger.tsx +17 -0
  17. package/templates/init/ui/reactrouter/rapidoc.tsx +15 -0
  18. package/templates/init/ui/reactrouter/redoc.tsx +9 -0
  19. package/templates/init/ui/reactrouter/scalar.tsx +14 -0
  20. package/templates/init/ui/reactrouter/stoplight.tsx +10 -0
  21. package/templates/init/ui/reactrouter/swagger.tsx +11 -0
  22. package/templates/init/ui/tanstack/rapidoc.tsx +21 -0
  23. package/templates/init/ui/tanstack/redoc.tsx +14 -0
  24. package/templates/init/ui/tanstack/scalar.tsx +19 -0
  25. package/templates/init/ui/tanstack/stoplight.tsx +15 -0
  26. package/templates/init/ui/tanstack/swagger.tsx +16 -0
  27. package/templates/init/ui/template-types.d.ts +9 -0
  28. package/README.md +0 -1047
  29. package/dist/commands/generate.js +0 -24
  30. package/dist/commands/init.js +0 -194
  31. package/dist/components/redoc.js +0 -17
  32. package/dist/components/swagger.js +0 -21
  33. package/dist/lib/app-router-strategy.js +0 -66
  34. package/dist/lib/drizzle-zod-processor.js +0 -329
  35. package/dist/lib/logger.js +0 -39
  36. package/dist/lib/openapi-generator.js +0 -171
  37. package/dist/lib/pages-router-strategy.js +0 -198
  38. package/dist/lib/route-processor.js +0 -349
  39. package/dist/lib/router-strategy.js +0 -1
  40. package/dist/lib/schema-processor.js +0 -1612
  41. package/dist/lib/utils.js +0 -283
  42. package/dist/lib/zod-converter.js +0 -2133
  43. package/dist/openapi-template.js +0 -99
  44. package/dist/types.js +0 -1
@@ -1,2133 +0,0 @@
1
- import fs from "fs";
2
- import path from "path";
3
- import traverseModule from "@babel/traverse";
4
- import * as t from "@babel/types";
5
- // Handle both ES modules and CommonJS
6
- const traverse = traverseModule.default || traverseModule;
7
- import { parseTypeScriptFile } from "./utils.js";
8
- import { logger } from "./logger.js";
9
- import { DrizzleZodProcessor } from "./drizzle-zod-processor.js";
10
- /**
11
- * Class for converting Zod schemas to OpenAPI specifications
12
- */
13
- export class ZodSchemaConverter {
14
- schemaDirs;
15
- apiDir;
16
- zodSchemas = {};
17
- processingSchemas = new Set();
18
- processedModules = new Set();
19
- typeToSchemaMapping = {};
20
- drizzleZodImports = new Set();
21
- factoryCache = new Map(); // Cache for analyzed factory functions
22
- factoryCheckCache = new Map(); // Cache for non-factory functions
23
- fileASTCache = new Map(); // Cache for parsed files
24
- fileImportsCache = new Map(); // Cache for file imports
25
- // Current processing context (set during file processing)
26
- currentFilePath;
27
- currentAST;
28
- currentImports;
29
- constructor(schemaDir, apiDir) {
30
- const dirs = Array.isArray(schemaDir) ? schemaDir : [schemaDir];
31
- this.schemaDirs = dirs.map((d) => path.resolve(d));
32
- this.apiDir = apiDir ? path.resolve(apiDir) : undefined;
33
- }
34
- /**
35
- * Find a Zod schema by name and convert it to OpenAPI spec
36
- */
37
- convertZodSchemaToOpenApi(schemaName) {
38
- // Run pre-scan only one time
39
- if (Object.keys(this.typeToSchemaMapping).length === 0) {
40
- this.preScanForTypeMappings();
41
- }
42
- logger.debug(`Looking for Zod schema: ${schemaName}`);
43
- // Check mapped types
44
- const mappedSchemaName = this.typeToSchemaMapping[schemaName];
45
- if (mappedSchemaName) {
46
- logger.debug(`Type '${schemaName}' is mapped to schema '${mappedSchemaName}'`);
47
- schemaName = mappedSchemaName;
48
- }
49
- // Check for circular references
50
- if (this.processingSchemas.has(schemaName)) {
51
- return { $ref: `#/components/schemas/${schemaName}` };
52
- }
53
- // Add to processing set
54
- this.processingSchemas.add(schemaName);
55
- try {
56
- // Return cached schema if it exists
57
- if (this.zodSchemas[schemaName]) {
58
- return this.zodSchemas[schemaName];
59
- }
60
- // Find all route files and process them first
61
- const routeFiles = this.findRouteFiles();
62
- for (const routeFile of routeFiles) {
63
- this.processFileForZodSchema(routeFile, schemaName);
64
- if (this.zodSchemas[schemaName]) {
65
- logger.debug(`Found Zod schema '${schemaName}' in route file: ${routeFile}`);
66
- return this.zodSchemas[schemaName];
67
- }
68
- }
69
- // Scan schema directories
70
- for (const dir of this.schemaDirs) {
71
- this.scanDirectoryForZodSchema(dir, schemaName);
72
- if (this.zodSchemas[schemaName])
73
- break;
74
- }
75
- // Return the schema if found, or null if not
76
- if (this.zodSchemas[schemaName]) {
77
- logger.debug(`Found and processed Zod schema: ${schemaName}`);
78
- return this.zodSchemas[schemaName];
79
- }
80
- logger.debug(`Could not find Zod schema: ${schemaName}`);
81
- return null;
82
- }
83
- finally {
84
- // Remove from processing set
85
- this.processingSchemas.delete(schemaName);
86
- }
87
- }
88
- /**
89
- * Find all route files in the project
90
- */
91
- findRouteFiles() {
92
- const routeFiles = [];
93
- // When apiDir is configured, scan only that directory to prevent
94
- // leaking schemas from routes outside the configured API boundary.
95
- if (this.apiDir) {
96
- if (fs.existsSync(this.apiDir)) {
97
- this.findRouteFilesInDir(this.apiDir, routeFiles);
98
- }
99
- return routeFiles;
100
- }
101
- // Look for route files in common Next.js API directories
102
- const possibleApiDirs = [
103
- path.join(process.cwd(), "src", "app", "api"),
104
- path.join(process.cwd(), "src", "pages", "api"),
105
- path.join(process.cwd(), "app", "api"),
106
- path.join(process.cwd(), "pages", "api"),
107
- ];
108
- for (const dir of possibleApiDirs) {
109
- if (fs.existsSync(dir)) {
110
- this.findRouteFilesInDir(dir, routeFiles);
111
- }
112
- }
113
- return routeFiles;
114
- }
115
- /**
116
- * Recursively find route files in a directory
117
- */
118
- findRouteFilesInDir(dir, routeFiles) {
119
- try {
120
- const files = fs.readdirSync(dir);
121
- for (const file of files) {
122
- const filePath = path.join(dir, file);
123
- const stats = fs.statSync(filePath);
124
- if (stats.isDirectory()) {
125
- this.findRouteFilesInDir(filePath, routeFiles);
126
- }
127
- else if (file === "route.ts" ||
128
- file === "route.tsx" ||
129
- (file.endsWith(".ts") && file.includes("api"))) {
130
- routeFiles.push(filePath);
131
- }
132
- }
133
- }
134
- catch (error) {
135
- logger.error(`Error scanning directory ${dir} for route files: ${error}`);
136
- }
137
- }
138
- /**
139
- * Recursively scan directory for Zod schemas
140
- */
141
- scanDirectoryForZodSchema(dir, schemaName) {
142
- try {
143
- const files = fs.readdirSync(dir);
144
- for (const file of files) {
145
- const filePath = path.join(dir, file);
146
- const stats = fs.statSync(filePath);
147
- if (stats.isDirectory()) {
148
- this.scanDirectoryForZodSchema(filePath, schemaName);
149
- }
150
- else if (file.endsWith(".ts") || file.endsWith(".tsx")) {
151
- this.processFileForZodSchema(filePath, schemaName);
152
- }
153
- }
154
- }
155
- catch (error) {
156
- logger.error(`Error scanning directory ${dir}: ${error}`);
157
- }
158
- }
159
- /**
160
- * Process a file to find Zod schema definitions
161
- */
162
- processFileForZodSchema(filePath, schemaName) {
163
- try {
164
- const content = fs.readFileSync(filePath, "utf-8");
165
- // Check if file contains schema we are looking for
166
- if (!content.includes(schemaName)) {
167
- return;
168
- }
169
- // Pre-process all schemas in file
170
- this.preprocessAllSchemasInFile(filePath);
171
- // Return it, if the schema has already been processed during pre-processing
172
- if (this.zodSchemas[schemaName]) {
173
- return;
174
- }
175
- // Parse the file
176
- const ast = parseTypeScriptFile(content);
177
- // Cache AST for later use
178
- this.fileASTCache.set(filePath, ast);
179
- // Create a map to store imported modules
180
- let importedModules = {};
181
- // Check if we have cached imports
182
- if (this.fileImportsCache.has(filePath)) {
183
- importedModules = this.fileImportsCache.get(filePath);
184
- }
185
- else {
186
- // Build imports cache
187
- traverse(ast, {
188
- ImportDeclaration: (path) => {
189
- const source = path.node.source.value;
190
- // Track drizzle-zod imports
191
- if (source === "drizzle-zod") {
192
- path.node.specifiers.forEach((specifier) => {
193
- if (t.isImportSpecifier(specifier) ||
194
- t.isImportDefaultSpecifier(specifier)) {
195
- this.drizzleZodImports.add(specifier.local.name);
196
- }
197
- });
198
- }
199
- // Process each import specifier
200
- path.node.specifiers.forEach((specifier) => {
201
- if (t.isImportSpecifier(specifier) ||
202
- t.isImportDefaultSpecifier(specifier)) {
203
- const importedName = specifier.local.name;
204
- importedModules[importedName] = source;
205
- }
206
- });
207
- },
208
- });
209
- // Cache imports for this file
210
- this.fileImportsCache.set(filePath, importedModules);
211
- }
212
- // Set current processing context for use by processZodNode during factory expansion
213
- this.currentFilePath = filePath;
214
- this.currentAST = ast;
215
- this.currentImports = importedModules;
216
- // Look for all exported Zod schemas
217
- traverse(ast, {
218
- // For export const SchemaName = z.object({...})
219
- ExportNamedDeclaration: (path) => {
220
- if (t.isVariableDeclaration(path.node.declaration)) {
221
- path.node.declaration.declarations.forEach((declaration) => {
222
- if (t.isIdentifier(declaration.id) &&
223
- declaration.id.name === schemaName &&
224
- declaration.init) {
225
- // Check if this is a drizzle-zod helper function
226
- if (t.isCallExpression(declaration.init) &&
227
- t.isIdentifier(declaration.init.callee) &&
228
- this.drizzleZodImports.has(declaration.init.callee.name)) {
229
- const schema = this.processZodNode(declaration.init);
230
- if (schema) {
231
- this.zodSchemas[schemaName] = schema;
232
- }
233
- }
234
- // Check if this is a call expression with .extend()
235
- else if (t.isCallExpression(declaration.init) &&
236
- t.isMemberExpression(declaration.init.callee) &&
237
- t.isIdentifier(declaration.init.callee.property) &&
238
- declaration.init.callee.property.name === "extend") {
239
- const schema = this.processZodNode(declaration.init);
240
- if (schema) {
241
- this.zodSchemas[schemaName] = schema;
242
- }
243
- }
244
- // Existing code for z.object({...})
245
- else if (t.isCallExpression(declaration.init) &&
246
- t.isMemberExpression(declaration.init.callee) &&
247
- t.isIdentifier(declaration.init.callee.object) &&
248
- declaration.init.callee.object.name === "z") {
249
- const schema = this.processZodNode(declaration.init);
250
- if (schema) {
251
- this.zodSchemas[schemaName] = schema;
252
- }
253
- }
254
- // Check if this is a factory function call
255
- else if (t.isCallExpression(declaration.init) &&
256
- t.isIdentifier(declaration.init.callee)) {
257
- const factoryName = declaration.init.callee.name;
258
- logger.debug(`[Schema] Detected potential factory function call: ${factoryName} for schema ${schemaName}`);
259
- const factoryNode = this.findFactoryFunction(factoryName, filePath, ast, importedModules);
260
- if (factoryNode) {
261
- logger.debug(`[Schema] Found factory function, attempting to expand...`);
262
- const schema = this.expandFactoryCall(factoryNode, declaration.init, filePath);
263
- if (schema) {
264
- this.zodSchemas[schemaName] = schema;
265
- logger.debug(`[Schema] Successfully expanded factory function '${factoryName}' for schema '${schemaName}'`);
266
- }
267
- else {
268
- logger.debug(`[Schema] Failed to expand factory function '${factoryName}'`);
269
- }
270
- }
271
- else {
272
- logger.debug(`[Schema] Could not find factory function '${factoryName}'`);
273
- }
274
- }
275
- }
276
- });
277
- }
278
- else if (t.isTSTypeAliasDeclaration(path.node.declaration)) {
279
- // Handle export type aliases with z schema definitions
280
- if (t.isIdentifier(path.node.declaration.id) &&
281
- path.node.declaration.id.name === schemaName) {
282
- const typeAnnotation = path.node.declaration.typeAnnotation;
283
- // Check if this is a reference to a z schema (e.g., export type UserSchema = z.infer<typeof UserSchema>)
284
- if (t.isTSTypeReference(typeAnnotation) &&
285
- t.isIdentifier(typeAnnotation.typeName) &&
286
- typeAnnotation.typeName.name === "z.infer") {
287
- // Extract the schema name from z.infer<typeof SchemaName>
288
- if (typeAnnotation.typeParameters &&
289
- typeAnnotation.typeParameters.params.length > 0 &&
290
- t.isTSTypeReference(typeAnnotation.typeParameters.params[0]) &&
291
- t.isTSTypeQuery(typeAnnotation.typeParameters.params[0].typeName) &&
292
- t.isIdentifier(
293
- // @ts-ignore
294
- typeAnnotation.typeParameters.params[0].typeName.exprName)) {
295
- const referencedSchema =
296
- // @ts-ignore
297
- typeAnnotation.typeParameters.params[0].typeName.exprName
298
- .name;
299
- // Look for the referenced schema in the same file
300
- if (!this.zodSchemas[referencedSchema]) {
301
- this.processFileForZodSchema(filePath, referencedSchema);
302
- }
303
- // Use the referenced schema for this type alias
304
- if (this.zodSchemas[referencedSchema]) {
305
- this.zodSchemas[schemaName] =
306
- this.zodSchemas[referencedSchema];
307
- }
308
- }
309
- }
310
- }
311
- }
312
- },
313
- // For const SchemaName = z.object({...})
314
- VariableDeclarator: (path) => {
315
- if (t.isIdentifier(path.node.id) &&
316
- path.node.id.name === schemaName &&
317
- path.node.init) {
318
- // Check if this is any Zod schema (including chained calls)
319
- if (this.isZodSchema(path.node.init)) {
320
- const schema = this.processZodNode(path.node.init);
321
- if (schema) {
322
- this.zodSchemas[schemaName] = schema;
323
- }
324
- return;
325
- }
326
- // Helper function for processing the call chain
327
- const processChainedCall = (node, baseSchema) => {
328
- if (!t.isCallExpression(node) ||
329
- !t.isMemberExpression(node.callee)) {
330
- return baseSchema;
331
- }
332
- // @ts-ignore
333
- const methodName = node.callee.property.name;
334
- let schema = baseSchema;
335
- // If there is an even deeper call, process it first
336
- if (t.isCallExpression(node.callee.object)) {
337
- schema = processChainedCall(node.callee.object, baseSchema);
338
- }
339
- // Now apply the current method
340
- switch (methodName) {
341
- case "omit":
342
- if (node.arguments.length > 0 &&
343
- t.isObjectExpression(node.arguments[0])) {
344
- node.arguments[0].properties.forEach((prop) => {
345
- if (t.isObjectProperty(prop) &&
346
- t.isBooleanLiteral(prop.value) &&
347
- prop.value.value === true) {
348
- const key = t.isIdentifier(prop.key)
349
- ? prop.key.name
350
- : t.isStringLiteral(prop.key)
351
- ? prop.key.value
352
- : null;
353
- if (key && schema.properties) {
354
- logger.debug(`Removing property: ${key}`);
355
- delete schema.properties[key];
356
- if (schema.required) {
357
- schema.required = schema.required.filter((r) => r !== key);
358
- }
359
- }
360
- }
361
- });
362
- }
363
- break;
364
- case "partial":
365
- // All fields become optional (T | undefined), not nullable
366
- if (schema.properties) {
367
- delete schema.required;
368
- }
369
- break;
370
- case "pick":
371
- if (node.arguments.length > 0 &&
372
- t.isObjectExpression(node.arguments[0])) {
373
- const keysToPick = [];
374
- node.arguments[0].properties.forEach((prop) => {
375
- if (t.isObjectProperty(prop) &&
376
- t.isBooleanLiteral(prop.value) &&
377
- prop.value.value === true) {
378
- const key = t.isIdentifier(prop.key)
379
- ? prop.key.name
380
- : t.isStringLiteral(prop.key)
381
- ? prop.key.value
382
- : null;
383
- if (key)
384
- keysToPick.push(key);
385
- }
386
- });
387
- // Keep only selected properties
388
- if (schema.properties) {
389
- const newProperties = {};
390
- keysToPick.forEach((key) => {
391
- if (schema.properties[key]) {
392
- newProperties[key] = schema.properties[key];
393
- }
394
- });
395
- schema.properties = newProperties;
396
- // Update required
397
- if (schema.required) {
398
- schema.required = schema.required.filter((key) => keysToPick.includes(key));
399
- }
400
- }
401
- }
402
- break;
403
- case "required":
404
- // All fields become required — preserve genuine nullable flags
405
- if (schema.properties) {
406
- schema.required = Object.keys(schema.properties);
407
- }
408
- break;
409
- case "extend":
410
- // Extend the schema with new properties
411
- if (node.arguments.length > 0 &&
412
- t.isObjectExpression(node.arguments[0])) {
413
- const extensionProperties = {};
414
- const extensionRequired = [];
415
- node.arguments[0].properties.forEach((prop) => {
416
- if (t.isObjectProperty(prop)) {
417
- const key = t.isIdentifier(prop.key)
418
- ? prop.key.name
419
- : t.isStringLiteral(prop.key)
420
- ? prop.key.value
421
- : null;
422
- if (key) {
423
- // Process the Zod type for this property
424
- const propSchema = this.processZodNode(prop.value);
425
- if (propSchema) {
426
- extensionProperties[key] = propSchema;
427
- const isOptional =
428
- // @ts-ignore
429
- this.isOptional(prop.value) || this.hasOptionalMethod(prop.value);
430
- if (!isOptional) {
431
- extensionRequired.push(key);
432
- }
433
- }
434
- }
435
- }
436
- });
437
- // Merge with existing schema
438
- if (schema.properties) {
439
- schema.properties = {
440
- ...schema.properties,
441
- ...extensionProperties,
442
- };
443
- }
444
- else {
445
- schema.properties = extensionProperties;
446
- }
447
- // Merge required arrays
448
- if (extensionRequired.length > 0) {
449
- schema.required = [
450
- ...(schema.required || []),
451
- ...extensionRequired,
452
- ];
453
- // Deduplicate
454
- schema.required = [...new Set(schema.required)];
455
- }
456
- }
457
- break;
458
- }
459
- return schema;
460
- };
461
- // Find the underlying schema (the most nested object in the chain)
462
- const findBaseSchema = (node) => {
463
- if (t.isIdentifier(node)) {
464
- return node.name;
465
- }
466
- else if (t.isMemberExpression(node)) {
467
- return findBaseSchema(node.object);
468
- }
469
- else if (t.isCallExpression(node) &&
470
- t.isMemberExpression(node.callee)) {
471
- return findBaseSchema(node.callee.object);
472
- }
473
- return null;
474
- };
475
- // Check method calls on other schemas
476
- if (t.isCallExpression(path.node.init)) {
477
- const baseSchemaName = findBaseSchema(path.node.init);
478
- if (baseSchemaName && baseSchemaName !== "z") {
479
- logger.debug(`Found chained call starting from: ${baseSchemaName}`);
480
- // First make sure the underlying schema is processed
481
- if (!this.zodSchemas[baseSchemaName]) {
482
- logger.debug(`Base schema ${baseSchemaName} not found, processing it first`);
483
- this.processFileForZodSchema(filePath, baseSchemaName);
484
- }
485
- if (this.zodSchemas[baseSchemaName]) {
486
- logger.debug("Base schema found, applying transformations");
487
- // Copy base schema
488
- const baseSchema = JSON.parse(JSON.stringify(this.zodSchemas[baseSchemaName]));
489
- // Process the entire call chain
490
- const finalSchema = processChainedCall(path.node.init, baseSchema);
491
- this.zodSchemas[schemaName] = finalSchema;
492
- logger.debug(`Created ${schemaName} with properties: ${Object.keys(finalSchema.properties || {})}`);
493
- return;
494
- }
495
- }
496
- }
497
- // Check if it is .extend()
498
- if (t.isCallExpression(path.node.init) &&
499
- t.isMemberExpression(path.node.init.callee) &&
500
- t.isIdentifier(path.node.init.callee.property) &&
501
- path.node.init.callee.property.name === "extend") {
502
- const schema = this.processZodNode(path.node.init);
503
- if (schema) {
504
- this.zodSchemas[schemaName] = schema;
505
- }
506
- }
507
- // Existing code
508
- else {
509
- const schema = this.processZodNode(path.node.init);
510
- if (schema) {
511
- this.zodSchemas[schemaName] = schema;
512
- }
513
- }
514
- }
515
- },
516
- // For type aliases that reference Zod schemas
517
- TSTypeAliasDeclaration: (path) => {
518
- if (t.isIdentifier(path.node.id)) {
519
- const typeName = path.node.id.name;
520
- if (t.isTSTypeReference(path.node.typeAnnotation) &&
521
- t.isTSQualifiedName(path.node.typeAnnotation.typeName) &&
522
- t.isIdentifier(path.node.typeAnnotation.typeName.left) &&
523
- path.node.typeAnnotation.typeName.left.name === "z" &&
524
- t.isIdentifier(path.node.typeAnnotation.typeName.right) &&
525
- path.node.typeAnnotation.typeName.right.name === "infer") {
526
- // Extract schema name from z.infer<typeof SchemaName>
527
- if (path.node.typeAnnotation.typeParameters &&
528
- path.node.typeAnnotation.typeParameters.params.length > 0) {
529
- const param = path.node.typeAnnotation.typeParameters.params[0];
530
- if (t.isTSTypeQuery(param) && t.isIdentifier(param.exprName)) {
531
- const referencedSchemaName = param.exprName.name;
532
- // Save mapping: TypeName -> SchemaName
533
- this.typeToSchemaMapping[typeName] = referencedSchemaName;
534
- logger.debug(`Mapped type '${typeName}' to schema '${referencedSchemaName}'`);
535
- // Process the referenced schema if not already processed
536
- if (!this.zodSchemas[referencedSchemaName]) {
537
- this.processFileForZodSchema(filePath, referencedSchemaName);
538
- }
539
- // Use the referenced schema for this type
540
- if (this.zodSchemas[referencedSchemaName]) {
541
- this.zodSchemas[typeName] =
542
- this.zodSchemas[referencedSchemaName];
543
- }
544
- }
545
- }
546
- }
547
- if (path.node.id.name === schemaName) {
548
- // Try to find if this is a z.infer<typeof SchemaName> pattern
549
- if (t.isTSTypeReference(path.node.typeAnnotation) &&
550
- t.isIdentifier(path.node.typeAnnotation.typeName) &&
551
- path.node.typeAnnotation.typeName.name === "infer" &&
552
- path.node.typeAnnotation.typeParameters &&
553
- path.node.typeAnnotation.typeParameters.params.length > 0) {
554
- const param = path.node.typeAnnotation.typeParameters.params[0];
555
- if (t.isTSTypeQuery(param) && t.isIdentifier(param.exprName)) {
556
- const referencedSchemaName = param.exprName.name;
557
- // Find the referenced schema
558
- this.processFileForZodSchema(filePath, referencedSchemaName);
559
- if (this.zodSchemas[referencedSchemaName]) {
560
- this.zodSchemas[schemaName] =
561
- this.zodSchemas[referencedSchemaName];
562
- }
563
- }
564
- }
565
- }
566
- }
567
- },
568
- });
569
- }
570
- catch (error) {
571
- logger.error(`Error processing file ${filePath} for schema ${schemaName}: ${error}`);
572
- }
573
- }
574
- /**
575
- * Process all exported schemas in a file, not just the one we're looking for
576
- */
577
- processAllSchemasInFile(filePath) {
578
- try {
579
- const content = fs.readFileSync(filePath, "utf-8");
580
- const ast = parseTypeScriptFile(content);
581
- traverse(ast, {
582
- ExportNamedDeclaration: (path) => {
583
- if (t.isVariableDeclaration(path.node.declaration)) {
584
- path.node.declaration.declarations.forEach((declaration) => {
585
- if (t.isIdentifier(declaration.id) &&
586
- declaration.init &&
587
- t.isCallExpression(declaration.init) &&
588
- t.isMemberExpression(declaration.init.callee) &&
589
- t.isIdentifier(declaration.init.callee.object) &&
590
- declaration.init.callee.object.name === "z") {
591
- const schemaName = declaration.id.name;
592
- if (!this.zodSchemas[schemaName] &&
593
- !this.processingSchemas.has(schemaName)) {
594
- this.processingSchemas.add(schemaName);
595
- const schema = this.processZodNode(declaration.init);
596
- if (schema) {
597
- this.zodSchemas[schemaName] = schema;
598
- }
599
- this.processingSchemas.delete(schemaName);
600
- }
601
- }
602
- });
603
- }
604
- },
605
- });
606
- }
607
- catch (error) {
608
- logger.error(`Error processing all schemas in file ${filePath}: ${error}`);
609
- }
610
- }
611
- /**
612
- * Process a Zod node and convert it to OpenAPI schema
613
- */
614
- processZodNode(node) {
615
- // Handle drizzle-zod helper functions (e.g., createInsertSchema, createSelectSchema)
616
- if (t.isCallExpression(node) &&
617
- t.isIdentifier(node.callee) &&
618
- this.drizzleZodImports.has(node.callee.name)) {
619
- return DrizzleZodProcessor.processSchema(node);
620
- }
621
- // Handle reference to another schema (e.g. UserBaseSchema.extend)
622
- if (t.isCallExpression(node) &&
623
- t.isMemberExpression(node.callee) &&
624
- t.isIdentifier(node.callee.object) &&
625
- t.isIdentifier(node.callee.property) &&
626
- node.callee.property.name === "extend") {
627
- const baseSchemaName = node.callee.object.name;
628
- // Check if the base schema already exists
629
- if (!this.zodSchemas[baseSchemaName]) {
630
- // Try to find the basic pattern
631
- this.convertZodSchemaToOpenApi(baseSchemaName);
632
- }
633
- return this.processZodChain(node);
634
- }
635
- // Handle z.coerce.TYPE() patterns
636
- if (t.isCallExpression(node) &&
637
- t.isMemberExpression(node.callee) &&
638
- t.isMemberExpression(node.callee.object) &&
639
- t.isIdentifier(node.callee.object.object) &&
640
- node.callee.object.object.name === "z" &&
641
- t.isIdentifier(node.callee.object.property) &&
642
- node.callee.object.property.name === "coerce" &&
643
- t.isIdentifier(node.callee.property)) {
644
- const coerceType = node.callee.property.name;
645
- // Create a synthetic node for the underlying type using Babel types
646
- const syntheticNode = t.callExpression(t.memberExpression(t.identifier("z"), t.identifier(coerceType)), []);
647
- return this.processZodPrimitive(syntheticNode);
648
- }
649
- // Handle z.object({...})
650
- if (t.isCallExpression(node) &&
651
- t.isMemberExpression(node.callee) &&
652
- t.isIdentifier(node.callee.object) &&
653
- node.callee.object.name === "z" &&
654
- t.isIdentifier(node.callee.property)) {
655
- const methodName = node.callee.property.name;
656
- if (methodName === "object" && node.arguments.length > 0) {
657
- return this.processZodObject(node);
658
- }
659
- else if (methodName === "union" && node.arguments.length > 0) {
660
- return this.processZodUnion(node);
661
- }
662
- else if (methodName === "intersection" && node.arguments.length > 0) {
663
- return this.processZodIntersection(node);
664
- }
665
- else if (methodName === "tuple" && node.arguments.length > 0) {
666
- return this.processZodTuple(node);
667
- }
668
- else if (methodName === "discriminatedUnion" &&
669
- node.arguments.length > 1) {
670
- return this.processZodDiscriminatedUnion(node);
671
- }
672
- else if (methodName === "literal" && node.arguments.length > 0) {
673
- return this.processZodLiteral(node);
674
- }
675
- else {
676
- return this.processZodPrimitive(node);
677
- }
678
- }
679
- // Handle schema reference with method calls, e.g., Image.optional(), UserSchema.nullable()
680
- if (t.isCallExpression(node) &&
681
- t.isMemberExpression(node.callee) &&
682
- t.isIdentifier(node.callee.object) &&
683
- t.isIdentifier(node.callee.property) &&
684
- node.callee.object.name !== "z" // Make sure it's not a z.* call
685
- ) {
686
- const schemaName = node.callee.object.name;
687
- const methodName = node.callee.property.name;
688
- // Process base schema first if not already processed
689
- if (!this.zodSchemas[schemaName]) {
690
- this.convertZodSchemaToOpenApi(schemaName);
691
- }
692
- // If the schema exists, create a reference and apply the method
693
- if (this.zodSchemas[schemaName]) {
694
- let schema = {
695
- allOf: [{ $ref: `#/components/schemas/${schemaName}` }],
696
- };
697
- // Apply method-specific transformations
698
- switch (methodName) {
699
- case "optional":
700
- case "nullable":
701
- case "nullish":
702
- // Don't add nullable flag here as it would be at the wrong level
703
- // The fact that it's optional is handled by not including it in required array
704
- break;
705
- case "describe":
706
- if (node.arguments.length > 0 &&
707
- t.isStringLiteral(node.arguments[0])) {
708
- schema.description = node.arguments[0].value;
709
- }
710
- break;
711
- default:
712
- // For other methods, process as a chain
713
- return this.processZodChain(node);
714
- }
715
- return schema;
716
- }
717
- }
718
- // Handle chained methods, e.g., z.string().email().min(5)
719
- if (t.isCallExpression(node) &&
720
- t.isMemberExpression(node.callee) &&
721
- t.isCallExpression(node.callee.object)) {
722
- return this.processZodChain(node);
723
- }
724
- // Handle schema references like z.lazy(() => AnotherSchema)
725
- if (t.isCallExpression(node) &&
726
- t.isMemberExpression(node.callee) &&
727
- t.isIdentifier(node.callee.object) &&
728
- node.callee.object.name === "z" &&
729
- t.isIdentifier(node.callee.property) &&
730
- node.callee.property.name === "lazy" &&
731
- node.arguments.length > 0) {
732
- return this.processZodLazy(node);
733
- }
734
- // Handle potential factory function calls (e.g., createPaginatedSchema(UserSchema))
735
- // This must be checked before falling back to "Unknown Zod schema node"
736
- if (t.isCallExpression(node) &&
737
- t.isIdentifier(node.callee)) {
738
- logger.debug(`[processZodNode] Attempting to handle potential factory function: ${node.callee.name}`);
739
- // We need the current file context - try to get it from the processing context
740
- // Note: This is a limitation - we may not have file context during preprocessing
741
- // In that case, we'll return a placeholder and let the main processing handle it
742
- const currentFilePath = this.currentFilePath;
743
- const currentAST = this.currentAST;
744
- const importedModules = this.currentImports;
745
- if (currentFilePath && currentAST && importedModules) {
746
- const factoryNode = this.findFactoryFunction(node.callee.name, currentFilePath, currentAST, importedModules);
747
- if (factoryNode) {
748
- logger.debug(`[processZodNode] Found factory function, expanding...`);
749
- const schema = this.expandFactoryCall(factoryNode, node, currentFilePath);
750
- if (schema) {
751
- logger.debug(`[processZodNode] Successfully expanded factory function '${node.callee.name}'`);
752
- return schema;
753
- }
754
- }
755
- }
756
- logger.debug(`[processZodNode] Could not expand factory function '${node.callee.name}' - missing context or not a factory`);
757
- }
758
- // Handle standalone identifier references (e.g., userSchema used directly)
759
- if (t.isIdentifier(node)) {
760
- const schemaName = node.name;
761
- // Try to find and process the referenced schema
762
- if (!this.zodSchemas[schemaName]) {
763
- this.convertZodSchemaToOpenApi(schemaName);
764
- }
765
- // Return a reference to the schema
766
- return { $ref: `#/components/schemas/${schemaName}` };
767
- }
768
- logger.debug("Unknown Zod schema node:", node);
769
- return { type: "object" };
770
- }
771
- /**
772
- * Process a Zod lazy schema: z.lazy(() => Schema)
773
- */
774
- processZodLazy(node) {
775
- // Get the function in z.lazy(() => Schema)
776
- if (node.arguments.length > 0 &&
777
- t.isArrowFunctionExpression(node.arguments[0]) &&
778
- node.arguments[0].body) {
779
- const returnExpr = node.arguments[0].body;
780
- // If the function returns an identifier, it's likely a reference to another schema
781
- if (t.isIdentifier(returnExpr)) {
782
- const schemaName = returnExpr.name;
783
- // Create a reference to the schema
784
- return { $ref: `#/components/schemas/${schemaName}` };
785
- }
786
- // If the function returns a complex expression, try to process it
787
- return this.processZodNode(returnExpr);
788
- }
789
- return { type: "object" };
790
- }
791
- /**
792
- * Process a Zod literal schema: z.literal("value")
793
- */
794
- processZodLiteral(node) {
795
- if (node.arguments.length === 0) {
796
- return { type: "string" };
797
- }
798
- const arg = node.arguments[0];
799
- if (t.isStringLiteral(arg)) {
800
- return {
801
- type: "string",
802
- enum: [arg.value],
803
- };
804
- }
805
- else if (t.isNumericLiteral(arg)) {
806
- return {
807
- type: "number",
808
- enum: [arg.value],
809
- };
810
- }
811
- else if (t.isBooleanLiteral(arg)) {
812
- return {
813
- type: "boolean",
814
- enum: [arg.value],
815
- };
816
- }
817
- return { type: "string" };
818
- }
819
- /**
820
- * Process a Zod discriminated union: z.discriminatedUnion("type", [schema1, schema2])
821
- */
822
- processZodDiscriminatedUnion(node) {
823
- if (node.arguments.length < 2) {
824
- return { type: "object" };
825
- }
826
- // Get the discriminator field name
827
- let discriminator = "";
828
- if (t.isStringLiteral(node.arguments[0])) {
829
- discriminator = node.arguments[0].value;
830
- }
831
- // Get the schemas array
832
- const schemasArray = node.arguments[1];
833
- if (!t.isArrayExpression(schemasArray)) {
834
- return { type: "object" };
835
- }
836
- const schemas = schemasArray.elements
837
- .map((element) => this.processZodNode(element))
838
- .filter((schema) => schema !== null);
839
- if (schemas.length === 0) {
840
- return { type: "object" };
841
- }
842
- // Create a discriminated mapping for oneOf
843
- return {
844
- type: "object",
845
- discriminator: discriminator
846
- ? {
847
- propertyName: discriminator,
848
- }
849
- : undefined,
850
- oneOf: schemas,
851
- };
852
- }
853
- /**
854
- * Process a Zod tuple schema: z.tuple([z.string(), z.number()])
855
- */
856
- processZodTuple(node) {
857
- if (node.arguments.length === 0 ||
858
- !t.isArrayExpression(node.arguments[0])) {
859
- return { type: "array", items: { type: "string" } };
860
- }
861
- const tupleItems = node.arguments[0].elements.map((element) => this.processZodNode(element));
862
- // In OpenAPI, we can represent this as an array with prefixItems (OpenAPI 3.1+)
863
- // For OpenAPI 3.0.x, we'll use items with type: array
864
- return {
865
- type: "array",
866
- items: tupleItems.length > 0 ? tupleItems[0] : { type: "string" },
867
- // For OpenAPI 3.1+: prefixItems: tupleItems
868
- };
869
- }
870
- /**
871
- * Process a Zod intersection schema: z.intersection(schema1, schema2)
872
- */
873
- processZodIntersection(node) {
874
- if (node.arguments.length < 2) {
875
- return { type: "object" };
876
- }
877
- const schema1 = this.processZodNode(node.arguments[0]);
878
- const schema2 = this.processZodNode(node.arguments[1]);
879
- // In OpenAPI, we can use allOf to represent intersection
880
- return {
881
- allOf: [schema1, schema2],
882
- };
883
- }
884
- /**
885
- * Process a Zod union schema: z.union([schema1, schema2])
886
- */
887
- processZodUnion(node) {
888
- if (node.arguments.length === 0 ||
889
- !t.isArrayExpression(node.arguments[0])) {
890
- return { type: "object" };
891
- }
892
- const unionItems = node.arguments[0].elements.map((element) => this.processZodNode(element));
893
- // Check for common pattern: z.union([z.string(), z.null()]) which should be nullable string
894
- if (unionItems.length === 2) {
895
- const isNullable = unionItems.some((item) => item.type === "null" ||
896
- (item.enum && item.enum.length === 1 && item.enum[0] === null));
897
- if (isNullable) {
898
- const nonNullItem = unionItems.find((item) => item.type !== "null" &&
899
- !(item.enum && item.enum.length === 1 && item.enum[0] === null));
900
- if (nonNullItem) {
901
- return {
902
- ...nonNullItem,
903
- nullable: true,
904
- };
905
- }
906
- }
907
- }
908
- // Check if all union items are of the same type with different enum values
909
- // This is common for string literals like: z.union([z.literal("a"), z.literal("b")])
910
- const allSameType = unionItems.length > 0 &&
911
- unionItems.every((item) => item.type === unionItems[0].type && item.enum);
912
- if (allSameType) {
913
- // Combine all enum values
914
- const combinedEnums = unionItems.flatMap((item) => item.enum || []);
915
- return {
916
- type: unionItems[0].type,
917
- enum: combinedEnums,
918
- };
919
- }
920
- // Otherwise, use oneOf for general unions
921
- return {
922
- oneOf: unionItems,
923
- };
924
- }
925
- /**
926
- * Process a Zod object schema: z.object({...})
927
- */
928
- processZodObject(node) {
929
- if (node.arguments.length === 0 ||
930
- !t.isObjectExpression(node.arguments[0])) {
931
- return { type: "object" };
932
- }
933
- const objectExpression = node.arguments[0];
934
- const properties = {};
935
- const required = [];
936
- objectExpression.properties.forEach((prop, index) => {
937
- if (t.isObjectProperty(prop)) {
938
- let propName;
939
- // Handle both identifier and string literal keys
940
- if (t.isIdentifier(prop.key)) {
941
- propName = prop.key.name;
942
- }
943
- else if (t.isStringLiteral(prop.key)) {
944
- propName = prop.key.value;
945
- }
946
- else {
947
- logger.debug(`Skipping property ${index} - unsupported key type`);
948
- return; // Skip if key is not identifier or string literal
949
- }
950
- if (t.isCallExpression(prop.value) &&
951
- t.isMemberExpression(prop.value.callee) &&
952
- t.isIdentifier(prop.value.callee.object)) {
953
- const schemaName = prop.value.callee.object.name;
954
- // @ts-ignore
955
- const methodName = prop.value.callee.property.name;
956
- // Process base schema first
957
- if (!this.zodSchemas[schemaName]) {
958
- this.convertZodSchemaToOpenApi(schemaName);
959
- }
960
- // For describe method, use reference with description
961
- if (methodName === "describe" && this.zodSchemas[schemaName]) {
962
- if (prop.value.arguments.length > 0 &&
963
- t.isStringLiteral(prop.value.arguments[0])) {
964
- properties[propName] = {
965
- allOf: [{ $ref: `#/components/schemas/${schemaName}` }],
966
- description: prop.value.arguments[0].value,
967
- };
968
- }
969
- else {
970
- properties[propName] = {
971
- $ref: `#/components/schemas/${schemaName}`,
972
- };
973
- }
974
- required.push(propName);
975
- return;
976
- }
977
- // For other methods, process normally
978
- const processedSchema = this.processZodNode(prop.value);
979
- if (processedSchema) {
980
- properties[propName] = processedSchema;
981
- const isOptional = this.isOptional(prop.value) || this.hasOptionalMethod(prop.value);
982
- if (!isOptional) {
983
- required.push(propName);
984
- }
985
- }
986
- return;
987
- }
988
- // Check if the property value is an identifier (reference to another schema)
989
- if (t.isIdentifier(prop.value)) {
990
- const referencedSchemaName = prop.value.name;
991
- // Try to find and convert the referenced schema
992
- if (!this.zodSchemas[referencedSchemaName]) {
993
- this.convertZodSchemaToOpenApi(referencedSchemaName);
994
- }
995
- // Create a reference
996
- properties[propName] = {
997
- $ref: `#/components/schemas/${referencedSchemaName}`,
998
- };
999
- required.push(propName); // Assuming it's required unless marked optional
1000
- return; // Skip further processing for this property
1001
- }
1002
- // For array of schemas (like z.array(PaymentMethodSchema))
1003
- if (t.isCallExpression(prop.value) &&
1004
- t.isMemberExpression(prop.value.callee) &&
1005
- t.isIdentifier(prop.value.callee.object) &&
1006
- prop.value.callee.object.name === "z" &&
1007
- t.isIdentifier(prop.value.callee.property) &&
1008
- prop.value.callee.property.name === "array" &&
1009
- prop.value.arguments.length > 0 &&
1010
- t.isIdentifier(prop.value.arguments[0])) {
1011
- const itemSchemaName = prop.value.arguments[0].name;
1012
- // Try to find and convert the referenced schema
1013
- if (!this.zodSchemas[itemSchemaName]) {
1014
- this.convertZodSchemaToOpenApi(itemSchemaName);
1015
- }
1016
- // Process as array with reference
1017
- const arraySchema = this.processZodNode(prop.value);
1018
- arraySchema.items = {
1019
- $ref: `#/components/schemas/${itemSchemaName}`,
1020
- };
1021
- properties[propName] = arraySchema;
1022
- const isOptional = this.isOptional(prop.value) || this.hasOptionalMethod(prop.value);
1023
- if (!isOptional) {
1024
- required.push(propName);
1025
- }
1026
- return; // Skip further processing for this property
1027
- }
1028
- // Process property value (a Zod schema)
1029
- const propSchema = this.processZodNode(prop.value);
1030
- if (propSchema) {
1031
- properties[propName] = propSchema;
1032
- // If the property is not marked as optional, add it to required list
1033
- const isOptional =
1034
- // @ts-ignore
1035
- this.isOptional(prop.value) || this.hasOptionalMethod(prop.value);
1036
- if (!isOptional) {
1037
- required.push(propName);
1038
- }
1039
- }
1040
- }
1041
- });
1042
- const schema = {
1043
- type: "object",
1044
- properties,
1045
- };
1046
- if (required.length > 0) {
1047
- // Deduplicate required array using Set
1048
- // @ts-ignore
1049
- schema.required = [...new Set(required)];
1050
- }
1051
- return schema;
1052
- }
1053
- /**
1054
- * Process a Zod primitive schema: z.string(), z.number(), etc.
1055
- */
1056
- processZodPrimitive(node) {
1057
- if (!t.isMemberExpression(node.callee) ||
1058
- !t.isIdentifier(node.callee.property)) {
1059
- return { type: "string" };
1060
- }
1061
- const zodType = node.callee.property.name;
1062
- let schema = {};
1063
- // Basic type mapping
1064
- switch (zodType) {
1065
- case "string":
1066
- schema = { type: "string" };
1067
- break;
1068
- case "number":
1069
- schema = { type: "number" };
1070
- break;
1071
- case "boolean":
1072
- schema = { type: "boolean" };
1073
- break;
1074
- case "date":
1075
- schema = { type: "string", format: "date-time" };
1076
- break;
1077
- case "bigint":
1078
- schema = { type: "integer", format: "int64" };
1079
- break;
1080
- case "any":
1081
- case "unknown":
1082
- schema = {}; // Empty schema matches anything
1083
- break;
1084
- case "null":
1085
- case "undefined":
1086
- schema = { type: "null" };
1087
- break;
1088
- case "array":
1089
- let itemsType = { type: "string" };
1090
- if (node.arguments.length > 0) {
1091
- // Check if argument is an identifier (schema reference)
1092
- if (t.isIdentifier(node.arguments[0])) {
1093
- const schemaName = node.arguments[0].name;
1094
- // Try to find and convert the referenced schema
1095
- if (!this.zodSchemas[schemaName]) {
1096
- this.convertZodSchemaToOpenApi(schemaName);
1097
- }
1098
- // @ts-ignore
1099
- itemsType = { $ref: `#/components/schemas/${schemaName}` };
1100
- }
1101
- else {
1102
- // @ts-ignore
1103
- itemsType = this.processZodNode(node.arguments[0]);
1104
- }
1105
- }
1106
- schema = { type: "array", items: itemsType };
1107
- break;
1108
- case "enum":
1109
- if (node.arguments.length > 0 &&
1110
- t.isArrayExpression(node.arguments[0])) {
1111
- const enumValues = node.arguments[0].elements
1112
- .filter((el) => t.isStringLiteral(el) || t.isNumericLiteral(el))
1113
- // @ts-ignore
1114
- .map((el) => el.value);
1115
- const firstValue = enumValues[0];
1116
- const valueType = typeof firstValue;
1117
- schema = {
1118
- type: valueType === "number" ? "number" : "string",
1119
- enum: enumValues,
1120
- };
1121
- }
1122
- else if (node.arguments.length > 0 &&
1123
- t.isObjectExpression(node.arguments[0])) {
1124
- // Handle z.enum({ KEY1: "value1", KEY2: "value2" })
1125
- const enumValues = [];
1126
- node.arguments[0].properties.forEach((prop) => {
1127
- if (t.isObjectProperty(prop) && t.isStringLiteral(prop.value)) {
1128
- enumValues.push(prop.value.value);
1129
- }
1130
- });
1131
- if (enumValues.length > 0) {
1132
- schema = {
1133
- type: "string",
1134
- enum: enumValues,
1135
- };
1136
- }
1137
- else {
1138
- schema = { type: "string" };
1139
- }
1140
- }
1141
- else {
1142
- schema = { type: "string" };
1143
- }
1144
- break;
1145
- case "record":
1146
- let valueType = { type: "string" };
1147
- if (node.arguments.length > 0) {
1148
- valueType = this.processZodNode(node.arguments[0]);
1149
- }
1150
- schema = {
1151
- type: "object",
1152
- additionalProperties: valueType,
1153
- };
1154
- break;
1155
- case "map":
1156
- schema = {
1157
- type: "object",
1158
- additionalProperties: true,
1159
- };
1160
- break;
1161
- case "set":
1162
- let setItemType = { type: "string" };
1163
- if (node.arguments.length > 0) {
1164
- setItemType = this.processZodNode(node.arguments[0]);
1165
- }
1166
- schema = {
1167
- type: "array",
1168
- items: setItemType,
1169
- uniqueItems: true,
1170
- };
1171
- break;
1172
- case "object":
1173
- if (node.arguments.length > 0) {
1174
- schema = this.processZodObject(node);
1175
- }
1176
- else {
1177
- schema = { type: "object" };
1178
- }
1179
- break;
1180
- case "custom":
1181
- // Check if it has TypeScript generic type parameters (z.custom<File>())
1182
- if (node.typeParameters && node.typeParameters.params.length > 0) {
1183
- const typeParam = node.typeParameters.params[0];
1184
- // Check if the generic type is File
1185
- if (t.isTSTypeReference(typeParam) &&
1186
- t.isIdentifier(typeParam.typeName) &&
1187
- typeParam.typeName.name === "File") {
1188
- schema = {
1189
- type: "string",
1190
- format: "binary",
1191
- };
1192
- }
1193
- else {
1194
- // Other generic types default to string
1195
- schema = { type: "string" };
1196
- }
1197
- }
1198
- else if (node.arguments.length > 0 &&
1199
- t.isArrowFunctionExpression(node.arguments[0])) {
1200
- // Legacy support: FormData validation
1201
- schema = {
1202
- type: "object",
1203
- additionalProperties: true,
1204
- };
1205
- }
1206
- else {
1207
- // Default case for z.custom() without specific type detection
1208
- schema = { type: "string" };
1209
- }
1210
- break;
1211
- default:
1212
- schema = { type: "string" };
1213
- break;
1214
- }
1215
- // Extract description if it exists from direct method calls
1216
- const description = this.extractDescriptionFromArguments(node);
1217
- if (description) {
1218
- schema.description = description;
1219
- }
1220
- return schema;
1221
- }
1222
- /**
1223
- * Extract description from method arguments if it's a .describe() call
1224
- */
1225
- extractDescriptionFromArguments(node) {
1226
- if (t.isMemberExpression(node.callee) &&
1227
- t.isIdentifier(node.callee.property) &&
1228
- node.callee.property.name === "describe" &&
1229
- node.arguments.length > 0 &&
1230
- t.isStringLiteral(node.arguments[0])) {
1231
- return node.arguments[0].value;
1232
- }
1233
- return null;
1234
- }
1235
- /**
1236
- * Process a Zod chained method call: z.string().email().min(5)
1237
- */
1238
- processZodChain(node) {
1239
- if (!t.isMemberExpression(node.callee) ||
1240
- !t.isIdentifier(node.callee.property)) {
1241
- return { type: "object" };
1242
- }
1243
- const methodName = node.callee.property.name;
1244
- // Process the parent chain first
1245
- let schema = this.processZodNode(node.callee.object);
1246
- // Apply the current method
1247
- switch (methodName) {
1248
- case "optional":
1249
- // optional means T | undefined — not in required array, no nullable flag
1250
- // Required array exclusion is handled by hasOptionalMethod() in processZodObject()
1251
- break;
1252
- case "nullable":
1253
- // nullable means T | null — field stays required but can be null
1254
- if (!schema.allOf) {
1255
- schema.nullable = true;
1256
- }
1257
- break;
1258
- case "nullish": // T | null | undefined
1259
- // Not in required array (handled by hasOptionalMethod) AND can be null
1260
- if (!schema.allOf) {
1261
- schema.nullable = true;
1262
- }
1263
- break;
1264
- case "describe":
1265
- if (node.arguments.length > 0 && t.isStringLiteral(node.arguments[0])) {
1266
- const description = node.arguments[0].value;
1267
- // Check if description includes @deprecated
1268
- if (description.startsWith("@deprecated")) {
1269
- schema.deprecated = true;
1270
- // Remove @deprecated from description
1271
- schema.description = description.replace("@deprecated", "").trim();
1272
- }
1273
- else {
1274
- schema.description = description;
1275
- }
1276
- }
1277
- break;
1278
- case "deprecated":
1279
- schema.deprecated = true;
1280
- break;
1281
- case "min":
1282
- if (node.arguments.length > 0 &&
1283
- t.isNumericLiteral(node.arguments[0])) {
1284
- if (schema.type === "string") {
1285
- schema.minLength = node.arguments[0].value;
1286
- }
1287
- else if (schema.type === "number" || schema.type === "integer") {
1288
- schema.minimum = node.arguments[0].value;
1289
- }
1290
- else if (schema.type === "array") {
1291
- schema.minItems = node.arguments[0].value;
1292
- }
1293
- }
1294
- break;
1295
- case "max":
1296
- if (node.arguments.length > 0 &&
1297
- t.isNumericLiteral(node.arguments[0])) {
1298
- if (schema.type === "string") {
1299
- schema.maxLength = node.arguments[0].value;
1300
- }
1301
- else if (schema.type === "number" || schema.type === "integer") {
1302
- schema.maximum = node.arguments[0].value;
1303
- }
1304
- else if (schema.type === "array") {
1305
- schema.maxItems = node.arguments[0].value;
1306
- }
1307
- }
1308
- break;
1309
- case "length":
1310
- if (node.arguments.length > 0 &&
1311
- t.isNumericLiteral(node.arguments[0])) {
1312
- if (schema.type === "string") {
1313
- schema.minLength = node.arguments[0].value;
1314
- schema.maxLength = node.arguments[0].value;
1315
- }
1316
- else if (schema.type === "array") {
1317
- schema.minItems = node.arguments[0].value;
1318
- schema.maxItems = node.arguments[0].value;
1319
- }
1320
- }
1321
- break;
1322
- case "email":
1323
- schema.format = "email";
1324
- break;
1325
- case "url":
1326
- schema.format = "uri";
1327
- break;
1328
- case "uri":
1329
- schema.format = "uri";
1330
- break;
1331
- case "uuid":
1332
- schema.format = "uuid";
1333
- break;
1334
- case "cuid":
1335
- schema.format = "cuid";
1336
- break;
1337
- case "regex":
1338
- if (node.arguments.length > 0 && t.isRegExpLiteral(node.arguments[0])) {
1339
- schema.pattern = node.arguments[0].pattern;
1340
- }
1341
- break;
1342
- case "startsWith":
1343
- if (node.arguments.length > 0 && t.isStringLiteral(node.arguments[0])) {
1344
- schema.pattern = `^${this.escapeRegExp(node.arguments[0].value)}`;
1345
- }
1346
- break;
1347
- case "endsWith":
1348
- if (node.arguments.length > 0 && t.isStringLiteral(node.arguments[0])) {
1349
- schema.pattern = `${this.escapeRegExp(node.arguments[0].value)}`;
1350
- }
1351
- case "includes":
1352
- if (node.arguments.length > 0 && t.isStringLiteral(node.arguments[0])) {
1353
- schema.pattern = this.escapeRegExp(node.arguments[0].value);
1354
- }
1355
- break;
1356
- case "int":
1357
- schema.type = "integer";
1358
- break;
1359
- case "positive":
1360
- schema.minimum = 0;
1361
- schema.exclusiveMinimum = true;
1362
- break;
1363
- case "nonnegative":
1364
- schema.minimum = 0;
1365
- break;
1366
- case "negative":
1367
- schema.maximum = 0;
1368
- schema.exclusiveMaximum = true;
1369
- break;
1370
- case "nonpositive":
1371
- schema.maximum = 0;
1372
- break;
1373
- case "finite":
1374
- // Can't express directly in OpenAPI
1375
- break;
1376
- case "safe":
1377
- // Number is within the IEEE-754 "safe integer" range
1378
- schema.minimum = -9007199254740991; // -(2^53 - 1)
1379
- schema.maximum = 9007199254740991; // 2^53 - 1
1380
- break;
1381
- case "default":
1382
- if (node.arguments.length > 0) {
1383
- if (t.isStringLiteral(node.arguments[0])) {
1384
- schema.default = node.arguments[0].value;
1385
- }
1386
- else if (t.isNumericLiteral(node.arguments[0])) {
1387
- schema.default = node.arguments[0].value;
1388
- }
1389
- else if (t.isBooleanLiteral(node.arguments[0])) {
1390
- schema.default = node.arguments[0].value;
1391
- }
1392
- else if (t.isNullLiteral(node.arguments[0])) {
1393
- schema.default = null;
1394
- }
1395
- else if (t.isObjectExpression(node.arguments[0])) {
1396
- // Try to create a default object, but this might not be complete
1397
- const defaultObj = {};
1398
- node.arguments[0].properties.forEach((prop) => {
1399
- if (t.isObjectProperty(prop) &&
1400
- (t.isIdentifier(prop.key) || t.isStringLiteral(prop.key)) &&
1401
- (t.isStringLiteral(prop.value) ||
1402
- t.isNumericLiteral(prop.value) ||
1403
- t.isBooleanLiteral(prop.value))) {
1404
- const key = t.isIdentifier(prop.key)
1405
- ? prop.key.name
1406
- : prop.key.value;
1407
- const value = t.isStringLiteral(prop.value)
1408
- ? prop.value.value
1409
- : t.isNumericLiteral(prop.value)
1410
- ? prop.value.value
1411
- : t.isBooleanLiteral(prop.value)
1412
- ? prop.value.value
1413
- : null;
1414
- if (key !== null && value !== null) {
1415
- defaultObj[key] = value;
1416
- }
1417
- }
1418
- });
1419
- schema.default = defaultObj;
1420
- }
1421
- }
1422
- break;
1423
- case "extend":
1424
- if (node.arguments.length > 0 &&
1425
- t.isObjectExpression(node.arguments[0])) {
1426
- // Get the base schema by processing the object that extend is called on
1427
- const baseSchemaResult = this.processZodNode(node.callee.object);
1428
- // If it's a reference, resolve it to the actual schema
1429
- let baseSchema = baseSchemaResult;
1430
- if (baseSchemaResult && baseSchemaResult.$ref) {
1431
- const schemaName = baseSchemaResult.$ref.replace("#/components/schemas/", "");
1432
- // Try to convert the base schema if not already processed
1433
- if (!this.zodSchemas[schemaName]) {
1434
- logger.debug(`[extend] Base schema ${schemaName} not found, attempting to convert it`);
1435
- this.convertZodSchemaToOpenApi(schemaName);
1436
- }
1437
- // Now retrieve the converted schema
1438
- if (this.zodSchemas[schemaName]) {
1439
- baseSchema = this.zodSchemas[schemaName];
1440
- }
1441
- else {
1442
- logger.debug(`Could not resolve reference for extend: ${schemaName}`);
1443
- }
1444
- }
1445
- // Process the extension object
1446
- const extendNode = {
1447
- type: "CallExpression",
1448
- callee: {
1449
- type: "MemberExpression",
1450
- object: { type: "Identifier", name: "z" },
1451
- property: { type: "Identifier", name: "object" },
1452
- computed: false,
1453
- optional: false,
1454
- },
1455
- arguments: [node.arguments[0]],
1456
- };
1457
- const extendedProps = this.processZodObject(extendNode);
1458
- // Merge base schema and extensions
1459
- if (baseSchema && baseSchema.properties) {
1460
- schema = {
1461
- type: "object",
1462
- properties: {
1463
- ...baseSchema.properties,
1464
- ...(extendedProps?.properties || {}),
1465
- },
1466
- required: [
1467
- ...(baseSchema.required || []),
1468
- ...(extendedProps?.required || []),
1469
- ].filter((item, index, arr) => arr.indexOf(item) === index), // Remove duplicates
1470
- };
1471
- // Copy other properties from base schema
1472
- if (baseSchema.description)
1473
- schema.description = baseSchema.description;
1474
- }
1475
- else {
1476
- logger.debug("Could not resolve base schema for extend");
1477
- schema = extendedProps || { type: "object" };
1478
- }
1479
- }
1480
- break;
1481
- case "refine":
1482
- case "superRefine":
1483
- // These are custom validators that cannot be easily represented in OpenAPI
1484
- // We preserve the schema as is
1485
- break;
1486
- case "transform":
1487
- // Transform doesn't change the schema validation, only the output format
1488
- break;
1489
- case "or":
1490
- if (node.arguments.length > 0) {
1491
- const alternativeSchema = this.processZodNode(node.arguments[0]);
1492
- if (alternativeSchema) {
1493
- schema = {
1494
- oneOf: [schema, alternativeSchema],
1495
- };
1496
- }
1497
- }
1498
- break;
1499
- case "and":
1500
- if (node.arguments.length > 0) {
1501
- const additionalSchema = this.processZodNode(node.arguments[0]);
1502
- if (additionalSchema) {
1503
- schema = {
1504
- allOf: [schema, additionalSchema],
1505
- };
1506
- }
1507
- }
1508
- break;
1509
- }
1510
- return schema;
1511
- }
1512
- /**
1513
- * Helper to escape special regex characters for pattern creation
1514
- */
1515
- escapeRegExp(string) {
1516
- return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1517
- }
1518
- /**
1519
- * Check if a Zod schema is optional
1520
- */
1521
- isOptional(node) {
1522
- // Direct .optional() call
1523
- if (t.isCallExpression(node) &&
1524
- t.isMemberExpression(node.callee) &&
1525
- t.isIdentifier(node.callee.property) &&
1526
- node.callee.property.name === "optional") {
1527
- return true;
1528
- }
1529
- // Check for chained calls that end with .optional()
1530
- if (t.isCallExpression(node) &&
1531
- t.isMemberExpression(node.callee) &&
1532
- t.isCallExpression(node.callee.object)) {
1533
- return this.hasOptionalMethod(node);
1534
- }
1535
- return false;
1536
- }
1537
- /**
1538
- * Check if a node has .optional() in its method chain
1539
- */
1540
- hasOptionalMethod(node) {
1541
- if (!t.isCallExpression(node)) {
1542
- return false;
1543
- }
1544
- if (t.isMemberExpression(node.callee) &&
1545
- t.isIdentifier(node.callee.property) &&
1546
- (node.callee.property.name === "optional" ||
1547
- node.callee.property.name === "nullish")) {
1548
- return true;
1549
- }
1550
- if (t.isMemberExpression(node.callee) &&
1551
- t.isCallExpression(node.callee.object)) {
1552
- return this.hasOptionalMethod(node.callee.object);
1553
- }
1554
- return false;
1555
- }
1556
- /**
1557
- * Get all processed Zod schemas
1558
- */
1559
- getProcessedSchemas() {
1560
- return this.zodSchemas;
1561
- }
1562
- /**
1563
- * Pre-scan all files to build type mappings
1564
- */
1565
- preScanForTypeMappings() {
1566
- logger.debug("Pre-scanning for type mappings...");
1567
- // Scan route files
1568
- const routeFiles = this.findRouteFiles();
1569
- for (const routeFile of routeFiles) {
1570
- this.scanFileForTypeMappings(routeFile);
1571
- }
1572
- // Scan schema directories
1573
- for (const dir of this.schemaDirs) {
1574
- this.scanDirectoryForTypeMappings(dir);
1575
- }
1576
- }
1577
- /**
1578
- * Scan a single file for type mappings
1579
- */
1580
- scanFileForTypeMappings(filePath) {
1581
- try {
1582
- const content = fs.readFileSync(filePath, "utf-8");
1583
- const ast = parseTypeScriptFile(content);
1584
- traverse(ast, {
1585
- TSTypeAliasDeclaration: (path) => {
1586
- if (t.isIdentifier(path.node.id)) {
1587
- const typeName = path.node.id.name;
1588
- // Check for z.infer<typeof SchemaName> pattern
1589
- if (t.isTSTypeReference(path.node.typeAnnotation)) {
1590
- const typeRef = path.node.typeAnnotation;
1591
- // Handle both z.infer and just infer (when z is imported)
1592
- let isInferType = false;
1593
- if (t.isTSQualifiedName(typeRef.typeName) &&
1594
- t.isIdentifier(typeRef.typeName.left) &&
1595
- typeRef.typeName.left.name === "z" &&
1596
- t.isIdentifier(typeRef.typeName.right) &&
1597
- typeRef.typeName.right.name === "infer") {
1598
- isInferType = true;
1599
- }
1600
- else if (t.isIdentifier(typeRef.typeName) &&
1601
- typeRef.typeName.name === "infer") {
1602
- isInferType = true;
1603
- }
1604
- if (isInferType &&
1605
- typeRef.typeParameters &&
1606
- typeRef.typeParameters.params.length > 0) {
1607
- const param = typeRef.typeParameters.params[0];
1608
- if (t.isTSTypeQuery(param) && t.isIdentifier(param.exprName)) {
1609
- const referencedSchemaName = param.exprName.name;
1610
- this.typeToSchemaMapping[typeName] = referencedSchemaName;
1611
- logger.debug(`Pre-scan: Mapped type '${typeName}' to schema '${referencedSchemaName}'`);
1612
- }
1613
- }
1614
- }
1615
- }
1616
- },
1617
- });
1618
- }
1619
- catch (error) {
1620
- logger.error(`Error scanning file ${filePath} for type mappings: ${error}`);
1621
- }
1622
- }
1623
- /**
1624
- * Recursively scan directory for type mappings
1625
- */
1626
- scanDirectoryForTypeMappings(dir) {
1627
- try {
1628
- const files = fs.readdirSync(dir);
1629
- for (const file of files) {
1630
- const filePath = path.join(dir, file);
1631
- const stats = fs.statSync(filePath);
1632
- if (stats.isDirectory()) {
1633
- this.scanDirectoryForTypeMappings(filePath);
1634
- }
1635
- else if (file.endsWith(".ts") || file.endsWith(".tsx")) {
1636
- this.scanFileForTypeMappings(filePath);
1637
- }
1638
- }
1639
- }
1640
- catch (error) {
1641
- logger.error(`Error scanning directory ${dir} for type mappings: ${error}`);
1642
- }
1643
- }
1644
- /**
1645
- * Pre-process all Zod schemas in a file
1646
- */
1647
- preprocessAllSchemasInFile(filePath) {
1648
- try {
1649
- const content = fs.readFileSync(filePath, "utf-8");
1650
- const ast = parseTypeScriptFile(content);
1651
- // Cache AST for later use
1652
- this.fileASTCache.set(filePath, ast);
1653
- // Collect imports to enable factory function resolution during preprocessing
1654
- let importedModules = {};
1655
- // First, collect all drizzle-zod imports and regular imports
1656
- traverse(ast, {
1657
- ImportDeclaration: (path) => {
1658
- const source = path.node.source.value;
1659
- if (source === "drizzle-zod") {
1660
- path.node.specifiers.forEach((specifier) => {
1661
- if (t.isImportSpecifier(specifier) ||
1662
- t.isImportDefaultSpecifier(specifier)) {
1663
- this.drizzleZodImports.add(specifier.local.name);
1664
- }
1665
- });
1666
- }
1667
- // Track all imports for factory function resolution
1668
- path.node.specifiers.forEach((specifier) => {
1669
- if (t.isImportSpecifier(specifier) ||
1670
- t.isImportDefaultSpecifier(specifier)) {
1671
- const importedName = specifier.local.name;
1672
- importedModules[importedName] = source;
1673
- }
1674
- });
1675
- },
1676
- });
1677
- // Cache imports for this file
1678
- this.fileImportsCache.set(filePath, importedModules);
1679
- // Set current processing context for factory function expansion
1680
- this.currentFilePath = filePath;
1681
- this.currentAST = ast;
1682
- this.currentImports = importedModules;
1683
- // Collect all exported Zod schemas
1684
- traverse(ast, {
1685
- ExportNamedDeclaration: (path) => {
1686
- if (t.isVariableDeclaration(path.node.declaration)) {
1687
- path.node.declaration.declarations.forEach((declaration) => {
1688
- if (t.isIdentifier(declaration.id) && declaration.init) {
1689
- const schemaName = declaration.id.name;
1690
- // Check if is Zos schema
1691
- if (this.isZodSchema(declaration.init) &&
1692
- !this.zodSchemas[schemaName]) {
1693
- logger.debug(`Pre-processing Zod schema: ${schemaName}`);
1694
- this.processingSchemas.add(schemaName);
1695
- const schema = this.processZodNode(declaration.init);
1696
- if (schema) {
1697
- this.zodSchemas[schemaName] = schema;
1698
- }
1699
- this.processingSchemas.delete(schemaName);
1700
- }
1701
- }
1702
- });
1703
- }
1704
- },
1705
- // Also process non-exported const declarations
1706
- VariableDeclaration: (path) => {
1707
- path.node.declarations.forEach((declaration) => {
1708
- if (t.isIdentifier(declaration.id) && declaration.init) {
1709
- const schemaName = declaration.id.name;
1710
- if (this.isZodSchema(declaration.init) &&
1711
- !this.zodSchemas[schemaName] &&
1712
- !this.processingSchemas.has(schemaName)) {
1713
- logger.debug(`Pre-processing Zod schema: ${schemaName}`);
1714
- this.processingSchemas.add(schemaName);
1715
- const schema = this.processZodNode(declaration.init);
1716
- if (schema) {
1717
- this.zodSchemas[schemaName] = schema;
1718
- }
1719
- this.processingSchemas.delete(schemaName);
1720
- }
1721
- }
1722
- });
1723
- },
1724
- });
1725
- }
1726
- catch (error) {
1727
- logger.error(`Error pre-processing file ${filePath}: ${error}`);
1728
- }
1729
- }
1730
- /**
1731
- * Check if node is Zod schema
1732
- */
1733
- isZodSchema(node) {
1734
- if (t.isCallExpression(node)) {
1735
- // Check for drizzle-zod helper functions (e.g., createInsertSchema, createSelectSchema)
1736
- if (t.isIdentifier(node.callee) &&
1737
- this.drizzleZodImports.has(node.callee.name)) {
1738
- logger.debug(`[isZodSchema] Detected drizzle-zod function: ${node.callee.name}`);
1739
- return true;
1740
- }
1741
- // Check direct z.method() calls
1742
- if (t.isMemberExpression(node.callee) &&
1743
- t.isIdentifier(node.callee.object) &&
1744
- node.callee.object.name === "z") {
1745
- return true;
1746
- }
1747
- // Check chained calls like z.string().regex()
1748
- if (t.isMemberExpression(node.callee) &&
1749
- t.isCallExpression(node.callee.object)) {
1750
- return this.isZodSchema(node.callee.object);
1751
- }
1752
- // Do NOT treat unknown function calls as potential Zod schemas here
1753
- // Factory functions will be detected and handled in processZodNode() instead
1754
- // This prevents false positives during preprocessing
1755
- }
1756
- return false;
1757
- }
1758
- /**
1759
- * Find a factory function by name (lazy detection with caching)
1760
- * @param functionName - Name of the function to find
1761
- * @param currentFilePath - Path of the current file being processed
1762
- * @param currentAST - Already parsed AST of current file
1763
- * @param importedModules - Map of imported module names to their sources
1764
- * @returns Factory function node if found and returns Zod schema, null otherwise
1765
- */
1766
- findFactoryFunction(functionName, currentFilePath, currentAST, importedModules) {
1767
- // Check positive cache first
1768
- if (this.factoryCache.has(functionName)) {
1769
- logger.debug(`[Factory] Cache hit for function '${functionName}'`);
1770
- return this.factoryCache.get(functionName);
1771
- }
1772
- // Check negative cache (already checked, not a factory)
1773
- if (this.factoryCheckCache.has(functionName)) {
1774
- logger.debug(`[Factory] Negative cache hit for function '${functionName}'`);
1775
- return null;
1776
- }
1777
- logger.debug(`[Factory] Searching for function '${functionName}'`);
1778
- // Look in current file first (AST already parsed)
1779
- const localFactory = this.findFunctionInAST(currentAST, functionName);
1780
- if (localFactory && this.returnsZodSchema(localFactory)) {
1781
- logger.debug(`[Factory] Found Zod factory function '${functionName}' in current file`);
1782
- this.factoryCache.set(functionName, localFactory);
1783
- return localFactory;
1784
- }
1785
- // Check if function is imported
1786
- const importSource = importedModules[functionName];
1787
- if (importSource) {
1788
- logger.debug(`[Factory] Function '${functionName}' is imported from '${importSource}'`);
1789
- // Resolve import path
1790
- const importedFilePath = this.resolveImportPath(currentFilePath, importSource);
1791
- if (importedFilePath && fs.existsSync(importedFilePath)) {
1792
- logger.debug(`[Factory] Resolved import to: ${importedFilePath}`);
1793
- // Parse imported file (with caching) - this will also cache imports
1794
- const importedAST = this.parseFileWithCache(importedFilePath);
1795
- if (importedAST) {
1796
- const importedFactory = this.findFunctionInAST(importedAST, functionName);
1797
- if (importedFactory && this.returnsZodSchema(importedFactory)) {
1798
- logger.debug(`[Factory] Found Zod factory function '${functionName}' in imported file`);
1799
- this.factoryCache.set(functionName, importedFactory);
1800
- return importedFactory;
1801
- }
1802
- else {
1803
- logger.debug(`[Factory] Function '${functionName}' found in imported file but does not return Zod schema`);
1804
- }
1805
- }
1806
- }
1807
- else {
1808
- logger.debug(`[Factory] Could not resolve import path for '${importSource}'`);
1809
- }
1810
- }
1811
- else {
1812
- logger.debug(`[Factory] Function '${functionName}' is not imported`);
1813
- }
1814
- // Not found or not a Zod factory - cache negative result
1815
- logger.debug(`[Factory] Function '${functionName}' is not a Zod factory`);
1816
- this.factoryCheckCache.set(functionName, false);
1817
- return null;
1818
- }
1819
- /**
1820
- * Find a function definition in an AST
1821
- */
1822
- findFunctionInAST(ast, functionName) {
1823
- let foundFunction = null;
1824
- traverse(ast, {
1825
- // Handle: export function createSchema() { ... }
1826
- FunctionDeclaration: (path) => {
1827
- if (t.isIdentifier(path.node.id) && path.node.id.name === functionName) {
1828
- foundFunction = path.node;
1829
- path.stop();
1830
- }
1831
- },
1832
- // Handle: export const createSchema = (...) => { ... }
1833
- VariableDeclarator: (path) => {
1834
- if (t.isIdentifier(path.node.id) &&
1835
- path.node.id.name === functionName &&
1836
- (t.isArrowFunctionExpression(path.node.init) ||
1837
- t.isFunctionExpression(path.node.init))) {
1838
- foundFunction = path.node.init;
1839
- path.stop();
1840
- }
1841
- },
1842
- });
1843
- return foundFunction;
1844
- }
1845
- /**
1846
- * Check if a function returns a Zod schema by analyzing return statements
1847
- */
1848
- returnsZodSchema(functionNode) {
1849
- if (!t.isFunctionDeclaration(functionNode) &&
1850
- !t.isArrowFunctionExpression(functionNode) &&
1851
- !t.isFunctionExpression(functionNode)) {
1852
- return false;
1853
- }
1854
- let returnsZod = false;
1855
- // For arrow functions with direct return (no block)
1856
- if (t.isArrowFunctionExpression(functionNode) &&
1857
- !t.isBlockStatement(functionNode.body)) {
1858
- returnsZod = this.isZodSchema(functionNode.body);
1859
- logger.debug(`[Factory] Arrow function direct return, isZodSchema: ${returnsZod}`);
1860
- return returnsZod;
1861
- }
1862
- // For functions with block statements, analyze return statements manually
1863
- const body = functionNode.body;
1864
- if (!t.isBlockStatement(body)) {
1865
- return false;
1866
- }
1867
- // Manually walk through statements instead of using traverse
1868
- const checkStatements = (statements) => {
1869
- for (const stmt of statements) {
1870
- if (t.isReturnStatement(stmt) && stmt.argument) {
1871
- if (this.isZodSchema(stmt.argument)) {
1872
- logger.debug(`[Factory] Found Zod schema in return statement`);
1873
- return true;
1874
- }
1875
- }
1876
- // Check nested blocks (if statements, etc.)
1877
- else if (t.isIfStatement(stmt)) {
1878
- if (t.isBlockStatement(stmt.consequent)) {
1879
- if (checkStatements(stmt.consequent.body))
1880
- return true;
1881
- }
1882
- else if (t.isReturnStatement(stmt.consequent) && stmt.consequent.argument) {
1883
- if (this.isZodSchema(stmt.consequent.argument))
1884
- return true;
1885
- }
1886
- if (stmt.alternate) {
1887
- if (t.isBlockStatement(stmt.alternate)) {
1888
- if (checkStatements(stmt.alternate.body))
1889
- return true;
1890
- }
1891
- else if (t.isReturnStatement(stmt.alternate) && stmt.alternate.argument) {
1892
- if (this.isZodSchema(stmt.alternate.argument))
1893
- return true;
1894
- }
1895
- }
1896
- }
1897
- }
1898
- return false;
1899
- };
1900
- returnsZod = checkStatements(body.body);
1901
- return returnsZod;
1902
- }
1903
- /**
1904
- * Parse a file with caching (also caches imports)
1905
- */
1906
- parseFileWithCache(filePath) {
1907
- if (this.fileASTCache.has(filePath)) {
1908
- return this.fileASTCache.get(filePath);
1909
- }
1910
- try {
1911
- const content = fs.readFileSync(filePath, "utf-8");
1912
- const ast = parseTypeScriptFile(content);
1913
- this.fileASTCache.set(filePath, ast);
1914
- // Also build and cache imports for this file
1915
- if (!this.fileImportsCache.has(filePath)) {
1916
- const importedModules = {};
1917
- traverse(ast, {
1918
- ImportDeclaration: (path) => {
1919
- const source = path.node.source.value;
1920
- // Track drizzle-zod imports
1921
- if (source === "drizzle-zod") {
1922
- path.node.specifiers.forEach((specifier) => {
1923
- if (t.isImportSpecifier(specifier) ||
1924
- t.isImportDefaultSpecifier(specifier)) {
1925
- this.drizzleZodImports.add(specifier.local.name);
1926
- }
1927
- });
1928
- }
1929
- // Process each import specifier
1930
- path.node.specifiers.forEach((specifier) => {
1931
- if (t.isImportSpecifier(specifier) ||
1932
- t.isImportDefaultSpecifier(specifier)) {
1933
- const importedName = specifier.local.name;
1934
- importedModules[importedName] = source;
1935
- }
1936
- });
1937
- },
1938
- });
1939
- this.fileImportsCache.set(filePath, importedModules);
1940
- }
1941
- return ast;
1942
- }
1943
- catch (error) {
1944
- logger.error(`[Factory] Error parsing file '${filePath}': ${error}`);
1945
- return null;
1946
- }
1947
- }
1948
- /**
1949
- * Resolve import path relative to current file
1950
- */
1951
- resolveImportPath(currentFilePath, importSource) {
1952
- // Handle relative imports
1953
- if (importSource.startsWith(".")) {
1954
- const currentDir = path.dirname(currentFilePath);
1955
- let resolvedPath = path.resolve(currentDir, importSource);
1956
- // Try adding extensions if not present
1957
- const extensions = [".ts", ".tsx", ".js", ".jsx"];
1958
- if (!path.extname(resolvedPath)) {
1959
- for (const ext of extensions) {
1960
- const withExt = resolvedPath + ext;
1961
- if (fs.existsSync(withExt)) {
1962
- return withExt;
1963
- }
1964
- }
1965
- // Try index files
1966
- for (const ext of extensions) {
1967
- const indexPath = path.join(resolvedPath, `index${ext}`);
1968
- if (fs.existsSync(indexPath)) {
1969
- return indexPath;
1970
- }
1971
- }
1972
- }
1973
- else if (fs.existsSync(resolvedPath)) {
1974
- return resolvedPath;
1975
- }
1976
- }
1977
- // Handle absolute imports from schemaDir
1978
- // This is a simplified approach - you might need to enhance this based on tsconfig paths
1979
- return null;
1980
- }
1981
- /**
1982
- * Expand a factory function call by substituting arguments
1983
- */
1984
- expandFactoryCall(factoryNode, callNode, filePath) {
1985
- if (!t.isFunctionDeclaration(factoryNode) &&
1986
- !t.isArrowFunctionExpression(factoryNode) &&
1987
- !t.isFunctionExpression(factoryNode)) {
1988
- return null;
1989
- }
1990
- logger.debug(`[Factory] Expanding factory call with ${callNode.arguments.length} arguments`);
1991
- // Build parameter -> argument mapping
1992
- const paramMap = new Map();
1993
- const params = factoryNode.params;
1994
- for (let i = 0; i < params.length && i < callNode.arguments.length; i++) {
1995
- const param = params[i];
1996
- const arg = callNode.arguments[i];
1997
- if (t.isIdentifier(param)) {
1998
- paramMap.set(param.name, arg);
1999
- logger.debug(`[Factory] Mapped parameter '${param.name}' to argument`);
2000
- }
2001
- else if (t.isObjectPattern(param)) {
2002
- // Handle destructured parameters - simplified for now
2003
- logger.debug(`[Factory] Skipping destructured parameter (not yet supported)`);
2004
- }
2005
- }
2006
- // Extract return statement
2007
- const returnNode = this.extractReturnNode(factoryNode);
2008
- if (!returnNode) {
2009
- logger.debug(`[Factory] No return statement found in factory`);
2010
- return null;
2011
- }
2012
- logger.debug(`[Factory] Return node type: ${returnNode.type}`);
2013
- // Clone and substitute parameters in return node
2014
- const substitutedNode = this.substituteParameters(returnNode, paramMap, filePath);
2015
- logger.debug(`[Factory] Substituted node type: ${substitutedNode.type}`);
2016
- // Process the substituted node as a normal Zod schema
2017
- const result = this.processZodNode(substitutedNode);
2018
- if (result) {
2019
- logger.debug(`[Factory] Successfully processed substituted node, result has ${Object.keys(result).length} keys`);
2020
- }
2021
- else {
2022
- logger.debug(`[Factory] Failed to process substituted node`);
2023
- }
2024
- return result;
2025
- }
2026
- /**
2027
- * Extract the return node from a function
2028
- */
2029
- extractReturnNode(functionNode) {
2030
- // For arrow functions with direct return (no block)
2031
- if (t.isArrowFunctionExpression(functionNode) &&
2032
- !t.isBlockStatement(functionNode.body)) {
2033
- return functionNode.body;
2034
- }
2035
- // For functions with block statements
2036
- const body = t.isFunctionDeclaration(functionNode) ||
2037
- t.isArrowFunctionExpression(functionNode) ||
2038
- t.isFunctionExpression(functionNode)
2039
- ? functionNode.body
2040
- : null;
2041
- if (!body || !t.isBlockStatement(body)) {
2042
- return null;
2043
- }
2044
- // Find first return statement manually
2045
- const findReturn = (statements) => {
2046
- for (const stmt of statements) {
2047
- if (t.isReturnStatement(stmt) && stmt.argument) {
2048
- return stmt.argument;
2049
- }
2050
- // Check nested blocks
2051
- if (t.isIfStatement(stmt)) {
2052
- if (t.isBlockStatement(stmt.consequent)) {
2053
- const found = findReturn(stmt.consequent.body);
2054
- if (found)
2055
- return found;
2056
- }
2057
- else if (t.isReturnStatement(stmt.consequent) && stmt.consequent.argument) {
2058
- return stmt.consequent.argument;
2059
- }
2060
- if (stmt.alternate) {
2061
- if (t.isBlockStatement(stmt.alternate)) {
2062
- const found = findReturn(stmt.alternate.body);
2063
- if (found)
2064
- return found;
2065
- }
2066
- else if (t.isReturnStatement(stmt.alternate) && stmt.alternate.argument) {
2067
- return stmt.alternate.argument;
2068
- }
2069
- }
2070
- }
2071
- }
2072
- return null;
2073
- };
2074
- return findReturn(body.body);
2075
- }
2076
- /**
2077
- * Substitute parameters with actual arguments in an AST node (deep clone and replace)
2078
- */
2079
- substituteParameters(node, paramMap, filePath) {
2080
- // Deep clone the node to avoid modifying the original
2081
- const cloned = t.cloneNode(node, /* deep */ true, /* withoutLoc */ false);
2082
- // Manual recursive substitution without traverse
2083
- const substitute = (n) => {
2084
- if (t.isIdentifier(n)) {
2085
- // Replace if this is a parameter
2086
- if (paramMap.has(n.name)) {
2087
- const replacement = paramMap.get(n.name);
2088
- return t.cloneNode(replacement, true, false);
2089
- }
2090
- return n;
2091
- }
2092
- // Handle CallExpression
2093
- if (t.isCallExpression(n)) {
2094
- return t.callExpression(substitute(n.callee), n.arguments.map((arg) => {
2095
- if (t.isSpreadElement(arg)) {
2096
- return t.spreadElement(substitute(arg.argument));
2097
- }
2098
- return substitute(arg);
2099
- }));
2100
- }
2101
- // Handle MemberExpression
2102
- if (t.isMemberExpression(n)) {
2103
- return t.memberExpression(substitute(n.object), n.computed ? substitute(n.property) : n.property, n.computed);
2104
- }
2105
- // Handle ObjectExpression
2106
- if (t.isObjectExpression(n)) {
2107
- return t.objectExpression(n.properties.map((prop) => {
2108
- if (t.isObjectProperty(prop)) {
2109
- return t.objectProperty(prop.computed ? substitute(prop.key) : prop.key, substitute(prop.value), prop.computed, prop.shorthand);
2110
- }
2111
- if (t.isSpreadElement(prop)) {
2112
- return t.spreadElement(substitute(prop.argument));
2113
- }
2114
- return prop;
2115
- }));
2116
- }
2117
- // Handle ArrayExpression
2118
- if (t.isArrayExpression(n)) {
2119
- return t.arrayExpression(n.elements.map((elem) => {
2120
- if (!elem)
2121
- return null;
2122
- if (t.isSpreadElement(elem)) {
2123
- return t.spreadElement(substitute(elem.argument));
2124
- }
2125
- return substitute(elem);
2126
- }));
2127
- }
2128
- // Return as-is for other node types
2129
- return n;
2130
- };
2131
- return substitute(cloned);
2132
- }
2133
- }