next-openapi-gen 0.8.4 → 0.8.6

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.
package/README.md CHANGED
@@ -738,6 +738,34 @@ type User = z.infer<typeof UserSchema>;
738
738
  // The library will be able to recognize this schema by reference `UserSchema` or `User` type.
739
739
  ```
740
740
 
741
+ ### Factory Functions (Schema Generators)
742
+
743
+ The library automatically detects and expands Zod factory functions - any function that returns a Zod schema:
744
+
745
+ ```typescript
746
+ // Define reusable schema factory
747
+ export function createPaginatedSchema<T extends z.ZodTypeAny>(dataSchema: T) {
748
+ return z.object({
749
+ data: z.array(dataSchema).describe("Array of items"),
750
+ pagination: z.object({
751
+ nextCursor: z.string().nullable(),
752
+ hasMore: z.boolean(),
753
+ limit: z.number().int().positive(),
754
+ }),
755
+ });
756
+ }
757
+
758
+ // Use in your schemas - automatically expanded in OpenAPI
759
+ export const PaginatedUsersSchema = createPaginatedSchema(UserSchema);
760
+ export const PaginatedProductsSchema = createPaginatedSchema(ProductSchema);
761
+ ```
762
+
763
+ Factory functions work with any naming convention and support:
764
+ - Generic type parameters
765
+ - Inline schemas as arguments
766
+ - Imported schemas
767
+ - Multiple factory patterns in the same project
768
+
741
769
  ### Drizzle-Zod Support
742
770
 
743
771
  The library fully supports **drizzle-zod** for generating Zod schemas from Drizzle ORM table definitions. This provides a single source of truth for your database schema, validation, and API documentation.
@@ -814,29 +842,18 @@ Custom schema files support YAML/JSON in OpenAPI 3.0 format. See **[next15-app-m
814
842
 
815
843
  ## Examples
816
844
 
817
- This repository includes several complete example projects:
845
+ Explore complete demo projects in the **[examples](./examples/)** directory, covering integrations with Zod, TypeScript, Drizzle and documentation tools like Scalar and Swagger.
818
846
 
819
- ### 📦 Available Examples
820
-
821
- | Example | Description | Features |
822
- | --------------------------------------------------------------- | ----------------------- | ----------------------------------------------- |
823
- | **[next15-app-zod](./examples/next15-app-zod)** | Zod schemas example | Users, Products, Orders API with Zod validation |
824
- | **[next15-app-drizzle-zod](./examples/next15-app-drizzle-zod)** | Drizzle-Zod integration | Blog API with Drizzle ORM + drizzle-zod |
825
- | **[next15-app-mixed-schemas](./examples/next15-app-mixed-schemas)** 🆕 | Multiple schema types | Zod + TypeScript + Custom YAML schemas combined |
826
- | **[next15-app-typescript](./examples/next15-app-typescript)** | TypeScript types | API with pure TypeScript type definitions |
827
- | **[next15-app-scalar](./examples/next15-app-scalar)** | Scalar UI | Modern API documentation interface |
828
- | **[next15-app-swagger](./examples/next15-app-swagger)** | Swagger UI | Classic Swagger documentation |
829
-
830
- ### 🚀 Running Examples
847
+ ### 🚀 Run an Example
831
848
 
832
849
  ```bash
833
- cd examples/next15-app-drizzle-zod
850
+ cd examples/next15-app-zod
834
851
  npm install
835
- npm run openapi:generate
852
+ npx next-openapi-gen generate
836
853
  npm run dev
837
854
  ```
838
855
 
839
- Visit `http://localhost:3000/api-docs` to see the generated documentation.
856
+ Then open `http://localhost:3000/api-docs` to view the generated docs.
840
857
 
841
858
  ## Available UI providers
842
859
 
