shokupan 0.10.4 → 0.11.0

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