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