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