shokupan 0.10.5 → 0.11.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/{analyzer-CKLGLFtx.cjs → analyzer-BAhvpNY_.cjs} +2 -7
- package/dist/{analyzer-CKLGLFtx.cjs.map → analyzer-BAhvpNY_.cjs.map} +1 -1
- package/dist/{analyzer-BqIe1p0R.js → analyzer-CnKnQ5KV.js} +3 -8
- package/dist/{analyzer-BqIe1p0R.js.map → analyzer-CnKnQ5KV.js.map} +1 -1
- package/dist/{analyzer.impl-D9Yi1Hax.cjs → analyzer.impl-CfpMu4-g.cjs} +586 -40
- package/dist/analyzer.impl-CfpMu4-g.cjs.map +1 -0
- package/dist/{analyzer.impl-CV6W1Eq7.js → analyzer.impl-DCiqlXI5.js} +586 -40
- package/dist/analyzer.impl-DCiqlXI5.js.map +1 -0
- package/dist/cli.cjs +206 -18
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +206 -18
- package/dist/cli.js.map +1 -1
- package/dist/context.d.ts +6 -1
- package/dist/index.cjs +2339 -984
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +2336 -982
- package/dist/index.js.map +1 -1
- package/dist/plugins/application/api-explorer/static/explorer-client.mjs +375 -29
- package/dist/plugins/application/api-explorer/static/style.css +327 -8
- package/dist/plugins/application/api-explorer/static/theme.css +7 -2
- package/dist/plugins/application/asyncapi/generator.d.ts +4 -0
- package/dist/plugins/application/asyncapi/static/asyncapi-client.mjs +154 -22
- package/dist/plugins/application/asyncapi/static/style.css +24 -8
- package/dist/plugins/application/dashboard/fetch-interceptor.d.ts +107 -0
- package/dist/plugins/application/dashboard/metrics-collector.d.ts +38 -2
- package/dist/plugins/application/dashboard/plugin.d.ts +44 -1
- package/dist/plugins/application/dashboard/static/charts.js +127 -62
- package/dist/plugins/application/dashboard/static/client.js +160 -0
- package/dist/plugins/application/dashboard/static/graph.mjs +167 -56
- package/dist/plugins/application/dashboard/static/reactflow.css +20 -10
- package/dist/plugins/application/dashboard/static/registry.js +112 -8
- package/dist/plugins/application/dashboard/static/requests.js +868 -58
- package/dist/plugins/application/dashboard/static/styles.css +186 -14
- package/dist/plugins/application/dashboard/static/tabs.js +44 -9
- package/dist/plugins/application/dashboard/static/theme.css +7 -2
- package/dist/plugins/application/openapi/analyzer.impl.d.ts +61 -1
- package/dist/plugins/application/openapi/openapi.d.ts +3 -0
- package/dist/plugins/application/shared/ast-utils.d.ts +7 -0
- package/dist/router.d.ts +55 -16
- package/dist/shokupan.d.ts +7 -2
- package/dist/util/adapter/adapters.d.ts +19 -0
- package/dist/util/adapter/filesystem.d.ts +20 -0
- package/dist/util/controller-scanner.d.ts +4 -0
- package/dist/util/cpu-monitor.d.ts +2 -0
- package/dist/util/middleware-tracker.d.ts +10 -0
- package/dist/util/types.d.ts +37 -0
- package/package.json +5 -5
- package/dist/analyzer.impl-CV6W1Eq7.js.map +0 -1
- package/dist/analyzer.impl-D9Yi1Hax.cjs.map +0 -1
- package/dist/http-server-BEMPIs33.cjs +0 -85
- package/dist/http-server-BEMPIs33.cjs.map +0 -1
- package/dist/http-server-CCeagTyU.js +0 -68
- package/dist/http-server-CCeagTyU.js.map +0 -1
- package/dist/plugins/application/dashboard/static/poll.js +0 -146
|
@@ -14,6 +14,8 @@ class OpenAPIAnalyzer {
|
|
|
14
14
|
applications = [];
|
|
15
15
|
program;
|
|
16
16
|
entrypoint;
|
|
17
|
+
// Track imports per file: filePath -> { importedName -> { modulePath, exportName } }
|
|
18
|
+
imports = /* @__PURE__ */ new Map();
|
|
17
19
|
/**
|
|
18
20
|
* Main analysis entry point
|
|
19
21
|
*/
|
|
@@ -23,6 +25,7 @@ class OpenAPIAnalyzer {
|
|
|
23
25
|
async analyze() {
|
|
24
26
|
await this.parseTypeScriptFiles();
|
|
25
27
|
await this.processSourceMaps();
|
|
28
|
+
await this.collectImports();
|
|
26
29
|
await this.findApplications();
|
|
27
30
|
await this.extractRoutes();
|
|
28
31
|
this.pruneApplications();
|
|
@@ -101,6 +104,43 @@ class OpenAPIAnalyzer {
|
|
|
101
104
|
}
|
|
102
105
|
}
|
|
103
106
|
}
|
|
107
|
+
/**
|
|
108
|
+
* Collect all imports from source files for later resolution
|
|
109
|
+
*/
|
|
110
|
+
async collectImports() {
|
|
111
|
+
if (!this.program) return;
|
|
112
|
+
for (const sourceFile of this.program.getSourceFiles()) {
|
|
113
|
+
if (sourceFile.fileName.includes("node_modules")) continue;
|
|
114
|
+
if (sourceFile.isDeclarationFile) continue;
|
|
115
|
+
const fileImports = /* @__PURE__ */ new Map();
|
|
116
|
+
ts.forEachChild(sourceFile, (node) => {
|
|
117
|
+
if (ts.isImportDeclaration(node)) {
|
|
118
|
+
const moduleSpecifier = node.moduleSpecifier;
|
|
119
|
+
if (ts.isStringLiteral(moduleSpecifier)) {
|
|
120
|
+
const modulePath = moduleSpecifier.text;
|
|
121
|
+
if (node.importClause?.name) {
|
|
122
|
+
const importedName = node.importClause.name.getText(sourceFile);
|
|
123
|
+
fileImports.set(importedName, { modulePath, exportName: "default" });
|
|
124
|
+
}
|
|
125
|
+
if (node.importClause?.namedBindings && ts.isNamedImports(node.importClause.namedBindings)) {
|
|
126
|
+
for (const element of node.importClause.namedBindings.elements) {
|
|
127
|
+
const importedName = element.name.getText(sourceFile);
|
|
128
|
+
const exportName = element.propertyName?.getText(sourceFile) || importedName;
|
|
129
|
+
fileImports.set(importedName, { modulePath, exportName });
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
if (node.importClause?.namedBindings && ts.isNamespaceImport(node.importClause.namedBindings)) {
|
|
133
|
+
const importedName = node.importClause.namedBindings.name.getText(sourceFile);
|
|
134
|
+
fileImports.set(importedName, { modulePath, exportName: "*" });
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
if (fileImports.size > 0) {
|
|
140
|
+
this.imports.set(sourceFile.fileName, fileImports);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
104
144
|
/**
|
|
105
145
|
* Parse TypeScript files and create AST
|
|
106
146
|
*/
|
|
@@ -205,7 +245,8 @@ class OpenAPIAnalyzer {
|
|
|
205
245
|
className: "Controller",
|
|
206
246
|
controllerPrefix,
|
|
207
247
|
routes: [],
|
|
208
|
-
mounted: []
|
|
248
|
+
mounted: [],
|
|
249
|
+
middleware: []
|
|
209
250
|
});
|
|
210
251
|
}
|
|
211
252
|
}
|
|
@@ -220,7 +261,8 @@ class OpenAPIAnalyzer {
|
|
|
220
261
|
filePath: sourceFile.fileName,
|
|
221
262
|
className,
|
|
222
263
|
routes: [],
|
|
223
|
-
mounted: []
|
|
264
|
+
mounted: [],
|
|
265
|
+
middleware: []
|
|
224
266
|
});
|
|
225
267
|
}
|
|
226
268
|
}
|
|
@@ -237,7 +279,8 @@ class OpenAPIAnalyzer {
|
|
|
237
279
|
filePath: sourceFile.fileName,
|
|
238
280
|
className: "Shokupan",
|
|
239
281
|
routes: [],
|
|
240
|
-
mounted: []
|
|
282
|
+
mounted: [],
|
|
283
|
+
middleware: []
|
|
241
284
|
});
|
|
242
285
|
}
|
|
243
286
|
}
|
|
@@ -295,6 +338,7 @@ class OpenAPIAnalyzer {
|
|
|
295
338
|
requestTypes: analysis.requestTypes,
|
|
296
339
|
responseType: analysis.responseType,
|
|
297
340
|
responseSchema: analysis.responseSchema,
|
|
341
|
+
hasUnknownFields: analysis.hasUnknownFields,
|
|
298
342
|
emits: analysis.emits,
|
|
299
343
|
sourceContext: {
|
|
300
344
|
file: sourceFile.fileName,
|
|
@@ -333,6 +377,14 @@ class OpenAPIAnalyzer {
|
|
|
333
377
|
if (mount) {
|
|
334
378
|
app.mounted.push(mount);
|
|
335
379
|
}
|
|
380
|
+
} else if (methodName === "use") {
|
|
381
|
+
const middleware = this.extractMiddlewareFromCall(node, sourceFile);
|
|
382
|
+
if (middleware) {
|
|
383
|
+
if (app.className === "Shokupan") {
|
|
384
|
+
middleware.scope = "global";
|
|
385
|
+
}
|
|
386
|
+
app.middleware.push(middleware);
|
|
387
|
+
}
|
|
336
388
|
}
|
|
337
389
|
}
|
|
338
390
|
}
|
|
@@ -342,6 +394,47 @@ class OpenAPIAnalyzer {
|
|
|
342
394
|
ts.forEachChild(sourceFile, visit);
|
|
343
395
|
}
|
|
344
396
|
}
|
|
397
|
+
/**
|
|
398
|
+
* Resolve string value from expression (literals, concatenation, templates, constants)
|
|
399
|
+
*/
|
|
400
|
+
resolveStringValue(node, sourceFile) {
|
|
401
|
+
if (ts.isStringLiteral(node)) {
|
|
402
|
+
return node.text;
|
|
403
|
+
}
|
|
404
|
+
if (ts.isNoSubstitutionTemplateLiteral(node)) {
|
|
405
|
+
return node.text;
|
|
406
|
+
}
|
|
407
|
+
if (ts.isTemplateExpression(node)) {
|
|
408
|
+
let result = node.head.text;
|
|
409
|
+
for (const span of node.templateSpans) {
|
|
410
|
+
const val = this.resolveStringValue(span.expression, sourceFile);
|
|
411
|
+
if (val === null) return null;
|
|
412
|
+
result += val + span.literal.text;
|
|
413
|
+
}
|
|
414
|
+
return result;
|
|
415
|
+
}
|
|
416
|
+
if (ts.isBinaryExpression(node) && node.operatorToken.kind === ts.SyntaxKind.PlusToken) {
|
|
417
|
+
const left = this.resolveStringValue(node.left, sourceFile);
|
|
418
|
+
const right = this.resolveStringValue(node.right, sourceFile);
|
|
419
|
+
if (left !== null && right !== null) {
|
|
420
|
+
return left + right;
|
|
421
|
+
}
|
|
422
|
+
return null;
|
|
423
|
+
}
|
|
424
|
+
if (ts.isParenthesizedExpression(node)) {
|
|
425
|
+
return this.resolveStringValue(node.expression, sourceFile);
|
|
426
|
+
}
|
|
427
|
+
if (ts.isIdentifier(node)) {
|
|
428
|
+
if (this.program) {
|
|
429
|
+
const checker = this.program.getTypeChecker();
|
|
430
|
+
const symbol = checker.getSymbolAtLocation(node);
|
|
431
|
+
if (symbol && symbol.valueDeclaration && ts.isVariableDeclaration(symbol.valueDeclaration) && symbol.valueDeclaration.initializer) {
|
|
432
|
+
return this.resolveStringValue(symbol.valueDeclaration.initializer, sourceFile);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
return null;
|
|
437
|
+
}
|
|
345
438
|
/**
|
|
346
439
|
* Extract route information from a route call (e.g., app.get('/path', handler))
|
|
347
440
|
*/
|
|
@@ -349,11 +442,21 @@ class OpenAPIAnalyzer {
|
|
|
349
442
|
const args = node.arguments;
|
|
350
443
|
if (args.length < 2) return null;
|
|
351
444
|
const pathArg = args[0];
|
|
352
|
-
let routePath =
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
445
|
+
let routePath = this.resolveStringValue(pathArg, sourceFile);
|
|
446
|
+
let dynamicHighlights = [];
|
|
447
|
+
if (!routePath) {
|
|
448
|
+
if (["EVENT", "ON"].includes(method.toUpperCase())) {
|
|
449
|
+
routePath = "__DYNAMIC_EVENT__";
|
|
450
|
+
} else {
|
|
451
|
+
routePath = "__DYNAMIC_ROUTE__";
|
|
452
|
+
}
|
|
453
|
+
const start = sourceFile.getLineAndCharacterOfPosition(pathArg.getStart());
|
|
454
|
+
const end = sourceFile.getLineAndCharacterOfPosition(pathArg.getEnd());
|
|
455
|
+
dynamicHighlights.push({
|
|
456
|
+
startLine: start.line + 1,
|
|
457
|
+
endLine: end.line + 1,
|
|
458
|
+
type: "dynamic-path"
|
|
459
|
+
});
|
|
357
460
|
}
|
|
358
461
|
const normalizedPath = routePath.replace(/:([a-zA-Z0-9_]+)/g, "{$1}");
|
|
359
462
|
let metadata = {};
|
|
@@ -392,7 +495,7 @@ class OpenAPIAnalyzer {
|
|
|
392
495
|
file: sourceFile.fileName,
|
|
393
496
|
startLine: sourceFile.getLineAndCharacterOfPosition(handlerArg.getStart()).line + 1,
|
|
394
497
|
endLine: sourceFile.getLineAndCharacterOfPosition(handlerArg.getEnd()).line + 1,
|
|
395
|
-
highlights: handlerInfo.highlights
|
|
498
|
+
highlights: [...handlerInfo.highlights || [], ...dynamicHighlights]
|
|
396
499
|
}
|
|
397
500
|
};
|
|
398
501
|
}
|
|
@@ -400,9 +503,10 @@ class OpenAPIAnalyzer {
|
|
|
400
503
|
* Analyze a route handler to extract type information
|
|
401
504
|
*/
|
|
402
505
|
analyzeHandler(handler, sourceFile) {
|
|
506
|
+
const typeChecker = this.program?.getTypeChecker();
|
|
403
507
|
const requestTypes = {};
|
|
404
508
|
let responseType;
|
|
405
|
-
let
|
|
509
|
+
let responseSchemas = [];
|
|
406
510
|
let hasExplicitReturnType = false;
|
|
407
511
|
const emits = [];
|
|
408
512
|
const highlights = [];
|
|
@@ -420,7 +524,7 @@ class OpenAPIAnalyzer {
|
|
|
420
524
|
if (handler.type) {
|
|
421
525
|
const returnSchema = this.convertTypeNodeToSchema(handler.type, sourceFile);
|
|
422
526
|
if (returnSchema) {
|
|
423
|
-
|
|
527
|
+
responseSchemas.push(returnSchema);
|
|
424
528
|
responseType = returnSchema.type;
|
|
425
529
|
hasExplicitReturnType = true;
|
|
426
530
|
}
|
|
@@ -437,7 +541,8 @@ class OpenAPIAnalyzer {
|
|
|
437
541
|
if (callObj === "ctx" || callObj.endsWith(".ctx")) {
|
|
438
542
|
if (callProp === "json") {
|
|
439
543
|
if (node.arguments.length > 0) {
|
|
440
|
-
|
|
544
|
+
const schema = this.convertExpressionToSchema(node.arguments[0], sourceFile, scope, typeChecker);
|
|
545
|
+
responseSchemas.push(schema);
|
|
441
546
|
responseType = "object";
|
|
442
547
|
}
|
|
443
548
|
return;
|
|
@@ -450,14 +555,14 @@ class OpenAPIAnalyzer {
|
|
|
450
555
|
}
|
|
451
556
|
}
|
|
452
557
|
}
|
|
453
|
-
if (!hasExplicitReturnType &&
|
|
454
|
-
const schema = this.convertExpressionToSchema(node, sourceFile, scope);
|
|
558
|
+
if (!hasExplicitReturnType && responseSchemas.length === 0) {
|
|
559
|
+
const schema = this.convertExpressionToSchema(node, sourceFile, scope, typeChecker);
|
|
455
560
|
if (schema && (schema.type !== "object" || Object.keys(schema.properties || {}).length > 0)) {
|
|
456
|
-
|
|
561
|
+
responseSchemas.push(schema);
|
|
457
562
|
responseType = schema.type;
|
|
458
563
|
}
|
|
459
564
|
}
|
|
460
|
-
if (
|
|
565
|
+
if (responseSchemas.length === 0 && !responseType) {
|
|
461
566
|
const returnText = node.getText(sourceFile);
|
|
462
567
|
if (returnText.startsWith("{")) {
|
|
463
568
|
responseType = "object";
|
|
@@ -473,23 +578,64 @@ class OpenAPIAnalyzer {
|
|
|
473
578
|
body = handler.body;
|
|
474
579
|
const visit = (node) => {
|
|
475
580
|
if (ts.isVariableDeclaration(node)) {
|
|
476
|
-
if (node.initializer
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
if (
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
581
|
+
if (node.initializer) {
|
|
582
|
+
if (ts.isIdentifier(node.name)) {
|
|
583
|
+
const varName = node.name.getText(sourceFile);
|
|
584
|
+
let initializer = node.initializer;
|
|
585
|
+
if (ts.isAsExpression(initializer)) {
|
|
586
|
+
if (this.isCtxBodyCall(initializer.expression, sourceFile)) {
|
|
587
|
+
const schema = this.convertTypeNodeToSchema(initializer.type, sourceFile);
|
|
588
|
+
if (schema) {
|
|
589
|
+
requestTypes.body = schema;
|
|
590
|
+
scope.set(varName, schema);
|
|
591
|
+
}
|
|
592
|
+
} else {
|
|
593
|
+
const schema = this.convertExpressionToSchema(initializer, sourceFile, scope, typeChecker);
|
|
484
594
|
scope.set(varName, schema);
|
|
485
595
|
}
|
|
486
596
|
} else {
|
|
487
|
-
const schema = this.convertExpressionToSchema(initializer, sourceFile, scope);
|
|
597
|
+
const schema = this.convertExpressionToSchema(initializer, sourceFile, scope, typeChecker);
|
|
488
598
|
scope.set(varName, schema);
|
|
489
599
|
}
|
|
490
|
-
} else {
|
|
491
|
-
const
|
|
492
|
-
|
|
600
|
+
} else if (ts.isArrayBindingPattern(node.name)) {
|
|
601
|
+
const initializerSchema = this.convertExpressionToSchema(node.initializer, sourceFile, scope, typeChecker);
|
|
602
|
+
if (initializerSchema?.type === "array" && initializerSchema.items) {
|
|
603
|
+
for (let i = 0; i < node.name.elements.length; i++) {
|
|
604
|
+
const element = node.name.elements[i];
|
|
605
|
+
if (ts.isBindingElement(element) && ts.isIdentifier(element.name)) {
|
|
606
|
+
const elementName = element.name.getText(sourceFile);
|
|
607
|
+
scope.set(elementName, initializerSchema.items);
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
} else {
|
|
611
|
+
for (let i = 0; i < node.name.elements.length; i++) {
|
|
612
|
+
const element = node.name.elements[i];
|
|
613
|
+
if (ts.isBindingElement(element) && ts.isIdentifier(element.name)) {
|
|
614
|
+
const elementName = element.name.getText(sourceFile);
|
|
615
|
+
scope.set(elementName, { "x-unknown": true });
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
} else if (ts.isObjectBindingPattern(node.name)) {
|
|
620
|
+
const initializerSchema = this.convertExpressionToSchema(node.initializer, sourceFile, scope, typeChecker);
|
|
621
|
+
if (initializerSchema?.type === "object" && initializerSchema.properties) {
|
|
622
|
+
for (let i = 0; i < node.name.elements.length; i++) {
|
|
623
|
+
const element = node.name.elements[i];
|
|
624
|
+
if (ts.isBindingElement(element) && ts.isIdentifier(element.name)) {
|
|
625
|
+
const elementName = element.name.getText(sourceFile);
|
|
626
|
+
const propertySchema = initializerSchema.properties[elementName];
|
|
627
|
+
scope.set(elementName, propertySchema || { "x-unknown": true });
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
} else {
|
|
631
|
+
for (let i = 0; i < node.name.elements.length; i++) {
|
|
632
|
+
const element = node.name.elements[i];
|
|
633
|
+
if (ts.isBindingElement(element) && ts.isIdentifier(element.name)) {
|
|
634
|
+
const elementName = element.name.getText(sourceFile);
|
|
635
|
+
scope.set(elementName, { "x-unknown": true });
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
}
|
|
493
639
|
}
|
|
494
640
|
}
|
|
495
641
|
}
|
|
@@ -516,7 +662,7 @@ class OpenAPIAnalyzer {
|
|
|
516
662
|
} else if (propText === "json") {
|
|
517
663
|
let isStatic = false;
|
|
518
664
|
if (node.arguments.length > 0) {
|
|
519
|
-
const schema = this.convertExpressionToSchema(node.arguments[0], sourceFile, scope);
|
|
665
|
+
const schema = this.convertExpressionToSchema(node.arguments[0], sourceFile, scope, typeChecker);
|
|
520
666
|
if (schema && (schema.type !== "object" || schema.properties && Object.keys(schema.properties).length > 0)) {
|
|
521
667
|
isStatic = true;
|
|
522
668
|
}
|
|
@@ -549,8 +695,9 @@ class OpenAPIAnalyzer {
|
|
|
549
695
|
const endLine = sourceFile.getLineAndCharacterOfPosition(node.getEnd()).line + 1;
|
|
550
696
|
let isStatic = false;
|
|
551
697
|
if (node.expression) {
|
|
698
|
+
const schemasBeforeReturn = responseSchemas.length;
|
|
552
699
|
analyzeReturnExpression(node.expression);
|
|
553
|
-
if (
|
|
700
|
+
if (responseSchemas.length > schemasBeforeReturn) {
|
|
554
701
|
isStatic = true;
|
|
555
702
|
}
|
|
556
703
|
} else if (hasExplicitReturnType) {
|
|
@@ -561,9 +708,10 @@ class OpenAPIAnalyzer {
|
|
|
561
708
|
if (ts.isArrowFunction(node) && !ts.isBlock(node.body)) {
|
|
562
709
|
const startLine = sourceFile.getLineAndCharacterOfPosition(node.body.getStart()).line + 1;
|
|
563
710
|
const endLine = sourceFile.getLineAndCharacterOfPosition(node.body.getEnd()).line + 1;
|
|
711
|
+
const schemasBeforeReturn = responseSchemas.length;
|
|
564
712
|
analyzeReturnExpression(node.body);
|
|
565
713
|
let isStatic = false;
|
|
566
|
-
if (
|
|
714
|
+
if (responseSchemas.length > schemasBeforeReturn) {
|
|
567
715
|
isStatic = true;
|
|
568
716
|
}
|
|
569
717
|
highlights.push({ startLine, endLine, type: isStatic ? "return-success" : "return-warning" });
|
|
@@ -582,7 +730,7 @@ class OpenAPIAnalyzer {
|
|
|
582
730
|
const eventName = eventNameArg.text;
|
|
583
731
|
let payload = { type: "object" };
|
|
584
732
|
if (expr.arguments.length >= 2) {
|
|
585
|
-
payload = this.convertExpressionToSchema(expr.arguments[1], sourceFile, scope);
|
|
733
|
+
payload = this.convertExpressionToSchema(expr.arguments[1], sourceFile, scope, typeChecker);
|
|
586
734
|
}
|
|
587
735
|
const emitLoc = {
|
|
588
736
|
startLine: sourceFile.getLineAndCharacterOfPosition(expr.getStart()).line + 1,
|
|
@@ -610,12 +758,32 @@ class OpenAPIAnalyzer {
|
|
|
610
758
|
ts.forEachChild(body, visit);
|
|
611
759
|
}
|
|
612
760
|
}
|
|
613
|
-
|
|
761
|
+
let finalResponseSchema;
|
|
762
|
+
if (responseSchemas.length > 1) {
|
|
763
|
+
const uniqueSchemas = this.deduplicateSchemas(responseSchemas);
|
|
764
|
+
if (uniqueSchemas.length === 1) {
|
|
765
|
+
finalResponseSchema = uniqueSchemas[0];
|
|
766
|
+
} else {
|
|
767
|
+
finalResponseSchema = {
|
|
768
|
+
oneOf: uniqueSchemas
|
|
769
|
+
};
|
|
770
|
+
}
|
|
771
|
+
} else if (responseSchemas.length === 1) {
|
|
772
|
+
finalResponseSchema = responseSchemas[0];
|
|
773
|
+
}
|
|
774
|
+
return {
|
|
775
|
+
requestTypes,
|
|
776
|
+
responseType,
|
|
777
|
+
responseSchema: finalResponseSchema,
|
|
778
|
+
hasUnknownFields: finalResponseSchema ? this.hasUnknownFields(finalResponseSchema) : false,
|
|
779
|
+
emits,
|
|
780
|
+
highlights
|
|
781
|
+
};
|
|
614
782
|
}
|
|
615
783
|
/**
|
|
616
784
|
* Convert an Expression node to an OpenAPI schema (best effort)
|
|
617
785
|
*/
|
|
618
|
-
convertExpressionToSchema(node, sourceFile, scope) {
|
|
786
|
+
convertExpressionToSchema(node, sourceFile, scope, typeChecker) {
|
|
619
787
|
if (ts.isObjectLiteralExpression(node)) {
|
|
620
788
|
const schema = {
|
|
621
789
|
type: "object",
|
|
@@ -626,7 +794,7 @@ class OpenAPIAnalyzer {
|
|
|
626
794
|
const prop = node.properties[i];
|
|
627
795
|
if (ts.isPropertyAssignment(prop)) {
|
|
628
796
|
const name = prop.name.getText(sourceFile);
|
|
629
|
-
const valueSchema = this.convertExpressionToSchema(prop.initializer, sourceFile, scope);
|
|
797
|
+
const valueSchema = this.convertExpressionToSchema(prop.initializer, sourceFile, scope, typeChecker);
|
|
630
798
|
schema.properties[name] = valueSchema;
|
|
631
799
|
schema.required.push(name);
|
|
632
800
|
} else if (ts.isShorthandPropertyAssignment(prop)) {
|
|
@@ -644,29 +812,130 @@ class OpenAPIAnalyzer {
|
|
|
644
812
|
if (ts.isArrayLiteralExpression(node)) {
|
|
645
813
|
const schema = { type: "array" };
|
|
646
814
|
if (node.elements.length > 0) {
|
|
647
|
-
schema.items = this.convertExpressionToSchema(node.elements[0], sourceFile, scope);
|
|
815
|
+
schema.items = this.convertExpressionToSchema(node.elements[0], sourceFile, scope, typeChecker);
|
|
648
816
|
} else {
|
|
649
817
|
schema.items = {};
|
|
650
818
|
}
|
|
651
819
|
return schema;
|
|
652
820
|
}
|
|
653
821
|
if (ts.isConditionalExpression(node)) {
|
|
654
|
-
const trueSchema = this.convertExpressionToSchema(node.whenTrue, sourceFile, scope);
|
|
822
|
+
const trueSchema = this.convertExpressionToSchema(node.whenTrue, sourceFile, scope, typeChecker);
|
|
655
823
|
return trueSchema;
|
|
656
824
|
}
|
|
657
825
|
if (ts.isTemplateExpression(node)) {
|
|
658
826
|
return { type: "string" };
|
|
659
827
|
}
|
|
828
|
+
if (ts.isAwaitExpression(node)) {
|
|
829
|
+
return this.convertExpressionToSchema(node.expression, sourceFile, scope, typeChecker);
|
|
830
|
+
}
|
|
831
|
+
if (ts.isCallExpression(node)) {
|
|
832
|
+
const callText = node.getText(sourceFile);
|
|
833
|
+
if (callText.startsWith("Date.now()") || callText.startsWith("Math.") || callText.startsWith("Number(") || callText.startsWith("parseInt(") || callText.startsWith("parseFloat(")) {
|
|
834
|
+
return { type: "number" };
|
|
835
|
+
}
|
|
836
|
+
if (callText.startsWith("String(") || callText.endsWith(".toString()") || callText.endsWith(".join(")) {
|
|
837
|
+
return { type: "string" };
|
|
838
|
+
}
|
|
839
|
+
if (callText.startsWith("Boolean(")) {
|
|
840
|
+
return { type: "boolean" };
|
|
841
|
+
}
|
|
842
|
+
if (callText.endsWith(".split(") || callText.endsWith(".map(") || callText.endsWith(".filter(")) {
|
|
843
|
+
return { type: "array", items: {} };
|
|
844
|
+
}
|
|
845
|
+
return { "x-unknown": true };
|
|
846
|
+
}
|
|
847
|
+
if (ts.isBinaryExpression(node)) {
|
|
848
|
+
const operator = node.operatorToken.kind;
|
|
849
|
+
if (operator === ts.SyntaxKind.PlusToken || operator === ts.SyntaxKind.MinusToken || operator === ts.SyntaxKind.AsteriskToken || operator === ts.SyntaxKind.SlashToken || operator === ts.SyntaxKind.PercentToken || operator === ts.SyntaxKind.AsteriskAsteriskToken) {
|
|
850
|
+
if (operator === ts.SyntaxKind.PlusToken) {
|
|
851
|
+
const leftSchema = this.convertExpressionToSchema(node.left, sourceFile, scope, typeChecker);
|
|
852
|
+
const rightSchema = this.convertExpressionToSchema(node.right, sourceFile, scope, typeChecker);
|
|
853
|
+
if (leftSchema.type === "string" || rightSchema.type === "string") {
|
|
854
|
+
return { type: "string" };
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
return { type: "number" };
|
|
858
|
+
}
|
|
859
|
+
if (operator === ts.SyntaxKind.GreaterThanToken || operator === ts.SyntaxKind.LessThanToken || operator === ts.SyntaxKind.GreaterThanEqualsToken || operator === ts.SyntaxKind.LessThanEqualsToken || operator === ts.SyntaxKind.EqualsEqualsToken || operator === ts.SyntaxKind.EqualsEqualsEqualsToken || operator === ts.SyntaxKind.ExclamationEqualsToken || operator === ts.SyntaxKind.ExclamationEqualsEqualsToken) {
|
|
860
|
+
return { type: "boolean" };
|
|
861
|
+
}
|
|
862
|
+
if (operator === ts.SyntaxKind.AmpersandAmpersandToken || operator === ts.SyntaxKind.BarBarToken) {
|
|
863
|
+
const leftSchema = this.convertExpressionToSchema(node.left, sourceFile, scope, typeChecker);
|
|
864
|
+
const rightSchema = this.convertExpressionToSchema(node.right, sourceFile, scope, typeChecker);
|
|
865
|
+
if (operator === ts.SyntaxKind.BarBarToken && rightSchema.type === "string") {
|
|
866
|
+
return { type: "string" };
|
|
867
|
+
}
|
|
868
|
+
if (leftSchema.type && leftSchema.type !== "boolean") {
|
|
869
|
+
return leftSchema;
|
|
870
|
+
}
|
|
871
|
+
if (rightSchema.type && rightSchema.type !== "boolean") {
|
|
872
|
+
return rightSchema;
|
|
873
|
+
}
|
|
874
|
+
return { type: "boolean" };
|
|
875
|
+
}
|
|
876
|
+
if (operator === ts.SyntaxKind.AmpersandToken || operator === ts.SyntaxKind.BarToken || operator === ts.SyntaxKind.CaretToken || operator === ts.SyntaxKind.LessThanLessThanToken || operator === ts.SyntaxKind.GreaterThanGreaterThanToken || operator === ts.SyntaxKind.GreaterThanGreaterThanGreaterThanToken) {
|
|
877
|
+
return { type: "number" };
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
if (ts.isPropertyAccessExpression(node) && typeChecker) {
|
|
881
|
+
try {
|
|
882
|
+
const type = typeChecker.getTypeAtLocation(node);
|
|
883
|
+
const schema = this.convertTypeToSchema(type, typeChecker);
|
|
884
|
+
if (schema && (schema.type !== "object" || schema.properties)) {
|
|
885
|
+
return schema;
|
|
886
|
+
}
|
|
887
|
+
} catch (e) {
|
|
888
|
+
}
|
|
889
|
+
}
|
|
660
890
|
if (ts.isIdentifier(node)) {
|
|
661
891
|
const name = node.getText(sourceFile);
|
|
662
892
|
const scopedSchema = scope.get(name);
|
|
663
893
|
if (scopedSchema) return scopedSchema;
|
|
664
|
-
|
|
894
|
+
if (typeChecker) {
|
|
895
|
+
try {
|
|
896
|
+
const type = typeChecker.getTypeAtLocation(node);
|
|
897
|
+
const schema = this.convertTypeToSchema(type, typeChecker);
|
|
898
|
+
if (schema && (schema.type !== "object" || schema.properties)) {
|
|
899
|
+
return schema;
|
|
900
|
+
}
|
|
901
|
+
} catch (e) {
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
return { type: "object", "x-unknown": true };
|
|
665
905
|
}
|
|
666
906
|
if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node)) return { type: "string" };
|
|
667
907
|
if (ts.isNumericLiteral(node)) return { type: "number" };
|
|
668
908
|
if (node.kind === ts.SyntaxKind.TrueKeyword || node.kind === ts.SyntaxKind.FalseKeyword) return { type: "boolean" };
|
|
669
|
-
return { type: "object" };
|
|
909
|
+
return { type: "object", "x-unknown": true };
|
|
910
|
+
}
|
|
911
|
+
/**
|
|
912
|
+
* Deduplicate schemas by comparing their JSON representations
|
|
913
|
+
*/
|
|
914
|
+
deduplicateSchemas(schemas) {
|
|
915
|
+
const seen = /* @__PURE__ */ new Map();
|
|
916
|
+
for (const schema of schemas) {
|
|
917
|
+
const key = JSON.stringify(schema);
|
|
918
|
+
if (!seen.has(key)) {
|
|
919
|
+
seen.set(key, schema);
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
return Array.from(seen.values());
|
|
923
|
+
}
|
|
924
|
+
/**
|
|
925
|
+
* Check if a schema contains fields with unknown types
|
|
926
|
+
*/
|
|
927
|
+
hasUnknownFields(schema) {
|
|
928
|
+
if (!schema) return false;
|
|
929
|
+
if (schema["x-unknown"]) return true;
|
|
930
|
+
if (schema.type === "object" && schema.properties) {
|
|
931
|
+
return Object.values(schema.properties).some(
|
|
932
|
+
(prop) => this.hasUnknownFields(prop)
|
|
933
|
+
);
|
|
934
|
+
}
|
|
935
|
+
if (schema.type === "array" && schema.items) {
|
|
936
|
+
return this.hasUnknownFields(schema.items);
|
|
937
|
+
}
|
|
938
|
+
return false;
|
|
670
939
|
}
|
|
671
940
|
/**
|
|
672
941
|
* Check if an expression is a call to ctx.body()
|
|
@@ -748,6 +1017,82 @@ class OpenAPIAnalyzer {
|
|
|
748
1017
|
return { type: "object" };
|
|
749
1018
|
}
|
|
750
1019
|
}
|
|
1020
|
+
/**
|
|
1021
|
+
* Convert a TypeScript Type (from type checker) to an OpenAPI schema
|
|
1022
|
+
*/
|
|
1023
|
+
convertTypeToSchema(type, typeChecker, depth = 0) {
|
|
1024
|
+
if (depth > 5) {
|
|
1025
|
+
return { type: "object", description: "Complex type (max depth reached)" };
|
|
1026
|
+
}
|
|
1027
|
+
if (type.flags & ts.TypeFlags.String) return { type: "string" };
|
|
1028
|
+
if (type.flags & ts.TypeFlags.Number) return { type: "number" };
|
|
1029
|
+
if (type.flags & ts.TypeFlags.Boolean) return { type: "boolean" };
|
|
1030
|
+
if (type.flags & ts.TypeFlags.Null) return { type: "null" };
|
|
1031
|
+
if (type.flags & ts.TypeFlags.Undefined) return { type: "object", nullable: true };
|
|
1032
|
+
if (type.flags & ts.TypeFlags.Any || type.flags & ts.TypeFlags.Unknown) return {};
|
|
1033
|
+
if (type.flags & ts.TypeFlags.StringLiteral) {
|
|
1034
|
+
const literalType = type;
|
|
1035
|
+
return { type: "string", enum: [literalType.value] };
|
|
1036
|
+
}
|
|
1037
|
+
if (type.flags & ts.TypeFlags.NumberLiteral) {
|
|
1038
|
+
const literalType = type;
|
|
1039
|
+
return { type: "number", enum: [literalType.value] };
|
|
1040
|
+
}
|
|
1041
|
+
if (type.flags & ts.TypeFlags.BooleanLiteral) {
|
|
1042
|
+
const intrinsicName = type.intrinsicName;
|
|
1043
|
+
return { type: "boolean", enum: [intrinsicName === "true"] };
|
|
1044
|
+
}
|
|
1045
|
+
if (type.flags & ts.TypeFlags.Union) {
|
|
1046
|
+
const unionType = type;
|
|
1047
|
+
const schemas = unionType.types.map((t) => this.convertTypeToSchema(t, typeChecker, depth + 1));
|
|
1048
|
+
const uniqueTypes = new Set(schemas.map((s) => s.type));
|
|
1049
|
+
if (uniqueTypes.size === 1 && schemas[0].type !== "object") {
|
|
1050
|
+
return schemas[0];
|
|
1051
|
+
}
|
|
1052
|
+
return { oneOf: schemas };
|
|
1053
|
+
}
|
|
1054
|
+
if (typeChecker.isArrayType(type)) {
|
|
1055
|
+
const typeArgs = type.typeArguments || type.resolvedTypeArguments;
|
|
1056
|
+
if (typeArgs && typeArgs.length > 0) {
|
|
1057
|
+
return {
|
|
1058
|
+
type: "array",
|
|
1059
|
+
items: this.convertTypeToSchema(typeArgs[0], typeChecker, depth + 1)
|
|
1060
|
+
};
|
|
1061
|
+
}
|
|
1062
|
+
return { type: "array", items: {} };
|
|
1063
|
+
}
|
|
1064
|
+
if (type.flags & ts.TypeFlags.Object) {
|
|
1065
|
+
const properties = {};
|
|
1066
|
+
const required = [];
|
|
1067
|
+
const props = typeChecker.getPropertiesOfType(type);
|
|
1068
|
+
const maxProps = 50;
|
|
1069
|
+
const propsToProcess = props.slice(0, maxProps);
|
|
1070
|
+
for (const prop of propsToProcess) {
|
|
1071
|
+
const propName = prop.getName();
|
|
1072
|
+
const propType = typeChecker.getTypeOfSymbol(prop);
|
|
1073
|
+
const signatures = propType.getCallSignatures();
|
|
1074
|
+
if (signatures && signatures.length > 0) {
|
|
1075
|
+
continue;
|
|
1076
|
+
}
|
|
1077
|
+
properties[propName] = this.convertTypeToSchema(propType, typeChecker, depth + 1);
|
|
1078
|
+
if (!(prop.flags & ts.SymbolFlags.Optional)) {
|
|
1079
|
+
required.push(propName);
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
const schema = { type: "object" };
|
|
1083
|
+
if (Object.keys(properties).length > 0) {
|
|
1084
|
+
schema.properties = properties;
|
|
1085
|
+
if (required.length > 0) {
|
|
1086
|
+
schema.required = required;
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
if (props.length > maxProps) {
|
|
1090
|
+
schema.description = `Type with ${props.length} properties (showing ${maxProps})`;
|
|
1091
|
+
}
|
|
1092
|
+
return schema;
|
|
1093
|
+
}
|
|
1094
|
+
return { type: "object", description: "Complex type" };
|
|
1095
|
+
}
|
|
751
1096
|
/**
|
|
752
1097
|
* Extract mount information from mount call
|
|
753
1098
|
*/
|
|
@@ -816,6 +1161,207 @@ class OpenAPIAnalyzer {
|
|
|
816
1161
|
}
|
|
817
1162
|
};
|
|
818
1163
|
}
|
|
1164
|
+
/**
|
|
1165
|
+
* Extract middleware information from .use() call
|
|
1166
|
+
*/
|
|
1167
|
+
extractMiddlewareFromCall(node, sourceFile) {
|
|
1168
|
+
const args = node.arguments;
|
|
1169
|
+
if (args.length < 1) return null;
|
|
1170
|
+
const middlewareArg = args[0];
|
|
1171
|
+
let middlewareName = "anonymous";
|
|
1172
|
+
let isImportedIdentifier = false;
|
|
1173
|
+
if (ts.isIdentifier(middlewareArg)) {
|
|
1174
|
+
middlewareName = middlewareArg.getText(sourceFile);
|
|
1175
|
+
isImportedIdentifier = true;
|
|
1176
|
+
} else if (ts.isCallExpression(middlewareArg) && ts.isIdentifier(middlewareArg.expression)) {
|
|
1177
|
+
middlewareName = middlewareArg.expression.getText(sourceFile);
|
|
1178
|
+
isImportedIdentifier = true;
|
|
1179
|
+
} else if (ts.isFunctionExpression(middlewareArg) || ts.isArrowFunction(middlewareArg)) {
|
|
1180
|
+
middlewareName = "inline-middleware";
|
|
1181
|
+
}
|
|
1182
|
+
let analysis = this.analyzeMiddleware(middlewareArg, sourceFile);
|
|
1183
|
+
if (isImportedIdentifier) {
|
|
1184
|
+
const resolvedAnalysis = this.resolveImportedMiddlewareDefinition(middlewareName, sourceFile);
|
|
1185
|
+
if (resolvedAnalysis.responseTypes) {
|
|
1186
|
+
analysis.responseTypes = { ...resolvedAnalysis.responseTypes, ...analysis.responseTypes };
|
|
1187
|
+
}
|
|
1188
|
+
if (resolvedAnalysis.headers) {
|
|
1189
|
+
analysis.headers = [...resolvedAnalysis.headers || [], ...analysis.headers || []];
|
|
1190
|
+
analysis.headers = Array.from(new Set(analysis.headers));
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
return {
|
|
1194
|
+
name: middlewareName,
|
|
1195
|
+
file: sourceFile.fileName,
|
|
1196
|
+
startLine: sourceFile.getLineAndCharacterOfPosition(middlewareArg.getStart()).line + 1,
|
|
1197
|
+
endLine: sourceFile.getLineAndCharacterOfPosition(middlewareArg.getEnd()).line + 1,
|
|
1198
|
+
handlerSource: middlewareArg.getText(sourceFile),
|
|
1199
|
+
responseTypes: analysis.responseTypes,
|
|
1200
|
+
headers: analysis.headers,
|
|
1201
|
+
scope: "router",
|
|
1202
|
+
// Will be updated during collection based on context
|
|
1203
|
+
sourceContext: {
|
|
1204
|
+
file: sourceFile.fileName,
|
|
1205
|
+
startLine: sourceFile.getLineAndCharacterOfPosition(middlewareArg.getStart()).line + 1,
|
|
1206
|
+
endLine: sourceFile.getLineAndCharacterOfPosition(middlewareArg.getEnd()).line + 1,
|
|
1207
|
+
snippet: middlewareArg.getText(sourceFile),
|
|
1208
|
+
highlights: analysis.highlights
|
|
1209
|
+
}
|
|
1210
|
+
};
|
|
1211
|
+
}
|
|
1212
|
+
/**
|
|
1213
|
+
* Analyze middleware function to extract response types and headers
|
|
1214
|
+
*/
|
|
1215
|
+
analyzeMiddleware(middleware, sourceFile) {
|
|
1216
|
+
const responseTypes = {};
|
|
1217
|
+
const headers = [];
|
|
1218
|
+
const highlights = [];
|
|
1219
|
+
const variableValues = /* @__PURE__ */ new Map();
|
|
1220
|
+
const visit = (node) => {
|
|
1221
|
+
if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name)) {
|
|
1222
|
+
const varName = node.name.getText(sourceFile);
|
|
1223
|
+
if (node.initializer) {
|
|
1224
|
+
if (ts.isNumericLiteral(node.initializer)) {
|
|
1225
|
+
const value = parseInt(node.initializer.text);
|
|
1226
|
+
if (!isNaN(value)) {
|
|
1227
|
+
variableValues.set(varName, value);
|
|
1228
|
+
}
|
|
1229
|
+
} else if (ts.isBinaryExpression(node.initializer) && node.initializer.operatorToken.kind === ts.SyntaxKind.BarBarToken) {
|
|
1230
|
+
if (ts.isNumericLiteral(node.initializer.right)) {
|
|
1231
|
+
const value = parseInt(node.initializer.right.text);
|
|
1232
|
+
if (!isNaN(value)) {
|
|
1233
|
+
variableValues.set(varName, value);
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
}
|
|
1239
|
+
if (ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression)) {
|
|
1240
|
+
const obj = node.expression.expression.getText(sourceFile);
|
|
1241
|
+
const prop = node.expression.name.getText(sourceFile);
|
|
1242
|
+
if (obj === "ctx" && ["json", "text", "html", "jsx"].includes(prop)) {
|
|
1243
|
+
if (node.arguments.length >= 2) {
|
|
1244
|
+
const statusArg = node.arguments[1];
|
|
1245
|
+
let statusCode;
|
|
1246
|
+
if (ts.isNumericLiteral(statusArg)) {
|
|
1247
|
+
statusCode = parseInt(statusArg.text);
|
|
1248
|
+
} else if (ts.isIdentifier(statusArg)) {
|
|
1249
|
+
const varName = statusArg.getText(sourceFile);
|
|
1250
|
+
const value = variableValues.get(varName);
|
|
1251
|
+
if (typeof value === "number") {
|
|
1252
|
+
statusCode = value;
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1255
|
+
if (statusCode && statusCode >= 100 && statusCode < 600) {
|
|
1256
|
+
const statusStr = String(statusCode);
|
|
1257
|
+
if (!responseTypes[statusStr]) {
|
|
1258
|
+
let description = `Error response (${statusCode})`;
|
|
1259
|
+
if (statusCode === 401) description = "Unauthorized";
|
|
1260
|
+
else if (statusCode === 403) description = "Forbidden";
|
|
1261
|
+
else if (statusCode === 429) description = "Too Many Requests";
|
|
1262
|
+
else if (statusCode === 500) description = "Internal Server Error";
|
|
1263
|
+
const content = {};
|
|
1264
|
+
if (prop === "json") {
|
|
1265
|
+
content["application/json"] = { schema: { type: "object" } };
|
|
1266
|
+
} else if (prop === "text") {
|
|
1267
|
+
content["text/plain"] = { schema: { type: "string" } };
|
|
1268
|
+
} else if (prop === "html" || prop === "jsx") {
|
|
1269
|
+
content["text/html"] = { schema: { type: "string" } };
|
|
1270
|
+
}
|
|
1271
|
+
responseTypes[statusStr] = {
|
|
1272
|
+
description,
|
|
1273
|
+
...Object.keys(content).length > 0 ? { content } : {}
|
|
1274
|
+
};
|
|
1275
|
+
}
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1279
|
+
if (["set", "header"].includes(prop)) {
|
|
1280
|
+
if (node.arguments.length >= 1 && ts.isStringLiteral(node.arguments[0])) {
|
|
1281
|
+
const headerName = node.arguments[0].text;
|
|
1282
|
+
if (headerName && !headers.includes(headerName)) {
|
|
1283
|
+
headers.push(headerName);
|
|
1284
|
+
}
|
|
1285
|
+
}
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
1288
|
+
ts.forEachChild(node, visit);
|
|
1289
|
+
};
|
|
1290
|
+
visit(middleware);
|
|
1291
|
+
const middlewareSource = middleware.getText(sourceFile);
|
|
1292
|
+
if (middlewareSource.includes("X-RateLimit")) {
|
|
1293
|
+
const rateLimitHeaders = ["X-RateLimit-Limit", "X-RateLimit-Remaining", "X-RateLimit-Reset", "Retry-After"];
|
|
1294
|
+
for (const header of rateLimitHeaders) {
|
|
1295
|
+
if (middlewareSource.includes(header) && !headers.includes(header)) {
|
|
1296
|
+
headers.push(header);
|
|
1297
|
+
}
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
return {
|
|
1301
|
+
responseTypes: Object.keys(responseTypes).length > 0 ? responseTypes : void 0,
|
|
1302
|
+
headers: headers.length > 0 ? headers : void 0,
|
|
1303
|
+
highlights: highlights.length > 0 ? highlights : void 0
|
|
1304
|
+
};
|
|
1305
|
+
}
|
|
1306
|
+
/**
|
|
1307
|
+
* Resolve an imported middleware identifier and analyze its definition
|
|
1308
|
+
*/
|
|
1309
|
+
resolveImportedMiddlewareDefinition(middlewareName, sourceFile) {
|
|
1310
|
+
const fileImports = this.imports.get(sourceFile.fileName);
|
|
1311
|
+
if (!fileImports || !fileImports.has(middlewareName)) {
|
|
1312
|
+
return {};
|
|
1313
|
+
}
|
|
1314
|
+
const importInfo = fileImports.get(middlewareName);
|
|
1315
|
+
const modulePath = importInfo.modulePath;
|
|
1316
|
+
if (!modulePath.startsWith(".")) {
|
|
1317
|
+
return {};
|
|
1318
|
+
}
|
|
1319
|
+
const dir = path.dirname(sourceFile.fileName);
|
|
1320
|
+
let absolutePath = path.resolve(dir, modulePath);
|
|
1321
|
+
const extensions = [".ts", ".js", ".tsx", ".jsx", "/index.ts", "/index.js"];
|
|
1322
|
+
let resolvedPath;
|
|
1323
|
+
for (const ext of extensions) {
|
|
1324
|
+
const testPath = absolutePath + ext;
|
|
1325
|
+
if (fs.existsSync(testPath)) {
|
|
1326
|
+
resolvedPath = testPath;
|
|
1327
|
+
break;
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
if (!resolvedPath && fs.existsSync(absolutePath)) {
|
|
1331
|
+
resolvedPath = absolutePath;
|
|
1332
|
+
}
|
|
1333
|
+
if (!resolvedPath) {
|
|
1334
|
+
return {};
|
|
1335
|
+
}
|
|
1336
|
+
const importedSourceFile = this.program?.getSourceFile(resolvedPath);
|
|
1337
|
+
if (!importedSourceFile) {
|
|
1338
|
+
return {};
|
|
1339
|
+
}
|
|
1340
|
+
let middlewareNode;
|
|
1341
|
+
const exportName = importInfo.exportName || middlewareName;
|
|
1342
|
+
ts.forEachChild(importedSourceFile, (node) => {
|
|
1343
|
+
if (ts.isFunctionDeclaration(node) && node.name?.getText(importedSourceFile) === exportName) {
|
|
1344
|
+
const modifiers = node.modifiers;
|
|
1345
|
+
if (modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword)) {
|
|
1346
|
+
middlewareNode = node;
|
|
1347
|
+
}
|
|
1348
|
+
}
|
|
1349
|
+
if (ts.isVariableStatement(node)) {
|
|
1350
|
+
const modifiers = node.modifiers;
|
|
1351
|
+
if (modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword)) {
|
|
1352
|
+
for (const declaration of node.declarationList.declarations) {
|
|
1353
|
+
if (ts.isIdentifier(declaration.name) && declaration.name.getText(importedSourceFile) === exportName) {
|
|
1354
|
+
middlewareNode = declaration.initializer;
|
|
1355
|
+
}
|
|
1356
|
+
}
|
|
1357
|
+
}
|
|
1358
|
+
}
|
|
1359
|
+
});
|
|
1360
|
+
if (!middlewareNode) {
|
|
1361
|
+
return {};
|
|
1362
|
+
}
|
|
1363
|
+
return this.analyzeMiddleware(middlewareNode, importedSourceFile);
|
|
1364
|
+
}
|
|
819
1365
|
/**
|
|
820
1366
|
* Check if a reference is to an external dependency
|
|
821
1367
|
*/
|
|
@@ -1005,4 +1551,4 @@ class OpenAPIAnalyzer {
|
|
|
1005
1551
|
}
|
|
1006
1552
|
}
|
|
1007
1553
|
exports.OpenAPIAnalyzer = OpenAPIAnalyzer;
|
|
1008
|
-
//# sourceMappingURL=analyzer.impl-
|
|
1554
|
+
//# sourceMappingURL=analyzer.impl-CfpMu4-g.cjs.map
|