next-openapi-gen 0.10.4 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/dist/cli.d.ts +4 -0
  2. package/dist/cli.js +8599 -0
  3. package/dist/index.d.ts +18 -0
  4. package/dist/index.js +8645 -26
  5. package/dist/next/index.d.ts +1 -0
  6. package/dist/next/index.js +7965 -0
  7. package/dist/react-router/index.d.ts +1 -0
  8. package/dist/react-router/index.js +7134 -0
  9. package/dist/vite/index.d.ts +1 -0
  10. package/dist/vite/index.js +7134 -0
  11. package/package.json +102 -79
  12. package/{dist/components/rapidoc.js → templates/init/ui/nextjs/rapidoc.tsx} +16 -20
  13. package/templates/init/ui/nextjs/redoc.tsx +11 -0
  14. package/{dist/components/scalar.js → templates/init/ui/nextjs/scalar.tsx} +15 -21
  15. package/{dist/components/stoplight.js → templates/init/ui/nextjs/stoplight.tsx} +11 -17
  16. package/templates/init/ui/nextjs/swagger.tsx +17 -0
  17. package/templates/init/ui/reactrouter/rapidoc.tsx +15 -0
  18. package/templates/init/ui/reactrouter/redoc.tsx +9 -0
  19. package/templates/init/ui/reactrouter/scalar.tsx +14 -0
  20. package/templates/init/ui/reactrouter/stoplight.tsx +10 -0
  21. package/templates/init/ui/reactrouter/swagger.tsx +11 -0
  22. package/templates/init/ui/tanstack/rapidoc.tsx +21 -0
  23. package/templates/init/ui/tanstack/redoc.tsx +14 -0
  24. package/templates/init/ui/tanstack/scalar.tsx +19 -0
  25. package/templates/init/ui/tanstack/stoplight.tsx +15 -0
  26. package/templates/init/ui/tanstack/swagger.tsx +16 -0
  27. package/templates/init/ui/template-types.d.ts +9 -0
  28. package/README.md +0 -1047
  29. package/dist/commands/generate.js +0 -24
  30. package/dist/commands/init.js +0 -194
  31. package/dist/components/redoc.js +0 -17
  32. package/dist/components/swagger.js +0 -21
  33. package/dist/lib/app-router-strategy.js +0 -66
  34. package/dist/lib/drizzle-zod-processor.js +0 -329
  35. package/dist/lib/logger.js +0 -39
  36. package/dist/lib/openapi-generator.js +0 -171
  37. package/dist/lib/pages-router-strategy.js +0 -198
  38. package/dist/lib/route-processor.js +0 -347
  39. package/dist/lib/router-strategy.js +0 -1
  40. package/dist/lib/schema-processor.js +0 -1612
  41. package/dist/lib/utils.js +0 -284
  42. package/dist/lib/zod-converter.js +0 -2133
  43. package/dist/openapi-template.js +0 -99
  44. package/dist/types.js +0 -1
