shokupan 0.10.5 → 0.12.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/README.md +46 -1815
- package/dist/{analyzer-BqIe1p0R.js → analyzer-BkNQHWj4.js} +3 -8
- package/dist/{analyzer-BqIe1p0R.js.map → analyzer-BkNQHWj4.js.map} +1 -1
- package/dist/{analyzer-CKLGLFtx.cjs → analyzer-DM-OlRq8.cjs} +2 -7
- package/dist/{analyzer-CKLGLFtx.cjs.map → analyzer-DM-OlRq8.cjs.map} +1 -1
- package/dist/{analyzer.impl-D9Yi1Hax.cjs → analyzer.impl-CVJ8zfGQ.cjs} +596 -42
- package/dist/analyzer.impl-CVJ8zfGQ.cjs.map +1 -0
- package/dist/{analyzer.impl-CV6W1Eq7.js → analyzer.impl-CsA1bS_s.js} +596 -42
- package/dist/analyzer.impl-CsA1bS_s.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 +46 -9
- package/dist/index.cjs +3239 -1173
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +3236 -1171
- 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 +11 -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/auth.d.ts +5 -0
- package/dist/plugins/application/dashboard/fetch-interceptor.d.ts +119 -0
- package/dist/plugins/application/dashboard/metrics-collector.d.ts +38 -2
- package/dist/plugins/application/dashboard/plugin.d.ts +53 -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 +1167 -71
- 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/tabulator.css +23 -3
- package/dist/plugins/application/dashboard/static/theme.css +11 -2
- package/dist/plugins/application/mcp-server/plugin.d.ts +39 -0
- package/dist/plugins/application/openapi/analyzer.impl.d.ts +65 -1
- package/dist/plugins/application/openapi/openapi.d.ts +3 -0
- package/dist/plugins/application/shared/ast-utils.d.ts +7 -0
- package/dist/plugins/middleware/compression.d.ts +12 -2
- package/dist/plugins/middleware/rate-limit.d.ts +5 -0
- package/dist/router.d.ts +59 -19
- package/dist/server.d.ts +22 -0
- package/dist/shokupan.d.ts +31 -3
- package/dist/util/adapter/bun.d.ts +8 -0
- package/dist/util/adapter/filesystem.d.ts +20 -0
- package/dist/util/adapter/index.d.ts +4 -0
- package/dist/util/adapter/interface.d.ts +12 -0
- package/dist/util/adapter/node.d.ts +8 -0
- package/dist/util/adapter/wintercg.d.ts +5 -0
- package/dist/util/body-parser.d.ts +30 -0
- package/dist/util/controller-scanner.d.ts +4 -0
- package/dist/util/cpu-monitor.d.ts +2 -0
- package/dist/util/decorators.d.ts +20 -3
- package/dist/util/di.d.ts +3 -8
- package/dist/util/metadata.d.ts +18 -0
- package/dist/util/middleware-tracker.d.ts +10 -0
- package/dist/util/request.d.ts +1 -0
- package/dist/util/symbol.d.ts +1 -0
- package/dist/util/types.d.ts +167 -1
- package/package.json +7 -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/failures.js +0 -85
- package/dist/plugins/application/dashboard/static/poll.js +0 -146
- package/dist/plugins/application/http-server.d.ts +0 -13
|
@@ -14,19 +14,30 @@ 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
|
*/
|
|
22
|
+
/**
|
|
23
|
+
* Main analysis entry point
|
|
24
|
+
*/
|
|
25
|
+
cachedResult;
|
|
20
26
|
/**
|
|
21
27
|
* Main analysis entry point
|
|
22
28
|
*/
|
|
23
29
|
async analyze() {
|
|
30
|
+
if (this.cachedResult) {
|
|
31
|
+
return this.cachedResult;
|
|
32
|
+
}
|
|
24
33
|
await this.parseTypeScriptFiles();
|
|
25
34
|
await this.processSourceMaps();
|
|
35
|
+
await this.collectImports();
|
|
26
36
|
await this.findApplications();
|
|
27
37
|
await this.extractRoutes();
|
|
28
38
|
this.pruneApplications();
|
|
29
|
-
|
|
39
|
+
this.cachedResult = { applications: this.applications };
|
|
40
|
+
return this.cachedResult;
|
|
30
41
|
}
|
|
31
42
|
/**
|
|
32
43
|
* Remove GenericModules that are not mounted by any Shokupan application/router
|
|
@@ -70,7 +81,7 @@ class OpenAPIAnalyzer {
|
|
|
70
81
|
const entry = entries[i];
|
|
71
82
|
const fullPath = path.join(dir, entry.name);
|
|
72
83
|
if (entry.isDirectory()) {
|
|
73
|
-
if (["node_modules", ".git", "dist"].includes(entry.name)) {
|
|
84
|
+
if (["node_modules", ".git", "dist", ".gemini", ".vscode", ".agent", "artifacts"].includes(entry.name)) {
|
|
74
85
|
continue;
|
|
75
86
|
}
|
|
76
87
|
await this.scanDirectory(fullPath);
|
|
@@ -101,6 +112,43 @@ class OpenAPIAnalyzer {
|
|
|
101
112
|
}
|
|
102
113
|
}
|
|
103
114
|
}
|
|
115
|
+
/**
|
|
116
|
+
* Collect all imports from source files for later resolution
|
|
117
|
+
*/
|
|
118
|
+
async collectImports() {
|
|
119
|
+
if (!this.program) return;
|
|
120
|
+
for (const sourceFile of this.program.getSourceFiles()) {
|
|
121
|
+
if (sourceFile.fileName.includes("node_modules")) continue;
|
|
122
|
+
if (sourceFile.isDeclarationFile) continue;
|
|
123
|
+
const fileImports = /* @__PURE__ */ new Map();
|
|
124
|
+
ts.forEachChild(sourceFile, (node) => {
|
|
125
|
+
if (ts.isImportDeclaration(node)) {
|
|
126
|
+
const moduleSpecifier = node.moduleSpecifier;
|
|
127
|
+
if (ts.isStringLiteral(moduleSpecifier)) {
|
|
128
|
+
const modulePath = moduleSpecifier.text;
|
|
129
|
+
if (node.importClause?.name) {
|
|
130
|
+
const importedName = node.importClause.name.getText(sourceFile);
|
|
131
|
+
fileImports.set(importedName, { modulePath, exportName: "default" });
|
|
132
|
+
}
|
|
133
|
+
if (node.importClause?.namedBindings && ts.isNamedImports(node.importClause.namedBindings)) {
|
|
134
|
+
for (const element of node.importClause.namedBindings.elements) {
|
|
135
|
+
const importedName = element.name.getText(sourceFile);
|
|
136
|
+
const exportName = element.propertyName?.getText(sourceFile) || importedName;
|
|
137
|
+
fileImports.set(importedName, { modulePath, exportName });
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
if (node.importClause?.namedBindings && ts.isNamespaceImport(node.importClause.namedBindings)) {
|
|
141
|
+
const importedName = node.importClause.namedBindings.name.getText(sourceFile);
|
|
142
|
+
fileImports.set(importedName, { modulePath, exportName: "*" });
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
if (fileImports.size > 0) {
|
|
148
|
+
this.imports.set(sourceFile.fileName, fileImports);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
104
152
|
/**
|
|
105
153
|
* Parse TypeScript files and create AST
|
|
106
154
|
*/
|
|
@@ -205,7 +253,8 @@ class OpenAPIAnalyzer {
|
|
|
205
253
|
className: "Controller",
|
|
206
254
|
controllerPrefix,
|
|
207
255
|
routes: [],
|
|
208
|
-
mounted: []
|
|
256
|
+
mounted: [],
|
|
257
|
+
middleware: []
|
|
209
258
|
});
|
|
210
259
|
}
|
|
211
260
|
}
|
|
@@ -220,7 +269,8 @@ class OpenAPIAnalyzer {
|
|
|
220
269
|
filePath: sourceFile.fileName,
|
|
221
270
|
className,
|
|
222
271
|
routes: [],
|
|
223
|
-
mounted: []
|
|
272
|
+
mounted: [],
|
|
273
|
+
middleware: []
|
|
224
274
|
});
|
|
225
275
|
}
|
|
226
276
|
}
|
|
@@ -237,7 +287,8 @@ class OpenAPIAnalyzer {
|
|
|
237
287
|
filePath: sourceFile.fileName,
|
|
238
288
|
className: "Shokupan",
|
|
239
289
|
routes: [],
|
|
240
|
-
mounted: []
|
|
290
|
+
mounted: [],
|
|
291
|
+
middleware: []
|
|
241
292
|
});
|
|
242
293
|
}
|
|
243
294
|
}
|
|
@@ -295,6 +346,7 @@ class OpenAPIAnalyzer {
|
|
|
295
346
|
requestTypes: analysis.requestTypes,
|
|
296
347
|
responseType: analysis.responseType,
|
|
297
348
|
responseSchema: analysis.responseSchema,
|
|
349
|
+
hasUnknownFields: analysis.hasUnknownFields,
|
|
298
350
|
emits: analysis.emits,
|
|
299
351
|
sourceContext: {
|
|
300
352
|
file: sourceFile.fileName,
|
|
@@ -333,6 +385,14 @@ class OpenAPIAnalyzer {
|
|
|
333
385
|
if (mount) {
|
|
334
386
|
app.mounted.push(mount);
|
|
335
387
|
}
|
|
388
|
+
} else if (methodName === "use") {
|
|
389
|
+
const middleware = this.extractMiddlewareFromCall(node, sourceFile);
|
|
390
|
+
if (middleware) {
|
|
391
|
+
if (app.className === "Shokupan") {
|
|
392
|
+
middleware.scope = "global";
|
|
393
|
+
}
|
|
394
|
+
app.middleware.push(middleware);
|
|
395
|
+
}
|
|
336
396
|
}
|
|
337
397
|
}
|
|
338
398
|
}
|
|
@@ -342,6 +402,47 @@ class OpenAPIAnalyzer {
|
|
|
342
402
|
ts.forEachChild(sourceFile, visit);
|
|
343
403
|
}
|
|
344
404
|
}
|
|
405
|
+
/**
|
|
406
|
+
* Resolve string value from expression (literals, concatenation, templates, constants)
|
|
407
|
+
*/
|
|
408
|
+
resolveStringValue(node, sourceFile) {
|
|
409
|
+
if (ts.isStringLiteral(node)) {
|
|
410
|
+
return node.text;
|
|
411
|
+
}
|
|
412
|
+
if (ts.isNoSubstitutionTemplateLiteral(node)) {
|
|
413
|
+
return node.text;
|
|
414
|
+
}
|
|
415
|
+
if (ts.isTemplateExpression(node)) {
|
|
416
|
+
let result = node.head.text;
|
|
417
|
+
for (const span of node.templateSpans) {
|
|
418
|
+
const val = this.resolveStringValue(span.expression, sourceFile);
|
|
419
|
+
if (val === null) return null;
|
|
420
|
+
result += val + span.literal.text;
|
|
421
|
+
}
|
|
422
|
+
return result;
|
|
423
|
+
}
|
|
424
|
+
if (ts.isBinaryExpression(node) && node.operatorToken.kind === ts.SyntaxKind.PlusToken) {
|
|
425
|
+
const left = this.resolveStringValue(node.left, sourceFile);
|
|
426
|
+
const right = this.resolveStringValue(node.right, sourceFile);
|
|
427
|
+
if (left !== null && right !== null) {
|
|
428
|
+
return left + right;
|
|
429
|
+
}
|
|
430
|
+
return null;
|
|
431
|
+
}
|
|
432
|
+
if (ts.isParenthesizedExpression(node)) {
|
|
433
|
+
return this.resolveStringValue(node.expression, sourceFile);
|
|
434
|
+
}
|
|
435
|
+
if (ts.isIdentifier(node)) {
|
|
436
|
+
if (this.program) {
|
|
437
|
+
const checker = this.program.getTypeChecker();
|
|
438
|
+
const symbol = checker.getSymbolAtLocation(node);
|
|
439
|
+
if (symbol && symbol.valueDeclaration && ts.isVariableDeclaration(symbol.valueDeclaration) && symbol.valueDeclaration.initializer) {
|
|
440
|
+
return this.resolveStringValue(symbol.valueDeclaration.initializer, sourceFile);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
return null;
|
|
445
|
+
}
|
|
345
446
|
/**
|
|
346
447
|
* Extract route information from a route call (e.g., app.get('/path', handler))
|
|
347
448
|
*/
|
|
@@ -349,11 +450,21 @@ class OpenAPIAnalyzer {
|
|
|
349
450
|
const args = node.arguments;
|
|
350
451
|
if (args.length < 2) return null;
|
|
351
452
|
const pathArg = args[0];
|
|
352
|
-
let routePath =
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
453
|
+
let routePath = this.resolveStringValue(pathArg, sourceFile);
|
|
454
|
+
let dynamicHighlights = [];
|
|
455
|
+
if (!routePath) {
|
|
456
|
+
if (["EVENT", "ON"].includes(method.toUpperCase())) {
|
|
457
|
+
routePath = "__DYNAMIC_EVENT__";
|
|
458
|
+
} else {
|
|
459
|
+
routePath = "__DYNAMIC_ROUTE__";
|
|
460
|
+
}
|
|
461
|
+
const start = sourceFile.getLineAndCharacterOfPosition(pathArg.getStart());
|
|
462
|
+
const end = sourceFile.getLineAndCharacterOfPosition(pathArg.getEnd());
|
|
463
|
+
dynamicHighlights.push({
|
|
464
|
+
startLine: start.line + 1,
|
|
465
|
+
endLine: end.line + 1,
|
|
466
|
+
type: "dynamic-path"
|
|
467
|
+
});
|
|
357
468
|
}
|
|
358
469
|
const normalizedPath = routePath.replace(/:([a-zA-Z0-9_]+)/g, "{$1}");
|
|
359
470
|
let metadata = {};
|
|
@@ -392,7 +503,7 @@ class OpenAPIAnalyzer {
|
|
|
392
503
|
file: sourceFile.fileName,
|
|
393
504
|
startLine: sourceFile.getLineAndCharacterOfPosition(handlerArg.getStart()).line + 1,
|
|
394
505
|
endLine: sourceFile.getLineAndCharacterOfPosition(handlerArg.getEnd()).line + 1,
|
|
395
|
-
highlights: handlerInfo.highlights
|
|
506
|
+
highlights: [...handlerInfo.highlights || [], ...dynamicHighlights]
|
|
396
507
|
}
|
|
397
508
|
};
|
|
398
509
|
}
|
|
@@ -400,9 +511,10 @@ class OpenAPIAnalyzer {
|
|
|
400
511
|
* Analyze a route handler to extract type information
|
|
401
512
|
*/
|
|
402
513
|
analyzeHandler(handler, sourceFile) {
|
|
514
|
+
const typeChecker = this.program?.getTypeChecker();
|
|
403
515
|
const requestTypes = {};
|
|
404
516
|
let responseType;
|
|
405
|
-
let
|
|
517
|
+
let responseSchemas = [];
|
|
406
518
|
let hasExplicitReturnType = false;
|
|
407
519
|
const emits = [];
|
|
408
520
|
const highlights = [];
|
|
@@ -420,7 +532,7 @@ class OpenAPIAnalyzer {
|
|
|
420
532
|
if (handler.type) {
|
|
421
533
|
const returnSchema = this.convertTypeNodeToSchema(handler.type, sourceFile);
|
|
422
534
|
if (returnSchema) {
|
|
423
|
-
|
|
535
|
+
responseSchemas.push(returnSchema);
|
|
424
536
|
responseType = returnSchema.type;
|
|
425
537
|
hasExplicitReturnType = true;
|
|
426
538
|
}
|
|
@@ -437,7 +549,8 @@ class OpenAPIAnalyzer {
|
|
|
437
549
|
if (callObj === "ctx" || callObj.endsWith(".ctx")) {
|
|
438
550
|
if (callProp === "json") {
|
|
439
551
|
if (node.arguments.length > 0) {
|
|
440
|
-
|
|
552
|
+
const schema = this.convertExpressionToSchema(node.arguments[0], sourceFile, scope, typeChecker);
|
|
553
|
+
responseSchemas.push(schema);
|
|
441
554
|
responseType = "object";
|
|
442
555
|
}
|
|
443
556
|
return;
|
|
@@ -450,14 +563,14 @@ class OpenAPIAnalyzer {
|
|
|
450
563
|
}
|
|
451
564
|
}
|
|
452
565
|
}
|
|
453
|
-
if (!hasExplicitReturnType &&
|
|
454
|
-
const schema = this.convertExpressionToSchema(node, sourceFile, scope);
|
|
566
|
+
if (!hasExplicitReturnType && responseSchemas.length === 0) {
|
|
567
|
+
const schema = this.convertExpressionToSchema(node, sourceFile, scope, typeChecker);
|
|
455
568
|
if (schema && (schema.type !== "object" || Object.keys(schema.properties || {}).length > 0)) {
|
|
456
|
-
|
|
569
|
+
responseSchemas.push(schema);
|
|
457
570
|
responseType = schema.type;
|
|
458
571
|
}
|
|
459
572
|
}
|
|
460
|
-
if (
|
|
573
|
+
if (responseSchemas.length === 0 && !responseType) {
|
|
461
574
|
const returnText = node.getText(sourceFile);
|
|
462
575
|
if (returnText.startsWith("{")) {
|
|
463
576
|
responseType = "object";
|
|
@@ -473,23 +586,64 @@ class OpenAPIAnalyzer {
|
|
|
473
586
|
body = handler.body;
|
|
474
587
|
const visit = (node) => {
|
|
475
588
|
if (ts.isVariableDeclaration(node)) {
|
|
476
|
-
if (node.initializer
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
if (
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
589
|
+
if (node.initializer) {
|
|
590
|
+
if (ts.isIdentifier(node.name)) {
|
|
591
|
+
const varName = node.name.getText(sourceFile);
|
|
592
|
+
let initializer = node.initializer;
|
|
593
|
+
if (ts.isAsExpression(initializer)) {
|
|
594
|
+
if (this.isCtxBodyCall(initializer.expression, sourceFile)) {
|
|
595
|
+
const schema = this.convertTypeNodeToSchema(initializer.type, sourceFile);
|
|
596
|
+
if (schema) {
|
|
597
|
+
requestTypes.body = schema;
|
|
598
|
+
scope.set(varName, schema);
|
|
599
|
+
}
|
|
600
|
+
} else {
|
|
601
|
+
const schema = this.convertExpressionToSchema(initializer, sourceFile, scope, typeChecker);
|
|
484
602
|
scope.set(varName, schema);
|
|
485
603
|
}
|
|
486
604
|
} else {
|
|
487
|
-
const schema = this.convertExpressionToSchema(initializer, sourceFile, scope);
|
|
605
|
+
const schema = this.convertExpressionToSchema(initializer, sourceFile, scope, typeChecker);
|
|
488
606
|
scope.set(varName, schema);
|
|
489
607
|
}
|
|
490
|
-
} else {
|
|
491
|
-
const
|
|
492
|
-
|
|
608
|
+
} else if (ts.isArrayBindingPattern(node.name)) {
|
|
609
|
+
const initializerSchema = this.convertExpressionToSchema(node.initializer, sourceFile, scope, typeChecker);
|
|
610
|
+
if (initializerSchema?.type === "array" && initializerSchema.items) {
|
|
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, initializerSchema.items);
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
} else {
|
|
619
|
+
for (let i = 0; i < node.name.elements.length; i++) {
|
|
620
|
+
const element = node.name.elements[i];
|
|
621
|
+
if (ts.isBindingElement(element) && ts.isIdentifier(element.name)) {
|
|
622
|
+
const elementName = element.name.getText(sourceFile);
|
|
623
|
+
scope.set(elementName, { "x-unknown": true });
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
} else if (ts.isObjectBindingPattern(node.name)) {
|
|
628
|
+
const initializerSchema = this.convertExpressionToSchema(node.initializer, sourceFile, scope, typeChecker);
|
|
629
|
+
if (initializerSchema?.type === "object" && initializerSchema.properties) {
|
|
630
|
+
for (let i = 0; i < node.name.elements.length; i++) {
|
|
631
|
+
const element = node.name.elements[i];
|
|
632
|
+
if (ts.isBindingElement(element) && ts.isIdentifier(element.name)) {
|
|
633
|
+
const elementName = element.name.getText(sourceFile);
|
|
634
|
+
const propertySchema = initializerSchema.properties[elementName];
|
|
635
|
+
scope.set(elementName, propertySchema || { "x-unknown": true });
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
} else {
|
|
639
|
+
for (let i = 0; i < node.name.elements.length; i++) {
|
|
640
|
+
const element = node.name.elements[i];
|
|
641
|
+
if (ts.isBindingElement(element) && ts.isIdentifier(element.name)) {
|
|
642
|
+
const elementName = element.name.getText(sourceFile);
|
|
643
|
+
scope.set(elementName, { "x-unknown": true });
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
}
|
|
493
647
|
}
|
|
494
648
|
}
|
|
495
649
|
}
|
|
@@ -516,7 +670,7 @@ class OpenAPIAnalyzer {
|
|
|
516
670
|
} else if (propText === "json") {
|
|
517
671
|
let isStatic = false;
|
|
518
672
|
if (node.arguments.length > 0) {
|
|
519
|
-
const schema = this.convertExpressionToSchema(node.arguments[0], sourceFile, scope);
|
|
673
|
+
const schema = this.convertExpressionToSchema(node.arguments[0], sourceFile, scope, typeChecker);
|
|
520
674
|
if (schema && (schema.type !== "object" || schema.properties && Object.keys(schema.properties).length > 0)) {
|
|
521
675
|
isStatic = true;
|
|
522
676
|
}
|
|
@@ -549,8 +703,9 @@ class OpenAPIAnalyzer {
|
|
|
549
703
|
const endLine = sourceFile.getLineAndCharacterOfPosition(node.getEnd()).line + 1;
|
|
550
704
|
let isStatic = false;
|
|
551
705
|
if (node.expression) {
|
|
706
|
+
const schemasBeforeReturn = responseSchemas.length;
|
|
552
707
|
analyzeReturnExpression(node.expression);
|
|
553
|
-
if (
|
|
708
|
+
if (responseSchemas.length > schemasBeforeReturn) {
|
|
554
709
|
isStatic = true;
|
|
555
710
|
}
|
|
556
711
|
} else if (hasExplicitReturnType) {
|
|
@@ -561,9 +716,10 @@ class OpenAPIAnalyzer {
|
|
|
561
716
|
if (ts.isArrowFunction(node) && !ts.isBlock(node.body)) {
|
|
562
717
|
const startLine = sourceFile.getLineAndCharacterOfPosition(node.body.getStart()).line + 1;
|
|
563
718
|
const endLine = sourceFile.getLineAndCharacterOfPosition(node.body.getEnd()).line + 1;
|
|
719
|
+
const schemasBeforeReturn = responseSchemas.length;
|
|
564
720
|
analyzeReturnExpression(node.body);
|
|
565
721
|
let isStatic = false;
|
|
566
|
-
if (
|
|
722
|
+
if (responseSchemas.length > schemasBeforeReturn) {
|
|
567
723
|
isStatic = true;
|
|
568
724
|
}
|
|
569
725
|
highlights.push({ startLine, endLine, type: isStatic ? "return-success" : "return-warning" });
|
|
@@ -582,7 +738,7 @@ class OpenAPIAnalyzer {
|
|
|
582
738
|
const eventName = eventNameArg.text;
|
|
583
739
|
let payload = { type: "object" };
|
|
584
740
|
if (expr.arguments.length >= 2) {
|
|
585
|
-
payload = this.convertExpressionToSchema(expr.arguments[1], sourceFile, scope);
|
|
741
|
+
payload = this.convertExpressionToSchema(expr.arguments[1], sourceFile, scope, typeChecker);
|
|
586
742
|
}
|
|
587
743
|
const emitLoc = {
|
|
588
744
|
startLine: sourceFile.getLineAndCharacterOfPosition(expr.getStart()).line + 1,
|
|
@@ -610,12 +766,32 @@ class OpenAPIAnalyzer {
|
|
|
610
766
|
ts.forEachChild(body, visit);
|
|
611
767
|
}
|
|
612
768
|
}
|
|
613
|
-
|
|
769
|
+
let finalResponseSchema;
|
|
770
|
+
if (responseSchemas.length > 1) {
|
|
771
|
+
const uniqueSchemas = this.deduplicateSchemas(responseSchemas);
|
|
772
|
+
if (uniqueSchemas.length === 1) {
|
|
773
|
+
finalResponseSchema = uniqueSchemas[0];
|
|
774
|
+
} else {
|
|
775
|
+
finalResponseSchema = {
|
|
776
|
+
oneOf: uniqueSchemas
|
|
777
|
+
};
|
|
778
|
+
}
|
|
779
|
+
} else if (responseSchemas.length === 1) {
|
|
780
|
+
finalResponseSchema = responseSchemas[0];
|
|
781
|
+
}
|
|
782
|
+
return {
|
|
783
|
+
requestTypes,
|
|
784
|
+
responseType,
|
|
785
|
+
responseSchema: finalResponseSchema,
|
|
786
|
+
hasUnknownFields: finalResponseSchema ? this.hasUnknownFields(finalResponseSchema) : false,
|
|
787
|
+
emits,
|
|
788
|
+
highlights
|
|
789
|
+
};
|
|
614
790
|
}
|
|
615
791
|
/**
|
|
616
792
|
* Convert an Expression node to an OpenAPI schema (best effort)
|
|
617
793
|
*/
|
|
618
|
-
convertExpressionToSchema(node, sourceFile, scope) {
|
|
794
|
+
convertExpressionToSchema(node, sourceFile, scope, typeChecker) {
|
|
619
795
|
if (ts.isObjectLiteralExpression(node)) {
|
|
620
796
|
const schema = {
|
|
621
797
|
type: "object",
|
|
@@ -626,7 +802,7 @@ class OpenAPIAnalyzer {
|
|
|
626
802
|
const prop = node.properties[i];
|
|
627
803
|
if (ts.isPropertyAssignment(prop)) {
|
|
628
804
|
const name = prop.name.getText(sourceFile);
|
|
629
|
-
const valueSchema = this.convertExpressionToSchema(prop.initializer, sourceFile, scope);
|
|
805
|
+
const valueSchema = this.convertExpressionToSchema(prop.initializer, sourceFile, scope, typeChecker);
|
|
630
806
|
schema.properties[name] = valueSchema;
|
|
631
807
|
schema.required.push(name);
|
|
632
808
|
} else if (ts.isShorthandPropertyAssignment(prop)) {
|
|
@@ -644,29 +820,130 @@ class OpenAPIAnalyzer {
|
|
|
644
820
|
if (ts.isArrayLiteralExpression(node)) {
|
|
645
821
|
const schema = { type: "array" };
|
|
646
822
|
if (node.elements.length > 0) {
|
|
647
|
-
schema.items = this.convertExpressionToSchema(node.elements[0], sourceFile, scope);
|
|
823
|
+
schema.items = this.convertExpressionToSchema(node.elements[0], sourceFile, scope, typeChecker);
|
|
648
824
|
} else {
|
|
649
825
|
schema.items = {};
|
|
650
826
|
}
|
|
651
827
|
return schema;
|
|
652
828
|
}
|
|
653
829
|
if (ts.isConditionalExpression(node)) {
|
|
654
|
-
const trueSchema = this.convertExpressionToSchema(node.whenTrue, sourceFile, scope);
|
|
830
|
+
const trueSchema = this.convertExpressionToSchema(node.whenTrue, sourceFile, scope, typeChecker);
|
|
655
831
|
return trueSchema;
|
|
656
832
|
}
|
|
657
833
|
if (ts.isTemplateExpression(node)) {
|
|
658
834
|
return { type: "string" };
|
|
659
835
|
}
|
|
836
|
+
if (ts.isAwaitExpression(node)) {
|
|
837
|
+
return this.convertExpressionToSchema(node.expression, sourceFile, scope, typeChecker);
|
|
838
|
+
}
|
|
839
|
+
if (ts.isCallExpression(node)) {
|
|
840
|
+
const callText = node.getText(sourceFile);
|
|
841
|
+
if (callText.startsWith("Date.now()") || callText.startsWith("Math.") || callText.startsWith("Number(") || callText.startsWith("parseInt(") || callText.startsWith("parseFloat(")) {
|
|
842
|
+
return { type: "number" };
|
|
843
|
+
}
|
|
844
|
+
if (callText.startsWith("String(") || callText.endsWith(".toString()") || callText.endsWith(".join(")) {
|
|
845
|
+
return { type: "string" };
|
|
846
|
+
}
|
|
847
|
+
if (callText.startsWith("Boolean(")) {
|
|
848
|
+
return { type: "boolean" };
|
|
849
|
+
}
|
|
850
|
+
if (callText.endsWith(".split(") || callText.endsWith(".map(") || callText.endsWith(".filter(")) {
|
|
851
|
+
return { type: "array", items: {} };
|
|
852
|
+
}
|
|
853
|
+
return { "x-unknown": true };
|
|
854
|
+
}
|
|
855
|
+
if (ts.isBinaryExpression(node)) {
|
|
856
|
+
const operator = node.operatorToken.kind;
|
|
857
|
+
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) {
|
|
858
|
+
if (operator === ts.SyntaxKind.PlusToken) {
|
|
859
|
+
const leftSchema = this.convertExpressionToSchema(node.left, sourceFile, scope, typeChecker);
|
|
860
|
+
const rightSchema = this.convertExpressionToSchema(node.right, sourceFile, scope, typeChecker);
|
|
861
|
+
if (leftSchema.type === "string" || rightSchema.type === "string") {
|
|
862
|
+
return { type: "string" };
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
return { type: "number" };
|
|
866
|
+
}
|
|
867
|
+
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) {
|
|
868
|
+
return { type: "boolean" };
|
|
869
|
+
}
|
|
870
|
+
if (operator === ts.SyntaxKind.AmpersandAmpersandToken || operator === ts.SyntaxKind.BarBarToken) {
|
|
871
|
+
const leftSchema = this.convertExpressionToSchema(node.left, sourceFile, scope, typeChecker);
|
|
872
|
+
const rightSchema = this.convertExpressionToSchema(node.right, sourceFile, scope, typeChecker);
|
|
873
|
+
if (operator === ts.SyntaxKind.BarBarToken && rightSchema.type === "string") {
|
|
874
|
+
return { type: "string" };
|
|
875
|
+
}
|
|
876
|
+
if (leftSchema.type && leftSchema.type !== "boolean") {
|
|
877
|
+
return leftSchema;
|
|
878
|
+
}
|
|
879
|
+
if (rightSchema.type && rightSchema.type !== "boolean") {
|
|
880
|
+
return rightSchema;
|
|
881
|
+
}
|
|
882
|
+
return { type: "boolean" };
|
|
883
|
+
}
|
|
884
|
+
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) {
|
|
885
|
+
return { type: "number" };
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
if (ts.isPropertyAccessExpression(node) && typeChecker) {
|
|
889
|
+
try {
|
|
890
|
+
const type = typeChecker.getTypeAtLocation(node);
|
|
891
|
+
const schema = this.convertTypeToSchema(type, typeChecker);
|
|
892
|
+
if (schema && (schema.type !== "object" || schema.properties)) {
|
|
893
|
+
return schema;
|
|
894
|
+
}
|
|
895
|
+
} catch (e) {
|
|
896
|
+
}
|
|
897
|
+
}
|
|
660
898
|
if (ts.isIdentifier(node)) {
|
|
661
899
|
const name = node.getText(sourceFile);
|
|
662
900
|
const scopedSchema = scope.get(name);
|
|
663
901
|
if (scopedSchema) return scopedSchema;
|
|
664
|
-
|
|
902
|
+
if (typeChecker) {
|
|
903
|
+
try {
|
|
904
|
+
const type = typeChecker.getTypeAtLocation(node);
|
|
905
|
+
const schema = this.convertTypeToSchema(type, typeChecker);
|
|
906
|
+
if (schema && (schema.type !== "object" || schema.properties)) {
|
|
907
|
+
return schema;
|
|
908
|
+
}
|
|
909
|
+
} catch (e) {
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
return { type: "object", "x-unknown": true };
|
|
665
913
|
}
|
|
666
914
|
if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node)) return { type: "string" };
|
|
667
915
|
if (ts.isNumericLiteral(node)) return { type: "number" };
|
|
668
916
|
if (node.kind === ts.SyntaxKind.TrueKeyword || node.kind === ts.SyntaxKind.FalseKeyword) return { type: "boolean" };
|
|
669
|
-
return { type: "object" };
|
|
917
|
+
return { type: "object", "x-unknown": true };
|
|
918
|
+
}
|
|
919
|
+
/**
|
|
920
|
+
* Deduplicate schemas by comparing their JSON representations
|
|
921
|
+
*/
|
|
922
|
+
deduplicateSchemas(schemas) {
|
|
923
|
+
const seen = /* @__PURE__ */ new Map();
|
|
924
|
+
for (const schema of schemas) {
|
|
925
|
+
const key = JSON.stringify(schema);
|
|
926
|
+
if (!seen.has(key)) {
|
|
927
|
+
seen.set(key, schema);
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
return Array.from(seen.values());
|
|
931
|
+
}
|
|
932
|
+
/**
|
|
933
|
+
* Check if a schema contains fields with unknown types
|
|
934
|
+
*/
|
|
935
|
+
hasUnknownFields(schema) {
|
|
936
|
+
if (!schema) return false;
|
|
937
|
+
if (schema["x-unknown"]) return true;
|
|
938
|
+
if (schema.type === "object" && schema.properties) {
|
|
939
|
+
return Object.values(schema.properties).some(
|
|
940
|
+
(prop) => this.hasUnknownFields(prop)
|
|
941
|
+
);
|
|
942
|
+
}
|
|
943
|
+
if (schema.type === "array" && schema.items) {
|
|
944
|
+
return this.hasUnknownFields(schema.items);
|
|
945
|
+
}
|
|
946
|
+
return false;
|
|
670
947
|
}
|
|
671
948
|
/**
|
|
672
949
|
* Check if an expression is a call to ctx.body()
|
|
@@ -748,6 +1025,82 @@ class OpenAPIAnalyzer {
|
|
|
748
1025
|
return { type: "object" };
|
|
749
1026
|
}
|
|
750
1027
|
}
|
|
1028
|
+
/**
|
|
1029
|
+
* Convert a TypeScript Type (from type checker) to an OpenAPI schema
|
|
1030
|
+
*/
|
|
1031
|
+
convertTypeToSchema(type, typeChecker, depth = 0) {
|
|
1032
|
+
if (depth > 5) {
|
|
1033
|
+
return { type: "object", description: "Complex type (max depth reached)" };
|
|
1034
|
+
}
|
|
1035
|
+
if (type.flags & ts.TypeFlags.String) return { type: "string" };
|
|
1036
|
+
if (type.flags & ts.TypeFlags.Number) return { type: "number" };
|
|
1037
|
+
if (type.flags & ts.TypeFlags.Boolean) return { type: "boolean" };
|
|
1038
|
+
if (type.flags & ts.TypeFlags.Null) return { type: "null" };
|
|
1039
|
+
if (type.flags & ts.TypeFlags.Undefined) return { type: "object", nullable: true };
|
|
1040
|
+
if (type.flags & ts.TypeFlags.Any || type.flags & ts.TypeFlags.Unknown) return {};
|
|
1041
|
+
if (type.flags & ts.TypeFlags.StringLiteral) {
|
|
1042
|
+
const literalType = type;
|
|
1043
|
+
return { type: "string", enum: [literalType.value] };
|
|
1044
|
+
}
|
|
1045
|
+
if (type.flags & ts.TypeFlags.NumberLiteral) {
|
|
1046
|
+
const literalType = type;
|
|
1047
|
+
return { type: "number", enum: [literalType.value] };
|
|
1048
|
+
}
|
|
1049
|
+
if (type.flags & ts.TypeFlags.BooleanLiteral) {
|
|
1050
|
+
const intrinsicName = type.intrinsicName;
|
|
1051
|
+
return { type: "boolean", enum: [intrinsicName === "true"] };
|
|
1052
|
+
}
|
|
1053
|
+
if (type.flags & ts.TypeFlags.Union) {
|
|
1054
|
+
const unionType = type;
|
|
1055
|
+
const schemas = unionType.types.map((t) => this.convertTypeToSchema(t, typeChecker, depth + 1));
|
|
1056
|
+
const uniqueTypes = new Set(schemas.map((s) => s.type));
|
|
1057
|
+
if (uniqueTypes.size === 1 && schemas[0].type !== "object") {
|
|
1058
|
+
return schemas[0];
|
|
1059
|
+
}
|
|
1060
|
+
return { oneOf: schemas };
|
|
1061
|
+
}
|
|
1062
|
+
if (typeChecker.isArrayType(type)) {
|
|
1063
|
+
const typeArgs = type.typeArguments || type.resolvedTypeArguments;
|
|
1064
|
+
if (typeArgs && typeArgs.length > 0) {
|
|
1065
|
+
return {
|
|
1066
|
+
type: "array",
|
|
1067
|
+
items: this.convertTypeToSchema(typeArgs[0], typeChecker, depth + 1)
|
|
1068
|
+
};
|
|
1069
|
+
}
|
|
1070
|
+
return { type: "array", items: {} };
|
|
1071
|
+
}
|
|
1072
|
+
if (type.flags & ts.TypeFlags.Object) {
|
|
1073
|
+
const properties = {};
|
|
1074
|
+
const required = [];
|
|
1075
|
+
const props = typeChecker.getPropertiesOfType(type);
|
|
1076
|
+
const maxProps = 50;
|
|
1077
|
+
const propsToProcess = props.slice(0, maxProps);
|
|
1078
|
+
for (const prop of propsToProcess) {
|
|
1079
|
+
const propName = prop.getName();
|
|
1080
|
+
const propType = typeChecker.getTypeOfSymbol(prop);
|
|
1081
|
+
const signatures = propType.getCallSignatures();
|
|
1082
|
+
if (signatures && signatures.length > 0) {
|
|
1083
|
+
continue;
|
|
1084
|
+
}
|
|
1085
|
+
properties[propName] = this.convertTypeToSchema(propType, typeChecker, depth + 1);
|
|
1086
|
+
if (!(prop.flags & ts.SymbolFlags.Optional)) {
|
|
1087
|
+
required.push(propName);
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
const schema = { type: "object" };
|
|
1091
|
+
if (Object.keys(properties).length > 0) {
|
|
1092
|
+
schema.properties = properties;
|
|
1093
|
+
if (required.length > 0) {
|
|
1094
|
+
schema.required = required;
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
if (props.length > maxProps) {
|
|
1098
|
+
schema.description = `Type with ${props.length} properties (showing ${maxProps})`;
|
|
1099
|
+
}
|
|
1100
|
+
return schema;
|
|
1101
|
+
}
|
|
1102
|
+
return { type: "object", description: "Complex type" };
|
|
1103
|
+
}
|
|
751
1104
|
/**
|
|
752
1105
|
* Extract mount information from mount call
|
|
753
1106
|
*/
|
|
@@ -816,6 +1169,207 @@ class OpenAPIAnalyzer {
|
|
|
816
1169
|
}
|
|
817
1170
|
};
|
|
818
1171
|
}
|
|
1172
|
+
/**
|
|
1173
|
+
* Extract middleware information from .use() call
|
|
1174
|
+
*/
|
|
1175
|
+
extractMiddlewareFromCall(node, sourceFile) {
|
|
1176
|
+
const args = node.arguments;
|
|
1177
|
+
if (args.length < 1) return null;
|
|
1178
|
+
const middlewareArg = args[0];
|
|
1179
|
+
let middlewareName = "anonymous";
|
|
1180
|
+
let isImportedIdentifier = false;
|
|
1181
|
+
if (ts.isIdentifier(middlewareArg)) {
|
|
1182
|
+
middlewareName = middlewareArg.getText(sourceFile);
|
|
1183
|
+
isImportedIdentifier = true;
|
|
1184
|
+
} else if (ts.isCallExpression(middlewareArg) && ts.isIdentifier(middlewareArg.expression)) {
|
|
1185
|
+
middlewareName = middlewareArg.expression.getText(sourceFile);
|
|
1186
|
+
isImportedIdentifier = true;
|
|
1187
|
+
} else if (ts.isFunctionExpression(middlewareArg) || ts.isArrowFunction(middlewareArg)) {
|
|
1188
|
+
middlewareName = "inline-middleware";
|
|
1189
|
+
}
|
|
1190
|
+
let analysis = this.analyzeMiddleware(middlewareArg, sourceFile);
|
|
1191
|
+
if (isImportedIdentifier) {
|
|
1192
|
+
const resolvedAnalysis = this.resolveImportedMiddlewareDefinition(middlewareName, sourceFile);
|
|
1193
|
+
if (resolvedAnalysis.responseTypes) {
|
|
1194
|
+
analysis.responseTypes = { ...resolvedAnalysis.responseTypes, ...analysis.responseTypes };
|
|
1195
|
+
}
|
|
1196
|
+
if (resolvedAnalysis.headers) {
|
|
1197
|
+
analysis.headers = [...resolvedAnalysis.headers || [], ...analysis.headers || []];
|
|
1198
|
+
analysis.headers = Array.from(new Set(analysis.headers));
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1201
|
+
return {
|
|
1202
|
+
name: middlewareName,
|
|
1203
|
+
file: sourceFile.fileName,
|
|
1204
|
+
startLine: sourceFile.getLineAndCharacterOfPosition(middlewareArg.getStart()).line + 1,
|
|
1205
|
+
endLine: sourceFile.getLineAndCharacterOfPosition(middlewareArg.getEnd()).line + 1,
|
|
1206
|
+
handlerSource: middlewareArg.getText(sourceFile),
|
|
1207
|
+
responseTypes: analysis.responseTypes,
|
|
1208
|
+
headers: analysis.headers,
|
|
1209
|
+
scope: "router",
|
|
1210
|
+
// Will be updated during collection based on context
|
|
1211
|
+
sourceContext: {
|
|
1212
|
+
file: sourceFile.fileName,
|
|
1213
|
+
startLine: sourceFile.getLineAndCharacterOfPosition(middlewareArg.getStart()).line + 1,
|
|
1214
|
+
endLine: sourceFile.getLineAndCharacterOfPosition(middlewareArg.getEnd()).line + 1,
|
|
1215
|
+
snippet: middlewareArg.getText(sourceFile),
|
|
1216
|
+
highlights: analysis.highlights
|
|
1217
|
+
}
|
|
1218
|
+
};
|
|
1219
|
+
}
|
|
1220
|
+
/**
|
|
1221
|
+
* Analyze middleware function to extract response types and headers
|
|
1222
|
+
*/
|
|
1223
|
+
analyzeMiddleware(middleware, sourceFile) {
|
|
1224
|
+
const responseTypes = {};
|
|
1225
|
+
const headers = [];
|
|
1226
|
+
const highlights = [];
|
|
1227
|
+
const variableValues = /* @__PURE__ */ new Map();
|
|
1228
|
+
const visit = (node) => {
|
|
1229
|
+
if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name)) {
|
|
1230
|
+
const varName = node.name.getText(sourceFile);
|
|
1231
|
+
if (node.initializer) {
|
|
1232
|
+
if (ts.isNumericLiteral(node.initializer)) {
|
|
1233
|
+
const value = parseInt(node.initializer.text);
|
|
1234
|
+
if (!isNaN(value)) {
|
|
1235
|
+
variableValues.set(varName, value);
|
|
1236
|
+
}
|
|
1237
|
+
} else if (ts.isBinaryExpression(node.initializer) && node.initializer.operatorToken.kind === ts.SyntaxKind.BarBarToken) {
|
|
1238
|
+
if (ts.isNumericLiteral(node.initializer.right)) {
|
|
1239
|
+
const value = parseInt(node.initializer.right.text);
|
|
1240
|
+
if (!isNaN(value)) {
|
|
1241
|
+
variableValues.set(varName, value);
|
|
1242
|
+
}
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
}
|
|
1246
|
+
}
|
|
1247
|
+
if (ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression)) {
|
|
1248
|
+
const obj = node.expression.expression.getText(sourceFile);
|
|
1249
|
+
const prop = node.expression.name.getText(sourceFile);
|
|
1250
|
+
if (obj === "ctx" && ["json", "text", "html", "jsx"].includes(prop)) {
|
|
1251
|
+
if (node.arguments.length >= 2) {
|
|
1252
|
+
const statusArg = node.arguments[1];
|
|
1253
|
+
let statusCode;
|
|
1254
|
+
if (ts.isNumericLiteral(statusArg)) {
|
|
1255
|
+
statusCode = parseInt(statusArg.text);
|
|
1256
|
+
} else if (ts.isIdentifier(statusArg)) {
|
|
1257
|
+
const varName = statusArg.getText(sourceFile);
|
|
1258
|
+
const value = variableValues.get(varName);
|
|
1259
|
+
if (typeof value === "number") {
|
|
1260
|
+
statusCode = value;
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
if (statusCode && statusCode >= 100 && statusCode < 600) {
|
|
1264
|
+
const statusStr = String(statusCode);
|
|
1265
|
+
if (!responseTypes[statusStr]) {
|
|
1266
|
+
let description = `Error response (${statusCode})`;
|
|
1267
|
+
if (statusCode === 401) description = "Unauthorized";
|
|
1268
|
+
else if (statusCode === 403) description = "Forbidden";
|
|
1269
|
+
else if (statusCode === 429) description = "Too Many Requests";
|
|
1270
|
+
else if (statusCode === 500) description = "Internal Server Error";
|
|
1271
|
+
const content = {};
|
|
1272
|
+
if (prop === "json") {
|
|
1273
|
+
content["application/json"] = { schema: { type: "object" } };
|
|
1274
|
+
} else if (prop === "text") {
|
|
1275
|
+
content["text/plain"] = { schema: { type: "string" } };
|
|
1276
|
+
} else if (prop === "html" || prop === "jsx") {
|
|
1277
|
+
content["text/html"] = { schema: { type: "string" } };
|
|
1278
|
+
}
|
|
1279
|
+
responseTypes[statusStr] = {
|
|
1280
|
+
description,
|
|
1281
|
+
...Object.keys(content).length > 0 ? { content } : {}
|
|
1282
|
+
};
|
|
1283
|
+
}
|
|
1284
|
+
}
|
|
1285
|
+
}
|
|
1286
|
+
}
|
|
1287
|
+
if (["set", "header"].includes(prop)) {
|
|
1288
|
+
if (node.arguments.length >= 1 && ts.isStringLiteral(node.arguments[0])) {
|
|
1289
|
+
const headerName = node.arguments[0].text;
|
|
1290
|
+
if (headerName && !headers.includes(headerName)) {
|
|
1291
|
+
headers.push(headerName);
|
|
1292
|
+
}
|
|
1293
|
+
}
|
|
1294
|
+
}
|
|
1295
|
+
}
|
|
1296
|
+
ts.forEachChild(node, visit);
|
|
1297
|
+
};
|
|
1298
|
+
visit(middleware);
|
|
1299
|
+
const middlewareSource = middleware.getText(sourceFile);
|
|
1300
|
+
if (middlewareSource.includes("X-RateLimit")) {
|
|
1301
|
+
const rateLimitHeaders = ["X-RateLimit-Limit", "X-RateLimit-Remaining", "X-RateLimit-Reset", "Retry-After"];
|
|
1302
|
+
for (const header of rateLimitHeaders) {
|
|
1303
|
+
if (middlewareSource.includes(header) && !headers.includes(header)) {
|
|
1304
|
+
headers.push(header);
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
1307
|
+
}
|
|
1308
|
+
return {
|
|
1309
|
+
responseTypes: Object.keys(responseTypes).length > 0 ? responseTypes : void 0,
|
|
1310
|
+
headers: headers.length > 0 ? headers : void 0,
|
|
1311
|
+
highlights: highlights.length > 0 ? highlights : void 0
|
|
1312
|
+
};
|
|
1313
|
+
}
|
|
1314
|
+
/**
|
|
1315
|
+
* Resolve an imported middleware identifier and analyze its definition
|
|
1316
|
+
*/
|
|
1317
|
+
resolveImportedMiddlewareDefinition(middlewareName, sourceFile) {
|
|
1318
|
+
const fileImports = this.imports.get(sourceFile.fileName);
|
|
1319
|
+
if (!fileImports || !fileImports.has(middlewareName)) {
|
|
1320
|
+
return {};
|
|
1321
|
+
}
|
|
1322
|
+
const importInfo = fileImports.get(middlewareName);
|
|
1323
|
+
const modulePath = importInfo.modulePath;
|
|
1324
|
+
if (!modulePath.startsWith(".")) {
|
|
1325
|
+
return {};
|
|
1326
|
+
}
|
|
1327
|
+
const dir = path.dirname(sourceFile.fileName);
|
|
1328
|
+
let absolutePath = path.resolve(dir, modulePath);
|
|
1329
|
+
const extensions = [".ts", ".js", ".tsx", ".jsx", "/index.ts", "/index.js"];
|
|
1330
|
+
let resolvedPath;
|
|
1331
|
+
for (const ext of extensions) {
|
|
1332
|
+
const testPath = absolutePath + ext;
|
|
1333
|
+
if (fs.existsSync(testPath)) {
|
|
1334
|
+
resolvedPath = testPath;
|
|
1335
|
+
break;
|
|
1336
|
+
}
|
|
1337
|
+
}
|
|
1338
|
+
if (!resolvedPath && fs.existsSync(absolutePath)) {
|
|
1339
|
+
resolvedPath = absolutePath;
|
|
1340
|
+
}
|
|
1341
|
+
if (!resolvedPath) {
|
|
1342
|
+
return {};
|
|
1343
|
+
}
|
|
1344
|
+
const importedSourceFile = this.program?.getSourceFile(resolvedPath);
|
|
1345
|
+
if (!importedSourceFile) {
|
|
1346
|
+
return {};
|
|
1347
|
+
}
|
|
1348
|
+
let middlewareNode;
|
|
1349
|
+
const exportName = importInfo.exportName || middlewareName;
|
|
1350
|
+
ts.forEachChild(importedSourceFile, (node) => {
|
|
1351
|
+
if (ts.isFunctionDeclaration(node) && node.name?.getText(importedSourceFile) === exportName) {
|
|
1352
|
+
const modifiers = node.modifiers;
|
|
1353
|
+
if (modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword)) {
|
|
1354
|
+
middlewareNode = node;
|
|
1355
|
+
}
|
|
1356
|
+
}
|
|
1357
|
+
if (ts.isVariableStatement(node)) {
|
|
1358
|
+
const modifiers = node.modifiers;
|
|
1359
|
+
if (modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword)) {
|
|
1360
|
+
for (const declaration of node.declarationList.declarations) {
|
|
1361
|
+
if (ts.isIdentifier(declaration.name) && declaration.name.getText(importedSourceFile) === exportName) {
|
|
1362
|
+
middlewareNode = declaration.initializer;
|
|
1363
|
+
}
|
|
1364
|
+
}
|
|
1365
|
+
}
|
|
1366
|
+
}
|
|
1367
|
+
});
|
|
1368
|
+
if (!middlewareNode) {
|
|
1369
|
+
return {};
|
|
1370
|
+
}
|
|
1371
|
+
return this.analyzeMiddleware(middlewareNode, importedSourceFile);
|
|
1372
|
+
}
|
|
819
1373
|
/**
|
|
820
1374
|
* Check if a reference is to an external dependency
|
|
821
1375
|
*/
|
|
@@ -1005,4 +1559,4 @@ class OpenAPIAnalyzer {
|
|
|
1005
1559
|
}
|
|
1006
1560
|
}
|
|
1007
1561
|
exports.OpenAPIAnalyzer = OpenAPIAnalyzer;
|
|
1008
|
-
//# sourceMappingURL=analyzer.impl-
|
|
1562
|
+
//# sourceMappingURL=analyzer.impl-CVJ8zfGQ.cjs.map
|