next-openapi-gen 0.9.1 → 0.9.3

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
@@ -213,7 +213,7 @@ export async function POST(request: NextRequest) {
213
213
  | `@description` | Endpoint description |
214
214
  | `@operationId` | Custom operation ID (overrides auto-generated ID) |
215
215
  | `@pathParams` | Path parameters type/schema |
216
- | `@params` | Query parameters type/schema |
216
+ | `@params` | Query parameters type/schema (use `@queryParams` if you have prettier-plugin-jsdoc conflicts) |
217
217
  | `@body` | Request body type/schema |
218
218
  | `@bodyDescription` | Request body description |
219
219
  | `@response` | Response type/schema with optional code and description (`User`, `201:User`, `User:Description`, `201:User:Description`) |
@@ -671,6 +671,19 @@ export async function GET() {
671
671
 
672
672
  If no type/schema is provided for path parameters, a default schema will be generated.
673
673
 
674
+ ### Intelligent Examples
675
+
676
+ The library generates intelligent examples for parameters based on their name:
677
+
678
+ | Parameter name | Example |
679
+ | -------------- | ---------------------------------------- |
680
+ | `id`, `*Id` | `"123"` or `123` |
681
+ | `slug` | `"example-slug"` |
682
+ | `uuid` | `"123e4567-e89b-12d3-a456-426614174000"` |
683
+ | `email` | `"user@example.com"` |
684
+ | `name` | `"example-name"` |
685
+ | `date` | `"2023-01-01"` |
686
+
674
687
  ### TypeScript Generics Support
675
688
 
676
689
  The library supports TypeScript generic types and automatically resolves them during documentation generation:
@@ -718,18 +731,64 @@ export async function GET() {
718
731
  }
719
732
  ```
720
733
 
721
- ### Intelligent Examples
734
+ ### TypeScript Utility Types Support
722
735
 
723
- The library generates intelligent examples for parameters based on their name:
736
+ The library supports TypeScript utility types for extracting types from functions:
724
737
 
725
- | Parameter name | Example |
726
- | -------------- | ---------------------------------------- |
727
- | `id`, `*Id` | `"123"` or `123` |
728
- | `slug` | `"example-slug"` |
729
- | `uuid` | `"123e4567-e89b-12d3-a456-426614174000"` |
730
- | `email` | `"user@example.com"` |
731
- | `name` | `"example-name"` |
732
- | `date` | `"2023-01-01"` |
738
+ ```typescript
739
+ // src/app/api/products/route.utils.ts
740
+ export async function getProductById(id: string): Promise<{
741
+ product: Product;
742
+ fetchedAt: string;
743
+ }> {
744
+ // Implementation...
745
+ }
746
+
747
+ export function createProduct(
748
+ data: { name: string; price: number },
749
+ options: { notify: boolean }
750
+ ): { success: boolean; productId: string } {
751
+ // Implementation...
752
+ }
753
+
754
+ // src/types/product-types.ts
755
+
756
+ // Extract return type from async functions
757
+ export type ProductResponse = Awaited<ReturnType<typeof getProductById>>;
758
+
759
+ // Extract return type from sync functions
760
+ export type CreateResult = ReturnType<typeof createProduct>;
761
+
762
+ // Extract parameter types as tuple
763
+ export type CreateProductParams = Parameters<typeof createProduct>;
764
+
765
+ // Extract specific parameter using indexed access
766
+ export type ProductData = Parameters<typeof createProduct>[0];
767
+ export type ProductOptions = Parameters<typeof createProduct>[1];
768
+
769
+ // Use with generic types
770
+ interface ApiResponse<T> {
771
+ success: boolean;
772
+ data: T;
773
+ }
774
+
775
+ export type ProductApiResponse = ApiResponse<ProductResponse>;
776
+
777
+ /**
778
+ * @response ProductApiResponse
779
+ * @openapi
780
+ */
781
+ export async function GET() {
782
+ // Fully typed response automatically documented
783
+ }
784
+ ```
785
+
786
+ **Supported utility types:**
787
+ - `Awaited<T>` - Unwraps Promise types
788
+ - `ReturnType<typeof func>` - Extracts function return type
789
+ - `Parameters<typeof func>` - Extracts function parameters as tuple
790
+ - `Parameters<typeof func>[N]` - Indexed access to specific parameter
791
+ - Generic interfaces like `ApiResponse<T>` with type parameter substitution
733
792
 
734
793
  ## Advanced Zod Features
735
794
 
@@ -27,6 +27,9 @@ export class SchemaProcessor {
27
27
  zodSchemaConverter = null;
28
28
  schemaTypes;
29
29
  isResolvingPickOmitBase = false;
30
+ // Track imports per file for resolving ReturnType<typeof func>
31
+ importMap = {}; // { filePath: { importName: importPath } }
32
+ currentFilePath = ""; // Track the file being processed
30
33
  constructor(schemaDir, schemaType = "typescript", schemaFiles) {
31
34
  this.schemaDir = path.resolve(schemaDir);
32
35
  this.schemaTypes = normalizeSchemaTypes(schemaType);
@@ -90,7 +93,10 @@ export class SchemaProcessor {
90
93
  // Layer 1: TypeScript types (base layer)
91
94
  const filteredSchemas = {};
92
95
  Object.entries(this.openapiDefinitions).forEach(([key, value]) => {
93
- if (!this.isGenericTypeParameter(key) && !this.isInvalidSchemaName(key)) {
96
+ if (!this.isGenericTypeParameter(key) &&
97
+ !this.isInvalidSchemaName(key) &&
98
+ !this.isBuiltInUtilityType(key) &&
99
+ !this.isFunctionSchema(key)) {
94
100
  filteredSchemas[key] = value;
95
101
  }
96
102
  });
@@ -105,7 +111,6 @@ export class SchemaProcessor {
105
111
  return merged;
106
112
  }
107
113
  findSchemaDefinition(schemaName, contentType) {
108
- let schemaNode = null;
109
114
  // Assign type that is actually processed
110
115
  this.contentType = contentType;
111
116
  // Check if the schemaName is a generic type (contains < and >)
@@ -136,7 +141,7 @@ export class SchemaProcessor {
136
141
  }
137
142
  // Fall back to TypeScript types
138
143
  this.scanSchemaDir(this.schemaDir, schemaName);
139
- return schemaNode;
144
+ return this.openapiDefinitions[schemaName] || {};
140
145
  }
141
146
  scanSchemaDir(dir, schemaName) {
142
147
  let files = this.directoryCache[dir];
@@ -159,40 +164,160 @@ export class SchemaProcessor {
159
164
  }
160
165
  });
161
166
  }
162
- collectTypeDefinitions(ast, schemaName) {
167
+ collectImports(ast, filePath) {
168
+ // Normalize path to avoid Windows/Unix path separator issues
169
+ const normalizedPath = path.normalize(filePath);
170
+ if (!this.importMap[normalizedPath]) {
171
+ this.importMap[normalizedPath] = {};
172
+ }
173
+ traverse(ast, {
174
+ ImportDeclaration: (path) => {
175
+ const importPath = path.node.source.value;
176
+ // Handle named imports: import { foo, bar } from './file'
177
+ path.node.specifiers.forEach((specifier) => {
178
+ if (t.isImportSpecifier(specifier)) {
179
+ const importedName = t.isIdentifier(specifier.imported) ? specifier.imported.name : specifier.imported.value;
180
+ this.importMap[normalizedPath][importedName] = importPath;
181
+ }
182
+ // Handle default imports: import foo from './file'
183
+ else if (t.isImportDefaultSpecifier(specifier)) {
184
+ const importedName = specifier.local.name;
185
+ this.importMap[normalizedPath][importedName] = importPath;
186
+ }
187
+ // Handle namespace imports: import * as foo from './file'
188
+ else if (t.isImportNamespaceSpecifier(specifier)) {
189
+ const importedName = specifier.local.name;
190
+ this.importMap[normalizedPath][importedName] = importPath;
191
+ }
192
+ });
193
+ },
194
+ });
195
+ }
196
+ /**
197
+ * Resolve an import path relative to the current file
198
+ * Converts import paths like "../app/api/products/route.utils" to absolute file paths
199
+ */
200
+ resolveImportPath(importPath, fromFilePath) {
201
+ // Skip node_modules imports
202
+ if (!importPath.startsWith('.')) {
203
+ return null;
204
+ }
205
+ const fromDir = path.dirname(fromFilePath);
206
+ let resolvedPath = path.resolve(fromDir, importPath);
207
+ // Try with .ts extension
208
+ if (fs.existsSync(resolvedPath + '.ts')) {
209
+ return resolvedPath + '.ts';
210
+ }
211
+ // Try with .tsx extension
212
+ if (fs.existsSync(resolvedPath + '.tsx')) {
213
+ return resolvedPath + '.tsx';
214
+ }
215
+ // Try as-is (might already have extension)
216
+ if (fs.existsSync(resolvedPath)) {
217
+ return resolvedPath;
218
+ }
219
+ return null;
220
+ }
221
+ /**
222
+ * Collect all exported type definitions from an AST without filtering by name
223
+ * Used when processing imported files to ensure all referenced types are available
224
+ */
225
+ collectAllExportedDefinitions(ast, filePath) {
226
+ const currentFile = filePath || this.currentFilePath;
227
+ traverse(ast, {
228
+ TSTypeAliasDeclaration: (path) => {
229
+ if (path.node.id && t.isIdentifier(path.node.id)) {
230
+ const name = path.node.id.name;
231
+ if (!this.typeDefinitions[name]) {
232
+ const node = (path.node.typeParameters && path.node.typeParameters.params.length > 0)
233
+ ? path.node
234
+ : path.node.typeAnnotation;
235
+ this.typeDefinitions[name] = { node, filePath: currentFile };
236
+ }
237
+ }
238
+ },
239
+ TSInterfaceDeclaration: (path) => {
240
+ if (path.node.id && t.isIdentifier(path.node.id)) {
241
+ const name = path.node.id.name;
242
+ if (!this.typeDefinitions[name]) {
243
+ this.typeDefinitions[name] = { node: path.node, filePath: currentFile };
244
+ }
245
+ }
246
+ },
247
+ TSEnumDeclaration: (path) => {
248
+ if (path.node.id && t.isIdentifier(path.node.id)) {
249
+ const name = path.node.id.name;
250
+ if (!this.typeDefinitions[name]) {
251
+ this.typeDefinitions[name] = { node: path.node, filePath: currentFile };
252
+ }
253
+ }
254
+ },
255
+ ExportNamedDeclaration: (path) => {
256
+ // Handle exported interfaces
257
+ if (t.isTSInterfaceDeclaration(path.node.declaration)) {
258
+ const interfaceDecl = path.node.declaration;
259
+ if (interfaceDecl.id && t.isIdentifier(interfaceDecl.id)) {
260
+ const name = interfaceDecl.id.name;
261
+ if (!this.typeDefinitions[name]) {
262
+ this.typeDefinitions[name] = { node: interfaceDecl, filePath: currentFile };
263
+ }
264
+ }
265
+ }
266
+ // Handle exported type aliases
267
+ if (t.isTSTypeAliasDeclaration(path.node.declaration)) {
268
+ const typeDecl = path.node.declaration;
269
+ if (typeDecl.id && t.isIdentifier(typeDecl.id)) {
270
+ const name = typeDecl.id.name;
271
+ if (!this.typeDefinitions[name]) {
272
+ const node = (typeDecl.typeParameters && typeDecl.typeParameters.params.length > 0)
273
+ ? typeDecl
274
+ : typeDecl.typeAnnotation;
275
+ this.typeDefinitions[name] = { node, filePath: currentFile };
276
+ }
277
+ }
278
+ }
279
+ },
280
+ });
281
+ }
282
+ collectTypeDefinitions(ast, schemaName, filePath) {
283
+ const currentFile = filePath || this.currentFilePath;
163
284
  traverse(ast, {
164
285
  VariableDeclarator: (path) => {
165
286
  if (t.isIdentifier(path.node.id, { name: schemaName })) {
166
287
  const name = path.node.id.name;
167
- this.typeDefinitions[name] = path.node.init || path.node;
288
+ this.typeDefinitions[name] = { node: path.node.init || path.node, filePath: currentFile };
168
289
  }
169
290
  },
170
291
  TSTypeAliasDeclaration: (path) => {
171
292
  if (t.isIdentifier(path.node.id, { name: schemaName })) {
172
293
  const name = path.node.id.name;
173
294
  // Store the full node for generic types, just the type annotation for regular types
174
- if (path.node.typeParameters &&
175
- path.node.typeParameters.params.length > 0) {
176
- this.typeDefinitions[name] = path.node; // Store the full declaration for generic types
177
- }
178
- else {
179
- this.typeDefinitions[name] = path.node.typeAnnotation; // Store just the type annotation for regular types
180
- }
295
+ const node = (path.node.typeParameters && path.node.typeParameters.params.length > 0)
296
+ ? path.node // Store the full declaration for generic types
297
+ : path.node.typeAnnotation; // Store just the type annotation for regular types
298
+ this.typeDefinitions[name] = { node, filePath: currentFile };
181
299
  }
182
300
  },
183
301
  TSInterfaceDeclaration: (path) => {
184
302
  if (t.isIdentifier(path.node.id, { name: schemaName })) {
185
303
  const name = path.node.id.name;
186
- this.typeDefinitions[name] = path.node;
304
+ this.typeDefinitions[name] = { node: path.node, filePath: currentFile };
187
305
  }
188
306
  },
189
307
  TSEnumDeclaration: (path) => {
190
308
  if (t.isIdentifier(path.node.id, { name: schemaName })) {
191
309
  const name = path.node.id.name;
192
- this.typeDefinitions[name] = path.node;
310
+ this.typeDefinitions[name] = { node: path.node, filePath: currentFile };
193
311
  }
194
312
  },
195
- // Collect exported zod schemas
313
+ // Collect function declarations for ReturnType<typeof func> support
314
+ FunctionDeclaration: (path) => {
315
+ if (path.node.id && t.isIdentifier(path.node.id, { name: schemaName })) {
316
+ const name = path.node.id.name;
317
+ this.typeDefinitions[name] = { node: path.node, filePath: currentFile };
318
+ }
319
+ },
320
+ // Collect exported zod schemas and functions
196
321
  ExportNamedDeclaration: (path) => {
197
322
  if (t.isVariableDeclaration(path.node.declaration)) {
198
323
  path.node.declaration.declarations.forEach((declaration) => {
@@ -205,11 +330,19 @@ export class SchemaProcessor {
205
330
  t.isIdentifier(declaration.init.callee.object) &&
206
331
  declaration.init.callee.object.name === "z") {
207
332
  const name = declaration.id.name;
208
- this.typeDefinitions[name] = declaration.init;
333
+ this.typeDefinitions[name] = { node: declaration.init, filePath: currentFile };
209
334
  }
210
335
  }
211
336
  });
212
337
  }
338
+ // Handle exported function declarations
339
+ if (t.isFunctionDeclaration(path.node.declaration)) {
340
+ const funcDecl = path.node.declaration;
341
+ if (funcDecl.id && t.isIdentifier(funcDecl.id, { name: schemaName })) {
342
+ const name = funcDecl.id.name;
343
+ this.typeDefinitions[name] = { node: funcDecl, filePath: currentFile };
344
+ }
345
+ }
213
346
  },
214
347
  });
215
348
  }
@@ -230,9 +363,10 @@ export class SchemaProcessor {
230
363
  return zodSchema;
231
364
  }
232
365
  }
233
- const typeNode = this.typeDefinitions[typeName.toString()];
234
- if (!typeNode)
366
+ const typeDefEntry = this.typeDefinitions[typeName.toString()];
367
+ if (!typeDefEntry)
235
368
  return {};
369
+ const typeNode = typeDefEntry.node || typeDefEntry; // Support both old and new format
236
370
  // Handle generic type alias declarations (full node)
237
371
  if (t.isTSTypeAliasDeclaration(typeNode)) {
238
372
  // This is a generic type, should be handled by the caller via resolveGenericType
@@ -303,6 +437,10 @@ export class SchemaProcessor {
303
437
  if (t.isTSTypeReference(typeNode)) {
304
438
  return this.resolveTSNodeType(typeNode);
305
439
  }
440
+ // Handle indexed access types (e.g., Parameters<typeof func>[0])
441
+ if (t.isTSIndexedAccessType(typeNode)) {
442
+ return this.resolveTSNodeType(typeNode);
443
+ }
306
444
  return {};
307
445
  }
308
446
  finally {
@@ -373,12 +511,53 @@ export class SchemaProcessor {
373
511
  return this.resolveTSNodeType(syntheticNode);
374
512
  }
375
513
  }
514
+ // Handle indexed access types: SomeType[0] or SomeType["key"]
515
+ if (t.isTSIndexedAccessType(node)) {
516
+ const objectType = this.resolveTSNodeType(node.objectType);
517
+ const indexType = node.indexType;
518
+ // Handle numeric index: Parameters<typeof func>[0]
519
+ if (t.isTSLiteralType(indexType) && t.isNumericLiteral(indexType.literal)) {
520
+ const index = indexType.literal.value;
521
+ // If objectType is a tuple (has prefixItems), get the specific item
522
+ if (objectType.prefixItems && Array.isArray(objectType.prefixItems)) {
523
+ if (index < objectType.prefixItems.length) {
524
+ return objectType.prefixItems[index];
525
+ }
526
+ else {
527
+ logger.warn(`Index ${index} is out of bounds for tuple type.`);
528
+ return { type: "object" };
529
+ }
530
+ }
531
+ // If objectType is a regular array, return the items type
532
+ if (objectType.type === "array" && objectType.items) {
533
+ return objectType.items;
534
+ }
535
+ }
536
+ // Handle string index: SomeType["propertyName"]
537
+ if (t.isTSLiteralType(indexType) && t.isStringLiteral(indexType.literal)) {
538
+ const key = indexType.literal.value;
539
+ // If objectType has properties, get the specific property
540
+ if (objectType.properties && objectType.properties[key]) {
541
+ return objectType.properties[key];
542
+ }
543
+ }
544
+ // Fallback
545
+ return { type: "object" };
546
+ }
376
547
  if (t.isTSTypeReference(node) && t.isIdentifier(node.typeName)) {
377
548
  const typeName = node.typeName.name;
378
549
  // Special handling for built-in types
379
550
  if (typeName === "Date") {
380
551
  return { type: "string", format: "date-time" };
381
552
  }
553
+ // Handle Promise<T> - in OpenAPI, promises are transparent (we document the resolved value)
554
+ if (typeName === "Promise") {
555
+ if (node.typeParameters && node.typeParameters.params.length > 0) {
556
+ // Return the inner type directly - promises are async wrappers
557
+ return this.resolveTSNodeType(node.typeParameters.params[0]);
558
+ }
559
+ return { type: "object" }; // Promise with no type parameter
560
+ }
382
561
  if (typeName === "Array" || typeName === "ReadonlyArray") {
383
562
  if (node.typeParameters && node.typeParameters.params.length > 0) {
384
563
  return {
@@ -406,6 +585,160 @@ export class SchemaProcessor {
406
585
  return this.resolveTSNodeType(node.typeParameters.params[0]);
407
586
  }
408
587
  }
588
+ // Handle Awaited<T> utility type
589
+ if (typeName === "Awaited") {
590
+ if (node.typeParameters && node.typeParameters.params.length > 0) {
591
+ // Unwrap the inner type - promises are transparent in OpenAPI
592
+ return this.resolveTSNodeType(node.typeParameters.params[0]);
593
+ }
594
+ }
595
+ // Handle ReturnType<typeof X> utility type
596
+ if (typeName === "ReturnType") {
597
+ if (node.typeParameters && node.typeParameters.params.length > 0) {
598
+ const typeParam = node.typeParameters.params[0];
599
+ // ReturnType<typeof functionName>
600
+ if (t.isTSTypeQuery(typeParam)) {
601
+ const funcName = t.isIdentifier(typeParam.exprName)
602
+ ? typeParam.exprName.name
603
+ : null;
604
+ if (funcName) {
605
+ // Save current file path before findSchemaDefinition which may change it
606
+ const savedFilePath = this.currentFilePath;
607
+ // First try to find the function in the current file
608
+ this.findSchemaDefinition(funcName, this.contentType);
609
+ let funcDefEntry = this.typeDefinitions[funcName];
610
+ let funcNode = funcDefEntry?.node || funcDefEntry; // Support both old and new format
611
+ const funcFilePath = funcDefEntry?.filePath;
612
+ // If not found, check if it's an imported function
613
+ // Use the saved file path (where the utility type is defined)
614
+ const sourceFilePath = savedFilePath;
615
+ const normalizedSourcePath = path.normalize(sourceFilePath);
616
+ if (!funcNode && sourceFilePath && this.importMap[normalizedSourcePath]) {
617
+ const importPath = this.importMap[normalizedSourcePath][funcName];
618
+ if (importPath) {
619
+ // Resolve the import path to an absolute file path
620
+ const resolvedPath = this.resolveImportPath(importPath, sourceFilePath);
621
+ if (resolvedPath) {
622
+ // Process the imported file to collect the function
623
+ const content = fs.readFileSync(resolvedPath, "utf-8");
624
+ const ast = parseTypeScriptFile(content);
625
+ // Collect imports and type definitions from the imported file
626
+ this.collectImports(ast, resolvedPath);
627
+ this.collectTypeDefinitions(ast, funcName, resolvedPath);
628
+ // Also collect all exported types/interfaces from the same file
629
+ // This ensures referenced types like Product are available
630
+ this.collectAllExportedDefinitions(ast, resolvedPath);
631
+ // Now try to get the function node again
632
+ funcDefEntry = this.typeDefinitions[funcName];
633
+ funcNode = funcDefEntry?.node || funcDefEntry;
634
+ }
635
+ }
636
+ }
637
+ if (funcNode) {
638
+ // Extract the return type annotation
639
+ const returnTypeNode = this.extractFunctionReturnType(funcNode);
640
+ if (returnTypeNode) {
641
+ // Recursively resolve the return type
642
+ return this.resolveTSNodeType(returnTypeNode);
643
+ }
644
+ else {
645
+ logger.warn(`ReturnType<typeof ${funcName}>: Function '${funcName}' does not have an explicit return type annotation. ` +
646
+ `Add a return type to the function signature for accurate schema generation.`);
647
+ return { type: "object" };
648
+ }
649
+ }
650
+ else {
651
+ logger.warn(`ReturnType<typeof ${funcName}>: Function '${funcName}' not found in schema files or imports. ` +
652
+ `Ensure the function is exported and imported correctly.`);
653
+ return { type: "object" };
654
+ }
655
+ }
656
+ }
657
+ // Fallback: If not TSTypeQuery, try resolving directly
658
+ logger.warn(`ReturnType<T>: Expected 'typeof functionName' but got a different type. ` +
659
+ `Use ReturnType<typeof yourFunction> pattern for best results.`);
660
+ return this.resolveTSNodeType(typeParam);
661
+ }
662
+ }
663
+ // Handle Parameters<typeof X> utility type
664
+ if (typeName === "Parameters") {
665
+ if (node.typeParameters && node.typeParameters.params.length > 0) {
666
+ const typeParam = node.typeParameters.params[0];
667
+ // Parameters<typeof functionName>
668
+ if (t.isTSTypeQuery(typeParam)) {
669
+ const funcName = t.isIdentifier(typeParam.exprName)
670
+ ? typeParam.exprName.name
671
+ : null;
672
+ if (funcName) {
673
+ // Save current file path before findSchemaDefinition which may change it
674
+ const savedFilePath = this.currentFilePath;
675
+ // First try to find the function in the current file
676
+ this.findSchemaDefinition(funcName, this.contentType);
677
+ let funcDefEntry = this.typeDefinitions[funcName];
678
+ let funcNode = funcDefEntry?.node || funcDefEntry; // Support both old and new format
679
+ const funcFilePath = funcDefEntry?.filePath;
680
+ // If not found, check if it's an imported function
681
+ // Use the saved file path (where the utility type is defined)
682
+ const sourceFilePath = savedFilePath;
683
+ const normalizedSourcePath = path.normalize(sourceFilePath);
684
+ if (!funcNode && sourceFilePath && this.importMap[normalizedSourcePath]) {
685
+ const importPath = this.importMap[normalizedSourcePath][funcName];
686
+ if (importPath) {
687
+ // Resolve the import path to an absolute file path
688
+ const resolvedPath = this.resolveImportPath(importPath, sourceFilePath);
689
+ if (resolvedPath) {
690
+ // Process the imported file to collect the function
691
+ const content = fs.readFileSync(resolvedPath, "utf-8");
692
+ const ast = parseTypeScriptFile(content);
693
+ // Collect imports and type definitions from the imported file
694
+ this.collectImports(ast, resolvedPath);
695
+ this.collectTypeDefinitions(ast, funcName, resolvedPath);
696
+ // Also collect all exported types/interfaces from the same file
697
+ // This ensures referenced types like Product are available
698
+ this.collectAllExportedDefinitions(ast, resolvedPath);
699
+ // Now try to get the function node again
700
+ funcDefEntry = this.typeDefinitions[funcName];
701
+ funcNode = funcDefEntry?.node || funcDefEntry;
702
+ }
703
+ }
704
+ }
705
+ if (funcNode) {
706
+ // Extract parameters from function
707
+ const params = this.extractFunctionParameters(funcNode);
708
+ if (params && params.length > 0) {
709
+ // Parameters<T> returns a tuple type [Param1, Param2, ...]
710
+ const paramTypes = params.map((param) => {
711
+ if (param.typeAnnotation &&
712
+ param.typeAnnotation.typeAnnotation) {
713
+ return this.resolveTSNodeType(param.typeAnnotation.typeAnnotation);
714
+ }
715
+ return { type: "any" };
716
+ });
717
+ // Return as tuple (array with prefixItems for OpenAPI 3.1)
718
+ return {
719
+ type: "array",
720
+ prefixItems: paramTypes,
721
+ items: false,
722
+ minItems: paramTypes.length,
723
+ maxItems: paramTypes.length,
724
+ };
725
+ }
726
+ else {
727
+ // No parameters
728
+ return {
729
+ type: "array",
730
+ maxItems: 0,
731
+ };
732
+ }
733
+ }
734
+ else {
735
+ logger.warn(`Parameters<typeof ${funcName}>: Function '${funcName}' not found in schema files or imports.`);
736
+ return { type: "array", items: { type: "object" } };
737
+ }
738
+ }
739
+ }
740
+ }
741
+ }
409
742
  if (typeName === "Pick" || typeName === "Omit") {
410
743
  if (node.typeParameters && node.typeParameters.params.length > 1) {
411
744
  const baseTypeParam = node.typeParameters.params[0];
@@ -444,7 +777,8 @@ export class SchemaProcessor {
444
777
  if (node.typeParameters && node.typeParameters.params.length > 0) {
445
778
  // Find the generic type definition first
446
779
  this.findSchemaDefinition(typeName, this.contentType);
447
- const genericTypeDefinition = this.typeDefinitions[typeName];
780
+ const genericDefEntry = this.typeDefinitions[typeName];
781
+ const genericTypeDefinition = genericDefEntry?.node || genericDefEntry;
448
782
  if (genericTypeDefinition) {
449
783
  // Resolve the generic type by substituting type parameters
450
784
  return this.resolveGenericType(genericTypeDefinition, node.typeParameters.params, typeName);
@@ -568,7 +902,12 @@ export class SchemaProcessor {
568
902
  // Recognizes different elements of TS like variable, type, interface, enum
569
903
  const content = fs.readFileSync(filePath, "utf-8");
570
904
  const ast = parseTypeScriptFile(content);
571
- this.collectTypeDefinitions(ast, schemaName);
905
+ // Track current file path for import resolution (normalize for consistency)
906
+ this.currentFilePath = path.normalize(filePath);
907
+ // Collect imports from this file
908
+ this.collectImports(ast, filePath);
909
+ // Collect type definitions, passing the file path explicitly
910
+ this.collectTypeDefinitions(ast, schemaName, filePath);
572
911
  // Reset the set of processed types before each schema processing
573
912
  this.processingTypes.clear();
574
913
  const definition = this.resolveType(schemaName);
@@ -838,28 +1177,24 @@ export class SchemaProcessor {
838
1177
  // Strip array notation for schema lookups
839
1178
  const baseBodyType = stripArrayNotation(bodyType);
840
1179
  const baseResponseType = stripArrayNotation(responseType);
841
- let params = paramsType ? this.openapiDefinitions[paramsType] : {};
842
- let pathParams = pathParamsType
843
- ? this.openapiDefinitions[pathParamsType]
844
- : {};
845
- let body = baseBodyType ? this.openapiDefinitions[baseBodyType] : {};
846
- let responses = baseResponseType ? this.openapiDefinitions[baseResponseType] : {};
847
- if (paramsType && !params) {
1180
+ // Check if schemas exist, if not try to find them
1181
+ if (paramsType && !this.openapiDefinitions[paramsType]) {
848
1182
  this.findSchemaDefinition(paramsType, "params");
849
- params = this.openapiDefinitions[paramsType] || {};
850
1183
  }
851
- if (pathParamsType && !pathParams) {
1184
+ if (pathParamsType && !this.openapiDefinitions[pathParamsType]) {
852
1185
  this.findSchemaDefinition(pathParamsType, "pathParams");
853
- pathParams = this.openapiDefinitions[pathParamsType] || {};
854
1186
  }
855
- if (baseBodyType && !body) {
1187
+ if (baseBodyType && !this.openapiDefinitions[baseBodyType]) {
856
1188
  this.findSchemaDefinition(baseBodyType, "body");
857
- body = this.openapiDefinitions[baseBodyType] || {};
858
1189
  }
859
- if (baseResponseType && !responses) {
1190
+ if (baseResponseType && !this.openapiDefinitions[baseResponseType]) {
860
1191
  this.findSchemaDefinition(baseResponseType, "response");
861
- responses = this.openapiDefinitions[baseResponseType] || {};
862
1192
  }
1193
+ // Now get the schemas (will be {} if still not found)
1194
+ let params = paramsType ? this.openapiDefinitions[paramsType] || {} : {};
1195
+ let pathParams = pathParamsType ? this.openapiDefinitions[pathParamsType] || {} : {};
1196
+ let body = baseBodyType ? this.openapiDefinitions[baseBodyType] || {} : {};
1197
+ let responses = baseResponseType ? this.openapiDefinitions[baseResponseType] || {} : {};
863
1198
  if (this.schemaTypes.includes("zod")) {
864
1199
  const schemasToProcess = [
865
1200
  paramsType,
@@ -895,7 +1230,8 @@ export class SchemaProcessor {
895
1230
  const { baseTypeName, typeArguments } = parsed;
896
1231
  // Find the base generic type definition
897
1232
  this.scanSchemaDir(this.schemaDir, baseTypeName);
898
- const genericTypeDefinition = this.typeDefinitions[baseTypeName];
1233
+ const genericDefEntry = this.typeDefinitions[baseTypeName];
1234
+ const genericTypeDefinition = genericDefEntry?.node || genericDefEntry;
899
1235
  if (!genericTypeDefinition) {
900
1236
  logger.debug(`Generic type definition not found for: ${baseTypeName}`);
901
1237
  return {};
@@ -936,6 +1272,34 @@ export class SchemaProcessor {
936
1272
  // Schema names should not contain { } : ? spaces or other special characters
937
1273
  return /[{}\s:?]/.test(schemaName);
938
1274
  }
1275
+ /**
1276
+ * Check if a type name is a built-in TypeScript utility type
1277
+ * @param {string} typeName - The type name to check
1278
+ * @returns {boolean} - True if it's a built-in utility type
1279
+ */
1280
+ isBuiltInUtilityType(typeName) {
1281
+ const builtInTypes = [
1282
+ 'Awaited', 'Partial', 'Required', 'Readonly', 'Record', 'Pick', 'Omit',
1283
+ 'Exclude', 'Extract', 'NonNullable', 'Parameters', 'ConstructorParameters',
1284
+ 'ReturnType', 'InstanceType', 'ThisParameterType', 'OmitThisParameter',
1285
+ 'ThisType', 'Uppercase', 'Lowercase', 'Capitalize', 'Uncapitalize',
1286
+ 'Promise', 'Array', 'ReadonlyArray', 'Map', 'Set', 'WeakMap', 'WeakSet'
1287
+ ];
1288
+ return builtInTypes.includes(typeName);
1289
+ }
1290
+ /**
1291
+ * Check if a schema name is a function (should not be included in schemas)
1292
+ * Functions are identified by having a node that is a function declaration
1293
+ */
1294
+ isFunctionSchema(schemaName) {
1295
+ const entry = this.typeDefinitions[schemaName];
1296
+ if (!entry)
1297
+ return false;
1298
+ const node = entry.node || entry;
1299
+ return t.isFunctionDeclaration(node) ||
1300
+ t.isFunctionExpression(node) ||
1301
+ t.isArrowFunctionExpression(node);
1302
+ }
939
1303
  /**
940
1304
  * Parse a generic type string into base type and arguments
941
1305
  * @param genericTypeString - The string like "MyApiSuccessResponseBody<LLMSResponse>"
@@ -1032,6 +1396,8 @@ export class SchemaProcessor {
1032
1396
  resolveGenericType(genericTypeDefinition, typeArguments, typeName) {
1033
1397
  // Extract type parameters from the generic type definition
1034
1398
  let typeParameters = [];
1399
+ let bodyToResolve = null;
1400
+ // Handle type alias declarations
1035
1401
  if (t.isTSTypeAliasDeclaration(genericTypeDefinition)) {
1036
1402
  if (genericTypeDefinition.typeParameters &&
1037
1403
  genericTypeDefinition.typeParameters.params) {
@@ -1044,18 +1410,35 @@ export class SchemaProcessor {
1044
1410
  : param.name?.name || param;
1045
1411
  });
1046
1412
  }
1047
- // Create a mapping from type parameters to actual types
1048
- const typeParameterMap = {};
1049
- typeParameters.forEach((param, index) => {
1050
- if (index < typeArguments.length) {
1051
- typeParameterMap[param] = typeArguments[index];
1052
- }
1053
- });
1054
- // Resolve the type annotation with substituted type parameters
1055
- return this.resolveTypeWithSubstitution(genericTypeDefinition.typeAnnotation, typeParameterMap);
1413
+ bodyToResolve = genericTypeDefinition.typeAnnotation;
1056
1414
  }
1057
- // If we can't process the generic type, return empty object
1058
- return {};
1415
+ // Handle interface declarations
1416
+ if (t.isTSInterfaceDeclaration(genericTypeDefinition)) {
1417
+ if (genericTypeDefinition.typeParameters &&
1418
+ genericTypeDefinition.typeParameters.params) {
1419
+ typeParameters = genericTypeDefinition.typeParameters.params.map((param) => {
1420
+ if (t.isTSTypeParameter(param)) {
1421
+ return param.name;
1422
+ }
1423
+ return t.isIdentifier(param)
1424
+ ? param.name
1425
+ : param.name?.name || param;
1426
+ });
1427
+ }
1428
+ bodyToResolve = genericTypeDefinition.body;
1429
+ }
1430
+ if (!bodyToResolve) {
1431
+ return {};
1432
+ }
1433
+ // Create a mapping from type parameters to actual types
1434
+ const typeParameterMap = {};
1435
+ typeParameters.forEach((param, index) => {
1436
+ if (index < typeArguments.length) {
1437
+ typeParameterMap[param] = typeArguments[index];
1438
+ }
1439
+ });
1440
+ // Resolve the type annotation with substituted type parameters
1441
+ return this.resolveTypeWithSubstitution(bodyToResolve, typeParameterMap);
1059
1442
  }
1060
1443
  /**
1061
1444
  * Resolve a type node with type parameter substitution
@@ -1149,7 +1532,66 @@ export class SchemaProcessor {
1149
1532
  });
1150
1533
  return { type: "object", properties };
1151
1534
  }
1535
+ // Handle interface body (from generic interfaces)
1536
+ if (t.isTSInterfaceBody(node)) {
1537
+ const properties = {};
1538
+ node.body.forEach((member) => {
1539
+ if (t.isTSPropertySignature(member) && t.isIdentifier(member.key)) {
1540
+ const propName = member.key.name;
1541
+ properties[propName] = this.resolveTypeWithSubstitution(member.typeAnnotation?.typeAnnotation, typeParameterMap);
1542
+ }
1543
+ });
1544
+ return { type: "object", properties };
1545
+ }
1152
1546
  // Fallback to standard type resolution
1153
1547
  return this.resolveTSNodeType(node);
1154
1548
  }
1549
+ /**
1550
+ * Extracts the return type annotation from a function AST node
1551
+ * @param funcNode - Function declaration or arrow function AST node
1552
+ * @returns The return type annotation node, or null if not found
1553
+ */
1554
+ extractFunctionReturnType(funcNode) {
1555
+ // Handle FunctionDeclaration: function foo(): ReturnType {}
1556
+ if (t.isFunctionDeclaration(funcNode) || t.isFunctionExpression(funcNode)) {
1557
+ return funcNode.returnType && t.isTSTypeAnnotation(funcNode.returnType)
1558
+ ? funcNode.returnType.typeAnnotation
1559
+ : null;
1560
+ }
1561
+ // Handle ArrowFunctionExpression: const foo = (): ReturnType => {}
1562
+ if (t.isArrowFunctionExpression(funcNode)) {
1563
+ return funcNode.returnType && t.isTSTypeAnnotation(funcNode.returnType)
1564
+ ? funcNode.returnType.typeAnnotation
1565
+ : null;
1566
+ }
1567
+ // Handle VariableDeclarator with arrow function
1568
+ if (t.isVariableDeclarator(funcNode) &&
1569
+ t.isArrowFunctionExpression(funcNode.init)) {
1570
+ return funcNode.init.returnType && t.isTSTypeAnnotation(funcNode.init.returnType)
1571
+ ? funcNode.init.returnType.typeAnnotation
1572
+ : null;
1573
+ }
1574
+ return null;
1575
+ }
1576
+ /**
1577
+ * Extracts parameter nodes from a function AST node
1578
+ * @param funcNode - Function declaration or arrow function AST node
1579
+ * @returns Array of parameter nodes
1580
+ */
1581
+ extractFunctionParameters(funcNode) {
1582
+ // Handle FunctionDeclaration
1583
+ if (t.isFunctionDeclaration(funcNode) || t.isFunctionExpression(funcNode)) {
1584
+ return funcNode.params || [];
1585
+ }
1586
+ // Handle ArrowFunctionExpression
1587
+ if (t.isArrowFunctionExpression(funcNode)) {
1588
+ return funcNode.params || [];
1589
+ }
1590
+ // Handle VariableDeclarator with arrow function
1591
+ if (t.isVariableDeclarator(funcNode) &&
1592
+ t.isArrowFunctionExpression(funcNode.init)) {
1593
+ return funcNode.init.params || [];
1594
+ }
1595
+ return [];
1596
+ }
1155
1597
  }
package/dist/lib/utils.js CHANGED
@@ -85,8 +85,9 @@ export function extractJSDocComments(path) {
85
85
  tag = match[1].trim();
86
86
  }
87
87
  }
88
- if (commentValue.includes("@params")) {
89
- paramsType = extractTypeFromComment(commentValue, "@params");
88
+ if (commentValue.includes("@params") || commentValue.includes("@queryParams")) {
89
+ paramsType = extractTypeFromComment(commentValue, "@queryParams") ||
90
+ extractTypeFromComment(commentValue, "@params");
90
91
  }
91
92
  if (commentValue.includes("@pathParams")) {
92
93
  pathParamsType = extractTypeFromComment(commentValue, "@pathParams");
@@ -169,8 +170,9 @@ export function extractJSDocComments(path) {
169
170
  }
170
171
  export function extractTypeFromComment(commentValue, tag) {
171
172
  // Updated regex to support generic types with angle brackets and array brackets
173
+ // Use multiline mode (m flag) to match tag at start of line (after optional * from JSDoc)
172
174
  return (commentValue
173
- .match(new RegExp(`${tag}\\s*\\s*([\\w<>,\\s\\[\\]]+)`))?.[1]
175
+ .match(new RegExp(`^\\s*\\*?\\s*${tag}\\s+([\\w<>,\\s\\[\\]]+)`, 'm'))?.[1]
174
176
  ?.trim() || "");
175
177
  }
176
178
  export function cleanComment(commentValue) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "next-openapi-gen",
3
- "version": "0.9.1",
3
+ "version": "0.9.3",
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",