@@ -1,1612 +0,0 @@
1
- import fs from "fs";
2
- import path from "path";
3
- import traverseModule from "@babel/traverse";
4
- import * as t from "@babel/types";
5
- import yaml from "js-yaml";
6
- // Handle both ES modules and CommonJS
7
- const traverse = traverseModule.default || traverseModule;
8
- import { parseTypeScriptFile } from "./utils.js";
9
- import { ZodSchemaConverter } from "./zod-converter.js";
10
- import { logger } from "./logger.js";
11
- /**
12
- * Normalize schemaType to array
13
- */
14
- function normalizeSchemaTypes(schemaType) {
15
- return Array.isArray(schemaType) ? schemaType : [schemaType];
16
- }
17
- /**
18
- * Normalize schemaDir to array
19
- */
20
- function normalizeSchemaDirs(schemaDir) {
21
- return Array.isArray(schemaDir) ? schemaDir : [schemaDir];
22
- }
23
- export class SchemaProcessor {
24
- schemaDirs;
25
- typeDefinitions = {};
26
- openapiDefinitions = {};
27
- contentType = "";
28
- customSchemas = {};
29
- directoryCache = {};
30
- statCache = {};
31
- processSchemaTracker = {};
32
- processingTypes = new Set();
33
- zodSchemaConverter = null;
34
- schemaTypes;
35
- isResolvingPickOmitBase = false;
36
- // Track imports per file for resolving ReturnType<typeof func>
37
- importMap = {}; // { filePath: { importName: importPath } }
38
- currentFilePath = ""; // Track the file being processed
39
- constructor(schemaDir, schemaType = "typescript", schemaFiles, apiDir) {
40
- this.schemaDirs = normalizeSchemaDirs(schemaDir).map((d) => path.resolve(d));
41
- this.schemaTypes = normalizeSchemaTypes(schemaType);
42
- // Initialize Zod converter if Zod is enabled
43
- if (this.schemaTypes.includes("zod")) {
44
- this.zodSchemaConverter = new ZodSchemaConverter(schemaDir, apiDir);
45
- }
46
- // Load custom schema files if provided
47
- if (schemaFiles && schemaFiles.length > 0) {
48
- this.loadCustomSchemas(schemaFiles);
49
- }
50
- }
51
- /**
52
- * Load custom OpenAPI schema files (YAML/JSON)
53
- */
54
- loadCustomSchemas(schemaFiles) {
55
- for (const filePath of schemaFiles) {
56
- try {
57
- const resolvedPath = path.resolve(filePath);
58
- if (!fs.existsSync(resolvedPath)) {
59
- logger.warn(`Schema file not found: ${filePath}`);
60
- continue;
61
- }
62
- const content = fs.readFileSync(resolvedPath, "utf-8");
63
- const ext = path.extname(filePath).toLowerCase();
64
- let parsed;
65
- if (ext === ".yaml" || ext === ".yml") {
66
- parsed = yaml.load(content);
67
- }
68
- else if (ext === ".json") {
69
- parsed = JSON.parse(content);
70
- }
71
- else {
72
- logger.warn(`Unsupported file type: ${filePath} (use .json, .yaml, or .yml)`);
73
- continue;
74
- }
75
- // Extract schemas from OpenAPI structure or use file content directly
76
- const schemas = parsed?.components?.schemas || parsed?.schemas || parsed;
77
- if (typeof schemas === "object" && schemas !== null) {
78
- Object.assign(this.customSchemas, schemas);
79
- logger.log(`✓ Loaded custom schemas from: ${filePath}`);
80
- }
81
- else {
82
- logger.warn(`No valid schemas found in ${filePath}. Expected OpenAPI format with components.schemas or plain object.`);
83
- }
84
- }
85
- catch (error) {
86
- logger.warn(`Failed to load schema file ${filePath}: ${error.message}`);
87
- }
88
- }
89
- }
90
- /**
91
- * Get all defined schemas (for components.schemas section)
92
- * Merges schemas from all sources with proper priority:
93
- * 1. TypeScript types (lowest priority - base layer)
94
- * 2. Zod schemas (medium priority)
95
- * 3. Custom files (highest priority - overrides all)
96
- */
97
- getDefinedSchemas() {
98
- const merged = {};
99
- // Layer 1: TypeScript types (base layer)
100
- const filteredSchemas = {};
101
- Object.entries(this.openapiDefinitions).forEach(([key, value]) => {
102
- if (!this.isGenericTypeParameter(key) &&
103
- !this.isInvalidSchemaName(key) &&
104
- !this.isBuiltInUtilityType(key) &&
105
- !this.isFunctionSchema(key)) {
106
- filteredSchemas[key] = value;
107
- }
108
- });
109
- Object.assign(merged, filteredSchemas);
110
- // Layer 2: Zod schemas (if enabled - overrides TypeScript)
111
- if (this.schemaTypes.includes("zod") && this.zodSchemaConverter) {
112
- const zodSchemas = this.zodSchemaConverter.getProcessedSchemas();
113
- Object.assign(merged, zodSchemas);
114
- }
115
- // Layer 3: Custom files (highest priority - overrides all)
116
- Object.assign(merged, this.customSchemas);
117
- return merged;
118
- }
119
- findSchemaDefinition(schemaName, contentType) {
120
- // Assign type that is actually processed
121
- this.contentType = contentType;
122
- // Check if the schemaName is a generic type (contains < and >)
123
- if (schemaName.includes("<") && schemaName.includes(">")) {
124
- return this.resolveGenericTypeFromString(schemaName);
125
- }
126
- // Priority 1: Check custom schemas first (highest priority)
127
- if (this.customSchemas[schemaName]) {
128
- logger.debug(`Found schema in custom files: ${schemaName}`);
129
- return this.customSchemas[schemaName];
130
- }
131
- // Priority 2: Try Zod schemas if enabled
132
- if (this.schemaTypes.includes("zod") && this.zodSchemaConverter) {
133
- logger.debug(`Looking for Zod schema: ${schemaName}`);
134
- // Check type mapping first
135
- const mappedSchemaName = this.zodSchemaConverter.typeToSchemaMapping[schemaName];
136
- if (mappedSchemaName) {
137
- logger.debug(`Type '${schemaName}' is mapped to Zod schema '${mappedSchemaName}'`);
138
- }
139
- // Try to convert Zod schema
140
- const zodSchema = this.zodSchemaConverter.convertZodSchemaToOpenApi(schemaName);
141
- if (zodSchema) {
142
- logger.debug(`Found and processed Zod schema: ${schemaName}`);
143
- this.openapiDefinitions[schemaName] = zodSchema;
144
- return zodSchema;
145
- }
146
- logger.debug(`No Zod schema found for ${schemaName}, trying TypeScript fallback`);
147
- }
148
- // Fall back to TypeScript types
149
- this.scanAllSchemaDirs(schemaName);
150
- return this.openapiDefinitions[schemaName] || {};
151
- }
152
- scanAllSchemaDirs(schemaName) {
153
- for (const dir of this.schemaDirs) {
154
- if (!fs.existsSync(dir)) {
155
- logger.warn(`Schema directory not found: ${dir}`);
156
- continue;
157
- }
158
- this.scanSchemaDir(dir, schemaName);
159
- }
160
- }
161
- scanSchemaDir(dir, schemaName) {
162
- let files = this.directoryCache[dir];
163
- if (typeof files === "undefined") {
164
- files = fs.readdirSync(dir);
165
- this.directoryCache[dir] = files;
166
- }
167
- files.forEach((file) => {
168
- const filePath = path.join(dir, file);
169
- let stat = this.statCache[filePath];
170
- if (typeof stat === "undefined") {
171
- stat = fs.statSync(filePath);
172
- this.statCache[filePath] = stat;
173
- }
174
- if (stat.isDirectory()) {
175
- this.scanSchemaDir(filePath, schemaName);
176
- }
177
- else if (file.endsWith(".ts") || file.endsWith(".tsx")) {
178
- this.processSchemaFile(filePath, schemaName);
179
- }
180
- });
181
- }
182
- collectImports(ast, filePath) {
183
- // Normalize path to avoid Windows/Unix path separator issues
184
- const normalizedPath = path.normalize(filePath);
185
- if (!this.importMap[normalizedPath]) {
186
- this.importMap[normalizedPath] = {};
187
- }
188
- traverse(ast, {
189
- ImportDeclaration: (path) => {
190
- const importPath = path.node.source.value;
191
- // Handle named imports: import { foo, bar } from './file'
192
- path.node.specifiers.forEach((specifier) => {
193
- if (t.isImportSpecifier(specifier)) {
194
- const importedName = t.isIdentifier(specifier.imported) ? specifier.imported.name : specifier.imported.value;
195
- this.importMap[normalizedPath][importedName] = importPath;
196
- }
197
- // Handle default imports: import foo from './file'
198
- else if (t.isImportDefaultSpecifier(specifier)) {
199
- const importedName = specifier.local.name;
200
- this.importMap[normalizedPath][importedName] = importPath;
201
- }
202
- // Handle namespace imports: import * as foo from './file'
203
- else if (t.isImportNamespaceSpecifier(specifier)) {
204
- const importedName = specifier.local.name;
205
- this.importMap[normalizedPath][importedName] = importPath;
206
- }
207
- });
208
- },
209
- });
210
- }
211
- /**
212
- * Resolve an import path relative to the current file
213
- * Converts import paths like "../app/api/products/route.utils" to absolute file paths
214
- */
215
- resolveImportPath(importPath, fromFilePath) {
216
- // Skip node_modules imports
217
- if (!importPath.startsWith('.')) {
218
- return null;
219
- }
220
- const fromDir = path.dirname(fromFilePath);
221
- let resolvedPath = path.resolve(fromDir, importPath);
222
- // Try with .ts extension
223
- if (fs.existsSync(resolvedPath + '.ts')) {
224
- return resolvedPath + '.ts';
225
- }
226
- // Try with .tsx extension
227
- if (fs.existsSync(resolvedPath + '.tsx')) {
228
- return resolvedPath + '.tsx';
229
- }
230
- // Try as-is (might already have extension)
231
- if (fs.existsSync(resolvedPath)) {
232
- return resolvedPath;
233
- }
234
- return null;
235
- }
236
- /**
237
- * Collect all exported type definitions from an AST without filtering by name
238
- * Used when processing imported files to ensure all referenced types are available
239
- */
240
- collectAllExportedDefinitions(ast, filePath) {
241
- const currentFile = filePath || this.currentFilePath;
242
- traverse(ast, {
243
- TSTypeAliasDeclaration: (path) => {
244
- if (path.node.id && t.isIdentifier(path.node.id)) {
245
- const name = path.node.id.name;
246
- if (!this.typeDefinitions[name]) {
247
- const node = (path.node.typeParameters && path.node.typeParameters.params.length > 0)
248
- ? path.node
249
- : path.node.typeAnnotation;
250
- this.typeDefinitions[name] = { node, filePath: currentFile };
251
- }
252
- }
253
- },
254
- TSInterfaceDeclaration: (path) => {
255
- if (path.node.id && t.isIdentifier(path.node.id)) {
256
- const name = path.node.id.name;
257
- if (!this.typeDefinitions[name]) {
258
- this.typeDefinitions[name] = { node: path.node, filePath: currentFile };
259
- }
260
- }
261
- },
262
- TSEnumDeclaration: (path) => {
263
- if (path.node.id && t.isIdentifier(path.node.id)) {
264
- const name = path.node.id.name;
265
- if (!this.typeDefinitions[name]) {
266
- this.typeDefinitions[name] = { node: path.node, filePath: currentFile };
267
- }
268
- }
269
- },
270
- ExportNamedDeclaration: (path) => {
271
- // Handle exported interfaces
272
- if (t.isTSInterfaceDeclaration(path.node.declaration)) {
273
- const interfaceDecl = path.node.declaration;
274
- if (interfaceDecl.id && t.isIdentifier(interfaceDecl.id)) {
275
- const name = interfaceDecl.id.name;
276
- if (!this.typeDefinitions[name]) {
277
- this.typeDefinitions[name] = { node: interfaceDecl, filePath: currentFile };
278
- }
279
- }
280
- }
281
- // Handle exported type aliases
282
- if (t.isTSTypeAliasDeclaration(path.node.declaration)) {
283
- const typeDecl = path.node.declaration;
284
- if (typeDecl.id && t.isIdentifier(typeDecl.id)) {
285
- const name = typeDecl.id.name;
286
- if (!this.typeDefinitions[name]) {
287
- const node = (typeDecl.typeParameters && typeDecl.typeParameters.params.length > 0)
288
- ? typeDecl
289
- : typeDecl.typeAnnotation;
290
- this.typeDefinitions[name] = { node, filePath: currentFile };
291
- }
292
- }
293
- }
294
- },
295
- });
296
- }
297
- collectTypeDefinitions(ast, schemaName, filePath) {
298
- const currentFile = filePath || this.currentFilePath;
299
- traverse(ast, {
300
- VariableDeclarator: (path) => {
301
- if (t.isIdentifier(path.node.id, { name: schemaName })) {
302
- const name = path.node.id.name;
303
- this.typeDefinitions[name] = { node: path.node.init || path.node, filePath: currentFile };
304
- }
305
- },
306
- TSTypeAliasDeclaration: (path) => {
307
- if (t.isIdentifier(path.node.id, { name: schemaName })) {
308
- const name = path.node.id.name;
309
- // Store the full node for generic types, just the type annotation for regular types
310
- const node = (path.node.typeParameters && path.node.typeParameters.params.length > 0)
311
- ? path.node // Store the full declaration for generic types
312
- : path.node.typeAnnotation; // Store just the type annotation for regular types
313
- this.typeDefinitions[name] = { node, filePath: currentFile };
314
- }
315
- },
316
- TSInterfaceDeclaration: (path) => {
317
- if (t.isIdentifier(path.node.id, { name: schemaName })) {
318
- const name = path.node.id.name;
319
- this.typeDefinitions[name] = { node: path.node, filePath: currentFile };
320
- }
321
- },
322
- TSEnumDeclaration: (path) => {
323
- if (t.isIdentifier(path.node.id, { name: schemaName })) {
324
- const name = path.node.id.name;
325
- this.typeDefinitions[name] = { node: path.node, filePath: currentFile };
326
- }
327
- },
328
- // Collect function declarations for ReturnType<typeof func> support
329
- FunctionDeclaration: (path) => {
330
- if (path.node.id && t.isIdentifier(path.node.id, { name: schemaName })) {
331
- const name = path.node.id.name;
332
- this.typeDefinitions[name] = { node: path.node, filePath: currentFile };
333
- }
334
- },
335
- // Collect exported zod schemas and functions
336
- ExportNamedDeclaration: (path) => {
337
- if (t.isVariableDeclaration(path.node.declaration)) {
338
- path.node.declaration.declarations.forEach((declaration) => {
339
- if (t.isIdentifier(declaration.id) &&
340
- declaration.id.name === schemaName &&
341
- declaration.init) {
342
- // Check if is Zod schema
343
- if (t.isCallExpression(declaration.init) &&
344
- t.isMemberExpression(declaration.init.callee) &&
345
- t.isIdentifier(declaration.init.callee.object) &&
346
- declaration.init.callee.object.name === "z") {
347
- const name = declaration.id.name;
348
- this.typeDefinitions[name] = { node: declaration.init, filePath: currentFile };
349
- }
350
- }
351
- });
352
- }
353
- // Handle exported function declarations
354
- if (t.isFunctionDeclaration(path.node.declaration)) {
355
- const funcDecl = path.node.declaration;
356
- if (funcDecl.id && t.isIdentifier(funcDecl.id, { name: schemaName })) {
357
- const name = funcDecl.id.name;
358
- this.typeDefinitions[name] = { node: funcDecl, filePath: currentFile };
359
- }
360
- }
361
- },
362
- });
363
- }
364
- resolveType(typeName) {
365
- if (this.processingTypes.has(typeName)) {
366
- // Return reference to type to avoid infinite recursion
367
- return { $ref: `#/components/schemas/${typeName}` };
368
- }
369
- // Add type to processing types
370
- this.processingTypes.add(typeName);
371
- try {
372
- // If we are using Zod and the given type is not found yet, try using Zod converter first
373
- if (this.schemaTypes.includes("zod") &&
374
- !this.openapiDefinitions[typeName]) {
375
- const zodSchema = this.zodSchemaConverter.convertZodSchemaToOpenApi(typeName);
376
- if (zodSchema) {
377
- this.openapiDefinitions[typeName] = zodSchema;
378
- return zodSchema;
379
- }
380
- }
381
- const typeDefEntry = this.typeDefinitions[typeName.toString()];
382
- if (!typeDefEntry)
383
- return {};
384
- const typeNode = typeDefEntry.node || typeDefEntry; // Support both old and new format
385
- // Handle generic type alias declarations (full node)
386
- if (t.isTSTypeAliasDeclaration(typeNode)) {
387
- // This is a generic type, should be handled by the caller via resolveGenericType
388
- // For non-generic access, just return the type annotation
389
- const typeAnnotation = typeNode.typeAnnotation;
390
- return this.resolveTSNodeType(typeAnnotation);
391
- }
392
- // Check if node is Zod
393
- if (t.isCallExpression(typeNode) &&
394
- t.isMemberExpression(typeNode.callee) &&
395
- t.isIdentifier(typeNode.callee.object) &&
396
- typeNode.callee.object.name === "z") {
397
- if (this.schemaTypes.includes("zod")) {
398
- const zodSchema = this.zodSchemaConverter.processZodNode(typeNode);
399
- if (zodSchema) {
400
- this.openapiDefinitions[typeName] = zodSchema;
401
- return zodSchema;
402
- }
403
- }
404
- }
405
- if (t.isTSEnumDeclaration(typeNode)) {
406
- const enumValues = this.processEnum(typeNode);
407
- return enumValues;
408
- }
409
- if (t.isTSTypeLiteral(typeNode) ||
410
- t.isTSInterfaceBody(typeNode) ||
411
- t.isTSInterfaceDeclaration(typeNode)) {
412
- const properties = {};
413
- // Handle interface extends clause
414
- if (t.isTSInterfaceDeclaration(typeNode) &&
415
- typeNode.extends &&
416
- typeNode.extends.length > 0) {
417
- typeNode.extends.forEach((extendedType) => {
418
- const extendedSchema = this.resolveTSNodeType(extendedType);
419
- if (extendedSchema.properties) {
420
- Object.assign(properties, extendedSchema.properties);
421
- }
422
- });
423
- }
424
- // Get members from interface declaration body or direct members
425
- const members = t.isTSInterfaceDeclaration(typeNode)
426
- ? typeNode.body.body
427
- : typeNode.members;
428
- if (members) {
429
- (members || []).forEach((member) => {
430
- if (t.isTSPropertySignature(member) && t.isIdentifier(member.key)) {
431
- const propName = member.key.name;
432
- const options = this.getPropertyOptions(member);
433
- const property = {
434
- ...this.resolveTSNodeType(member.typeAnnotation?.typeAnnotation),
435
- ...options,
436
- };
437
- properties[propName] = property;
438
- }
439
- });
440
- }
441
- return { type: "object", properties };
442
- }
443
- if (t.isTSArrayType(typeNode)) {
444
- return {
445
- type: "array",
446
- items: this.resolveTSNodeType(typeNode.elementType),
447
- };
448
- }
449
- if (t.isTSUnionType(typeNode)) {
450
- return this.resolveTSNodeType(typeNode);
451
- }
452
- if (t.isTSTypeReference(typeNode)) {
453
- return this.resolveTSNodeType(typeNode);
454
- }
455
- // Handle indexed access types (e.g., Parameters<typeof func>[0])
456
- if (t.isTSIndexedAccessType(typeNode)) {
457
- return this.resolveTSNodeType(typeNode);
458
- }
459
- return {};
460
- }
461
- finally {
462
- // Remove type from processed set after we finish
463
- this.processingTypes.delete(typeName);
464
- }
465
- }
466
- isDateString(node) {
467
- if (t.isStringLiteral(node)) {
468
- const dateRegex = /^\d{4}-\d{2}-\d{2}(T\d{2}:\d{2}:\d{2}(\.\d{3})?Z)?$/;
469
- return dateRegex.test(node.value);
470
- }
471
- return false;
472
- }
473
- isDateObject(node) {
474
- return (t.isNewExpression(node) && t.isIdentifier(node.callee, { name: "Date" }));
475
- }
476
- isDateNode(node) {
477
- return this.isDateString(node) || this.isDateObject(node);
478
- }
479
- resolveTSNodeType(node) {
480
- if (!node)
481
- return { type: "object" }; // Default type for undefined/null
482
- if (t.isTSStringKeyword(node))
483
- return { type: "string" };
484
- if (t.isTSNumberKeyword(node))
485
- return { type: "number" };
486
- if (t.isTSBooleanKeyword(node))
487
- return { type: "boolean" };
488
- if (t.isTSAnyKeyword(node) || t.isTSUnknownKeyword(node))
489
- return { type: "object" };
490
- if (t.isTSVoidKeyword(node) ||
491
- t.isTSNullKeyword(node) ||
492
- t.isTSUndefinedKeyword(node))
493
- return { type: "null" };
494
- if (this.isDateNode(node))
495
- return { type: "string", format: "date-time" };
496
- // Handle literal types like "admin" | "member" | "guest"
497
- if (t.isTSLiteralType(node)) {
498
- if (t.isStringLiteral(node.literal)) {
499
- return {
500
- type: "string",
501
- enum: [node.literal.value],
502
- };
503
- }
504
- else if (t.isNumericLiteral(node.literal)) {
505
- return {
506
- type: "number",
507
- enum: [node.literal.value],
508
- };
509
- }
510
- else if (t.isBooleanLiteral(node.literal)) {
511
- return {
512
- type: "boolean",
513
- enum: [node.literal.value],
514
- };
515
- }
516
- }
517
- // Handle TSExpressionWithTypeArguments (used in interface extends)
518
- if (t.isTSExpressionWithTypeArguments(node)) {
519
- if (t.isIdentifier(node.expression)) {
520
- // Convert to TSTypeReference-like structure for processing
521
- const syntheticNode = {
522
- type: "TSTypeReference",
523
- typeName: node.expression,
524
- typeParameters: node.typeParameters,
525
- };
526
- return this.resolveTSNodeType(syntheticNode);
527
- }
528
- }
529
- // Handle indexed access types: SomeType[0] or SomeType["key"]
530
- if (t.isTSIndexedAccessType(node)) {
531
- const objectType = this.resolveTSNodeType(node.objectType);
532
- const indexType = node.indexType;
533
- // Handle numeric index: Parameters<typeof func>[0]
534
- if (t.isTSLiteralType(indexType) && t.isNumericLiteral(indexType.literal)) {
535
- const index = indexType.literal.value;
536
- // If objectType is a tuple (has prefixItems), get the specific item
537
- if (objectType.prefixItems && Array.isArray(objectType.prefixItems)) {
538
- if (index < objectType.prefixItems.length) {
539
- return objectType.prefixItems[index];
540
- }
541
- else {
542
- logger.warn(`Index ${index} is out of bounds for tuple type.`);
543
- return { type: "object" };
544
- }
545
- }
546
- // If objectType is a regular array, return the items type
547
- if (objectType.type === "array" && objectType.items) {
548
- return objectType.items;
549
- }
550
- }
551
- // Handle string index: SomeType["propertyName"]
552
- if (t.isTSLiteralType(indexType) && t.isStringLiteral(indexType.literal)) {
553
- const key = indexType.literal.value;
554
- // If objectType has properties, get the specific property
555
- if (objectType.properties && objectType.properties[key]) {
556
- return objectType.properties[key];
557
- }
558
- }
559
- // Fallback
560
- return { type: "object" };
561
- }
562
- if (t.isTSTypeReference(node) && t.isIdentifier(node.typeName)) {
563
- const typeName = node.typeName.name;
564
- // Special handling for built-in types
565
- if (typeName === "Date") {
566
- return { type: "string", format: "date-time" };
567
- }
568
- // Handle Promise<T> - in OpenAPI, promises are transparent (we document the resolved value)
569
- if (typeName === "Promise") {
570
- if (node.typeParameters && node.typeParameters.params.length > 0) {
571
- // Return the inner type directly - promises are async wrappers
572
- return this.resolveTSNodeType(node.typeParameters.params[0]);
573
- }
574
- return { type: "object" }; // Promise with no type parameter
575
- }
576
- if (typeName === "Array" || typeName === "ReadonlyArray") {
577
- if (node.typeParameters && node.typeParameters.params.length > 0) {
578
- return {
579
- type: "array",
580
- items: this.resolveTSNodeType(node.typeParameters.params[0]),
581
- };
582
- }
583
- return { type: "array", items: { type: "object" } };
584
- }
585
- if (typeName === "Record") {
586
- if (node.typeParameters && node.typeParameters.params.length > 1) {
587
- const keyType = this.resolveTSNodeType(node.typeParameters.params[0]);
588
- const valueType = this.resolveTSNodeType(node.typeParameters.params[1]);
589
- return {
590
- type: "object",
591
- additionalProperties: valueType,
592
- };
593
- }
594
- return { type: "object", additionalProperties: true };
595
- }
596
- if (typeName === "Partial" ||
597
- typeName === "Required" ||
598
- typeName === "Readonly") {
599
- if (node.typeParameters && node.typeParameters.params.length > 0) {
600
- return this.resolveTSNodeType(node.typeParameters.params[0]);
601
- }
602
- }
603
- // Handle Awaited<T> utility type
604
- if (typeName === "Awaited") {
605
- if (node.typeParameters && node.typeParameters.params.length > 0) {
606
- // Unwrap the inner type - promises are transparent in OpenAPI
607
- return this.resolveTSNodeType(node.typeParameters.params[0]);
608
- }
609
- }
610
- // Handle ReturnType<typeof X> utility type
611
- if (typeName === "ReturnType") {
612
- if (node.typeParameters && node.typeParameters.params.length > 0) {
613
- const typeParam = node.typeParameters.params[0];
614
- // ReturnType<typeof functionName>
615
- if (t.isTSTypeQuery(typeParam)) {
616
- const funcName = t.isIdentifier(typeParam.exprName)
617
- ? typeParam.exprName.name
618
- : null;
619
- if (funcName) {
620
- // Save current file path before findSchemaDefinition which may change it
621
- const savedFilePath = this.currentFilePath;
622
- // First try to find the function in the current file
623
- this.findSchemaDefinition(funcName, this.contentType);
624
- let funcDefEntry = this.typeDefinitions[funcName];
625
- let funcNode = funcDefEntry?.node || funcDefEntry; // Support both old and new format
626
- const funcFilePath = funcDefEntry?.filePath;
627
- // If not found, check if it's an imported function
628
- // Use the saved file path (where the utility type is defined)
629
- const sourceFilePath = savedFilePath;
630
- const normalizedSourcePath = path.normalize(sourceFilePath);
631
- if (!funcNode && sourceFilePath && this.importMap[normalizedSourcePath]) {
632
- const importPath = this.importMap[normalizedSourcePath][funcName];
633
- if (importPath) {
634
- // Resolve the import path to an absolute file path
635
- const resolvedPath = this.resolveImportPath(importPath, sourceFilePath);
636
- if (resolvedPath) {
637
- // Process the imported file to collect the function
638
- const content = fs.readFileSync(resolvedPath, "utf-8");
639
- const ast = parseTypeScriptFile(content);
640
- // Collect imports and type definitions from the imported file
641
- this.collectImports(ast, resolvedPath);
642
- this.collectTypeDefinitions(ast, funcName, resolvedPath);
643
- // Also collect all exported types/interfaces from the same file
644
- // This ensures referenced types like Product are available
645
- this.collectAllExportedDefinitions(ast, resolvedPath);
646
- // Now try to get the function node again
647
- funcDefEntry = this.typeDefinitions[funcName];
648
- funcNode = funcDefEntry?.node || funcDefEntry;
649
- }
650
- }
651
- }
652
- if (funcNode) {
653
- // Extract the return type annotation
654
- const returnTypeNode = this.extractFunctionReturnType(funcNode);
655
- if (returnTypeNode) {
656
- // Recursively resolve the return type
657
- return this.resolveTSNodeType(returnTypeNode);
658
- }
659
- else {
660
- logger.warn(`ReturnType<typeof ${funcName}>: Function '${funcName}' does not have an explicit return type annotation. ` +
661
- `Add a return type to the function signature for accurate schema generation.`);
662
- return { type: "object" };
663
- }
664
- }
665
- else {
666
- logger.warn(`ReturnType<typeof ${funcName}>: Function '${funcName}' not found in schema files or imports. ` +
667
- `Ensure the function is exported and imported correctly.`);
668
- return { type: "object" };
669
- }
670
- }
671
- }
672
- // Fallback: If not TSTypeQuery, try resolving directly
673
- logger.warn(`ReturnType<T>: Expected 'typeof functionName' but got a different type. ` +
674
- `Use ReturnType<typeof yourFunction> pattern for best results.`);
675
- return this.resolveTSNodeType(typeParam);
676
- }
677
- }
678
- // Handle Parameters<typeof X> utility type
679
- if (typeName === "Parameters") {
680
- if (node.typeParameters && node.typeParameters.params.length > 0) {
681
- const typeParam = node.typeParameters.params[0];
682
- // Parameters<typeof functionName>
683
- if (t.isTSTypeQuery(typeParam)) {
684
- const funcName = t.isIdentifier(typeParam.exprName)
685
- ? typeParam.exprName.name
686
- : null;
687
- if (funcName) {
688
- // Save current file path before findSchemaDefinition which may change it
689
- const savedFilePath = this.currentFilePath;
690
- // First try to find the function in the current file
691
- this.findSchemaDefinition(funcName, this.contentType);
692
- let funcDefEntry = this.typeDefinitions[funcName];
693
- let funcNode = funcDefEntry?.node || funcDefEntry; // Support both old and new format
694
- const funcFilePath = funcDefEntry?.filePath;
695
- // If not found, check if it's an imported function
696
- // Use the saved file path (where the utility type is defined)
697
- const sourceFilePath = savedFilePath;
698
- const normalizedSourcePath = path.normalize(sourceFilePath);
699
- if (!funcNode && sourceFilePath && this.importMap[normalizedSourcePath]) {
700
- const importPath = this.importMap[normalizedSourcePath][funcName];
701
- if (importPath) {
702
- // Resolve the import path to an absolute file path
703
- const resolvedPath = this.resolveImportPath(importPath, sourceFilePath);
704
- if (resolvedPath) {
705
- // Process the imported file to collect the function
706
- const content = fs.readFileSync(resolvedPath, "utf-8");
707
- const ast = parseTypeScriptFile(content);
708
- // Collect imports and type definitions from the imported file
709
- this.collectImports(ast, resolvedPath);
710
- this.collectTypeDefinitions(ast, funcName, resolvedPath);
711
- // Also collect all exported types/interfaces from the same file
712
- // This ensures referenced types like Product are available
713
- this.collectAllExportedDefinitions(ast, resolvedPath);
714
- // Now try to get the function node again
715
- funcDefEntry = this.typeDefinitions[funcName];
716
- funcNode = funcDefEntry?.node || funcDefEntry;
717
- }
718
- }
719
- }
720
- if (funcNode) {
721
- // Extract parameters from function
722
- const params = this.extractFunctionParameters(funcNode);
723
- if (params && params.length > 0) {
724
- // Parameters<T> returns a tuple type [Param1, Param2, ...]
725
- const paramTypes = params.map((param) => {
726
- if (param.typeAnnotation &&
727
- param.typeAnnotation.typeAnnotation) {
728
- return this.resolveTSNodeType(param.typeAnnotation.typeAnnotation);
729
- }
730
- return { type: "any" };
731
- });
732
- // Return as tuple (array with prefixItems for OpenAPI 3.1)
733
- return {
734
- type: "array",
735
- prefixItems: paramTypes,
736
- items: false,
737
- minItems: paramTypes.length,
738
- maxItems: paramTypes.length,
739
- };
740
- }
741
- else {
742
- // No parameters
743
- return {
744
- type: "array",
745
- maxItems: 0,
746
- };
747
- }
748
- }
749
- else {
750
- logger.warn(`Parameters<typeof ${funcName}>: Function '${funcName}' not found in schema files or imports.`);
751
- return { type: "array", items: { type: "object" } };
752
- }
753
- }
754
- }
755
- }
756
- }
757
- if (typeName === "Pick" || typeName === "Omit") {
758
- if (node.typeParameters && node.typeParameters.params.length > 1) {
759
- const baseTypeParam = node.typeParameters.params[0];
760
- const keysParam = node.typeParameters.params[1];
761
- // Resolve base type without adding it to schema definitions
762
- this.isResolvingPickOmitBase = true;
763
- const baseType = this.resolveTSNodeType(baseTypeParam);
764
- this.isResolvingPickOmitBase = false;
765
- if (baseType.properties) {
766
- const properties = {};
767
- const keyNames = this.extractKeysFromLiteralType(keysParam);
768
- if (typeName === "Pick") {
769
- keyNames.forEach((key) => {
770
- if (baseType.properties[key]) {
771
- properties[key] = baseType.properties[key];
772
- }
773
- });
774
- }
775
- else {
776
- // Omit
777
- Object.entries(baseType.properties).forEach(([key, value]) => {
778
- if (!keyNames.includes(key)) {
779
- properties[key] = value;
780
- }
781
- });
782
- }
783
- return { type: "object", properties };
784
- }
785
- }
786
- // Fallback to just the base type if we can't process properly
787
- if (node.typeParameters && node.typeParameters.params.length > 0) {
788
- return this.resolveTSNodeType(node.typeParameters.params[0]);
789
- }
790
- }
791
- // Handle custom generic types
792
- if (node.typeParameters && node.typeParameters.params.length > 0) {
793
- // Find the generic type definition first
794
- this.findSchemaDefinition(typeName, this.contentType);
795
- const genericDefEntry = this.typeDefinitions[typeName];
796
- const genericTypeDefinition = genericDefEntry?.node || genericDefEntry;
797
- if (genericTypeDefinition) {
798
- // Resolve the generic type by substituting type parameters
799
- return this.resolveGenericType(genericTypeDefinition, node.typeParameters.params, typeName);
800
- }
801
- }
802
- // Check if it is a type that we are already processing
803
- if (this.processingTypes.has(typeName)) {
804
- return { $ref: `#/components/schemas/${typeName}` };
805
- }
806
- // Find type definition
807
- this.findSchemaDefinition(typeName, this.contentType);
808
- return this.resolveType(node.typeName.name);
809
- }
810
- if (t.isTSArrayType(node)) {
811
- return {
812
- type: "array",
813
- items: this.resolveTSNodeType(node.elementType),
814
- };
815
- }
816
- if (t.isTSTypeLiteral(node)) {
817
- const properties = {};
818
- node.members.forEach((member) => {
819
- if (t.isTSPropertySignature(member) && t.isIdentifier(member.key)) {
820
- const propName = member.key.name;
821
- properties[propName] = this.resolveTSNodeType(member.typeAnnotation?.typeAnnotation);
822
- }
823
- });
824
- return { type: "object", properties };
825
- }
826
- if (t.isTSUnionType(node)) {
827
- // Handle union types with literal types, like "admin" | "member" | "guest"
828
- const literals = node.types.filter((type) => t.isTSLiteralType(type));
829
- // Check if all union elements are literals
830
- if (literals.length === node.types.length) {
831
- // All union members are literals, convert to enum
832
- const enumValues = literals
833
- .map((type) => {
834
- if (t.isTSLiteralType(type) && t.isStringLiteral(type.literal)) {
835
- return type.literal.value;
836
- }
837
- else if (t.isTSLiteralType(type) &&
838
- t.isNumericLiteral(type.literal)) {
839
- return type.literal.value;
840
- }
841
- else if (t.isTSLiteralType(type) &&
842
- t.isBooleanLiteral(type.literal)) {
843
- return type.literal.value;
844
- }
845
- return null;
846
- })
847
- .filter((value) => value !== null);
848
- if (enumValues.length > 0) {
849
- // Check if all enum values are of the same type
850
- const firstType = typeof enumValues[0];
851
- const sameType = enumValues.every((val) => typeof val === firstType);
852
- if (sameType) {
853
- return {
854
- type: firstType,
855
- enum: enumValues,
856
- };
857
- }
858
- }
859
- }
860
- // Handling null | undefined in type union
861
- const nullableTypes = node.types.filter((type) => t.isTSNullKeyword(type) ||
862
- t.isTSUndefinedKeyword(type) ||
863
- t.isTSVoidKeyword(type));
864
- const nonNullableTypes = node.types.filter((type) => !t.isTSNullKeyword(type) &&
865
- !t.isTSUndefinedKeyword(type) &&
866
- !t.isTSVoidKeyword(type));
867
- // If a type can be null/undefined, we mark it as nullable
868
- if (nullableTypes.length > 0 && nonNullableTypes.length === 1) {
869
- const mainType = this.resolveTSNodeType(nonNullableTypes[0]);
870
- return {
871
- ...mainType,
872
- nullable: true,
873
- };
874
- }
875
- // Standard union type support via oneOf
876
- return {
877
- oneOf: node.types
878
- .filter((type) => !t.isTSNullKeyword(type) &&
879
- !t.isTSUndefinedKeyword(type) &&
880
- !t.isTSVoidKeyword(type))
881
- .map((subNode) => this.resolveTSNodeType(subNode)),
882
- };
883
- }
884
- if (t.isTSIntersectionType(node)) {
885
- // For intersection types, we combine properties
886
- const allProperties = {};
887
- const requiredProperties = [];
888
- node.types.forEach((typeNode) => {
889
- const resolvedType = this.resolveTSNodeType(typeNode);
890
- if (resolvedType.type === "object" && resolvedType.properties) {
891
- Object.entries(resolvedType.properties).forEach(([key, value]) => {
892
- allProperties[key] = value;
893
- if (value.required) {
894
- requiredProperties.push(key);
895
- }
896
- });
897
- }
898
- });
899
- return {
900
- type: "object",
901
- properties: allProperties,
902
- required: requiredProperties.length > 0 ? requiredProperties : undefined,
903
- };
904
- }
905
- // Case where a type is a reference to another defined type
906
- if (t.isTSTypeReference(node) && t.isIdentifier(node.typeName)) {
907
- return { $ref: `#/components/schemas/${node.typeName.name}` };
908
- }
909
- logger.debug("Unrecognized TypeScript type node:", node);
910
- return { type: "object" }; // By default we return an object
911
- }
912
- processSchemaFile(filePath, schemaName) {
913
- // Check if the file has already been processed
914
- if (this.processSchemaTracker[`${filePath}-${schemaName}`])
915
- return;
916
- try {
917
- // Recognizes different elements of TS like variable, type, interface, enum
918
- const content = fs.readFileSync(filePath, "utf-8");
919
- const ast = parseTypeScriptFile(content);
920
- // Track current file path for import resolution (normalize for consistency)
921
- this.currentFilePath = path.normalize(filePath);
922
- // Collect imports from this file
923
- this.collectImports(ast, filePath);
924
- // Collect type definitions, passing the file path explicitly
925
- this.collectTypeDefinitions(ast, schemaName, filePath);
926
- // Reset the set of processed types before each schema processing
927
- this.processingTypes.clear();
928
- const definition = this.resolveType(schemaName);
929
- if (!this.isResolvingPickOmitBase) {
930
- this.openapiDefinitions[schemaName] = definition;
931
- }
932
- this.processSchemaTracker[`${filePath}-${schemaName}`] = true;
933
- return definition;
934
- }
935
- catch (error) {
936
- logger.error(`Error processing schema file ${filePath} for schema ${schemaName}: ${error}`);
937
- return { type: "object" }; // By default we return an empty object on error
938
- }
939
- }
940
- processEnum(enumNode) {
941
- // Initialization OpenAPI enum object
942
- const enumSchema = {
943
- type: "string",
944
- enum: [],
945
- };
946
- // Iterate throught enum members
947
- enumNode.members.forEach((member) => {
948
- if (t.isTSEnumMember(member)) {
949
- // @ts-ignore
950
- const name = member.id?.name;
951
- // @ts-ignore
952
- const value = member.initializer?.value;
953
- let type = member.initializer?.type;
954
- if (type === "NumericLiteral") {
955
- enumSchema.type = "number";
956
- }
957
- const targetValue = value || name;
958
- if (enumSchema.enum) {
959
- enumSchema.enum.push(targetValue);
960
- }
961
- }
962
- });
963
- return enumSchema;
964
- }
965
- extractKeysFromLiteralType(node) {
966
- if (t.isTSLiteralType(node) && t.isStringLiteral(node.literal)) {
967
- return [node.literal.value];
968
- }
969
- if (t.isTSUnionType(node)) {
970
- const keys = [];
971
- node.types.forEach((type) => {
972
- if (t.isTSLiteralType(type) && t.isStringLiteral(type.literal)) {
973
- keys.push(type.literal.value);
974
- }
975
- });
976
- return keys;
977
- }
978
- return [];
979
- }
980
- getPropertyOptions(node) {
981
- const isOptional = !!node.optional; // check if property is optional
982
- let description = null;
983
- // get comments for field
984
- if (node.trailingComments && node.trailingComments.length) {
985
- description = node.trailingComments[0].value.trim(); // get first comment
986
- }
987
- const options = {};
988
- if (description) {
989
- options.description = description;
990
- }
991
- if (this.contentType === "body") {
992
- options.nullable = isOptional;
993
- }
994
- return options;
995
- }
996
- /**
997
- * Generate example values based on parameter type and name
998
- */
999
- getExampleForParam(paramName, type = "string") {
1000
- // Common ID-like parameters
1001
- if (paramName === "id" ||
1002
- paramName.endsWith("Id") ||
1003
- paramName.endsWith("_id")) {
1004
- return type === "string" ? "123" : 123;
1005
- }
1006
- // For specific common parameter names
1007
- switch (paramName.toLowerCase()) {
1008
- case "slug":
1009
- return "slug";
1010
- case "uuid":
1011
- return "123e4567-e89b-12d3-a456-426614174000";
1012
- case "username":
1013
- return "johndoe";
1014
- case "email":
1015
- return "user@example.com";
1016
- case "name":
1017
- return "name";
1018
- case "date":
1019
- return "2023-01-01";
1020
- case "page":
1021
- return 1;
1022
- case "role":
1023
- return "admin";
1024
- default:
1025
- // Default examples by type
1026
- if (type === "string")
1027
- return "example";
1028
- if (type === "number")
1029
- return 1;
1030
- if (type === "boolean")
1031
- return true;
1032
- return "example";
1033
- }
1034
- }
1035
- detectContentType(bodyType, explicitContentType) {
1036
- if (explicitContentType) {
1037
- return explicitContentType;
1038
- }
1039
- // Automatic detection based on type name
1040
- if (bodyType &&
1041
- (bodyType.toLowerCase().includes("formdata") ||
1042
- bodyType.toLowerCase().includes("fileupload") ||
1043
- bodyType.toLowerCase().includes("multipart"))) {
1044
- return "multipart/form-data";
1045
- }
1046
- return "application/json";
1047
- }
1048
- createMultipleResponsesSchema(responses, defaultDescription) {
1049
- const result = {};
1050
- Object.entries(responses).forEach(([code, response]) => {
1051
- if (typeof response === "string") {
1052
- // Reference do components/responses
1053
- result[code] = { $ref: `#/components/responses/${response}` };
1054
- }
1055
- else {
1056
- result[code] = {
1057
- description: response.description || defaultDescription || "Response",
1058
- content: {
1059
- "application/json": {
1060
- schema: response.schema || response,
1061
- },
1062
- },
1063
- };
1064
- }
1065
- });
1066
- return result;
1067
- }
1068
- createFormDataSchema(body) {
1069
- if (!body.properties) {
1070
- return body;
1071
- }
1072
- const formDataProperties = {};
1073
- Object.entries(body.properties).forEach(([key, value]) => {
1074
- // Convert File types to binary format
1075
- if (value.type === "object" &&
1076
- (key.toLowerCase().includes("file") ||
1077
- value.description?.toLowerCase().includes("file"))) {
1078
- formDataProperties[key] = {
1079
- type: "string",
1080
- format: "binary",
1081
- description: value.description,
1082
- };
1083
- }
1084
- else {
1085
- formDataProperties[key] = value;
1086
- }
1087
- });
1088
- return {
1089
- ...body,
1090
- properties: formDataProperties,
1091
- };
1092
- }
1093
- /**
1094
- * Create a default schema for path parameters when no schema is defined
1095
- */
1096
- createDefaultPathParamsSchema(paramNames) {
1097
- return paramNames.map((paramName) => {
1098
- // Guess the parameter type based on the name
1099
- let type = "string";
1100
- if (paramName === "id" ||
1101
- paramName.endsWith("Id") ||
1102
- paramName === "page" ||
1103
- paramName === "limit" ||
1104
- paramName === "size" ||
1105
- paramName === "count") {
1106
- type = "number";
1107
- }
1108
- const example = this.getExampleForParam(paramName, type);
1109
- return {
1110
- name: paramName,
1111
- in: "path",
1112
- required: true,
1113
- schema: {
1114
- type: type,
1115
- },
1116
- example: example,
1117
- description: `Path parameter: ${paramName}`,
1118
- };
1119
- });
1120
- }
1121
- createRequestParamsSchema(params, isPathParam = false) {
1122
- const queryParams = [];
1123
- if (params.properties) {
1124
- for (let [name, value] of Object.entries(params.properties)) {
1125
- const param = {
1126
- in: isPathParam ? "path" : "query",
1127
- name,
1128
- schema: {
1129
- type: value.type,
1130
- },
1131
- required: isPathParam ? true : !!value.required, // Path parameters are always required
1132
- };
1133
- if (value.enum) {
1134
- param.schema.enum = value.enum;
1135
- }
1136
- if (value.description) {
1137
- param.description = value.description;
1138
- param.schema.description = value.description;
1139
- }
1140
- // Add examples for path parameters
1141
- if (isPathParam) {
1142
- const example = this.getExampleForParam(name, value.type);
1143
- param.example = example;
1144
- }
1145
- queryParams.push(param);
1146
- }
1147
- }
1148
- return queryParams;
1149
- }
1150
- createRequestBodySchema(body, description, contentType) {
1151
- const detectedContentType = this.detectContentType(body?.type || "", contentType);
1152
- let schema = body;
1153
- // If it is multipart/form-data, convert schema
1154
- if (detectedContentType === "multipart/form-data") {
1155
- schema = this.createFormDataSchema(body);
1156
- }
1157
- const requestBody = {
1158
- content: {
1159
- [detectedContentType]: {
1160
- schema: schema,
1161
- },
1162
- },
1163
- };
1164
- if (description) {
1165
- requestBody.description = description;
1166
- }
1167
- return requestBody;
1168
- }
1169
- createResponseSchema(responses, description) {
1170
- return {
1171
- 200: {
1172
- description: description || "Successful response",
1173
- content: {
1174
- "application/json": {
1175
- schema: responses,
1176
- },
1177
- },
1178
- },
1179
- };
1180
- }
1181
- getSchemaContent({ tag, paramsType, pathParamsType, bodyType, responseType, }) {
1182
- // Helper function to strip array notation from type names
1183
- const stripArrayNotation = (typeName) => {
1184
- if (!typeName)
1185
- return typeName;
1186
- let baseType = typeName;
1187
- while (baseType.endsWith('[]')) {
1188
- baseType = baseType.slice(0, -2);
1189
- }
1190
- return baseType;
1191
- };
1192
- // Strip array notation for schema lookups
1193
- const baseBodyType = stripArrayNotation(bodyType);
1194
- const baseResponseType = stripArrayNotation(responseType);
1195
- // Check if schemas exist, if not try to find them
1196
- if (paramsType && !this.openapiDefinitions[paramsType]) {
1197
- this.findSchemaDefinition(paramsType, "params");
1198
- }
1199
- if (pathParamsType && !this.openapiDefinitions[pathParamsType]) {
1200
- this.findSchemaDefinition(pathParamsType, "pathParams");
1201
- }
1202
- if (baseBodyType && !this.openapiDefinitions[baseBodyType]) {
1203
- this.findSchemaDefinition(baseBodyType, "body");
1204
- }
1205
- if (baseResponseType && !this.openapiDefinitions[baseResponseType]) {
1206
- this.findSchemaDefinition(baseResponseType, "response");
1207
- }
1208
- // Now get the schemas (will be {} if still not found)
1209
- let params = paramsType ? this.openapiDefinitions[paramsType] || {} : {};
1210
- let pathParams = pathParamsType ? this.openapiDefinitions[pathParamsType] || {} : {};
1211
- let body = baseBodyType ? this.openapiDefinitions[baseBodyType] || {} : {};
1212
- let responses = baseResponseType ? this.openapiDefinitions[baseResponseType] || {} : {};
1213
- if (this.schemaTypes.includes("zod")) {
1214
- const schemasToProcess = [
1215
- paramsType,
1216
- pathParamsType,
1217
- baseBodyType,
1218
- baseResponseType,
1219
- ].filter(Boolean);
1220
- schemasToProcess.forEach((schemaName) => {
1221
- if (!this.openapiDefinitions[schemaName]) {
1222
- this.findSchemaDefinition(schemaName, "");
1223
- }
1224
- });
1225
- }
1226
- return {
1227
- tag,
1228
- params,
1229
- pathParams,
1230
- body,
1231
- responses,
1232
- };
1233
- }
1234
- /**
1235
- * Parse and resolve a generic type from a string like "MyApiSuccessResponseBody<LLMSResponse>"
1236
- * @param genericTypeString - The generic type string to parse and resolve
1237
- * @returns The resolved OpenAPI schema
1238
- */
1239
- resolveGenericTypeFromString(genericTypeString) {
1240
- // Parse the generic type string
1241
- const parsed = this.parseGenericTypeString(genericTypeString);
1242
- if (!parsed) {
1243
- return {};
1244
- }
1245
- const { baseTypeName, typeArguments } = parsed;
1246
- // Find the base generic type definition
1247
- this.scanAllSchemaDirs(baseTypeName);
1248
- const genericDefEntry = this.typeDefinitions[baseTypeName];
1249
- const genericTypeDefinition = genericDefEntry?.node || genericDefEntry;
1250
- if (!genericTypeDefinition) {
1251
- logger.debug(`Generic type definition not found for: ${baseTypeName}`);
1252
- return {};
1253
- }
1254
- // Also find all the type argument definitions
1255
- typeArguments.forEach((argTypeName) => {
1256
- // If it's a simple type reference (not another generic), find its definition
1257
- if (!argTypeName.includes("<") &&
1258
- !this.isGenericTypeParameter(argTypeName)) {
1259
- this.scanAllSchemaDirs(argTypeName);
1260
- }
1261
- });
1262
- // Create AST nodes for the type arguments by parsing them
1263
- const typeArgumentNodes = typeArguments.map((arg) => this.createTypeNodeFromString(arg));
1264
- // Resolve the generic type
1265
- const resolved = this.resolveGenericType(genericTypeDefinition, typeArgumentNodes, baseTypeName);
1266
- // Cache the resolved type for future reference
1267
- this.openapiDefinitions[genericTypeString] = resolved;
1268
- return resolved;
1269
- }
1270
- /**
1271
- * Check if a type name is likely a generic type parameter (e.g., T, U, K, V)
1272
- * @param {string} typeName - The type name to check
1273
- * @returns {boolean} - True if it's likely a generic type parameter
1274
- */
1275
- isGenericTypeParameter(typeName) {
1276
- // Common generic type parameter patterns:
1277
- // - Single uppercase letters (T, U, K, V, etc.)
1278
- // - TKey, TValue, etc.
1279
- return /^[A-Z]$|^T[A-Z][a-zA-Z]*$/.test(typeName);
1280
- }
1281
- /**
1282
- * Check if a schema name is invalid (contains special characters, brackets, etc.)
1283
- * @param {string} schemaName - The schema name to check
1284
- * @returns {boolean} - True if the schema name is invalid
1285
- */
1286
- isInvalidSchemaName(schemaName) {
1287
- // Schema names should not contain { } : ? spaces or other special characters
1288
- return /[{}\s:?]/.test(schemaName);
1289
- }
1290
- /**
1291
- * Check if a type name is a built-in TypeScript utility type
1292
- * @param {string} typeName - The type name to check
1293
- * @returns {boolean} - True if it's a built-in utility type
1294
- */
1295
- isBuiltInUtilityType(typeName) {
1296
- const builtInTypes = [
1297
- 'Awaited', 'Partial', 'Required', 'Readonly', 'Record', 'Pick', 'Omit',
1298
- 'Exclude', 'Extract', 'NonNullable', 'Parameters', 'ConstructorParameters',
1299
- 'ReturnType', 'InstanceType', 'ThisParameterType', 'OmitThisParameter',
1300
- 'ThisType', 'Uppercase', 'Lowercase', 'Capitalize', 'Uncapitalize',
1301
- 'Promise', 'Array', 'ReadonlyArray', 'Map', 'Set', 'WeakMap', 'WeakSet'
1302
- ];
1303
- return builtInTypes.includes(typeName);
1304
- }
1305
- /**
1306
- * Check if a schema name is a function (should not be included in schemas)
1307
- * Functions are identified by having a node that is a function declaration
1308
- */
1309
- isFunctionSchema(schemaName) {
1310
- const entry = this.typeDefinitions[schemaName];
1311
- if (!entry)
1312
- return false;
1313
- const node = entry.node || entry;
1314
- return t.isFunctionDeclaration(node) ||
1315
- t.isFunctionExpression(node) ||
1316
- t.isArrowFunctionExpression(node);
1317
- }
1318
- /**
1319
- * Parse a generic type string into base type and arguments
1320
- * @param genericTypeString - The string like "MyApiSuccessResponseBody<LLMSResponse>"
1321
- * @returns Object with baseTypeName and typeArguments array
1322
- */
1323
- parseGenericTypeString(genericTypeString) {
1324
- const match = genericTypeString.match(/^([^<]+)<(.+)>$/);
1325
- if (!match) {
1326
- return null;
1327
- }
1328
- const baseTypeName = match[1].trim();
1329
- const typeArgsString = match[2].trim();
1330
- // Split type arguments by comma, handling nested generics
1331
- const typeArguments = this.splitTypeArguments(typeArgsString);
1332
- return { baseTypeName, typeArguments };
1333
- }
1334
- /**
1335
- * Split type arguments by comma, handling nested generics correctly
1336
- * @param typeArgsString - The string inside angle brackets
1337
- * @returns Array of individual type argument strings
1338
- */
1339
- splitTypeArguments(typeArgsString) {
1340
- const args = [];
1341
- let currentArg = "";
1342
- let bracketDepth = 0;
1343
- for (let i = 0; i < typeArgsString.length; i++) {
1344
- const char = typeArgsString[i];
1345
- if (char === "<") {
1346
- bracketDepth++;
1347
- }
1348
- else if (char === ">") {
1349
- bracketDepth--;
1350
- }
1351
- else if (char === "," && bracketDepth === 0) {
1352
- args.push(currentArg.trim());
1353
- currentArg = "";
1354
- continue;
1355
- }
1356
- currentArg += char;
1357
- }
1358
- if (currentArg.trim()) {
1359
- args.push(currentArg.trim());
1360
- }
1361
- return args;
1362
- }
1363
- /**
1364
- * Create a TypeScript AST node from a type string
1365
- * @param typeString - The type string like "LLMSResponse"
1366
- * @returns A TypeScript AST node
1367
- */
1368
- createTypeNodeFromString(typeString) {
1369
- // For simple type references, create a TSTypeReference node
1370
- if (!typeString.includes("<")) {
1371
- return {
1372
- type: "TSTypeReference",
1373
- typeName: {
1374
- type: "Identifier",
1375
- name: typeString,
1376
- },
1377
- };
1378
- }
1379
- // For nested generics, recursively parse
1380
- const parsed = this.parseGenericTypeString(typeString);
1381
- if (parsed) {
1382
- const typeParameterNodes = parsed.typeArguments.map((arg) => this.createTypeNodeFromString(arg));
1383
- return {
1384
- type: "TSTypeReference",
1385
- typeName: {
1386
- type: "Identifier",
1387
- name: parsed.baseTypeName,
1388
- },
1389
- typeParameters: {
1390
- type: "TSTypeParameterInstantiation",
1391
- params: typeParameterNodes,
1392
- },
1393
- };
1394
- }
1395
- // Fallback for unknown patterns
1396
- return {
1397
- type: "TSTypeReference",
1398
- typeName: {
1399
- type: "Identifier",
1400
- name: typeString,
1401
- },
1402
- };
1403
- }
1404
- /**
1405
- * Resolve generic types by substituting type parameters with actual types
1406
- * @param genericTypeDefinition - The AST node of the generic type definition
1407
- * @param typeArguments - The type arguments passed to the generic type
1408
- * @param typeName - The name of the generic type
1409
- * @returns The resolved OpenAPI schema
1410
- */
1411
- resolveGenericType(genericTypeDefinition, typeArguments, typeName) {
1412
- // Extract type parameters from the generic type definition
1413
- let typeParameters = [];
1414
- let bodyToResolve = null;
1415
- // Handle type alias declarations
1416
- if (t.isTSTypeAliasDeclaration(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.typeAnnotation;
1429
- }
1430
- // Handle interface declarations
1431
- if (t.isTSInterfaceDeclaration(genericTypeDefinition)) {
1432
- if (genericTypeDefinition.typeParameters &&
1433
- genericTypeDefinition.typeParameters.params) {
1434
- typeParameters = genericTypeDefinition.typeParameters.params.map((param) => {
1435
- if (t.isTSTypeParameter(param)) {
1436
- return param.name;
1437
- }
1438
- return t.isIdentifier(param)
1439
- ? param.name
1440
- : param.name?.name || param;
1441
- });
1442
- }
1443
- bodyToResolve = genericTypeDefinition.body;
1444
- }
1445
- if (!bodyToResolve) {
1446
- return {};
1447
- }
1448
- // Create a mapping from type parameters to actual types
1449
- const typeParameterMap = {};
1450
- typeParameters.forEach((param, index) => {
1451
- if (index < typeArguments.length) {
1452
- typeParameterMap[param] = typeArguments[index];
1453
- }
1454
- });
1455
- // Resolve the type annotation with substituted type parameters
1456
- return this.resolveTypeWithSubstitution(bodyToResolve, typeParameterMap);
1457
- }
1458
- /**
1459
- * Resolve a type node with type parameter substitution
1460
- * @param node - The AST node to resolve
1461
- * @param typeParameterMap - Mapping from type parameter names to actual types
1462
- * @returns The resolved OpenAPI schema
1463
- */
1464
- resolveTypeWithSubstitution(node, typeParameterMap) {
1465
- if (!node)
1466
- return { type: "object" };
1467
- // If this is a type parameter reference, substitute it
1468
- if (t.isTSTypeReference(node) && t.isIdentifier(node.typeName)) {
1469
- const paramName = node.typeName.name;
1470
- if (typeParameterMap[paramName]) {
1471
- // The mapped value is an AST node, resolve it
1472
- const mappedNode = typeParameterMap[paramName];
1473
- if (t.isTSTypeReference(mappedNode) &&
1474
- t.isIdentifier(mappedNode.typeName)) {
1475
- // If it's a reference to another type, get the resolved schema from openapiDefinitions
1476
- const referencedTypeName = mappedNode.typeName.name;
1477
- if (this.openapiDefinitions[referencedTypeName]) {
1478
- return this.openapiDefinitions[referencedTypeName];
1479
- }
1480
- // If not in openapiDefinitions, try to resolve it
1481
- this.findSchemaDefinition(referencedTypeName, this.contentType);
1482
- return this.openapiDefinitions[referencedTypeName] || {};
1483
- }
1484
- return this.resolveTSNodeType(typeParameterMap[paramName]);
1485
- }
1486
- }
1487
- // Handle intersection types (e.g., T & { success: true })
1488
- if (t.isTSIntersectionType(node)) {
1489
- const allProperties = {};
1490
- const requiredProperties = [];
1491
- node.types.forEach((typeNode, index) => {
1492
- let resolvedType;
1493
- // Check if this is a type parameter reference
1494
- if (t.isTSTypeReference(typeNode) &&
1495
- t.isIdentifier(typeNode.typeName)) {
1496
- const paramName = typeNode.typeName.name;
1497
- if (typeParameterMap[paramName]) {
1498
- const mappedNode = typeParameterMap[paramName];
1499
- if (t.isTSTypeReference(mappedNode) &&
1500
- t.isIdentifier(mappedNode.typeName)) {
1501
- // If it's a reference to another type, get the resolved schema
1502
- const referencedTypeName = mappedNode.typeName.name;
1503
- if (this.openapiDefinitions[referencedTypeName]) {
1504
- resolvedType = this.openapiDefinitions[referencedTypeName];
1505
- }
1506
- else {
1507
- // If not in openapiDefinitions, try to resolve it
1508
- this.findSchemaDefinition(referencedTypeName, this.contentType);
1509
- resolvedType =
1510
- this.openapiDefinitions[referencedTypeName] || {};
1511
- }
1512
- }
1513
- else {
1514
- resolvedType = this.resolveTSNodeType(mappedNode);
1515
- }
1516
- }
1517
- else {
1518
- resolvedType = this.resolveTSNodeType(typeNode);
1519
- }
1520
- }
1521
- else {
1522
- resolvedType = this.resolveTypeWithSubstitution(typeNode, typeParameterMap);
1523
- }
1524
- if (resolvedType.type === "object" && resolvedType.properties) {
1525
- Object.entries(resolvedType.properties).forEach(([key, value]) => {
1526
- allProperties[key] = value;
1527
- if (value.required) {
1528
- requiredProperties.push(key);
1529
- }
1530
- });
1531
- }
1532
- });
1533
- return {
1534
- type: "object",
1535
- properties: allProperties,
1536
- required: requiredProperties.length > 0 ? requiredProperties : undefined,
1537
- };
1538
- }
1539
- // For other types, use the standard resolution but with parameter substitution
1540
- if (t.isTSTypeLiteral(node)) {
1541
- const properties = {};
1542
- node.members.forEach((member) => {
1543
- if (t.isTSPropertySignature(member) && t.isIdentifier(member.key)) {
1544
- const propName = member.key.name;
1545
- properties[propName] = this.resolveTypeWithSubstitution(member.typeAnnotation?.typeAnnotation, typeParameterMap);
1546
- }
1547
- });
1548
- return { type: "object", properties };
1549
- }
1550
- // Handle interface body (from generic interfaces)
1551
- if (t.isTSInterfaceBody(node)) {
1552
- const properties = {};
1553
- node.body.forEach((member) => {
1554
- if (t.isTSPropertySignature(member) && t.isIdentifier(member.key)) {
1555
- const propName = member.key.name;
1556
- properties[propName] = this.resolveTypeWithSubstitution(member.typeAnnotation?.typeAnnotation, typeParameterMap);
1557
- }
1558
- });
1559
- return { type: "object", properties };
1560
- }
1561
- // Fallback to standard type resolution
1562
- return this.resolveTSNodeType(node);
1563
- }
1564
- /**
1565
- * Extracts the return type annotation from a function AST node
1566
- * @param funcNode - Function declaration or arrow function AST node
1567
- * @returns The return type annotation node, or null if not found
1568
- */
1569
- extractFunctionReturnType(funcNode) {
1570
- // Handle FunctionDeclaration: function foo(): ReturnType {}
1571
- if (t.isFunctionDeclaration(funcNode) || t.isFunctionExpression(funcNode)) {
1572
- return funcNode.returnType && t.isTSTypeAnnotation(funcNode.returnType)
1573
- ? funcNode.returnType.typeAnnotation
1574
- : null;
1575
- }
1576
- // Handle ArrowFunctionExpression: const foo = (): ReturnType => {}
1577
- if (t.isArrowFunctionExpression(funcNode)) {
1578
- return funcNode.returnType && t.isTSTypeAnnotation(funcNode.returnType)
1579
- ? funcNode.returnType.typeAnnotation
1580
- : null;
1581
- }
1582
- // Handle VariableDeclarator with arrow function
1583
- if (t.isVariableDeclarator(funcNode) &&
1584
- t.isArrowFunctionExpression(funcNode.init)) {
1585
- return funcNode.init.returnType && t.isTSTypeAnnotation(funcNode.init.returnType)
1586
- ? funcNode.init.returnType.typeAnnotation
1587
- : null;
1588
- }
1589
- return null;
1590
- }
1591
- /**
1592
- * Extracts parameter nodes from a function AST node
1593
- * @param funcNode - Function declaration or arrow function AST node
1594
- * @returns Array of parameter nodes
1595
- */
1596
- extractFunctionParameters(funcNode) {
1597
- // Handle FunctionDeclaration
1598
- if (t.isFunctionDeclaration(funcNode) || t.isFunctionExpression(funcNode)) {
1599
- return funcNode.params || [];
1600
- }
1601
- // Handle ArrowFunctionExpression
1602
- if (t.isArrowFunctionExpression(funcNode)) {
1603
- return funcNode.params || [];
1604
- }
1605
- // Handle VariableDeclarator with arrow function
1606
- if (t.isVariableDeclarator(funcNode) &&
1607
- t.isArrowFunctionExpression(funcNode.init)) {
1608
- return funcNode.init.params || [];
1609
- }
1610
- return [];
1611
- }
1612
- }