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