next-openapi-gen 0.10.5 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.d.ts +4 -0
- package/dist/cli.js +8599 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.js +8645 -26
- package/dist/next/index.d.ts +1 -0
- package/dist/next/index.js +7965 -0
- package/dist/react-router/index.d.ts +1 -0
- package/dist/react-router/index.js +7134 -0
- package/dist/vite/index.d.ts +1 -0
- package/dist/vite/index.js +7134 -0
- package/package.json +102 -79
- package/{dist/components/rapidoc.js → templates/init/ui/nextjs/rapidoc.tsx} +16 -20
- package/templates/init/ui/nextjs/redoc.tsx +11 -0
- package/{dist/components/scalar.js → templates/init/ui/nextjs/scalar.tsx} +15 -21
- package/{dist/components/stoplight.js → templates/init/ui/nextjs/stoplight.tsx} +11 -17
- package/templates/init/ui/nextjs/swagger.tsx +17 -0
- package/templates/init/ui/reactrouter/rapidoc.tsx +15 -0
- package/templates/init/ui/reactrouter/redoc.tsx +9 -0
- package/templates/init/ui/reactrouter/scalar.tsx +14 -0
- package/templates/init/ui/reactrouter/stoplight.tsx +10 -0
- package/templates/init/ui/reactrouter/swagger.tsx +11 -0
- package/templates/init/ui/tanstack/rapidoc.tsx +21 -0
- package/templates/init/ui/tanstack/redoc.tsx +14 -0
- package/templates/init/ui/tanstack/scalar.tsx +19 -0
- package/templates/init/ui/tanstack/stoplight.tsx +15 -0
- package/templates/init/ui/tanstack/swagger.tsx +16 -0
- package/templates/init/ui/template-types.d.ts +9 -0
- package/README.md +0 -1047
- package/dist/commands/generate.js +0 -24
- package/dist/commands/init.js +0 -194
- package/dist/components/redoc.js +0 -17
- package/dist/components/swagger.js +0 -21
- package/dist/lib/app-router-strategy.js +0 -66
- package/dist/lib/drizzle-zod-processor.js +0 -329
- package/dist/lib/logger.js +0 -39
- package/dist/lib/openapi-generator.js +0 -171
- package/dist/lib/pages-router-strategy.js +0 -198
- package/dist/lib/route-processor.js +0 -349
- package/dist/lib/router-strategy.js +0 -1
- package/dist/lib/schema-processor.js +0 -1612
- package/dist/lib/utils.js +0 -283
- package/dist/lib/zod-converter.js +0 -2133
- package/dist/openapi-template.js +0 -99
- 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
|
-
}
|