@@ -25,15 +25,39 @@ export class RouteProcessor {
25
25
  // 1. Add success response
26
26
  const successCode = dataTypes.successCode || this.getDefaultSuccessCode(method);
27
27
  if (dataTypes.responseType) {
28
- // Ensure the schema is defined in components/schemas
28
+ // Handle array notation (e.g., "Type[]", "Type[][]", "Generic<T>[]")
29
+ let schema;
30
+ let baseType = dataTypes.responseType;
31
+ let arrayDepth = 0;
32
+ // Count and remove array brackets
33
+ while (baseType.endsWith('[]')) {
34
+ arrayDepth++;
35
+ baseType = baseType.slice(0, -2);
36
+ }
37
+ // Ensure the base schema is defined in components/schemas
29
38
  this.schemaProcessor.getSchemaContent({
30
- responseType: dataTypes.responseType,
39
+ responseType: baseType,
31
40
  });
41
+ // Build schema reference
42
+ if (arrayDepth === 0) {
43
+ // Not an array
44
+ schema = { $ref: `#/components/schemas/${baseType}` };
45
+ }
46
+ else {
47
+ // Build nested array schema
48
+ schema = { $ref: `#/components/schemas/${baseType}` };
49
+ for (let i = 0; i < arrayDepth; i++) {
50
+ schema = {
51
+ type: "array",
52
+ items: schema,
53
+ };
54
+ }
55
+ }
32
56
  responses[successCode] = {
33
57
  description: dataTypes.responseDescription || "Successful response",
34
58
  content: {
35
59
  "application/json": {
36
- schema: { $ref: `#/components/schemas/${dataTypes.responseType}` },
60
+ schema: schema,
37
61
  },
38
62
  },
39
63
  };
@@ -825,12 +825,25 @@ export class SchemaProcessor {
825
825
  };
826
826
  }
827
827
  getSchemaContent({ tag, paramsType, pathParamsType, bodyType, responseType, }) {
828
+ // Helper function to strip array notation from type names
829
+ const stripArrayNotation = (typeName) => {
830
+ if (!typeName)
831
+ return typeName;
832
+ let baseType = typeName;
833
+ while (baseType.endsWith('[]')) {
834
+ baseType = baseType.slice(0, -2);
835
+ }
836
+ return baseType;
837
+ };
838
+ // Strip array notation for schema lookups
839
+ const baseBodyType = stripArrayNotation(bodyType);
840
+ const baseResponseType = stripArrayNotation(responseType);
828
841
  let params = paramsType ? this.openapiDefinitions[paramsType] : {};
829
842
  let pathParams = pathParamsType
830
843
  ? this.openapiDefinitions[pathParamsType]
831
844
  : {};
832
- let body = bodyType ? this.openapiDefinitions[bodyType] : {};
833
- let responses = responseType ? this.openapiDefinitions[responseType] : {};
845
+ let body = baseBodyType ? this.openapiDefinitions[baseBodyType] : {};
846
+ let responses = baseResponseType ? this.openapiDefinitions[baseResponseType] : {};
834
847
  if (paramsType && !params) {
835
848
  this.findSchemaDefinition(paramsType, "params");
836
849
  params = this.openapiDefinitions[paramsType] || {};
@@ -839,20 +852,20 @@ export class SchemaProcessor {
839
852
  this.findSchemaDefinition(pathParamsType, "pathParams");
840
853
  pathParams = this.openapiDefinitions[pathParamsType] || {};
841
854
  }
842
- if (bodyType && !body) {
843
- this.findSchemaDefinition(bodyType, "body");
844
- body = this.openapiDefinitions[bodyType] || {};
855
+ if (baseBodyType && !body) {
856
+ this.findSchemaDefinition(baseBodyType, "body");
857
+ body = this.openapiDefinitions[baseBodyType] || {};
845
858
  }
846
- if (responseType && !responses) {
847
- this.findSchemaDefinition(responseType, "response");
848
- responses = this.openapiDefinitions[responseType] || {};
859
+ if (baseResponseType && !responses) {
860
+ this.findSchemaDefinition(baseResponseType, "response");
861
+ responses = this.openapiDefinitions[baseResponseType] || {};
849
862
  }
850
863
  if (this.schemaTypes.includes("zod")) {
851
864
  const schemasToProcess = [
852
865
  paramsType,
853
866
  pathParamsType,
854
- bodyType,
855
- responseType,
867
+ baseBodyType,
868
+ baseResponseType,
856
869
  ].filter(Boolean);
857
870
  schemasToProcess.forEach((schemaName) => {
858
871
  if (!this.openapiDefinitions[schemaName]) {
package/dist/lib/utils.js CHANGED
@@ -159,9 +159,9 @@ export function extractJSDocComments(path) {
159
159
  };
160
160
  }
161
161
  export function extractTypeFromComment(commentValue, tag) {
162
- // Updated regex to support generic types with angle brackets
162
+ // Updated regex to support generic types with angle brackets and array brackets
163
163
  return (commentValue
164
- .match(new RegExp(`${tag}\\s*\\s*([\\w<>,\\s]+)`))?.[1]
164
+ .match(new RegExp(`${tag}\\s*\\s*([\\w<>,\\s\\[\\]]+)`))?.[1]
165
165
  ?.trim() || "");
166
166
  }
167
167
  export function cleanComment(commentValue) {
@@ -17,6 +17,14 @@ export class ZodSchemaConverter {
17
17
  processedModules = new Set();
18
18
  typeToSchemaMapping = {};
19
19
  drizzleZodImports = new Set();
20
+ factoryCache = new Map(); // Cache for analyzed factory functions
21
+ factoryCheckCache = new Map(); // Cache for non-factory functions
22
+ fileASTCache = new Map(); // Cache for parsed files
23
+ fileImportsCache = new Map(); // Cache for file imports
24
+ // Current processing context (set during file processing)
25
+ currentFilePath;
26
+ currentAST;
27
+ currentImports;
20
28
  constructor(schemaDir) {
21
29
  this.schemaDir = path.resolve(schemaDir);
22
30
  }
@@ -151,32 +159,47 @@ export class ZodSchemaConverter {
151
159
  }
152
160
  // Parse the file
153
161
  const ast = parseTypeScriptFile(content);
162
+ // Cache AST for later use
163
+ this.fileASTCache.set(filePath, ast);
154
164
  // Create a map to store imported modules
155
- const importedModules = {};
156
- // Look for all exported Zod schemas
157
- traverse(ast, {
158
- // Track imports for resolving local and imported schemas
159
- ImportDeclaration: (path) => {
160
- // Keep track of imports to resolve external schemas
161
- const source = path.node.source.value;
162
- // Track drizzle-zod imports
163
- if (source === "drizzle-zod") {
165
+ let importedModules = {};
166
+ // Check if we have cached imports
167
+ if (this.fileImportsCache.has(filePath)) {
168
+ importedModules = this.fileImportsCache.get(filePath);
169
+ }
170
+ else {
171
+ // Build imports cache
172
+ traverse(ast, {
173
+ ImportDeclaration: (path) => {
174
+ const source = path.node.source.value;
175
+ // Track drizzle-zod imports
176
+ if (source === "drizzle-zod") {
177
+ path.node.specifiers.forEach((specifier) => {
178
+ if (t.isImportSpecifier(specifier) ||
179
+ t.isImportDefaultSpecifier(specifier)) {
180
+ this.drizzleZodImports.add(specifier.local.name);
181
+ }
182
+ });
183
+ }
184
+ // Process each import specifier
164
185
  path.node.specifiers.forEach((specifier) => {
165
186
  if (t.isImportSpecifier(specifier) ||
166
187
  t.isImportDefaultSpecifier(specifier)) {
167
- this.drizzleZodImports.add(specifier.local.name);
188
+ const importedName = specifier.local.name;
189
+ importedModules[importedName] = source;
168
190
  }
169
191
  });
170
- }
171
- // Process each import specifier
172
- path.node.specifiers.forEach((specifier) => {
173
- if (t.isImportSpecifier(specifier) ||
174
- t.isImportDefaultSpecifier(specifier)) {
175
- const importedName = specifier.local.name;
176
- importedModules[importedName] = source;
177
- }
178
- });
179
- },
192
+ },
193
+ });
194
+ // Cache imports for this file
195
+ this.fileImportsCache.set(filePath, importedModules);
196
+ }
197
+ // Set current processing context for use by processZodNode during factory expansion
198
+ this.currentFilePath = filePath;
199
+ this.currentAST = ast;
200
+ this.currentImports = importedModules;
201
+ // Look for all exported Zod schemas
202
+ traverse(ast, {
180
203
  // For export const SchemaName = z.object({...})
181
204
  ExportNamedDeclaration: (path) => {
182
205
  if (t.isVariableDeclaration(path.node.declaration)) {
@@ -213,6 +236,27 @@ export class ZodSchemaConverter {
213
236
  this.zodSchemas[schemaName] = schema;
214
237
  }
215
238
  }
239
+ // Check if this is a factory function call
240
+ else if (t.isCallExpression(declaration.init) &&
241
+ t.isIdentifier(declaration.init.callee)) {
242
+ const factoryName = declaration.init.callee.name;
243
+ logger.debug(`[Schema] Detected potential factory function call: ${factoryName} for schema ${schemaName}`);
244
+ const factoryNode = this.findFactoryFunction(factoryName, filePath, ast, importedModules);
245
+ if (factoryNode) {
246
+ logger.debug(`[Schema] Found factory function, attempting to expand...`);
247
+ const schema = this.expandFactoryCall(factoryNode, declaration.init, filePath);
248
+ if (schema) {
249
+ this.zodSchemas[schemaName] = schema;
250
+ logger.debug(`[Schema] Successfully expanded factory function '${factoryName}' for schema '${schemaName}'`);
251
+ }
252
+ else {
253
+ logger.debug(`[Schema] Failed to expand factory function '${factoryName}'`);
254
+ }
255
+ }
256
+ else {
257
+ logger.debug(`[Schema] Could not find factory function '${factoryName}'`);
258
+ }
259
+ }
216
260
  }
217
261
  });
218
262
  }
@@ -632,6 +676,30 @@ export class ZodSchemaConverter {
632
676
  node.arguments.length > 0) {
633
677
  return this.processZodLazy(node);
634
678
  }
679
+ // Handle potential factory function calls (e.g., createPaginatedSchema(UserSchema))
680
+ // This must be checked before falling back to "Unknown Zod schema node"
681
+ if (t.isCallExpression(node) &&
682
+ t.isIdentifier(node.callee)) {
683
+ logger.debug(`[processZodNode] Attempting to handle potential factory function: ${node.callee.name}`);
684
+ // We need the current file context - try to get it from the processing context
685
+ // Note: This is a limitation - we may not have file context during preprocessing
686
+ // In that case, we'll return a placeholder and let the main processing handle it
687
+ const currentFilePath = this.currentFilePath;
688
+ const currentAST = this.currentAST;
689
+ const importedModules = this.currentImports;
690
+ if (currentFilePath && currentAST && importedModules) {
691
+ const factoryNode = this.findFactoryFunction(node.callee.name, currentFilePath, currentAST, importedModules);
692
+ if (factoryNode) {
693
+ logger.debug(`[processZodNode] Found factory function, expanding...`);
694
+ const schema = this.expandFactoryCall(factoryNode, node, currentFilePath);
695
+ if (schema) {
696
+ logger.debug(`[processZodNode] Successfully expanded factory function '${node.callee.name}'`);
697
+ return schema;
698
+ }
699
+ }
700
+ }
701
+ logger.debug(`[processZodNode] Could not expand factory function '${node.callee.name}' - missing context or not a factory`);
702
+ }
635
703
  logger.debug("Unknown Zod schema node:", node);
636
704
  return { type: "object" };
637
705
  }
@@ -1497,7 +1565,11 @@ export class ZodSchemaConverter {
1497
1565
  try {
1498
1566
  const content = fs.readFileSync(filePath, "utf-8");
1499
1567
  const ast = parseTypeScriptFile(content);
1500
- // First, collect all drizzle-zod imports
1568
+ // Cache AST for later use
1569
+ this.fileASTCache.set(filePath, ast);
1570
+ // Collect imports to enable factory function resolution during preprocessing
1571
+ let importedModules = {};
1572
+ // First, collect all drizzle-zod imports and regular imports
1501
1573
  traverse(ast, {
1502
1574
  ImportDeclaration: (path) => {
1503
1575
  const source = path.node.source.value;
@@ -1509,8 +1581,22 @@ export class ZodSchemaConverter {
1509
1581
  }
1510
1582
  });
1511
1583
  }
1584
+ // Track all imports for factory function resolution
1585
+ path.node.specifiers.forEach((specifier) => {
1586
+ if (t.isImportSpecifier(specifier) ||
1587
+ t.isImportDefaultSpecifier(specifier)) {
1588
+ const importedName = specifier.local.name;
1589
+ importedModules[importedName] = source;
1590
+ }
1591
+ });
1512
1592
  },
1513
1593
  });
1594
+ // Cache imports for this file
1595
+ this.fileImportsCache.set(filePath, importedModules);
1596
+ // Set current processing context for factory function expansion
1597
+ this.currentFilePath = filePath;
1598
+ this.currentAST = ast;
1599
+ this.currentImports = importedModules;
1514
1600
  // Collect all exported Zod schemas
1515
1601
  traverse(ast, {
1516
1602
  ExportNamedDeclaration: (path) => {
@@ -1580,7 +1666,385 @@ export class ZodSchemaConverter {
1580
1666
  t.isCallExpression(node.callee.object)) {
1581
1667
  return this.isZodSchema(node.callee.object);
1582
1668
  }
1669
+ // Do NOT treat unknown function calls as potential Zod schemas here
1670
+ // Factory functions will be detected and handled in processZodNode() instead
1671
+ // This prevents false positives during preprocessing
1583
1672
  }
1584
1673
  return false;
1585
1674
  }
1675
+ /**
1676
+ * Find a factory function by name (lazy detection with caching)
1677
+ * @param functionName - Name of the function to find
1678
+ * @param currentFilePath - Path of the current file being processed
1679
+ * @param currentAST - Already parsed AST of current file
1680
+ * @param importedModules - Map of imported module names to their sources
1681
+ * @returns Factory function node if found and returns Zod schema, null otherwise
1682
+ */
1683
+ findFactoryFunction(functionName, currentFilePath, currentAST, importedModules) {
1684
+ // Check positive cache first
1685
+ if (this.factoryCache.has(functionName)) {
1686
+ logger.debug(`[Factory] Cache hit for function '${functionName}'`);
1687
+ return this.factoryCache.get(functionName);
1688
+ }
1689
+ // Check negative cache (already checked, not a factory)
1690
+ if (this.factoryCheckCache.has(functionName)) {
1691
+ logger.debug(`[Factory] Negative cache hit for function '${functionName}'`);
1692
+ return null;
1693
+ }
1694
+ logger.debug(`[Factory] Searching for function '${functionName}'`);
1695
+ // Look in current file first (AST already parsed)
1696
+ const localFactory = this.findFunctionInAST(currentAST, functionName);
1697
+ if (localFactory && this.returnsZodSchema(localFactory)) {
1698
+ logger.debug(`[Factory] Found Zod factory function '${functionName}' in current file`);
1699
+ this.factoryCache.set(functionName, localFactory);
1700
+ return localFactory;
1701
+ }
1702
+ // Check if function is imported
1703
+ const importSource = importedModules[functionName];
1704
+ if (importSource) {
1705
+ logger.debug(`[Factory] Function '${functionName}' is imported from '${importSource}'`);
1706
+ // Resolve import path
1707
+ const importedFilePath = this.resolveImportPath(currentFilePath, importSource);
1708
+ if (importedFilePath && fs.existsSync(importedFilePath)) {
1709
+ logger.debug(`[Factory] Resolved import to: ${importedFilePath}`);
1710
+ // Parse imported file (with caching) - this will also cache imports
1711
+ const importedAST = this.parseFileWithCache(importedFilePath);
1712
+ if (importedAST) {
1713
+ const importedFactory = this.findFunctionInAST(importedAST, functionName);
1714
+ if (importedFactory && this.returnsZodSchema(importedFactory)) {
1715
+ logger.debug(`[Factory] Found Zod factory function '${functionName}' in imported file`);
1716
+ this.factoryCache.set(functionName, importedFactory);
1717
+ return importedFactory;
1718
+ }
1719
+ else {
1720
+ logger.debug(`[Factory] Function '${functionName}' found in imported file but does not return Zod schema`);
1721
+ }
1722
+ }
1723
+ }
1724
+ else {
1725
+ logger.debug(`[Factory] Could not resolve import path for '${importSource}'`);
1726
+ }
1727
+ }
1728
+ else {
1729
+ logger.debug(`[Factory] Function '${functionName}' is not imported`);
1730
+ }
1731
+ // Not found or not a Zod factory - cache negative result
1732
+ logger.debug(`[Factory] Function '${functionName}' is not a Zod factory`);
1733
+ this.factoryCheckCache.set(functionName, false);
1734
+ return null;
1735
+ }
1736
+ /**
1737
+ * Find a function definition in an AST
1738
+ */
1739
+ findFunctionInAST(ast, functionName) {
1740
+ let foundFunction = null;
1741
+ traverse(ast, {
1742
+ // Handle: export function createSchema() { ... }
1743
+ FunctionDeclaration: (path) => {
1744
+ if (t.isIdentifier(path.node.id) && path.node.id.name === functionName) {
1745
+ foundFunction = path.node;
1746
+ path.stop();
1747
+ }
1748
+ },
1749
+ // Handle: export const createSchema = (...) => { ... }
1750
+ VariableDeclarator: (path) => {
1751
+ if (t.isIdentifier(path.node.id) &&
1752
+ path.node.id.name === functionName &&
1753
+ (t.isArrowFunctionExpression(path.node.init) ||
1754
+ t.isFunctionExpression(path.node.init))) {
1755
+ foundFunction = path.node.init;
1756
+ path.stop();
1757
+ }
1758
+ },
1759
+ });
1760
+ return foundFunction;
1761
+ }
1762
+ /**
1763
+ * Check if a function returns a Zod schema by analyzing return statements
1764
+ */
1765
+ returnsZodSchema(functionNode) {
1766
+ if (!t.isFunctionDeclaration(functionNode) &&
1767
+ !t.isArrowFunctionExpression(functionNode) &&
1768
+ !t.isFunctionExpression(functionNode)) {
1769
+ return false;
1770
+ }
1771
+ let returnsZod = false;
1772
+ // For arrow functions with direct return (no block)
1773
+ if (t.isArrowFunctionExpression(functionNode) &&
1774
+ !t.isBlockStatement(functionNode.body)) {
1775
+ returnsZod = this.isZodSchema(functionNode.body);
1776
+ logger.debug(`[Factory] Arrow function direct return, isZodSchema: ${returnsZod}`);
1777
+ return returnsZod;
1778
+ }
1779
+ // For functions with block statements, analyze return statements manually
1780
+ const body = functionNode.body;
1781
+ if (!t.isBlockStatement(body)) {
1782
+ return false;
1783
+ }
1784
+ // Manually walk through statements instead of using traverse
1785
+ const checkStatements = (statements) => {
1786
+ for (const stmt of statements) {
1787
+ if (t.isReturnStatement(stmt) && stmt.argument) {
1788
+ if (this.isZodSchema(stmt.argument)) {
1789
+ logger.debug(`[Factory] Found Zod schema in return statement`);
1790
+ return true;
1791
+ }
1792
+ }
1793
+ // Check nested blocks (if statements, etc.)
1794
+ else if (t.isIfStatement(stmt)) {
1795
+ if (t.isBlockStatement(stmt.consequent)) {
1796
+ if (checkStatements(stmt.consequent.body))
1797
+ return true;
1798
+ }
1799
+ else if (t.isReturnStatement(stmt.consequent) && stmt.consequent.argument) {
1800
+ if (this.isZodSchema(stmt.consequent.argument))
1801
+ return true;
1802
+ }
1803
+ if (stmt.alternate) {
1804
+ if (t.isBlockStatement(stmt.alternate)) {
1805
+ if (checkStatements(stmt.alternate.body))
1806
+ return true;
1807
+ }
1808
+ else if (t.isReturnStatement(stmt.alternate) && stmt.alternate.argument) {
1809
+ if (this.isZodSchema(stmt.alternate.argument))
1810
+ return true;
1811
+ }
1812
+ }
1813
+ }
1814
+ }
1815
+ return false;
1816
+ };
1817
+ returnsZod = checkStatements(body.body);
1818
+ return returnsZod;
1819
+ }
1820
+ /**
1821
+ * Parse a file with caching (also caches imports)
1822
+ */
1823
+ parseFileWithCache(filePath) {
1824
+ if (this.fileASTCache.has(filePath)) {
1825
+ return this.fileASTCache.get(filePath);
1826
+ }
1827
+ try {
1828
+ const content = fs.readFileSync(filePath, "utf-8");
1829
+ const ast = parseTypeScriptFile(content);
1830
+ this.fileASTCache.set(filePath, ast);
1831
+ // Also build and cache imports for this file
1832
+ if (!this.fileImportsCache.has(filePath)) {
1833
+ const importedModules = {};
1834
+ traverse(ast, {
1835
+ ImportDeclaration: (path) => {
1836
+ const source = path.node.source.value;
1837
+ // Track drizzle-zod imports
1838
+ if (source === "drizzle-zod") {
1839
+ path.node.specifiers.forEach((specifier) => {
1840
+ if (t.isImportSpecifier(specifier) ||
1841
+ t.isImportDefaultSpecifier(specifier)) {
1842
+ this.drizzleZodImports.add(specifier.local.name);
1843
+ }
1844
+ });
1845
+ }
1846
+ // Process each import specifier
1847
+ path.node.specifiers.forEach((specifier) => {
1848
+ if (t.isImportSpecifier(specifier) ||
1849
+ t.isImportDefaultSpecifier(specifier)) {
1850
+ const importedName = specifier.local.name;
1851
+ importedModules[importedName] = source;
1852
+ }
1853
+ });
1854
+ },
1855
+ });
1856
+ this.fileImportsCache.set(filePath, importedModules);
1857
+ }
1858
+ return ast;
1859
+ }
1860
+ catch (error) {
1861
+ logger.error(`[Factory] Error parsing file '${filePath}': ${error}`);
1862
+ return null;
1863
+ }
1864
+ }
1865
+ /**
1866
+ * Resolve import path relative to current file
1867
+ */
1868
+ resolveImportPath(currentFilePath, importSource) {
1869
+ // Handle relative imports
1870
+ if (importSource.startsWith(".")) {
1871
+ const currentDir = path.dirname(currentFilePath);
1872
+ let resolvedPath = path.resolve(currentDir, importSource);
1873
+ // Try adding extensions if not present
1874
+ const extensions = [".ts", ".tsx", ".js", ".jsx"];
1875
+ if (!path.extname(resolvedPath)) {
1876
+ for (const ext of extensions) {
1877
+ const withExt = resolvedPath + ext;
1878
+ if (fs.existsSync(withExt)) {
1879
+ return withExt;
1880
+ }
1881
+ }
1882
+ // Try index files
1883
+ for (const ext of extensions) {
1884
+ const indexPath = path.join(resolvedPath, `index${ext}`);
1885
+ if (fs.existsSync(indexPath)) {
1886
+ return indexPath;
1887
+ }
1888
+ }
1889
+ }
1890
+ else if (fs.existsSync(resolvedPath)) {
1891
+ return resolvedPath;
1892
+ }
1893
+ }
1894
+ // Handle absolute imports from schemaDir
1895
+ // This is a simplified approach - you might need to enhance this based on tsconfig paths
1896
+ return null;
1897
+ }
1898
+ /**
1899
+ * Expand a factory function call by substituting arguments
1900
+ */
1901
+ expandFactoryCall(factoryNode, callNode, filePath) {
1902
+ if (!t.isFunctionDeclaration(factoryNode) &&
1903
+ !t.isArrowFunctionExpression(factoryNode) &&
1904
+ !t.isFunctionExpression(factoryNode)) {
1905
+ return null;
1906
+ }
1907
+ logger.debug(`[Factory] Expanding factory call with ${callNode.arguments.length} arguments`);
1908
+ // Build parameter -> argument mapping
1909
+ const paramMap = new Map();
1910
+ const params = factoryNode.params;
1911
+ for (let i = 0; i < params.length && i < callNode.arguments.length; i++) {
1912
+ const param = params[i];
1913
+ const arg = callNode.arguments[i];
1914
+ if (t.isIdentifier(param)) {
1915
+ paramMap.set(param.name, arg);
1916
+ logger.debug(`[Factory] Mapped parameter '${param.name}' to argument`);
1917
+ }
1918
+ else if (t.isObjectPattern(param)) {
1919
+ // Handle destructured parameters - simplified for now
1920
+ logger.debug(`[Factory] Skipping destructured parameter (not yet supported)`);
1921
+ }
1922
+ }
1923
+ // Extract return statement
1924
+ const returnNode = this.extractReturnNode(factoryNode);
1925
+ if (!returnNode) {
1926
+ logger.debug(`[Factory] No return statement found in factory`);
1927
+ return null;
1928
+ }
1929
+ logger.debug(`[Factory] Return node type: ${returnNode.type}`);
1930
+ // Clone and substitute parameters in return node
1931
+ const substitutedNode = this.substituteParameters(returnNode, paramMap, filePath);
1932
+ logger.debug(`[Factory] Substituted node type: ${substitutedNode.type}`);
1933
+ // Process the substituted node as a normal Zod schema
1934
+ const result = this.processZodNode(substitutedNode);
1935
+ if (result) {
1936
+ logger.debug(`[Factory] Successfully processed substituted node, result has ${Object.keys(result).length} keys`);
1937
+ }
1938
+ else {
1939
+ logger.debug(`[Factory] Failed to process substituted node`);
1940
+ }
1941
+ return result;
1942
+ }
1943
+ /**
1944
+ * Extract the return node from a function
1945
+ */
1946
+ extractReturnNode(functionNode) {
1947
+ // For arrow functions with direct return (no block)
1948
+ if (t.isArrowFunctionExpression(functionNode) &&
1949
+ !t.isBlockStatement(functionNode.body)) {
1950
+ return functionNode.body;
1951
+ }
1952
+ // For functions with block statements
1953
+ const body = t.isFunctionDeclaration(functionNode) ||
1954
+ t.isArrowFunctionExpression(functionNode) ||
1955
+ t.isFunctionExpression(functionNode)
1956
+ ? functionNode.body
1957
+ : null;
1958
+ if (!body || !t.isBlockStatement(body)) {
1959
+ return null;
1960
+ }
1961
+ // Find first return statement manually
1962
+ const findReturn = (statements) => {
1963
+ for (const stmt of statements) {
1964
+ if (t.isReturnStatement(stmt) && stmt.argument) {
1965
+ return stmt.argument;
1966
+ }
1967
+ // Check nested blocks
1968
+ if (t.isIfStatement(stmt)) {
1969
+ if (t.isBlockStatement(stmt.consequent)) {
1970
+ const found = findReturn(stmt.consequent.body);
1971
+ if (found)
1972
+ return found;
1973
+ }
1974
+ else if (t.isReturnStatement(stmt.consequent) && stmt.consequent.argument) {
1975
+ return stmt.consequent.argument;
1976
+ }
1977
+ if (stmt.alternate) {
1978
+ if (t.isBlockStatement(stmt.alternate)) {
1979
+ const found = findReturn(stmt.alternate.body);
1980
+ if (found)
1981
+ return found;
1982
+ }
1983
+ else if (t.isReturnStatement(stmt.alternate) && stmt.alternate.argument) {
1984
+ return stmt.alternate.argument;
1985
+ }
1986
+ }
1987
+ }
1988
+ }
1989
+ return null;
1990
+ };
1991
+ return findReturn(body.body);
1992
+ }
1993
+ /**
1994
+ * Substitute parameters with actual arguments in an AST node (deep clone and replace)
1995
+ */
1996
+ substituteParameters(node, paramMap, filePath) {
1997
+ // Deep clone the node to avoid modifying the original
1998
+ const cloned = t.cloneNode(node, /* deep */ true, /* withoutLoc */ false);
1999
+ // Manual recursive substitution without traverse
2000
+ const substitute = (n) => {
2001
+ if (t.isIdentifier(n)) {
2002
+ // Replace if this is a parameter
2003
+ if (paramMap.has(n.name)) {
2004
+ const replacement = paramMap.get(n.name);
2005
+ return t.cloneNode(replacement, true, false);
2006
+ }
2007
+ return n;
2008
+ }
2009
+ // Handle CallExpression
2010
+ if (t.isCallExpression(n)) {
2011
+ return t.callExpression(substitute(n.callee), n.arguments.map((arg) => {
2012
+ if (t.isSpreadElement(arg)) {
2013
+ return t.spreadElement(substitute(arg.argument));
2014
+ }
2015
+ return substitute(arg);
2016
+ }));
2017
+ }
2018
+ // Handle MemberExpression
2019
+ if (t.isMemberExpression(n)) {
2020
+ return t.memberExpression(substitute(n.object), n.computed ? substitute(n.property) : n.property, n.computed);
2021
+ }
2022
+ // Handle ObjectExpression
2023
+ if (t.isObjectExpression(n)) {
2024
+ return t.objectExpression(n.properties.map((prop) => {
2025
+ if (t.isObjectProperty(prop)) {
2026
+ return t.objectProperty(prop.computed ? substitute(prop.key) : prop.key, substitute(prop.value), prop.computed, prop.shorthand);
2027
+ }
2028
+ if (t.isSpreadElement(prop)) {
2029
+ return t.spreadElement(substitute(prop.argument));
2030
+ }
2031
+ return prop;
2032
+ }));
2033
+ }
2034
+ // Handle ArrayExpression
2035
+ if (t.isArrayExpression(n)) {
2036
+ return t.arrayExpression(n.elements.map((elem) => {
2037
+ if (!elem)
2038
+ return null;
2039
+ if (t.isSpreadElement(elem)) {
2040
+ return t.spreadElement(substitute(elem.argument));
2041
+ }
2042
+ return substitute(elem);
2043
+ }));
2044
+ }
2045
+ // Return as-is for other node types
2046
+ return n;
2047
+ };
2048
+ return substitute(cloned);
2049
+ }
1586
2050
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "next-openapi-gen",
3
- "version": "0.8.4",
3
+ "version": "0.8.6",
4
4
  "description": "Automatically generate OpenAPI 3.0 documentation from Next.js projects, with support for Zod schemas and TypeScript types.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",