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
@@ -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
- return { applications: this.applications };
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
- if (ts.isStringLiteral(pathArg)) {
352
- routePath = pathArg.text;
353
- } else {
354
- routePath = "__DYNAMIC_EVENT__";
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 responseSchema;
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
- responseSchema = returnSchema;
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
- responseSchema = this.convertExpressionToSchema(node.arguments[0], sourceFile, scope);
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 && (!responseSchema || responseSchema.type === "object")) {
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
- responseSchema = schema;
567
+ responseSchemas.push(schema);
455
568
  responseType = schema.type;
456
569
  }
457
570
  }
458
- if (!responseSchema && !responseType) {
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 && ts.isIdentifier(node.name)) {
475
- const varName = node.name.getText(sourceFile);
476
- let initializer = node.initializer;
477
- if (ts.isAsExpression(initializer)) {
478
- if (this.isCtxBodyCall(initializer.expression, sourceFile)) {
479
- const schema = this.convertTypeNodeToSchema(initializer.type, sourceFile);
480
- if (schema) {
481
- requestTypes.body = schema;
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 schema = this.convertExpressionToSchema(initializer, sourceFile, scope);
490
- scope.set(varName, schema);
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 (responseSchema && (responseSchema.type !== "object" || responseSchema.properties && Object.keys(responseSchema.properties).length > 0)) {
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 (responseSchema && (responseSchema.type !== "object" || responseSchema.properties && Object.keys(responseSchema.properties).length > 0)) {
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
- return { requestTypes, responseType, responseSchema, emits, highlights };
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
- return { type: "object" };
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-CV6W1Eq7.js.map
1562
+ //# sourceMappingURL=analyzer.impl-CsA1bS_s.js.map