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.
Files changed (73) hide show
  1. package/README.md +46 -1815
  2. package/dist/{analyzer-BqIe1p0R.js → analyzer-BkNQHWj4.js} +3 -8
  3. package/dist/{analyzer-BqIe1p0R.js.map → analyzer-BkNQHWj4.js.map} +1 -1
  4. package/dist/{analyzer-CKLGLFtx.cjs → analyzer-DM-OlRq8.cjs} +2 -7
  5. package/dist/{analyzer-CKLGLFtx.cjs.map → analyzer-DM-OlRq8.cjs.map} +1 -1
  6. package/dist/{analyzer.impl-D9Yi1Hax.cjs → analyzer.impl-CVJ8zfGQ.cjs} +596 -42
  7. package/dist/analyzer.impl-CVJ8zfGQ.cjs.map +1 -0
  8. package/dist/{analyzer.impl-CV6W1Eq7.js → analyzer.impl-CsA1bS_s.js} +596 -42
  9. package/dist/analyzer.impl-CsA1bS_s.js.map +1 -0
  10. package/dist/cli.cjs +206 -18
  11. package/dist/cli.cjs.map +1 -1
  12. package/dist/cli.js +206 -18
  13. package/dist/cli.js.map +1 -1
  14. package/dist/context.d.ts +46 -9
  15. package/dist/index.cjs +3239 -1173
  16. package/dist/index.cjs.map +1 -1
  17. package/dist/index.js +3236 -1171
  18. package/dist/index.js.map +1 -1
  19. package/dist/plugins/application/api-explorer/static/explorer-client.mjs +375 -29
  20. package/dist/plugins/application/api-explorer/static/style.css +327 -8
  21. package/dist/plugins/application/api-explorer/static/theme.css +11 -2
  22. package/dist/plugins/application/asyncapi/generator.d.ts +4 -0
  23. package/dist/plugins/application/asyncapi/static/asyncapi-client.mjs +154 -22
  24. package/dist/plugins/application/asyncapi/static/style.css +24 -8
  25. package/dist/plugins/application/auth.d.ts +5 -0
  26. package/dist/plugins/application/dashboard/fetch-interceptor.d.ts +119 -0
  27. package/dist/plugins/application/dashboard/metrics-collector.d.ts +38 -2
  28. package/dist/plugins/application/dashboard/plugin.d.ts +53 -1
  29. package/dist/plugins/application/dashboard/static/charts.js +127 -62
  30. package/dist/plugins/application/dashboard/static/client.js +160 -0
  31. package/dist/plugins/application/dashboard/static/graph.mjs +167 -56
  32. package/dist/plugins/application/dashboard/static/reactflow.css +20 -10
  33. package/dist/plugins/application/dashboard/static/registry.js +112 -8
  34. package/dist/plugins/application/dashboard/static/requests.js +1167 -71
  35. package/dist/plugins/application/dashboard/static/styles.css +186 -14
  36. package/dist/plugins/application/dashboard/static/tabs.js +44 -9
  37. package/dist/plugins/application/dashboard/static/tabulator.css +23 -3
  38. package/dist/plugins/application/dashboard/static/theme.css +11 -2
  39. package/dist/plugins/application/mcp-server/plugin.d.ts +39 -0
  40. package/dist/plugins/application/openapi/analyzer.impl.d.ts +65 -1
  41. package/dist/plugins/application/openapi/openapi.d.ts +3 -0
  42. package/dist/plugins/application/shared/ast-utils.d.ts +7 -0
  43. package/dist/plugins/middleware/compression.d.ts +12 -2
  44. package/dist/plugins/middleware/rate-limit.d.ts +5 -0
  45. package/dist/router.d.ts +59 -19
  46. package/dist/server.d.ts +22 -0
  47. package/dist/shokupan.d.ts +31 -3
  48. package/dist/util/adapter/bun.d.ts +8 -0
  49. package/dist/util/adapter/filesystem.d.ts +20 -0
  50. package/dist/util/adapter/index.d.ts +4 -0
  51. package/dist/util/adapter/interface.d.ts +12 -0
  52. package/dist/util/adapter/node.d.ts +8 -0
  53. package/dist/util/adapter/wintercg.d.ts +5 -0
  54. package/dist/util/body-parser.d.ts +30 -0
  55. package/dist/util/controller-scanner.d.ts +4 -0
  56. package/dist/util/cpu-monitor.d.ts +2 -0
  57. package/dist/util/decorators.d.ts +20 -3
  58. package/dist/util/di.d.ts +3 -8
  59. package/dist/util/metadata.d.ts +18 -0
  60. package/dist/util/middleware-tracker.d.ts +10 -0
  61. package/dist/util/request.d.ts +1 -0
  62. package/dist/util/symbol.d.ts +1 -0
  63. package/dist/util/types.d.ts +167 -1
  64. package/package.json +7 -5
  65. package/dist/analyzer.impl-CV6W1Eq7.js.map +0 -1
  66. package/dist/analyzer.impl-D9Yi1Hax.cjs.map +0 -1
  67. package/dist/http-server-BEMPIs33.cjs +0 -85
  68. package/dist/http-server-BEMPIs33.cjs.map +0 -1
  69. package/dist/http-server-CCeagTyU.js +0 -68
  70. package/dist/http-server-CCeagTyU.js.map +0 -1
  71. package/dist/plugins/application/dashboard/static/failures.js +0 -85
  72. package/dist/plugins/application/dashboard/static/poll.js +0 -146
  73. 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
- return { applications: this.applications };
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
- if (ts.isStringLiteral(pathArg)) {
354
- routePath = pathArg.text;
355
- } else {
356
- routePath = "__DYNAMIC_EVENT__";
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 responseSchema;
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
- responseSchema = returnSchema;
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
- responseSchema = this.convertExpressionToSchema(node.arguments[0], sourceFile, scope);
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 && (!responseSchema || responseSchema.type === "object")) {
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
- responseSchema = schema;
569
+ responseSchemas.push(schema);
457
570
  responseType = schema.type;
458
571
  }
459
572
  }
460
- if (!responseSchema && !responseType) {
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 && ts.isIdentifier(node.name)) {
477
- const varName = node.name.getText(sourceFile);
478
- let initializer = node.initializer;
479
- if (ts.isAsExpression(initializer)) {
480
- if (this.isCtxBodyCall(initializer.expression, sourceFile)) {
481
- const schema = this.convertTypeNodeToSchema(initializer.type, sourceFile);
482
- if (schema) {
483
- requestTypes.body = schema;
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 schema = this.convertExpressionToSchema(initializer, sourceFile, scope);
492
- scope.set(varName, schema);
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 (responseSchema && (responseSchema.type !== "object" || responseSchema.properties && Object.keys(responseSchema.properties).length > 0)) {
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 (responseSchema && (responseSchema.type !== "object" || responseSchema.properties && Object.keys(responseSchema.properties).length > 0)) {
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
- return { requestTypes, responseType, responseSchema, emits, highlights };
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
- return { type: "object" };
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-D9Yi1Hax.cjs.map
1562
+ //# sourceMappingURL=analyzer.impl-CVJ8zfGQ.cjs.map