next-openapi-gen 0.9.2 → 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 +69 -10
- package/dist/lib/schema-processor.js +488 -46
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -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
|
-
###
|
|
734
|
+
### TypeScript Utility Types Support
|
|
722
735
|
|
|
723
|
-
The library
|
|
736
|
+
The library supports TypeScript utility types for extracting types from functions:
|
|
724
737
|
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
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) &&
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
175
|
-
path.node
|
|
176
|
-
|
|
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
|
|
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
|
|
234
|
-
if (!
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
842
|
-
|
|
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 && !
|
|
1184
|
+
if (pathParamsType && !this.openapiDefinitions[pathParamsType]) {
|
|
852
1185
|
this.findSchemaDefinition(pathParamsType, "pathParams");
|
|
853
|
-
pathParams = this.openapiDefinitions[pathParamsType] || {};
|
|
854
1186
|
}
|
|
855
|
-
if (baseBodyType && !
|
|
1187
|
+
if (baseBodyType && !this.openapiDefinitions[baseBodyType]) {
|
|
856
1188
|
this.findSchemaDefinition(baseBodyType, "body");
|
|
857
|
-
body = this.openapiDefinitions[baseBodyType] || {};
|
|
858
1189
|
}
|
|
859
|
-
if (baseResponseType && !
|
|
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
|
|
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
|
-
|
|
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
|
-
//
|
|
1058
|
-
|
|
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/package.json
CHANGED