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
package/dist/index.js CHANGED
@@ -1,24 +1,27 @@
1
1
  import { nanoid } from "nanoid";
2
2
  import { readFile } from "node:fs/promises";
3
3
  import { inspect } from "node:util";
4
- import { RecordId, Surreal } from "surrealdb";
5
4
  import { Eta } from "eta";
6
5
  import { stat, readdir, readFile as readFile$1 } from "fs/promises";
7
6
  import { resolve, join, sep, basename } from "path";
8
7
  import { trace, SpanKind, SpanStatusCode, context } from "@opentelemetry/api";
8
+ import { RecordId, Surreal } from "surrealdb";
9
9
  import { dump } from "js-yaml";
10
+ import * as http$1 from "node:http";
11
+ import "node:https";
10
12
  import { AsyncLocalStorage } from "node:async_hooks";
11
- import * as os from "node:os";
12
- import os__default from "node:os";
13
13
  import { dirname, join as join$1 } from "node:path";
14
- import { fileURLToPath } from "node:url";
14
+ import { fileURLToPath, URL as URL$1 } from "node:url";
15
15
  import renderToString from "preact-render-to-string";
16
16
  import { jsxs, jsx, Fragment } from "preact/jsx-runtime";
17
17
  import cluster from "node:cluster";
18
18
  import net from "node:net";
19
+ import * as os from "node:os";
20
+ import os__default from "node:os";
21
+ import { createRequire } from "node:module";
19
22
  import { monitorEventLoopDelay } from "node:perf_hooks";
20
23
  import { readFileSync } from "node:fs";
21
- import { OpenAPIAnalyzer } from "./analyzer-BqIe1p0R.js";
24
+ import { OpenAPIAnalyzer } from "./analyzer-CnKnQ5KV.js";
22
25
  import * as zlib from "node:zlib";
23
26
  import Ajv from "ajv";
24
27
  import addFormats from "ajv-formats";
@@ -185,12 +188,12 @@ class ShokupanResponse {
185
188
  }
186
189
  const $isApplication = /* @__PURE__ */ Symbol.for("Shokupan.app");
187
190
  const $appRoot = /* @__PURE__ */ Symbol.for("Shokupan.app-root");
188
- const $isMounted = /* @__PURE__ */ Symbol("Shokupan.isMounted");
189
- const $routeMethods = /* @__PURE__ */ Symbol("Shokupan.routeMethods");
190
- const $eventMethods = /* @__PURE__ */ Symbol("Shokupan.eventMethods");
191
- const $routeArgs = /* @__PURE__ */ Symbol("Shokupan.routeArgs");
192
- const $controllerPath = /* @__PURE__ */ Symbol("Shokupan.controllerPath");
193
- const $middleware = /* @__PURE__ */ Symbol("Shokupan.middleware");
191
+ const $isMounted = /* @__PURE__ */ Symbol.for("Shokupan.isMounted");
192
+ const $routeMethods = /* @__PURE__ */ Symbol.for("Shokupan.routeMethods");
193
+ const $eventMethods = /* @__PURE__ */ Symbol.for("Shokupan.eventMethods");
194
+ const $routeArgs = /* @__PURE__ */ Symbol.for("Shokupan.routeArgs");
195
+ const $controllerPath = /* @__PURE__ */ Symbol.for("Shokupan.controllerPath");
196
+ const $middleware = /* @__PURE__ */ Symbol.for("Shokupan.middleware");
194
197
  const $isRouter = /* @__PURE__ */ Symbol.for("Shokupan.router");
195
198
  const $parent = /* @__PURE__ */ Symbol.for("Shokupan.parent");
196
199
  const $childRouters = /* @__PURE__ */ Symbol.for("Shokupan.child-routers");
@@ -227,12 +230,13 @@ function isValidCookieDomain(domain, currentHost) {
227
230
  return false;
228
231
  }
229
232
  class ShokupanContext {
230
- constructor(request, server, state, app, signal, enableMiddlewareTracking = false) {
233
+ constructor(request, server, state, app, signal, enableMiddlewareTracking = false, requestId) {
231
234
  this.request = request;
232
235
  this.server = server;
233
236
  this.app = app;
234
237
  this.signal = signal;
235
238
  this.state = state || {};
239
+ this[$requestId] = requestId;
236
240
  if (enableMiddlewareTracking) {
237
241
  const self = this;
238
242
  this.state = new Proxy(this.state, {
@@ -300,7 +304,10 @@ class ShokupanContext {
300
304
  get requestId() {
301
305
  return this[$requestId] ??= this.app?.applicationConfig?.idGenerator?.() ?? nanoid();
302
306
  }
303
- [/* @__PURE__ */ Symbol.for("nodejs.util.inspect.custom")]() {
307
+ [
308
+ // Only apply a custom inspect symbol in Node.js, Deno, or Bun.
309
+ globalThis.navigator?.userAgent?.match(/Node\.js|Deno|Bun/) ? /* @__PURE__ */ Symbol.for("nodejs.util.inspect.custom") : /* @__PURE__ */ Symbol.for("no-op")
310
+ ]() {
304
311
  const innerString = inspect({
305
312
  method: this.request.method,
306
313
  url: this.request.url,
@@ -435,6 +442,12 @@ class ShokupanContext {
435
442
  get res() {
436
443
  return this.response;
437
444
  }
445
+ /**
446
+ * Get the raw response body content (if available)
447
+ */
448
+ get responseBody() {
449
+ return this[$rawBody];
450
+ }
438
451
  /**
439
452
  * Raw WebSocket connection
440
453
  */
@@ -553,10 +566,10 @@ class ShokupanContext {
553
566
  * The body is only parsed once and cached for subsequent reads.
554
567
  */
555
568
  async body() {
556
- if (this[$bodyParseError]) {
569
+ if (this[$bodyParseError] !== void 0) {
557
570
  throw this[$bodyParseError];
558
571
  }
559
- if (this[$bodyParsed]) {
572
+ if (this[$bodyParsed] === true) {
560
573
  return this[$cachedBody];
561
574
  }
562
575
  const contentType = this.request.headers.get("content-type") || "";
@@ -674,6 +687,7 @@ class ShokupanContext {
674
687
  if (!VALID_HTTP_STATUSES.has(finalStatus)) {
675
688
  throw new Error(`Invalid HTTP status code: ${finalStatus}`);
676
689
  }
690
+ this.response.status = finalStatus;
677
691
  const jsonString = JSON.stringify(data instanceof Promise ? await data : data);
678
692
  this[$rawBody] = jsonString;
679
693
  if (!headers && !this.response.hasPopulatedHeaders) {
@@ -696,6 +710,7 @@ class ShokupanContext {
696
710
  if (this.app.applicationConfig.validateStatusCodes && !VALID_HTTP_STATUSES.has(finalStatus)) {
697
711
  throw new Error(`Invalid HTTP status code: ${finalStatus}`);
698
712
  }
713
+ this.response.status = finalStatus;
699
714
  this[$rawBody] = data instanceof Promise ? await data : data;
700
715
  if (!headers && !this.response.hasPopulatedHeaders) {
701
716
  this[$finalResponse] = new Response(this[$rawBody], {
@@ -717,6 +732,7 @@ class ShokupanContext {
717
732
  if (this.app.applicationConfig.validateStatusCodes && !VALID_HTTP_STATUSES.has(finalStatus)) {
718
733
  throw new Error(`Invalid HTTP status code: ${finalStatus}`);
719
734
  }
735
+ this.response.status = finalStatus;
720
736
  const finalHeaders = this.mergeHeaders(headers);
721
737
  finalHeaders.set("content-type", "text/html; charset=utf-8");
722
738
  this[$rawBody] = html instanceof Promise ? await html : html;
@@ -730,6 +746,7 @@ class ShokupanContext {
730
746
  if (this.app.applicationConfig.validateStatusCodes && !VALID_REDIRECT_STATUSES.has(status)) {
731
747
  throw new Error(`Invalid redirect status code: ${status}`);
732
748
  }
749
+ this.response.status = status;
733
750
  const finalHeaders = this.mergeHeaders();
734
751
  finalHeaders.set("Location", url instanceof Promise ? await url : url);
735
752
  this[$finalResponse] = new Response(null, { status, headers: finalHeaders });
@@ -744,6 +761,7 @@ class ShokupanContext {
744
761
  if (this.app.applicationConfig.validateStatusCodes && !VALID_HTTP_STATUSES.has(status)) {
745
762
  throw new Error(`Invalid HTTP status code: ${status}`);
746
763
  }
764
+ this.response.status = status;
747
765
  const finalHeaders = this.mergeHeaders();
748
766
  this[$finalResponse] = new Response(null, { status, headers: finalHeaders });
749
767
  return this[$finalResponse];
@@ -757,6 +775,7 @@ class ShokupanContext {
757
775
  if (this.app.applicationConfig.validateStatusCodes && !VALID_HTTP_STATUSES.has(status)) {
758
776
  throw new Error(`Invalid HTTP status code: ${status}`);
759
777
  }
778
+ if (status) this.response.status = status;
760
779
  if (typeof Bun !== "undefined") {
761
780
  this[$finalResponse] = new Response(Bun.file(path, fileOptions), { status, headers: finalHeaders });
762
781
  return this[$finalResponse];
@@ -859,6 +878,91 @@ function deepMerge(target, ...sources) {
859
878
  }
860
879
  return deepMerge(target, ...sources);
861
880
  }
881
+ async function getAstRoutes(applications, options = {}) {
882
+ const { includePrefix = true, pathTransform } = options;
883
+ const astRoutes = [];
884
+ const getExpandedRoutes = (app, prefix = "", seen = /* @__PURE__ */ new Set(), sourceOverride) => {
885
+ if (seen.has(app.name)) return [];
886
+ const newSeen = new Set(seen);
887
+ newSeen.add(app.name);
888
+ const expanded = [];
889
+ let currentPrefix = prefix;
890
+ if (includePrefix && app.controllerPrefix) {
891
+ const cleanPrefix = currentPrefix.endsWith("/") ? currentPrefix.slice(0, -1) : currentPrefix;
892
+ const cleanCont = app.controllerPrefix.startsWith("/") ? app.controllerPrefix : "/" + app.controllerPrefix;
893
+ currentPrefix = cleanPrefix + cleanCont;
894
+ }
895
+ for (const route of app.routes) {
896
+ let path = route.path;
897
+ if (includePrefix) {
898
+ const cleanPrefix = currentPrefix.endsWith("/") ? currentPrefix.slice(0, -1) : currentPrefix;
899
+ const cleanPath = path.startsWith("/") ? path : "/" + path;
900
+ path = cleanPrefix + cleanPath;
901
+ if (path.length > 1 && path.endsWith("/")) {
902
+ path = path.slice(0, -1);
903
+ }
904
+ }
905
+ if (pathTransform) {
906
+ path = pathTransform(path);
907
+ } else if (includePrefix && !path.startsWith("/")) {
908
+ path = "/" + path;
909
+ }
910
+ const expandedRoute = {
911
+ ...route,
912
+ path: path || "/"
913
+ };
914
+ if (sourceOverride) {
915
+ expandedRoute.sourceContext = sourceOverride;
916
+ }
917
+ expanded.push(expandedRoute);
918
+ }
919
+ if (app.mounted) {
920
+ for (const mount of app.mounted) {
921
+ const targetApp = applications.find((a) => a.name === mount.target || a.className === mount.target);
922
+ if (targetApp) {
923
+ let nextPrefix = "";
924
+ if (includePrefix) {
925
+ const cleanPrefix = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
926
+ const mountPrefix = mount.prefix.startsWith("/") ? mount.prefix : "/" + mount.prefix;
927
+ nextPrefix = cleanPrefix + mountPrefix;
928
+ }
929
+ let nextSourceOverride = sourceOverride;
930
+ if (mount.dependency || mount.targetFilePath && mount.targetFilePath.includes("node_modules")) {
931
+ if (mount.sourceContext) {
932
+ nextSourceOverride = {
933
+ ...mount.sourceContext,
934
+ // Add highlight for the mount line to make it clear
935
+ highlightLines: [mount.sourceContext.startLine, mount.sourceContext.endLine],
936
+ highlights: [{
937
+ startLine: mount.sourceContext.startLine,
938
+ endLine: mount.sourceContext.endLine,
939
+ type: "return-success"
940
+ // Use the success color (cyan) for the mount point
941
+ }]
942
+ };
943
+ }
944
+ }
945
+ expanded.push(...getExpandedRoutes(targetApp, nextPrefix, newSeen, nextSourceOverride));
946
+ }
947
+ }
948
+ }
949
+ return expanded;
950
+ };
951
+ applications.forEach((app) => {
952
+ astRoutes.push(...getExpandedRoutes(app));
953
+ });
954
+ const dedupedRoutes = /* @__PURE__ */ new Map();
955
+ for (const route of astRoutes) {
956
+ const key = `${route.method.toUpperCase()}:${route.path}`;
957
+ let score = 0;
958
+ if (route.responseSchema) score += 10;
959
+ if (route.handlerSource) score += 5;
960
+ if (!dedupedRoutes.has(key) || score > dedupedRoutes.get(key).score) {
961
+ dedupedRoutes.set(key, { route, score });
962
+ }
963
+ }
964
+ return Array.from(dedupedRoutes.values()).map((v) => v.route);
965
+ }
862
966
  const REGEX_PATTERNS = {
863
967
  QUERY_INT: /parseInt\(ctx\.query\.(\w+)\)/g,
864
968
  QUERY_FLOAT: /parseFloat\(ctx\.query\.(\w+)\)/g,
@@ -998,92 +1102,45 @@ function analyzeHandler(handler) {
998
1102
  }
999
1103
  return { inferredSpec };
1000
1104
  }
1001
- async function getAstRoutes$1(applications) {
1002
- const astRoutes = [];
1003
- const getExpandedRoutes = (app, prefix = "", seen = /* @__PURE__ */ new Set(), sourceOverride) => {
1004
- if (seen.has(app.name)) return [];
1005
- const newSeen = new Set(seen);
1006
- newSeen.add(app.name);
1007
- const expanded = [];
1008
- let currentPrefix = prefix;
1009
- if (app.controllerPrefix) {
1010
- const cleanPrefix = currentPrefix.endsWith("/") ? currentPrefix.slice(0, -1) : currentPrefix;
1011
- const cleanCont = app.controllerPrefix.startsWith("/") ? app.controllerPrefix : "/" + app.controllerPrefix;
1012
- currentPrefix = cleanPrefix + cleanCont;
1013
- }
1014
- for (const route of app.routes) {
1015
- const cleanPrefix = currentPrefix.endsWith("/") ? currentPrefix.slice(0, -1) : currentPrefix;
1016
- const cleanPath = route.path.startsWith("/") ? route.path : "/" + route.path;
1017
- let joined = cleanPrefix + cleanPath;
1018
- if (joined.length > 1 && joined.endsWith("/")) {
1019
- joined = joined.slice(0, -1);
1020
- }
1021
- const expandedRoute = {
1022
- ...route,
1023
- path: joined || "/"
1024
- };
1025
- if (sourceOverride) {
1026
- expandedRoute.sourceContext = sourceOverride;
1027
- }
1028
- expanded.push(expandedRoute);
1029
- }
1030
- if (app.mounted) {
1031
- for (const mount of app.mounted) {
1032
- const targetApp = applications.find((a) => a.name === mount.target || a.className === mount.target);
1033
- if (targetApp) {
1034
- const cleanPrefix = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
1035
- const mountPrefix = mount.prefix.startsWith("/") ? mount.prefix : "/" + mount.prefix;
1036
- let nextSourceOverride = sourceOverride;
1037
- if (mount.dependency || mount.targetFilePath && mount.targetFilePath.includes("node_modules")) {
1038
- if (mount.sourceContext) {
1039
- nextSourceOverride = {
1040
- ...mount.sourceContext,
1041
- // Add highlight for the mount line to make it clear
1042
- highlightLines: [mount.sourceContext.startLine, mount.sourceContext.endLine],
1043
- highlights: [{
1044
- startLine: mount.sourceContext.startLine,
1045
- endLine: mount.sourceContext.endLine,
1046
- type: "return-success"
1047
- // Use the success color (cyan) for the mount point
1048
- }]
1049
- };
1050
- }
1051
- }
1052
- expanded.push(...getExpandedRoutes(targetApp, cleanPrefix + mountPrefix, newSeen, nextSourceOverride));
1053
- }
1054
- }
1055
- }
1056
- return expanded;
1057
- };
1058
- applications.forEach((app) => {
1059
- astRoutes.push(...getExpandedRoutes(app));
1060
- });
1061
- const dedupedRoutes = /* @__PURE__ */ new Map();
1062
- for (const route of astRoutes) {
1063
- const key = `${route.method.toUpperCase()}:${route.path}`;
1064
- let score = 0;
1065
- if (route.responseSchema) score += 10;
1066
- if (route.handlerSource) score += 5;
1067
- if (!dedupedRoutes.has(key) || score > dedupedRoutes.get(key).score) {
1068
- dedupedRoutes.set(key, { route, score });
1069
- }
1070
- }
1071
- return Array.from(dedupedRoutes.values()).map((v) => v.route);
1072
- }
1073
1105
  async function generateOpenApi(rootRouter, options = {}) {
1074
1106
  const paths = {};
1075
1107
  const tagGroups = /* @__PURE__ */ new Map();
1076
1108
  const defaultTagGroup = options.defaultTagGroup || "General";
1077
1109
  const defaultTagName = options.defaultTag || "Application";
1078
1110
  let astRoutes = [];
1111
+ let astMiddlewareRegistry = {};
1112
+ let applications = [];
1079
1113
  try {
1080
- const { OpenAPIAnalyzer: OpenAPIAnalyzer2 } = await import("./analyzer-BqIe1p0R.js");
1081
- const analyzer = new OpenAPIAnalyzer2(process.cwd());
1082
- const { applications } = await analyzer.analyze();
1083
- astRoutes = await getAstRoutes$1(applications);
1114
+ const { OpenAPIAnalyzer: OpenAPIAnalyzer2 } = await import("./analyzer-CnKnQ5KV.js");
1115
+ const entrypoint = rootRouter.metadata?.file;
1116
+ const analyzer = new OpenAPIAnalyzer2(process.cwd(), entrypoint);
1117
+ const analysisResult = await analyzer.analyze();
1118
+ applications = analysisResult.applications;
1119
+ astRoutes = await getAstRoutes(applications);
1120
+ let middlewareId = 0;
1121
+ for (const app of applications) {
1122
+ if (app.middleware && app.middleware.length > 0) {
1123
+ for (const mw of app.middleware) {
1124
+ const id = `middleware-${middlewareId++}`;
1125
+ astMiddlewareRegistry[id] = {
1126
+ ...mw,
1127
+ id,
1128
+ usedBy: []
1129
+ // Will be populated when processing routes
1130
+ };
1131
+ }
1132
+ }
1133
+ }
1084
1134
  } catch (e) {
1135
+ if (options.warnings) {
1136
+ options.warnings.push({
1137
+ type: "ast-analysis-failed",
1138
+ message: "AST Analysis failed or skipped",
1139
+ detail: e.message
1140
+ });
1141
+ }
1085
1142
  }
1086
- const collect = (router, prefix = "", currentGroup = defaultTagGroup, defaultTag = defaultTagName, inheritedMiddleware = []) => {
1143
+ const collect = (router, prefix = "", currentGroup = defaultTagGroup, defaultTag = defaultTagName, inheritedMiddleware = [], isRootLevel = true) => {
1087
1144
  let group = currentGroup;
1088
1145
  let tag = defaultTag;
1089
1146
  if (router.config?.group) group = router.config.group;
@@ -1091,7 +1148,8 @@ async function generateOpenApi(rootRouter, options = {}) {
1091
1148
  tag = router.config.name;
1092
1149
  } else {
1093
1150
  const mountPath = router[$mountPath];
1094
- if (mountPath && mountPath !== "/") {
1151
+ const isDirectChild = router[$parent] === rootRouter;
1152
+ if ((isRootLevel || isDirectChild) && mountPath && mountPath !== "/") {
1095
1153
  const segments = mountPath.split("/").filter(Boolean);
1096
1154
  if (segments.length > 0) {
1097
1155
  const lastSegment = segments[segments.length - 1];
@@ -1099,6 +1157,20 @@ async function generateOpenApi(rootRouter, options = {}) {
1099
1157
  }
1100
1158
  }
1101
1159
  }
1160
+ let isBuiltinPlugin = false;
1161
+ let pluginName = "";
1162
+ if (router.metadata?.pluginName) {
1163
+ isBuiltinPlugin = true;
1164
+ pluginName = router.metadata.pluginName;
1165
+ tag = pluginName.replace(/[-_]/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
1166
+ } else if (router.metadata?.file && router.metadata.file.includes("plugins/application/")) {
1167
+ isBuiltinPlugin = true;
1168
+ const match = router.metadata.file.match(/plugins\/application\/([^/]+)/);
1169
+ if (match) {
1170
+ pluginName = match[1].replace(/\.(ts|js|mjs|mts|cjs)$/, "");
1171
+ tag = pluginName.replace(/[-_]/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
1172
+ }
1173
+ }
1102
1174
  if (!tagGroups.has(group)) tagGroups.set(group, /* @__PURE__ */ new Set());
1103
1175
  const routerMiddleware = router.middleware || [];
1104
1176
  const routes = router[$routes] || [];
@@ -1121,11 +1193,45 @@ async function generateOpenApi(rootRouter, options = {}) {
1121
1193
  };
1122
1194
  const routeMiddleware = route.middleware || [];
1123
1195
  const allMiddleware = [...inheritedMiddleware, ...routerMiddleware, ...routeMiddleware];
1124
- if (allMiddleware.length > 0) {
1125
- operation["x-shokupan-middleware"] = allMiddleware.map((mw) => ({
1126
- name: mw.name || "middleware",
1127
- metadata: mw.metadata
1128
- }));
1196
+ const astMiddlewareForRoute = [];
1197
+ for (const [mwId, mw] of Object.entries(astMiddlewareRegistry)) {
1198
+ const appForRoute = applications.find(
1199
+ (app) => app.routes?.some((r) => r.path === fullPath && r.method === route.method.toUpperCase())
1200
+ );
1201
+ const appForMiddleware = applications.find(
1202
+ (app) => app.middleware?.some((m) => m.name === mw.name && m.file === mw.file)
1203
+ );
1204
+ if (appForRoute && appForMiddleware && appForRoute.filePath === appForMiddleware.filePath) {
1205
+ astMiddlewareForRoute.push({ ...mw, id: mwId });
1206
+ if (!mw.usedBy.includes(fullPath)) {
1207
+ mw.usedBy.push(fullPath);
1208
+ }
1209
+ }
1210
+ }
1211
+ for (const astMw of astMiddlewareForRoute) {
1212
+ if (astMw.responseTypes) {
1213
+ for (const [statusCode, responseSpec] of Object.entries(astMw.responseTypes)) {
1214
+ if (!operation.responses[statusCode]) {
1215
+ operation.responses[statusCode] = responseSpec;
1216
+ }
1217
+ }
1218
+ }
1219
+ }
1220
+ if (allMiddleware.length > 0 || astMiddlewareForRoute.length > 0) {
1221
+ operation["x-shokupan-middleware"] = [
1222
+ ...allMiddleware.map((mw) => ({
1223
+ name: mw.name || "middleware",
1224
+ metadata: mw.metadata
1225
+ })),
1226
+ ...astMiddlewareForRoute.map((mw) => ({
1227
+ id: mw.id,
1228
+ name: mw.name,
1229
+ responses: mw.responseTypes,
1230
+ headers: mw.headers,
1231
+ file: mw.file,
1232
+ line: mw.startLine
1233
+ }))
1234
+ ];
1129
1235
  }
1130
1236
  if (route.guards) {
1131
1237
  for (const guard of route.guards) {
@@ -1191,6 +1297,18 @@ async function generateOpenApi(rootRouter, options = {}) {
1191
1297
  description: "Successful response",
1192
1298
  content: { "application/json": { schema: astMatch.responseSchema } }
1193
1299
  };
1300
+ if (astMatch.hasUnknownFields) {
1301
+ if (options.warnings) {
1302
+ options.warnings.push({
1303
+ type: "unknown-fields",
1304
+ message: "Response contains fields with unknown types",
1305
+ detail: `Route: ${fullPath} [${route.method}]`,
1306
+ location: { file: astMatch.sourceContext?.file, line: astMatch.sourceContext?.startLine }
1307
+ });
1308
+ }
1309
+ operation["x-warning"] = true;
1310
+ operation["x-warning-reason"] = "Response contains fields with unknown types that could not be statically analyzed";
1311
+ }
1194
1312
  } else if (astMatch.responseType) {
1195
1313
  let contentType = "application/json";
1196
1314
  if (astMatch.responseType === "string") contentType = "text/plain";
@@ -1210,6 +1328,14 @@ async function generateOpenApi(rootRouter, options = {}) {
1210
1328
  operation.parameters = params;
1211
1329
  }
1212
1330
  } else {
1331
+ if (options.warnings) {
1332
+ options.warnings.push({
1333
+ type: "route-not-found",
1334
+ message: "Route could not be statically analyzed",
1335
+ detail: `Route: ${fullPath} [${route.method}]`,
1336
+ location: route.metadata ? { file: route.metadata.file, line: route.metadata.line } : void 0
1337
+ });
1338
+ }
1213
1339
  const runtimeSource = (route.handler.originalHandler || route.handler).toString();
1214
1340
  let file;
1215
1341
  let line;
@@ -1226,10 +1352,22 @@ async function generateOpenApi(rootRouter, options = {}) {
1226
1352
  operation["x-shokupan-source"] = {
1227
1353
  file,
1228
1354
  line: line || 1,
1229
- code: runtimeSource
1355
+ code: runtimeSource,
1356
+ pluginName: route.handler.pluginName
1357
+ // Inject pluginName from handler
1230
1358
  };
1231
1359
  }
1232
1360
  }
1361
+ if (isBuiltinPlugin) {
1362
+ operation["x-shokupan-builtin"] = true;
1363
+ }
1364
+ if (route.handler.pluginName) {
1365
+ operation["x-shokupan-plugin-name"] = route.handler.pluginName;
1366
+ if (!operation["x-shokupan-source"]) operation["x-shokupan-source"] = {};
1367
+ operation["x-shokupan-source"].pluginName = route.handler.pluginName;
1368
+ } else if (pluginName) {
1369
+ operation["x-shokupan-plugin-name"] = pluginName;
1370
+ }
1233
1371
  if (route.keys.length > 0) {
1234
1372
  const pathParams = route.keys.map((key) => ({
1235
1373
  name: key,
@@ -1298,7 +1436,7 @@ async function generateOpenApi(rootRouter, options = {}) {
1298
1436
  const cleanPrefix = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
1299
1437
  const cleanMount = mountPath.startsWith("/") ? mountPath : "/" + mountPath;
1300
1438
  const nextPrefix = cleanPrefix + cleanMount || "/";
1301
- collect(child, nextPrefix, group, tag, [...inheritedMiddleware, ...routerMiddleware]);
1439
+ collect(child, nextPrefix, group, tag, [...inheritedMiddleware, ...routerMiddleware], false);
1302
1440
  }
1303
1441
  };
1304
1442
  collect(rootRouter);
@@ -1306,7 +1444,46 @@ async function generateOpenApi(rootRouter, options = {}) {
1306
1444
  for (const [name, tags] of tagGroups.entries()) {
1307
1445
  xTagGroups.push({ name, tags: Array.from(tags).sort() });
1308
1446
  }
1309
- return {
1447
+ if (!options.compliant) {
1448
+ for (const [id, mw] of Object.entries(astMiddlewareRegistry || {})) {
1449
+ const virtualPath = `/_middleware/${id}`;
1450
+ paths[virtualPath] = {
1451
+ get: {
1452
+ tags: ["System", "Middleware"],
1453
+ summary: `Middleware: ${mw.name}`,
1454
+ description: `Virtual endpoint for middleware analysis.
1455
+ **File**: ${mw.file}
1456
+ **Line**: ${mw.startLine}`,
1457
+ operationId: `getMiddleware_${id}`,
1458
+ parameters: [],
1459
+ responses: {
1460
+ "200": {
1461
+ description: "Middleware Analysis",
1462
+ content: {
1463
+ "application/json": {
1464
+ schema: { type: "object" }
1465
+ }
1466
+ }
1467
+ }
1468
+ },
1469
+ "x-middleware-metadata": mw,
1470
+ "x-virtual": true,
1471
+ "x-middleware-detail": true,
1472
+ "x-source-info": {
1473
+ file: mw.file,
1474
+ line: mw.startLine,
1475
+ code: mw.snippet
1476
+ },
1477
+ "x-shokupan-source": {
1478
+ file: mw.file,
1479
+ line: mw.startLine,
1480
+ code: mw.snippet
1481
+ }
1482
+ }
1483
+ };
1484
+ }
1485
+ }
1486
+ const spec = {
1310
1487
  openapi: "3.1.0",
1311
1488
  info: { title: "Shokupan API", version: "1.0.0", ...options.info },
1312
1489
  paths,
@@ -1314,8 +1491,29 @@ async function generateOpenApi(rootRouter, options = {}) {
1314
1491
  servers: options.servers,
1315
1492
  tags: options.tags,
1316
1493
  externalDocs: options.externalDocs,
1317
- "x-tagGroups": xTagGroups
1494
+ "x-tagGroups": xTagGroups,
1495
+ "x-middleware-registry": astMiddlewareRegistry
1318
1496
  };
1497
+ if (options.compliant) {
1498
+ spec["x-tagGroups"] = void 0;
1499
+ spec["x-middleware-registry"] = void 0;
1500
+ const stripExtensions = (obj) => {
1501
+ if (!obj || typeof obj !== "object") return;
1502
+ if (Array.isArray(obj)) {
1503
+ obj.forEach(stripExtensions);
1504
+ return;
1505
+ }
1506
+ for (const key of Object.keys(obj)) {
1507
+ if (key.startsWith("x-")) {
1508
+ delete obj[key];
1509
+ } else {
1510
+ stripExtensions(obj[key]);
1511
+ }
1512
+ }
1513
+ };
1514
+ stripExtensions(spec);
1515
+ }
1516
+ return spec;
1319
1517
  }
1320
1518
  const eta = new Eta();
1321
1519
  function serveStatic(config, prefix) {
@@ -1502,35 +1700,6 @@ function Inject(token) {
1502
1700
  });
1503
1701
  };
1504
1702
  }
1505
- class HttpError extends Error {
1506
- status;
1507
- constructor(message, status) {
1508
- super(message);
1509
- this.name = "HttpError";
1510
- this.status = status;
1511
- if (Error.captureStackTrace) {
1512
- Error.captureStackTrace(this, HttpError);
1513
- }
1514
- }
1515
- }
1516
- function getErrorStatus(err) {
1517
- if (!err || typeof err !== "object") {
1518
- return 500;
1519
- }
1520
- if (typeof err.status === "number") {
1521
- return err.status;
1522
- }
1523
- if (typeof err.statusCode === "number") {
1524
- return err.statusCode;
1525
- }
1526
- return 500;
1527
- }
1528
- class EventError extends HttpError {
1529
- constructor(message = "Event Error") {
1530
- super(message, 500);
1531
- this.name = "EventError";
1532
- }
1533
- }
1534
1703
  const tracer = trace.getTracer("shokupan.middleware");
1535
1704
  function traceHandler(fn, name) {
1536
1705
  return async function(...args) {
@@ -1554,31 +1723,6 @@ function traceHandler(fn, name) {
1554
1723
  });
1555
1724
  };
1556
1725
  }
1557
- class ShokupanRequestBase {
1558
- method;
1559
- url;
1560
- headers;
1561
- body;
1562
- async json() {
1563
- return JSON.parse(this.body);
1564
- }
1565
- async text() {
1566
- return this.body;
1567
- }
1568
- async formData() {
1569
- if (this.body instanceof FormData) {
1570
- return this.body;
1571
- }
1572
- return new Response(this.body, { headers: this.headers }).formData();
1573
- }
1574
- constructor(props) {
1575
- Object.assign(this, props);
1576
- if (!(this.headers instanceof Headers)) {
1577
- this.headers = new Headers(this.headers);
1578
- }
1579
- }
1580
- }
1581
- const ShokupanRequest = ShokupanRequestBase;
1582
1726
  function getCallerInfo(skipFrames = 1) {
1583
1727
  let file = "unknown";
1584
1728
  let line = 0;
@@ -1610,27 +1754,379 @@ function getCallerInfo(skipFrames = 1) {
1610
1754
  }
1611
1755
  return { file, line };
1612
1756
  }
1613
- class RouterTrie {
1614
- root;
1615
- constructor() {
1616
- this.root = this.createNode();
1617
- }
1618
- createNode() {
1619
- return {
1620
- children: {}
1621
- };
1622
- }
1623
- insert(method, path, handler) {
1624
- let node = this.root;
1625
- const segments = this.splitPath(path);
1626
- for (let i = 0; i < segments.length; i++) {
1627
- const segment = segments[i];
1628
- if (segment === "**") {
1629
- if (!node.recursiveChild) {
1630
- node.recursiveChild = this.createNode();
1631
- }
1632
- node = node.recursiveChild;
1633
- } else if (segment === "*") {
1757
+ const HTTPMethods = ["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS", "ALL"];
1758
+ var RouteParamType = /* @__PURE__ */ ((RouteParamType2) => {
1759
+ RouteParamType2["BODY"] = "BODY";
1760
+ RouteParamType2["PARAM"] = "PARAM";
1761
+ RouteParamType2["QUERY"] = "QUERY";
1762
+ RouteParamType2["HEADER"] = "HEADER";
1763
+ RouteParamType2["REQUEST"] = "REQUEST";
1764
+ RouteParamType2["CONTEXT"] = "CONTEXT";
1765
+ return RouteParamType2;
1766
+ })(RouteParamType || {});
1767
+ class ControllerScanner {
1768
+ static scan(router, prefix, controller) {
1769
+ let instance = controller;
1770
+ if (typeof controller === "function") {
1771
+ instance = Container.resolve(controller);
1772
+ const controllerPath = controller[$controllerPath];
1773
+ if (controllerPath !== void 0) {
1774
+ const p1 = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
1775
+ const p2 = controllerPath.startsWith("/") ? controllerPath : "/" + controllerPath;
1776
+ prefix = p1 + p2;
1777
+ if (!prefix) prefix = "/";
1778
+ }
1779
+ } else {
1780
+ const ctor = instance.constructor;
1781
+ const controllerPath = ctor[$controllerPath];
1782
+ if (controllerPath !== void 0) {
1783
+ const p1 = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
1784
+ const p2 = controllerPath.startsWith("/") ? controllerPath : "/" + controllerPath;
1785
+ prefix = p1 + p2;
1786
+ if (!prefix) prefix = "/";
1787
+ }
1788
+ }
1789
+ instance[$mountPath] = prefix;
1790
+ const info = getCallerInfo();
1791
+ instance.metadata = {
1792
+ file: info.file,
1793
+ line: info.line,
1794
+ name: instance.constructor.name
1795
+ };
1796
+ router.registerControllerInstance(instance);
1797
+ const controllerMiddleware = (typeof controller === "function" ? controller[$middleware] : instance[$middleware]) || [];
1798
+ const proto = Object.getPrototypeOf(instance);
1799
+ const methods = /* @__PURE__ */ new Set();
1800
+ let current = proto;
1801
+ while (current !== void 0 && current !== Object.prototype) {
1802
+ Object.getOwnPropertyNames(current).forEach((name) => methods.add(name));
1803
+ current = Object.getPrototypeOf(current);
1804
+ }
1805
+ Object.getOwnPropertyNames(instance).forEach((name) => methods.add(name));
1806
+ const decoratedRoutes = instance[$routeMethods] || proto && proto[$routeMethods];
1807
+ const decoratedArgs = instance[$routeArgs] || proto && proto[$routeArgs];
1808
+ const methodMiddlewareMap = instance[$middleware] || proto && proto[$middleware];
1809
+ const decoratedEvents = instance[$eventMethods] || proto && proto[$eventMethods];
1810
+ let routesAttached = 0;
1811
+ for (let i = 0; i < Array.from(methods).length; i++) {
1812
+ const name = Array.from(methods)[i];
1813
+ if (name === "constructor") continue;
1814
+ if (["arguments", "caller", "callee"].includes(name)) continue;
1815
+ const originalHandler = instance[name];
1816
+ if (typeof originalHandler !== "function") continue;
1817
+ let method;
1818
+ let subPath = "";
1819
+ let methodSource;
1820
+ const routeConfig = decoratedRoutes?.get(name);
1821
+ if (routeConfig !== void 0) {
1822
+ method = routeConfig.method;
1823
+ subPath = routeConfig.path;
1824
+ methodSource = routeConfig.source;
1825
+ } else {
1826
+ for (let j = 0; j < HTTPMethods.length; j++) {
1827
+ const m = HTTPMethods[j];
1828
+ if (name.toUpperCase().startsWith(m)) {
1829
+ method = m;
1830
+ const rest = name.slice(m.length);
1831
+ if (rest.length === 0) {
1832
+ subPath = "/";
1833
+ } else {
1834
+ subPath = "";
1835
+ let buffer = "";
1836
+ const flush = () => {
1837
+ if (buffer.length > 0) {
1838
+ subPath += "/" + buffer.toLowerCase();
1839
+ buffer = "";
1840
+ }
1841
+ };
1842
+ for (let i2 = 0; i2 < rest.length; i2++) {
1843
+ const char = rest[i2];
1844
+ if (char === "$") {
1845
+ flush();
1846
+ subPath += "/:";
1847
+ continue;
1848
+ }
1849
+ buffer += char;
1850
+ }
1851
+ if (buffer.length > 0) flush();
1852
+ subPath = rest.replace(/\$/g, "/:").replace(/([a-z0-9])([A-Z])/g, "$1/$2").toLowerCase();
1853
+ if (!subPath.startsWith("/")) {
1854
+ subPath = "/" + subPath;
1855
+ }
1856
+ }
1857
+ break;
1858
+ }
1859
+ }
1860
+ }
1861
+ if (method !== void 0 && method !== "") {
1862
+ routesAttached++;
1863
+ const cleanPrefix = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
1864
+ const cleanSubPath = subPath === "/" ? "" : subPath;
1865
+ let joined;
1866
+ if (cleanSubPath.length === 0) {
1867
+ joined = cleanPrefix;
1868
+ } else if (cleanSubPath.startsWith("/")) {
1869
+ joined = cleanPrefix + cleanSubPath;
1870
+ } else {
1871
+ joined = cleanPrefix + "/" + cleanSubPath;
1872
+ }
1873
+ const fullPath = joined || "/";
1874
+ const normalizedPath = fullPath.replace(/\/+/g, "/");
1875
+ const methodMw = methodMiddlewareMap instanceof Map ? methodMiddlewareMap.get(name) || [] : [];
1876
+ const allMiddleware = [...controllerMiddleware, ...methodMw];
1877
+ const routeArgs = decoratedArgs && decoratedArgs.get(name);
1878
+ const wrappedHandler = async (ctx) => {
1879
+ let args = [ctx];
1880
+ if (routeArgs?.length > 0) {
1881
+ args = [];
1882
+ const sortedArgs = [...routeArgs].sort((a, b) => a.index - b.index);
1883
+ for (let k = 0; k < sortedArgs.length; k++) {
1884
+ const arg = sortedArgs[k];
1885
+ switch (arg.type) {
1886
+ case RouteParamType.BODY:
1887
+ args[arg.index] = await ctx.body();
1888
+ break;
1889
+ case RouteParamType.PARAM:
1890
+ args[arg.index] = arg.name ? ctx.params[arg.name] : ctx.params;
1891
+ break;
1892
+ case RouteParamType.QUERY: {
1893
+ const url = new URL(ctx.req.url);
1894
+ if (arg.name) {
1895
+ const vals = url.searchParams.getAll(arg.name);
1896
+ args[arg.index] = vals.length > 1 ? vals : vals[0];
1897
+ } else {
1898
+ const query = {};
1899
+ const keys = Object.keys(url.searchParams);
1900
+ for (let k2 = 0; k2 < keys.length; k2++) {
1901
+ const key = keys[k2];
1902
+ const vals = url.searchParams.getAll(key);
1903
+ query[key] = vals.length > 1 ? vals : vals[0];
1904
+ }
1905
+ args[arg.index] = query;
1906
+ }
1907
+ break;
1908
+ }
1909
+ case RouteParamType.HEADER:
1910
+ args[arg.index] = arg.name ? ctx.req.headers.get(arg.name) : ctx.req.headers;
1911
+ break;
1912
+ case RouteParamType.REQUEST:
1913
+ args[arg.index] = ctx.req;
1914
+ break;
1915
+ case RouteParamType.CONTEXT:
1916
+ args[arg.index] = ctx;
1917
+ break;
1918
+ }
1919
+ }
1920
+ }
1921
+ const tracedOriginalHandler = ctx.app?.applicationConfig.enableTracing ? traceHandler(originalHandler, normalizedPath) : originalHandler;
1922
+ return tracedOriginalHandler.apply(instance, args);
1923
+ };
1924
+ let finalHandler = wrappedHandler;
1925
+ if (allMiddleware.length > 0) {
1926
+ const composed = compose(allMiddleware);
1927
+ finalHandler = async (ctx) => {
1928
+ return composed(ctx, () => wrappedHandler(ctx));
1929
+ };
1930
+ }
1931
+ finalHandler.originalHandler = originalHandler;
1932
+ if (finalHandler !== wrappedHandler) {
1933
+ wrappedHandler.originalHandler = originalHandler;
1934
+ }
1935
+ const tagName = instance.constructor.name;
1936
+ const decoratedSpecs = instance[$routeSpec] || proto && proto[$routeSpec];
1937
+ const userSpec = decoratedSpecs && decoratedSpecs.get(name);
1938
+ const spec = { tags: [tagName], ...userSpec };
1939
+ router.add({
1940
+ method,
1941
+ path: normalizedPath,
1942
+ handler: finalHandler,
1943
+ spec,
1944
+ controller: instance,
1945
+ metadata: methodSource || instance.metadata,
1946
+ middleware: allMiddleware
1947
+ });
1948
+ }
1949
+ const eventConfig = decoratedEvents?.get(name);
1950
+ if (eventConfig !== void 0) {
1951
+ routesAttached++;
1952
+ const routeArgs = decoratedArgs?.get(name);
1953
+ const wrappedHandler = async (ctx) => {
1954
+ let args = [ctx];
1955
+ if (routeArgs?.length > 0) {
1956
+ args = [];
1957
+ const sortedArgs = [...routeArgs].sort((a, b) => a.index - b.index);
1958
+ for (let k = 0; k < sortedArgs.length; k++) {
1959
+ const arg = sortedArgs[k];
1960
+ switch (arg.type) {
1961
+ case RouteParamType.BODY:
1962
+ args[arg.index] = await ctx.body();
1963
+ break;
1964
+ case RouteParamType.CONTEXT:
1965
+ args[arg.index] = ctx;
1966
+ break;
1967
+ case RouteParamType.REQUEST:
1968
+ args[arg.index] = ctx.req;
1969
+ break;
1970
+ case RouteParamType.HEADER:
1971
+ args[arg.index] = arg.name ? ctx.req.headers.get(arg.name) : ctx.req.headers;
1972
+ break;
1973
+ default:
1974
+ args[arg.index] = void 0;
1975
+ }
1976
+ }
1977
+ }
1978
+ return originalHandler.apply(instance, args);
1979
+ };
1980
+ const decoratedSpecs = instance[$routeSpec] || proto && proto[$routeSpec];
1981
+ const userSpec = decoratedSpecs && decoratedSpecs.get(name);
1982
+ const spec = { tags: [{ name: instance.constructor.name }], ...userSpec };
1983
+ wrappedHandler.spec = spec;
1984
+ wrappedHandler.originalHandler = originalHandler;
1985
+ router.event(eventConfig.eventName, wrappedHandler);
1986
+ }
1987
+ }
1988
+ if (routesAttached === 0) {
1989
+ console.warn(`No routes attached to controller ${instance.constructor.name}`);
1990
+ }
1991
+ instance[$isMounted] = true;
1992
+ }
1993
+ }
1994
+ class HttpError extends Error {
1995
+ status;
1996
+ constructor(message, status) {
1997
+ super(message);
1998
+ this.name = "HttpError";
1999
+ this.status = status;
2000
+ if (Error.captureStackTrace) {
2001
+ Error.captureStackTrace(this, HttpError);
2002
+ }
2003
+ }
2004
+ }
2005
+ function getErrorStatus(err) {
2006
+ if (!err || typeof err !== "object") {
2007
+ return 500;
2008
+ }
2009
+ if (typeof err.status === "number") {
2010
+ return err.status;
2011
+ }
2012
+ if (typeof err.statusCode === "number") {
2013
+ return err.statusCode;
2014
+ }
2015
+ return 500;
2016
+ }
2017
+ class EventError extends HttpError {
2018
+ constructor(message = "Event Error") {
2019
+ super(message, 500);
2020
+ this.name = "EventError";
2021
+ }
2022
+ }
2023
+ class MiddlewareTracker {
2024
+ static wrap(handler, context2) {
2025
+ const { file, line, name, isBuiltin, pluginName } = context2;
2026
+ const handlerName = name || handler.name || "anonymous";
2027
+ const trackedHandler = async (ctx, next) => {
2028
+ if (!ctx.app?.applicationConfig.enableMiddlewareTracking) {
2029
+ return handler(ctx, next);
2030
+ }
2031
+ const startTime = performance.now();
2032
+ let error = void 0;
2033
+ try {
2034
+ ctx.handlerStack.push({
2035
+ name: handlerName,
2036
+ file,
2037
+ line,
2038
+ isBuiltin,
2039
+ startTime,
2040
+ duration: -1
2041
+ });
2042
+ return await handler(ctx, next);
2043
+ } catch (e) {
2044
+ error = e;
2045
+ throw e;
2046
+ } finally {
2047
+ const duration = performance.now() - startTime;
2048
+ const stackItem = ctx.handlerStack[ctx.handlerStack.length - 1];
2049
+ if (stackItem && stackItem.name === handlerName) {
2050
+ stackItem.duration = duration;
2051
+ }
2052
+ Promise.resolve().then(async () => {
2053
+ try {
2054
+ const db = ctx.app?.db;
2055
+ if (!db) return;
2056
+ const timestamp = Date.now();
2057
+ await db.upsert(new RecordId("middleware_tracking", {
2058
+ timestamp,
2059
+ name: handlerName
2060
+ }), {
2061
+ name: handlerName,
2062
+ path: ctx.path,
2063
+ timestamp,
2064
+ duration,
2065
+ file,
2066
+ line,
2067
+ error: error ? String(error) : void 0,
2068
+ metadata: {
2069
+ isBuiltin,
2070
+ pluginName
2071
+ }
2072
+ });
2073
+ } catch (err) {
2074
+ }
2075
+ });
2076
+ }
2077
+ };
2078
+ trackedHandler.metadata = handler.metadata || context2;
2079
+ Object.defineProperty(trackedHandler, "name", { value: handlerName });
2080
+ trackedHandler.originalHandler = handler.originalHandler || handler;
2081
+ return trackedHandler;
2082
+ }
2083
+ }
2084
+ class ShokupanRequestBase {
2085
+ method;
2086
+ url;
2087
+ headers;
2088
+ body;
2089
+ async json() {
2090
+ return JSON.parse(this.body);
2091
+ }
2092
+ async text() {
2093
+ return this.body;
2094
+ }
2095
+ async formData() {
2096
+ if (this.body instanceof FormData) {
2097
+ return this.body;
2098
+ }
2099
+ return new Response(this.body, { headers: this.headers }).formData();
2100
+ }
2101
+ constructor(props) {
2102
+ Object.assign(this, props);
2103
+ if (!(this.headers instanceof Headers)) {
2104
+ this.headers = new Headers(this.headers);
2105
+ }
2106
+ }
2107
+ }
2108
+ const ShokupanRequest = ShokupanRequestBase;
2109
+ class RouterTrie {
2110
+ root;
2111
+ constructor() {
2112
+ this.root = this.createNode();
2113
+ }
2114
+ createNode() {
2115
+ return {
2116
+ children: {}
2117
+ };
2118
+ }
2119
+ insert(method, path, handler) {
2120
+ let node = this.root;
2121
+ const segments = this.splitPath(path);
2122
+ for (let i = 0; i < segments.length; i++) {
2123
+ const segment = segments[i];
2124
+ if (segment === "**") {
2125
+ if (!node.recursiveChild) {
2126
+ node.recursiveChild = this.createNode();
2127
+ }
2128
+ node = node.recursiveChild;
2129
+ } else if (segment === "*") {
1634
2130
  if (!node.wildcardChild) {
1635
2131
  node.wildcardChild = this.createNode();
1636
2132
  }
@@ -1710,16 +2206,6 @@ class RouterTrie {
1710
2206
  return s.split("/");
1711
2207
  }
1712
2208
  }
1713
- const HTTPMethods = ["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS", "ALL"];
1714
- var RouteParamType = /* @__PURE__ */ ((RouteParamType2) => {
1715
- RouteParamType2["BODY"] = "BODY";
1716
- RouteParamType2["PARAM"] = "PARAM";
1717
- RouteParamType2["QUERY"] = "QUERY";
1718
- RouteParamType2["HEADER"] = "HEADER";
1719
- RouteParamType2["REQUEST"] = "REQUEST";
1720
- RouteParamType2["CONTEXT"] = "CONTEXT";
1721
- return RouteParamType2;
1722
- })(RouteParamType || {});
1723
2209
  const RouterRegistry = /* @__PURE__ */ new Map();
1724
2210
  const ShokupanApplicationTree = {};
1725
2211
  class ShokupanRouter {
@@ -1728,6 +2214,9 @@ class ShokupanRouter {
1728
2214
  if (config?.requestTimeout) {
1729
2215
  this.requestTimeout = config.requestTimeout;
1730
2216
  }
2217
+ if (config?.hooks) {
2218
+ this.ensureHooksInitialized();
2219
+ }
1731
2220
  }
1732
2221
  // Internal marker to identify Router vs. Application
1733
2222
  [$isApplication] = false;
@@ -1739,6 +2228,47 @@ class ShokupanRouter {
1739
2228
  [$parent] = null;
1740
2229
  [$childRouters] = [];
1741
2230
  [$childControllers] = [];
2231
+ _hasOnResponseEndHook;
2232
+ _hasOnRequestStartHook;
2233
+ _hasOnRequestEndHook;
2234
+ _hasOnResponseStartHook;
2235
+ _hasOnErrorHook;
2236
+ _hasOnRequestTimeoutHook;
2237
+ _hasOnReadTimeoutHook;
2238
+ _hasOnWriteTimeoutHook;
2239
+ _hasBeforeValidateHook;
2240
+ _hasAfterValidateHook;
2241
+ get hasOnResponseEndHook() {
2242
+ return this._hasOnResponseEndHook;
2243
+ }
2244
+ get hasOnRequestStartHook() {
2245
+ return this._hasOnRequestStartHook;
2246
+ }
2247
+ get hasOnRequestEndHook() {
2248
+ return this._hasOnRequestEndHook;
2249
+ }
2250
+ get hasOnResponseStartHook() {
2251
+ return this._hasOnResponseStartHook;
2252
+ }
2253
+ get hasOnErrorHook() {
2254
+ return this._hasOnErrorHook;
2255
+ }
2256
+ get hasOnRequestTimeoutHook() {
2257
+ return this._hasOnRequestTimeoutHook;
2258
+ }
2259
+ get hasOnReadTimeoutHook() {
2260
+ return this._hasOnReadTimeoutHook;
2261
+ }
2262
+ get hasOnWriteTimeoutHook() {
2263
+ return this._hasOnWriteTimeoutHook;
2264
+ }
2265
+ get hasBeforeValidateHook() {
2266
+ return this._hasBeforeValidateHook;
2267
+ }
2268
+ get hasAfterValidateHook() {
2269
+ return this._hasAfterValidateHook;
2270
+ }
2271
+ requestTimeout;
1742
2272
  get db() {
1743
2273
  return this.root?.db;
1744
2274
  }
@@ -1774,7 +2304,7 @@ class ShokupanRouter {
1774
2304
  const r = this[$routes][i];
1775
2305
  const entry = {
1776
2306
  type: "route",
1777
- path: r.path,
2307
+ path: r.path.startsWith("/") ? r.path : "/" + r.path,
1778
2308
  method: r.method,
1779
2309
  metadata: r.metadata,
1780
2310
  handlerName: r.handler.name,
@@ -1801,7 +2331,7 @@ class ShokupanRouter {
1801
2331
  })) : [];
1802
2332
  const routers = this[$childRouters].map((r) => ({
1803
2333
  type: "router",
1804
- path: r[$mountPath],
2334
+ path: r[$mountPath].startsWith("/") ? r[$mountPath] : "/" + r[$mountPath],
1805
2335
  metadata: r.metadata,
1806
2336
  children: r.getComponentRegistry()
1807
2337
  }));
@@ -1815,12 +2345,25 @@ class ShokupanRouter {
1815
2345
  children: { routes }
1816
2346
  };
1817
2347
  });
2348
+ const events = [];
2349
+ this.eventHandlers.forEach((handlers, name) => {
2350
+ handlers.forEach((h) => {
2351
+ events.push({
2352
+ type: "event",
2353
+ name,
2354
+ handlerName: h.name,
2355
+ metadata: h.source ? { file: h.source.file, line: h.source.line } : void 0,
2356
+ _fn: h
2357
+ });
2358
+ });
2359
+ });
1818
2360
  return {
1819
2361
  metadata: this.metadata,
1820
2362
  middleware,
1821
2363
  routes: localRoutes,
1822
2364
  routers,
1823
- controllers
2365
+ controllers,
2366
+ events
1824
2367
  };
1825
2368
  }
1826
2369
  isRouterInstance(target) {
@@ -1843,12 +2386,38 @@ class ShokupanRouter {
1843
2386
  }
1844
2387
  return this;
1845
2388
  }
2389
+ /**
2390
+ * Registers a lifecycle hook dynamically.
2391
+ */
2392
+ hook(name, handler) {
2393
+ if (!this.hooksInitialized) {
2394
+ this.ensureHooksInitialized();
2395
+ }
2396
+ let handlers = this.hookCache.get(name);
2397
+ if (!handlers) {
2398
+ handlers = [];
2399
+ this.hookCache.set(name, handlers);
2400
+ this._hasOnErrorHook ||= name === "onError";
2401
+ this._hasOnRequestStartHook ||= name === "onRequestStart";
2402
+ this._hasOnRequestEndHook ||= name === "onRequestEnd";
2403
+ this._hasOnResponseStartHook ||= name === "onResponseStart";
2404
+ this._hasOnResponseEndHook ||= name === "onResponseEnd";
2405
+ this._hasOnRequestTimeoutHook ||= name === "onRequestTimeout";
2406
+ this._hasOnReadTimeoutHook ||= name === "onReadTimeout";
2407
+ this._hasOnWriteTimeoutHook ||= name === "onWriteTimeout";
2408
+ this._hasBeforeValidateHook ||= name === "beforeValidate";
2409
+ this._hasAfterValidateHook ||= name === "afterValidate";
2410
+ }
2411
+ handlers.push(handler);
2412
+ return this;
2413
+ }
1846
2414
  /**
1847
2415
  * Finds an event handler(s) by name.
1848
2416
  */
1849
2417
  findEvent(name) {
1850
- if (this.eventHandlers.has(name)) {
1851
- return this.eventHandlers.get(name);
2418
+ const handlers = this.eventHandlers.get(name);
2419
+ if (handlers !== void 0) {
2420
+ return handlers;
1852
2421
  }
1853
2422
  for (const child of this[$childRouters]) {
1854
2423
  const handler = child.findEvent(name);
@@ -1856,6 +2425,12 @@ class ShokupanRouter {
1856
2425
  }
1857
2426
  return null;
1858
2427
  }
2428
+ /**
2429
+ * Registers a controller instance to the router.
2430
+ */
2431
+ registerControllerInstance(controller) {
2432
+ this[$childControllers].push(controller);
2433
+ }
1859
2434
  /**
1860
2435
  * Returns all registered event handlers.
1861
2436
  */
@@ -1882,7 +2457,7 @@ class ShokupanRouter {
1882
2457
  if (this.isRouterInstance(controller)) {
1883
2458
  this.mountRouter(prefix, controller);
1884
2459
  } else {
1885
- this.scanControllerRoutes(prefix, controller);
2460
+ ControllerScanner.scan(this, prefix, controller);
1886
2461
  }
1887
2462
  return this;
1888
2463
  }
@@ -2010,290 +2585,49 @@ class ShokupanRouter {
2010
2585
  let previousNode;
2011
2586
  if (debug) {
2012
2587
  debugId = originalHandler._debugId || originalHandler.name || "handler";
2013
- previousNode = debug.getCurrentNode();
2014
- debug.trackEdge(previousNode, debugId);
2015
- debug.setNode(debugId);
2016
- }
2017
- const start = performance.now();
2018
- try {
2019
- const res = await originalHandler(ctx);
2020
- debug?.trackStep(debugId, "handler", performance.now() - start, "success");
2021
- await this.runHooks("onRequestEnd", ctx);
2022
- return res;
2023
- } catch (err) {
2024
- debug?.trackStep(debugId, "handler", performance.now() - start, "error", err);
2025
- await this.runHooks("onError", ctx, err);
2026
- throw err;
2027
- } finally {
2028
- if (debug && previousNode) debug.setNode(previousNode);
2029
- }
2030
- };
2031
- wrapped.originalHandler = originalHandler.originalHandler ?? originalHandler;
2032
- return wrapped;
2033
- }
2034
- mountRouter(prefix, router) {
2035
- if (router[$isMounted]) {
2036
- throw new Error("Router is already mounted");
2037
- }
2038
- router[$mountPath] = prefix;
2039
- if (!router.metadata) {
2040
- const info = getCallerInfo();
2041
- router.metadata = {
2042
- file: info.file,
2043
- line: info.line,
2044
- name: "MountedRouter"
2045
- };
2046
- }
2047
- this[$childRouters].push(router);
2048
- router[$parent] = this;
2049
- const setRouterContext = (router2) => {
2050
- router2[$appRoot] = this.root;
2051
- router2[$childRouters].forEach((child) => setRouterContext(child));
2052
- };
2053
- setRouterContext(router);
2054
- router[$appRoot] = this.root;
2055
- router[$isMounted] = true;
2056
- }
2057
- scanControllerRoutes(prefix, controller) {
2058
- let instance = controller;
2059
- if (typeof controller === "function") {
2060
- instance = Container.resolve(controller);
2061
- const controllerPath = controller[$controllerPath];
2062
- if (controllerPath) {
2063
- const p1 = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
2064
- const p2 = controllerPath.startsWith("/") ? controllerPath : "/" + controllerPath;
2065
- prefix = p1 + p2;
2066
- if (!prefix) prefix = "/";
2067
- }
2068
- } else {
2069
- const ctor = instance.constructor;
2070
- const controllerPath = ctor[$controllerPath];
2071
- if (controllerPath) {
2072
- const p1 = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
2073
- const p2 = controllerPath.startsWith("/") ? controllerPath : "/" + controllerPath;
2074
- prefix = p1 + p2;
2075
- if (!prefix) prefix = "/";
2076
- }
2077
- }
2078
- instance[$mountPath] = prefix;
2079
- const info = getCallerInfo();
2080
- instance.metadata = {
2081
- file: info.file,
2082
- line: info.line,
2083
- name: instance.constructor.name
2084
- };
2085
- this[$childControllers].push(instance);
2086
- const controllerMiddleware = (typeof controller === "function" ? controller[$middleware] : instance[$middleware]) || [];
2087
- const proto = Object.getPrototypeOf(instance);
2088
- const methods = /* @__PURE__ */ new Set();
2089
- let current = proto;
2090
- while (current && current !== Object.prototype) {
2091
- Object.getOwnPropertyNames(current).forEach((name) => methods.add(name));
2092
- current = Object.getPrototypeOf(current);
2093
- }
2094
- Object.getOwnPropertyNames(instance).forEach((name) => methods.add(name));
2095
- const decoratedRoutes = instance[$routeMethods] || proto && proto[$routeMethods];
2096
- const decoratedArgs = instance[$routeArgs] || proto && proto[$routeArgs];
2097
- const methodMiddlewareMap = instance[$middleware] || proto && proto[$middleware];
2098
- const decoratedEvents = instance[$eventMethods] || proto && proto[$eventMethods];
2099
- let routesAttached = 0;
2100
- for (let i = 0; i < Array.from(methods).length; i++) {
2101
- const name = Array.from(methods)[i];
2102
- if (name === "constructor") continue;
2103
- if (["arguments", "caller", "callee"].includes(name)) continue;
2104
- const originalHandler = instance[name];
2105
- if (typeof originalHandler !== "function") continue;
2106
- let method;
2107
- let subPath = "";
2108
- let methodSource;
2109
- if (decoratedRoutes && decoratedRoutes.has(name)) {
2110
- const config = decoratedRoutes.get(name);
2111
- method = config.method;
2112
- subPath = config.path;
2113
- methodSource = config.source;
2114
- } else {
2115
- for (let j = 0; j < HTTPMethods.length; j++) {
2116
- const m = HTTPMethods[j];
2117
- if (name.toUpperCase().startsWith(m)) {
2118
- method = m;
2119
- const rest = name.slice(m.length);
2120
- if (rest.length === 0) {
2121
- subPath = "/";
2122
- } else {
2123
- subPath = "";
2124
- let buffer = "";
2125
- const flush = () => {
2126
- if (buffer.length > 0) {
2127
- subPath += "/" + buffer.toLowerCase();
2128
- buffer = "";
2129
- }
2130
- };
2131
- for (let i2 = 0; i2 < rest.length; i2++) {
2132
- const char = rest[i2];
2133
- if (char === "$") {
2134
- flush();
2135
- subPath += "/:";
2136
- continue;
2137
- }
2138
- buffer += char;
2139
- }
2140
- if (buffer.length > 0) flush();
2141
- subPath = rest.replace(/\$/g, "/:").replace(/([a-z0-9])([A-Z])/g, "$1/$2").toLowerCase();
2142
- if (!subPath.startsWith("/")) {
2143
- subPath = "/" + subPath;
2144
- }
2145
- }
2146
- break;
2147
- }
2148
- }
2149
- }
2150
- if (method) {
2151
- routesAttached++;
2152
- const cleanPrefix = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
2153
- const cleanSubPath = subPath === "/" ? "" : subPath;
2154
- let joined;
2155
- if (cleanSubPath.length === 0) {
2156
- joined = cleanPrefix;
2157
- } else if (cleanSubPath.startsWith("/")) {
2158
- joined = cleanPrefix + cleanSubPath;
2159
- } else {
2160
- joined = cleanPrefix + "/" + cleanSubPath;
2161
- }
2162
- const fullPath = joined || "/";
2163
- const normalizedPath = fullPath.replace(/\/+/g, "/");
2164
- const methodMw = methodMiddlewareMap instanceof Map ? methodMiddlewareMap.get(name) || [] : [];
2165
- const allMiddleware = [...controllerMiddleware, ...methodMw];
2166
- const routeArgs = decoratedArgs && decoratedArgs.get(name);
2167
- const wrappedHandler = async (ctx) => {
2168
- let args = [ctx];
2169
- if (routeArgs?.length > 0) {
2170
- args = [];
2171
- const sortedArgs = [...routeArgs].sort((a, b) => a.index - b.index);
2172
- for (let k = 0; k < sortedArgs.length; k++) {
2173
- const arg = sortedArgs[k];
2174
- switch (arg.type) {
2175
- case RouteParamType.BODY:
2176
- try {
2177
- if (ctx.req.headers.get("content-type")?.includes("application/json")) {
2178
- args[arg.index] = await ctx.req.json();
2179
- } else {
2180
- const text = await ctx.req.text();
2181
- if (!text) {
2182
- args[arg.index] = {};
2183
- } else {
2184
- args[arg.index] = JSON.parse(text);
2185
- }
2186
- }
2187
- } catch (e) {
2188
- const err = new Error("Invalid JSON body");
2189
- err.status = 400;
2190
- throw err;
2191
- }
2192
- break;
2193
- case RouteParamType.PARAM:
2194
- args[arg.index] = arg.name ? ctx.params[arg.name] : ctx.params;
2195
- break;
2196
- case RouteParamType.QUERY: {
2197
- const url = new URL(ctx.req.url);
2198
- if (arg.name) {
2199
- const vals = url.searchParams.getAll(arg.name);
2200
- args[arg.index] = vals.length > 1 ? vals : vals[0];
2201
- } else {
2202
- const query = {};
2203
- const keys = Object.keys(url.searchParams);
2204
- for (let k2 = 0; k2 < keys.length; k2++) {
2205
- const key = keys[k2];
2206
- const vals = url.searchParams.getAll(key);
2207
- query[key] = vals.length > 1 ? vals : vals[0];
2208
- }
2209
- args[arg.index] = query;
2210
- }
2211
- break;
2212
- }
2213
- case RouteParamType.HEADER:
2214
- args[arg.index] = arg.name ? ctx.req.headers.get(arg.name) : ctx.req.headers;
2215
- break;
2216
- case RouteParamType.REQUEST:
2217
- args[arg.index] = ctx.req;
2218
- break;
2219
- case RouteParamType.CONTEXT:
2220
- args[arg.index] = ctx;
2221
- break;
2222
- }
2223
- }
2224
- }
2225
- const tracedOriginalHandler = ctx.app?.applicationConfig.enableTracing ? traceHandler(originalHandler, normalizedPath) : originalHandler;
2226
- return tracedOriginalHandler.apply(instance, args);
2227
- };
2228
- let finalHandler = wrappedHandler;
2229
- if (allMiddleware.length > 0) {
2230
- const composed = compose(allMiddleware);
2231
- finalHandler = async (ctx) => {
2232
- return composed(ctx, () => wrappedHandler(ctx));
2233
- };
2234
- }
2235
- finalHandler.originalHandler = originalHandler;
2236
- if (finalHandler !== wrappedHandler) {
2237
- wrappedHandler.originalHandler = originalHandler;
2238
- }
2239
- const tagName = instance.constructor.name;
2240
- const decoratedSpecs = instance[$routeSpec] || proto && proto[$routeSpec];
2241
- const userSpec = decoratedSpecs && decoratedSpecs.get(name);
2242
- const spec = { tags: [tagName], ...userSpec };
2243
- this.add({
2244
- method,
2245
- path: normalizedPath,
2246
- handler: finalHandler,
2247
- spec,
2248
- controller: instance,
2249
- metadata: methodSource || instance.metadata,
2250
- middleware: allMiddleware
2251
- // Capture all resolved middleware
2252
- });
2253
- }
2254
- if (decoratedEvents?.has(name)) {
2255
- routesAttached++;
2256
- const config = decoratedEvents.get(name);
2257
- const routeArgs = decoratedArgs?.get(name);
2258
- const wrappedHandler = async (ctx) => {
2259
- let args = [ctx];
2260
- if (routeArgs?.length > 0) {
2261
- args = [];
2262
- const sortedArgs = [...routeArgs].sort((a, b) => a.index - b.index);
2263
- for (let k = 0; k < sortedArgs.length; k++) {
2264
- const arg = sortedArgs[k];
2265
- switch (arg.type) {
2266
- case RouteParamType.BODY:
2267
- args[arg.index] = await ctx.body();
2268
- break;
2269
- case RouteParamType.CONTEXT:
2270
- args[arg.index] = ctx;
2271
- break;
2272
- case RouteParamType.REQUEST:
2273
- args[arg.index] = ctx.req;
2274
- break;
2275
- case RouteParamType.HEADER:
2276
- args[arg.index] = arg.name ? ctx.req.headers.get(arg.name) : ctx.req.headers;
2277
- break;
2278
- default:
2279
- args[arg.index] = void 0;
2280
- }
2281
- }
2282
- }
2283
- return originalHandler.apply(instance, args);
2284
- };
2285
- const decoratedSpecs = instance[$routeSpec] || proto && proto[$routeSpec];
2286
- const userSpec = decoratedSpecs && decoratedSpecs.get(name);
2287
- const spec = { tags: [{ name: instance.constructor.name }], ...userSpec };
2288
- wrappedHandler.spec = spec;
2289
- wrappedHandler.originalHandler = originalHandler;
2290
- this.event(config.eventName, wrappedHandler);
2588
+ previousNode = debug.getCurrentNode();
2589
+ debug.trackEdge(previousNode, debugId);
2590
+ debug.setNode(debugId);
2591
+ }
2592
+ const start = performance.now();
2593
+ try {
2594
+ const res = await originalHandler(ctx);
2595
+ debug?.trackStep(debugId, "handler", performance.now() - start, "success");
2596
+ await this.runHooks("onRequestEnd", ctx);
2597
+ return res;
2598
+ } catch (err) {
2599
+ debug?.trackStep(debugId, "handler", performance.now() - start, "error", err);
2600
+ await this.runHooks("onError", ctx, err);
2601
+ throw err;
2602
+ } finally {
2603
+ if (debug && previousNode) debug.setNode(previousNode);
2291
2604
  }
2605
+ };
2606
+ wrapped.originalHandler = originalHandler.originalHandler ?? originalHandler;
2607
+ return wrapped;
2608
+ }
2609
+ mountRouter(prefix, router) {
2610
+ if (router[$isMounted]) {
2611
+ throw new Error("Router is already mounted");
2292
2612
  }
2293
- if (routesAttached === 0) {
2294
- console.warn(`No routes attached to controller ${instance.constructor.name}`);
2613
+ router[$mountPath] = prefix;
2614
+ if (!router.metadata) {
2615
+ const info = getCallerInfo();
2616
+ router.metadata = {
2617
+ file: info.file,
2618
+ line: info.line,
2619
+ name: "MountedRouter"
2620
+ };
2295
2621
  }
2296
- instance[$isMounted] = true;
2622
+ this[$childRouters].push(router);
2623
+ router[$parent] = this;
2624
+ const setRouterContext = (router2) => {
2625
+ router2[$appRoot] = this.root;
2626
+ router2[$childRouters].forEach((child) => setRouterContext(child));
2627
+ };
2628
+ setRouterContext(router);
2629
+ router[$appRoot] = this.root;
2630
+ router[$isMounted] = true;
2297
2631
  }
2298
2632
  /**
2299
2633
  * Find a route matching the given method and path.
@@ -2327,6 +2661,9 @@ class ShokupanRouter {
2327
2661
  return null;
2328
2662
  }
2329
2663
  parsePath(path) {
2664
+ if (typeof path !== "string") {
2665
+ throw new Error(`Route path must be a string or regexp, received ${typeof path == "function" ? path["name"] || path["constructor"]?.["name"] || "function" : typeof path}. Dynamic paths are **highly** discouraged.`);
2666
+ }
2330
2667
  const keys = [];
2331
2668
  if (path.length > 2048) {
2332
2669
  throw new Error("Path too long");
@@ -2340,8 +2677,6 @@ class ShokupanRouter {
2340
2677
  keys
2341
2678
  };
2342
2679
  }
2343
- // --- Functional Routing ---
2344
- requestTimeout;
2345
2680
  /**
2346
2681
  * Adds a route to the router.
2347
2682
  *
@@ -2375,9 +2710,6 @@ class ShokupanRouter {
2375
2710
  }
2376
2711
  }
2377
2712
  let wrappedHandler = async (ctx) => {
2378
- if (ctx.upgrade()) {
2379
- return void 0;
2380
- }
2381
2713
  return handler(ctx);
2382
2714
  };
2383
2715
  wrappedHandler.originalHandler = handler.originalHandler || handler;
@@ -2432,67 +2764,13 @@ class ShokupanRouter {
2432
2764
  };
2433
2765
  }
2434
2766
  const { file, line } = metadata || getCallerInfo();
2435
- const trackingHandler = wrappedHandler;
2436
- wrappedHandler = async (ctx) => {
2437
- if (!ctx.app?.applicationConfig.enableMiddlewareTracking) {
2438
- return trackingHandler(ctx);
2439
- }
2440
- const startTime = performance.now();
2441
- let error = void 0;
2442
- try {
2443
- if (ctx.app?.applicationConfig.enableMiddlewareTracking) {
2444
- ctx.handlerStack.push({
2445
- name: handler.name || "anonymous",
2446
- file,
2447
- line
2448
- });
2449
- }
2450
- return await trackingHandler(ctx);
2451
- } catch (e) {
2452
- error = e;
2453
- throw e;
2454
- } finally {
2455
- if (ctx.app?.applicationConfig.enableMiddlewareTracking) {
2456
- const duration = performance.now() - startTime;
2457
- const config = ctx.app.applicationConfig;
2458
- Promise.resolve().then(async () => {
2459
- try {
2460
- const db = ctx.app?.db;
2461
- if (!db) return;
2462
- const timestamp = Date.now();
2463
- await db.upsert(new RecordId("middleware_tracking", {
2464
- timestamp,
2465
- name: handler.name || "anonymous"
2466
- }), {
2467
- name: handler.name || "anonymous",
2468
- path: ctx.path,
2469
- timestamp,
2470
- duration,
2471
- file,
2472
- line,
2473
- error: error ? String(error) : void 0,
2474
- metadata: {
2475
- isBuiltin: handler.isBuiltin,
2476
- pluginName: handler.pluginName
2477
- }
2478
- });
2479
- const ttl = config.middlewareTrackingTTL ?? 864e5;
2480
- const maxCapacity = config.middlewareTrackingMaxCapacity ?? 1e4;
2481
- const cutoff = Date.now() - ttl;
2482
- await db.query(`DELETE middleware_tracking WHERE timestamp < ${cutoff}`);
2483
- const results = await db.query("SELECT count() FROM middleware_tracking GROUP ALL");
2484
- if (results?.[0]?.count > maxCapacity) {
2485
- const toDelete = results[0].count - maxCapacity;
2486
- await db.query(`DELETE middleware_tracking ORDER BY timestamp ASC LIMIT ${toDelete}`);
2487
- }
2488
- } catch (datastoreError) {
2489
- console.error("Failed to store middleware tracking:", datastoreError);
2490
- }
2491
- });
2492
- }
2493
- }
2494
- };
2495
- wrappedHandler.originalHandler = trackingHandler.originalHandler || trackingHandler;
2767
+ wrappedHandler = MiddlewareTracker.wrap(wrappedHandler, {
2768
+ file,
2769
+ line,
2770
+ name: handler.name || "anonymous",
2771
+ isBuiltin: handler.isBuiltin,
2772
+ pluginName: handler.pluginName
2773
+ });
2496
2774
  let bakedHandler = wrappedHandler;
2497
2775
  if (this.config?.hooks) {
2498
2776
  bakedHandler = this.wrapWithHooks(wrappedHandler);
@@ -2567,17 +2845,11 @@ class ShokupanRouter {
2567
2845
  }
2568
2846
  } catch (e) {
2569
2847
  }
2570
- const trackedGuard = async (ctx, next) => {
2571
- if (ctx.app?.applicationConfig.enableMiddlewareTracking) {
2572
- ctx.handlerStack.push({
2573
- name: guardHandler.name || "guard",
2574
- file,
2575
- line
2576
- });
2577
- }
2578
- return guardHandler(ctx, next);
2579
- };
2580
- trackedGuard.originalHandler = guardHandler.originalHandler ?? guardHandler;
2848
+ const trackedGuard = MiddlewareTracker.wrap(guardHandler, {
2849
+ file,
2850
+ line,
2851
+ name: guardHandler.name || "guard"
2852
+ });
2581
2853
  this.currentGuards.push({ handler: trackedGuard, spec });
2582
2854
  return this;
2583
2855
  }
@@ -2631,88 +2903,351 @@ class ShokupanRouter {
2631
2903
  handlers = args;
2632
2904
  }
2633
2905
  }
2634
- if (handlers.length === 0) {
2635
- return;
2636
- }
2637
- let finalHandler = handlers[handlers.length - 1];
2638
- if (handlers.length > 1) {
2639
- const fn = compose(handlers);
2640
- finalHandler = (ctx) => fn(ctx);
2641
- }
2642
- this.add({
2643
- method,
2644
- path,
2645
- spec,
2646
- handler: finalHandler,
2647
- middleware: handlers.slice(0, handlers.length - 1)
2648
- });
2906
+ if (handlers.length === 0) {
2907
+ return;
2908
+ }
2909
+ let finalHandler = handlers[handlers.length - 1];
2910
+ if (handlers.length > 1) {
2911
+ const fn = compose(handlers);
2912
+ finalHandler = (ctx) => fn(ctx);
2913
+ }
2914
+ this.add({
2915
+ method,
2916
+ path,
2917
+ spec,
2918
+ handler: finalHandler,
2919
+ middleware: handlers.slice(0, handlers.length - 1)
2920
+ });
2921
+ }
2922
+ /**
2923
+ * Generates an OpenAPI 3.1 Document by recursing through the router and its descendants.
2924
+ * Now includes runtime analysis of handler functions to infer request/response types.
2925
+ */
2926
+ generateApiSpec(options = {}) {
2927
+ return generateOpenApi(this, options);
2928
+ }
2929
+ hasHooks(name) {
2930
+ if (!this.hooksInitialized) {
2931
+ this.ensureHooksInitialized();
2932
+ }
2933
+ const hooks = this.hookCache.get(name);
2934
+ return hooks !== void 0 && hooks.length > 0;
2935
+ }
2936
+ ensureHooksInitialized() {
2937
+ const hooks = this.config?.hooks;
2938
+ if (hooks) {
2939
+ const hookList = Array.isArray(hooks) ? hooks : [hooks];
2940
+ const hookTypes = [
2941
+ "onRequestStart",
2942
+ "onRequestEnd",
2943
+ "onResponseStart",
2944
+ "onResponseEnd",
2945
+ "onError",
2946
+ "beforeValidate",
2947
+ "afterValidate",
2948
+ "onRequestTimeout",
2949
+ "onReadTimeout",
2950
+ "onWriteTimeout"
2951
+ ];
2952
+ for (let i = 0; i < hookTypes.length; i++) {
2953
+ const type = hookTypes[i];
2954
+ const fns = [];
2955
+ for (let j = 0; j < hookList.length; j++) {
2956
+ const h = hookList[j];
2957
+ if (h[type]) fns.push(h[type]);
2958
+ }
2959
+ if (fns.length > 0) {
2960
+ this._hasOnErrorHook ||= type === "onError";
2961
+ this._hasOnRequestStartHook ||= type === "onRequestStart";
2962
+ this._hasOnRequestEndHook ||= type === "onRequestEnd";
2963
+ this._hasOnResponseStartHook ||= type === "onResponseStart";
2964
+ this._hasOnResponseEndHook ||= type === "onResponseEnd";
2965
+ this._hasOnRequestTimeoutHook ||= type === "onRequestTimeout";
2966
+ this._hasOnReadTimeoutHook ||= type === "onReadTimeout";
2967
+ this._hasOnWriteTimeoutHook ||= type === "onWriteTimeout";
2968
+ this._hasBeforeValidateHook ||= type === "beforeValidate";
2969
+ this._hasAfterValidateHook ||= type === "afterValidate";
2970
+ this.hookCache.set(type, fns);
2971
+ }
2972
+ }
2973
+ }
2974
+ this.hooksInitialized = true;
2975
+ }
2976
+ runHooks(name, ...args) {
2977
+ if (!this.hooksInitialized) {
2978
+ this.ensureHooksInitialized();
2979
+ }
2980
+ const fns = this.hookCache.get(name);
2981
+ if (!fns) return;
2982
+ const ctx = args?.[0] instanceof ShokupanContext ? args[0] : void 0;
2983
+ const debug = ctx?.[$debug];
2984
+ if (debug) {
2985
+ return Promise.all(fns.map(async (fn, index) => {
2986
+ const hookId = `hook_${name}_${fn.name || index}`;
2987
+ const previousNode = debug.getCurrentNode();
2988
+ debug.trackEdge(previousNode, hookId);
2989
+ debug.setNode(hookId);
2990
+ const start = performance.now();
2991
+ try {
2992
+ await fn(...args);
2993
+ const duration = performance.now() - start;
2994
+ debug.trackStep(hookId, "hook", duration, "success");
2995
+ } catch (error) {
2996
+ const duration = performance.now() - start;
2997
+ debug.trackStep(hookId, "hook", duration, "error", error);
2998
+ throw error;
2999
+ } finally {
3000
+ if (previousNode) debug.setNode(previousNode);
3001
+ }
3002
+ }));
3003
+ } else {
3004
+ return Promise.all(fns.map((fn) => fn(...args)));
3005
+ }
3006
+ }
3007
+ }
3008
+ function createHttpServer() {
3009
+ return async (options) => {
3010
+ const server = http$1.createServer(async (req, res) => {
3011
+ const url = new URL(req.url, `http://${req.headers.host}`);
3012
+ const request = new Request(url.toString(), {
3013
+ method: req.method,
3014
+ headers: req.headers,
3015
+ body: ["GET", "HEAD"].includes(req.method) ? void 0 : new ReadableStream({
3016
+ start(controller) {
3017
+ req.on("data", (chunk) => controller.enqueue(chunk));
3018
+ req.on("end", () => controller.close());
3019
+ req.on("error", (err) => controller.error(err));
3020
+ }
3021
+ }),
3022
+ // Required for Node.js undici when sending a body
3023
+ duplex: "half"
3024
+ });
3025
+ const response = await options.fetch(request, fauxServer);
3026
+ res.statusCode = response.status;
3027
+ response.headers.forEach((v, k) => res.setHeader(k, v));
3028
+ if (response.body) {
3029
+ const buffer = await response.arrayBuffer();
3030
+ res.end(Buffer.from(buffer));
3031
+ } else {
3032
+ res.end();
3033
+ }
3034
+ });
3035
+ const fauxServer = {
3036
+ stop: () => {
3037
+ server.close();
3038
+ return Promise.resolve();
3039
+ },
3040
+ upgrade(req, options2) {
3041
+ return false;
3042
+ },
3043
+ reload(options2) {
3044
+ return fauxServer;
3045
+ },
3046
+ get port() {
3047
+ const addr = server.address();
3048
+ if (typeof addr === "object" && addr !== null) {
3049
+ return addr.port;
3050
+ }
3051
+ return options.port;
3052
+ },
3053
+ hostname: options.hostname,
3054
+ development: options.development,
3055
+ pendingRequests: 0,
3056
+ requestIP: (req) => null,
3057
+ publish: () => 0,
3058
+ subscriberCount: () => 0,
3059
+ url: new URL(`http://${options.hostname}:${options.port}`),
3060
+ // Expose the raw Node.js server for generic socket/websocket support (e.g. Socket.IO)
3061
+ nodeServer: server
3062
+ };
3063
+ return new Promise((resolve2) => {
3064
+ server.listen(options.port, options.hostname, () => {
3065
+ resolve2(fauxServer);
3066
+ });
3067
+ });
3068
+ };
3069
+ }
3070
+ class BunAdapter {
3071
+ server;
3072
+ async listen(port, app) {
3073
+ if (typeof Bun === "undefined") {
3074
+ throw new Error("BunAdapter requires the Bun runtime.");
3075
+ }
3076
+ const serveOptions = {
3077
+ port,
3078
+ hostname: app.applicationConfig.hostname,
3079
+ development: app.applicationConfig.development,
3080
+ fetch: app.fetch.bind(app),
3081
+ reusePort: app.applicationConfig.reusePort,
3082
+ idleTimeout: app.applicationConfig.readTimeout ? app.applicationConfig.readTimeout / 1e3 : void 0,
3083
+ websocket: {
3084
+ // @ts-ignore
3085
+ open(ws) {
3086
+ ws.data?.handler?.open?.(ws);
3087
+ },
3088
+ // @ts-ignore
3089
+ async message(ws, message) {
3090
+ if (ws.data?.handler?.message) {
3091
+ return ws.data.handler.message(ws, message);
3092
+ }
3093
+ let msgString = "";
3094
+ if (typeof message === "string") {
3095
+ msgString = message;
3096
+ } else if (message instanceof Uint8Array || message instanceof ArrayBuffer) {
3097
+ msgString = new TextDecoder().decode(message);
3098
+ } else if (typeof Buffer !== "undefined" && message instanceof Buffer) {
3099
+ msgString = message.toString();
3100
+ } else {
3101
+ return;
3102
+ }
3103
+ if (typeof msgString !== "string") return;
3104
+ let payload;
3105
+ let isJSONPayload = false;
3106
+ if (msgString.startsWith("{")) {
3107
+ try {
3108
+ payload = JSON.parse(msgString);
3109
+ isJSONPayload = true;
3110
+ } catch {
3111
+ }
3112
+ }
3113
+ if (payload) {
3114
+ const self = app;
3115
+ if (isJSONPayload && self.applicationConfig["enableHttpBridge"] && payload.type === "HTTP") {
3116
+ const { id, method, path, headers, body } = payload;
3117
+ const url = new URL(path, `http://${self.applicationConfig.hostname || "localhost"}:${port}`);
3118
+ const req = new Request(url.toString(), {
3119
+ method,
3120
+ headers,
3121
+ body: typeof body === "object" ? JSON.stringify(body) : body
3122
+ });
3123
+ const res = await self.fetch(req);
3124
+ const resBody = await res.json().catch((err) => res.text());
3125
+ const resHeaders = {};
3126
+ res.headers.forEach((v, k) => resHeaders[k] = v);
3127
+ ws.send(JSON.stringify({
3128
+ type: "RESPONSE",
3129
+ id,
3130
+ status: res.status,
3131
+ headers: resHeaders,
3132
+ body: resBody
3133
+ }));
3134
+ return;
3135
+ }
3136
+ const eventName = payload.event || (payload.type === "EVENT" ? payload.name : void 0);
3137
+ if (eventName) {
3138
+ const handlers = self.findEvent(eventName);
3139
+ const handler = handlers?.length == 1 ? handlers[0] : compose(handlers || []);
3140
+ if (handler) {
3141
+ const data = payload.data || payload.body || payload.payload || payload;
3142
+ const req = new ShokupanRequest({
3143
+ url: `http://${self.applicationConfig.hostname || "localhost"}/event/${eventName}`,
3144
+ method: "POST",
3145
+ headers: new Headers({ "content-type": "application/json" }),
3146
+ body: JSON.stringify(data)
3147
+ });
3148
+ const ctx = new ShokupanContext(
3149
+ // @ts-ignore
3150
+ req,
3151
+ // @ts-ignore
3152
+ self.server,
3153
+ {},
3154
+ self,
3155
+ null,
3156
+ self.applicationConfig.enableMiddlewareTracking,
3157
+ payload.id
3158
+ );
3159
+ ctx[$ws] = ws;
3160
+ ws.data ??= {};
3161
+ ws.data["ctx"] = ctx;
3162
+ try {
3163
+ await handler(ctx);
3164
+ } catch (err) {
3165
+ if (self.applicationConfig["websocketErrorHandler"]) {
3166
+ await self.applicationConfig["websocketErrorHandler"](err, ctx);
3167
+ } else {
3168
+ console.error(`Error in event ${eventName}:`, err);
3169
+ }
3170
+ }
3171
+ }
3172
+ }
3173
+ }
3174
+ },
3175
+ // @ts-ignore
3176
+ drain(ws) {
3177
+ ws.data?.handler?.drain?.(ws);
3178
+ },
3179
+ // @ts-ignore
3180
+ close(ws, code, reason) {
3181
+ ws.data?.handler?.close?.(ws, code, reason);
3182
+ const ctx = ws.data?.["ctx"];
3183
+ if (ctx && typeof ctx.getDisconnectCallbacks === "function") {
3184
+ const callbacks = ctx.getDisconnectCallbacks();
3185
+ if (Array.isArray(callbacks) && callbacks.length > 0) {
3186
+ Promise.all(callbacks.map((cb) => cb())).catch((err) => {
3187
+ console.error("Error executing socket disconnect hook:", err);
3188
+ });
3189
+ }
3190
+ }
3191
+ }
3192
+ }
3193
+ };
3194
+ this.server = Bun.serve(serveOptions);
3195
+ return this.server;
3196
+ }
3197
+ async stop() {
3198
+ if (this.server) {
3199
+ this.server.stop();
3200
+ }
2649
3201
  }
2650
- /**
2651
- * Generates an OpenAPI 3.1 Document by recursing through the router and its descendants.
2652
- * Now includes runtime analysis of handler functions to infer request/response types.
2653
- */
2654
- generateApiSpec(options = {}) {
2655
- return generateOpenApi(this, options);
3202
+ }
3203
+ class NodeAdapter {
3204
+ server;
3205
+ async listen(port, app) {
3206
+ let factory = app.applicationConfig.serverFactory;
3207
+ if (!factory) {
3208
+ factory = createHttpServer();
3209
+ }
3210
+ const serveOptions = {
3211
+ port,
3212
+ hostname: app.applicationConfig.hostname,
3213
+ development: app.applicationConfig.development,
3214
+ fetch: app.fetch.bind(app),
3215
+ reusePort: app.applicationConfig.reusePort
3216
+ // Node adapter might not support all options exactly the same
3217
+ };
3218
+ this.server = await factory(serveOptions);
3219
+ return this.server;
2656
3220
  }
2657
- ensureHooksInitialized() {
2658
- const hooks = this.config?.hooks;
2659
- if (hooks) {
2660
- const hookList = Array.isArray(hooks) ? hooks : [hooks];
2661
- const hookTypes = [
2662
- "onRequestStart",
2663
- "onRequestEnd",
2664
- "onResponseStart",
2665
- "onResponseEnd",
2666
- "onError",
2667
- "beforeValidate",
2668
- "afterValidate",
2669
- "onRequestTimeout",
2670
- "onReadTimeout",
2671
- "onWriteTimeout"
2672
- ];
2673
- for (let i = 0; i < hookTypes.length; i++) {
2674
- const type = hookTypes[i];
2675
- const fns = [];
2676
- for (let j = 0; j < hookList.length; j++) {
2677
- const h = hookList[j];
2678
- if (h[type]) fns.push(h[type]);
2679
- }
2680
- if (fns.length > 0) {
2681
- this.hookCache.set(type, fns);
2682
- }
2683
- }
3221
+ async stop() {
3222
+ if (this.server?.stop) {
3223
+ await this.server.stop();
2684
3224
  }
2685
- this.hooksInitialized = true;
2686
3225
  }
2687
- runHooks(name, ...args) {
2688
- if (!this.hooksInitialized) {
2689
- this.ensureHooksInitialized();
3226
+ }
3227
+ let fs;
3228
+ class DefaultFileSystemAdapter {
3229
+ async readFile(path) {
3230
+ if (typeof Bun !== "undefined") {
3231
+ return Bun.file(path);
3232
+ } else {
3233
+ fs ??= await import("node:fs/promises");
3234
+ return fs.readFile(path);
2690
3235
  }
2691
- const fns = this.hookCache.get(name);
2692
- if (!fns) return;
2693
- const ctx = args?.[0] instanceof ShokupanContext ? args[0] : void 0;
2694
- const debug = ctx?.[$debug];
2695
- if (debug) {
2696
- return Promise.all(fns.map(async (fn, index) => {
2697
- const hookId = `hook_${name}_${fn.name || index}`;
2698
- const previousNode = debug.getCurrentNode();
2699
- debug.trackEdge(previousNode, hookId);
2700
- debug.setNode(hookId);
2701
- const start = performance.now();
2702
- try {
2703
- await fn(...args);
2704
- const duration = performance.now() - start;
2705
- debug.trackStep(hookId, "hook", duration, "success");
2706
- } catch (error) {
2707
- const duration = performance.now() - start;
2708
- debug.trackStep(hookId, "hook", duration, "error", error);
2709
- throw error;
2710
- } finally {
2711
- if (previousNode) debug.setNode(previousNode);
2712
- }
2713
- }));
3236
+ }
3237
+ async stat(path) {
3238
+ if (typeof Bun !== "undefined") {
3239
+ const file = Bun.file(path);
3240
+ return {
3241
+ size: file.size,
3242
+ mtime: new Date(file.lastModified)
3243
+ };
2714
3244
  } else {
2715
- return Promise.all(fns.map((fn) => fn(...args)));
3245
+ fs ??= await import("node:fs/promises");
3246
+ const stats = await fs.stat(path);
3247
+ return {
3248
+ size: stats.size,
3249
+ mtime: stats.mtime
3250
+ };
2716
3251
  }
2717
3252
  }
2718
3253
  }
@@ -2724,13 +3259,24 @@ const asyncContext = new AsyncLocalStorage();
2724
3259
  class SystemCpuMonitor {
2725
3260
  constructor(intervalMs = 1e3) {
2726
3261
  this.intervalMs = intervalMs;
3262
+ this.init();
2727
3263
  }
2728
3264
  interval = null;
2729
3265
  lastCpus = [];
2730
3266
  currentUsage = 0;
3267
+ osStub = null;
3268
+ async init() {
3269
+ try {
3270
+ if (typeof process !== "undefined" && process.versions && process.versions.node) {
3271
+ this.osStub = await import("node:os");
3272
+ }
3273
+ } catch (e) {
3274
+ }
3275
+ }
2731
3276
  start() {
2732
3277
  if (this.interval) return;
2733
- this.lastCpus = os.cpus();
3278
+ if (!this.osStub) return;
3279
+ this.lastCpus = this.osStub.cpus();
2734
3280
  this.interval = setInterval(() => this.update(), this.intervalMs);
2735
3281
  }
2736
3282
  stop() {
@@ -2743,12 +3289,14 @@ class SystemCpuMonitor {
2743
3289
  return this.currentUsage;
2744
3290
  }
2745
3291
  update() {
2746
- const cpus = os.cpus();
3292
+ if (!this.osStub) return;
3293
+ const cpus = this.osStub.cpus();
2747
3294
  let idle = 0;
2748
3295
  let total = 0;
2749
3296
  for (let i = 0; i < cpus.length; i++) {
2750
3297
  const cpu = cpus[i];
2751
3298
  const prev = this.lastCpus[i];
3299
+ if (!prev) continue;
2752
3300
  let type;
2753
3301
  for (type in cpu.times) {
2754
3302
  const ticks = cpu.times[type];
@@ -2866,11 +3414,12 @@ const defaults = {
2866
3414
  enableHttpBridge: false,
2867
3415
  reusePort: false
2868
3416
  };
2869
- trace.getTracer("shokupan.application");
2870
3417
  class Shokupan extends ShokupanRouter {
2871
3418
  applicationConfig = {};
2872
3419
  openApiSpec;
2873
3420
  asyncApiSpec;
3421
+ openApiSpecPromise;
3422
+ asyncApiSpecPromise;
2874
3423
  composedMiddleware;
2875
3424
  cpuMonitor;
2876
3425
  server;
@@ -2884,6 +3433,7 @@ class Shokupan extends ShokupanRouter {
2884
3433
  }
2885
3434
  constructor(applicationConfig = {}) {
2886
3435
  const config = Object.assign({}, defaults, applicationConfig);
3436
+ config.fileSystem ??= new DefaultFileSystemAdapter();
2887
3437
  const { hooks, ...routerConfig } = config;
2888
3438
  super({ ...routerConfig, hooks });
2889
3439
  this[$isApplication] = true;
@@ -2895,61 +3445,51 @@ class Shokupan extends ShokupanRouter {
2895
3445
  line,
2896
3446
  name: "ShokupanApplication"
2897
3447
  };
2898
- this.dbPromise = this.initDatastore();
3448
+ if (this.applicationConfig.adapter !== "wintercg") {
3449
+ this.dbPromise = this.initDatastore().catch((err) => {
3450
+ this.logger?.debug("Failed to initialize default datastore", { error: err });
3451
+ });
3452
+ }
2899
3453
  }
2900
3454
  async initDatastore() {
2901
- const db = new Surreal({ engines: this.applicationConfig.surreal?.engines ?? (await import("@surrealdb/node")).createNodeEngines() });
3455
+ let engines = this.applicationConfig.surreal?.engines;
3456
+ if (!engines && !this.applicationConfig.surreal?.url?.match(/^(?:wss?|https?):\/\//)) {
3457
+ engines = (await import("@surrealdb/node")).createNodeEngines();
3458
+ }
3459
+ const db = new Surreal({ engines });
2902
3460
  this.datastore = new SurrealDatastore(db);
2903
3461
  await db.connect(
2904
3462
  this.applicationConfig.surreal?.url ?? (process.env.NODE_ENV === "test" ? "mem://" : "surrealkv://database"),
2905
3463
  this.applicationConfig.surreal?.connectOptions
2906
- );
3464
+ ).catch((err) => {
3465
+ this.logger?.error("Failed to connect to SurrealDB", { error: err });
3466
+ });
2907
3467
  await db.use({
2908
3468
  namespace: this.applicationConfig.surreal?.namespace ?? "vendor",
2909
3469
  database: this.applicationConfig.surreal?.database ?? "shokupan"
2910
3470
  });
3471
+ await db.query("DEFINE TABLE OVERWRITE request;");
3472
+ await db.query("DEFINE TABLE OVERWRITE failed_request;");
3473
+ await db.query("DEFINE TABLE OVERWRITE metric;");
2911
3474
  }
3475
+ /**
3476
+ * Adds middleware to the application.
3477
+ */
2912
3478
  /**
2913
3479
  * Adds middleware to the application.
2914
3480
  */
2915
3481
  use(middleware) {
2916
3482
  const { file, line } = getCallerInfo();
2917
- if (!middleware.metadata) {
2918
- middleware.metadata = {
2919
- file,
2920
- line,
2921
- name: middleware.name || "middleware",
2922
- isBuiltin: middleware.isBuiltin,
2923
- pluginName: middleware.pluginName
2924
- };
2925
- }
3483
+ const wrapped = MiddlewareTracker.wrap(middleware, {
3484
+ file,
3485
+ line,
3486
+ name: middleware.name || "middleware",
3487
+ isBuiltin: middleware.isBuiltin,
3488
+ pluginName: middleware.pluginName
3489
+ });
2926
3490
  if (this.applicationConfig.enableMiddlewareTracking) {
2927
- const trackedMiddleware = async (ctx, next) => {
2928
- const c = ctx;
2929
- if (c.handlerStack && c.app?.applicationConfig.enableMiddlewareTracking) {
2930
- const metadata = middleware.metadata || {};
2931
- const start = performance.now();
2932
- const item = {
2933
- name: metadata.pluginName ? `${metadata.pluginName} (${metadata.name})` : metadata.name || middleware.name || "middleware",
2934
- file: metadata.file || file,
2935
- line: metadata.line || line,
2936
- isBuiltin: metadata.isBuiltin,
2937
- startTime: start,
2938
- duration: -1
2939
- };
2940
- c.handlerStack.push(item);
2941
- try {
2942
- return await middleware(ctx, next);
2943
- } finally {
2944
- item.duration = performance.now() - start;
2945
- }
2946
- }
2947
- return middleware(ctx, next);
2948
- };
2949
- trackedMiddleware.metadata = middleware.metadata;
2950
- Object.defineProperty(trackedMiddleware, "name", { value: middleware.name || "middleware" });
2951
- trackedMiddleware.order = this.middleware.length;
2952
- this.middleware.push(trackedMiddleware);
3491
+ wrapped.order = this.middleware.length;
3492
+ this.middleware.push(wrapped);
2953
3493
  } else {
2954
3494
  this.middleware.push(middleware);
2955
3495
  }
@@ -2992,18 +3532,19 @@ class Shokupan extends ShokupanRouter {
2992
3532
  }
2993
3533
  await Promise.all(this.startupHooks.map((hook) => hook()));
2994
3534
  if (this.applicationConfig.enableOpenApiGen) {
2995
- this.openApiSpec = await generateOpenApi(this);
2996
- this.get("/.well-known/openapi.yaml", (ctx) => {
3535
+ this.get("/.well-known/openapi.yaml", async (ctx) => {
2997
3536
  try {
3537
+ await this.openApiSpecPromise;
2998
3538
  const yaml = dump(this.openApiSpec);
2999
3539
  return ctx.send(yaml, { status: 200, headers: { "content-type": "application/yaml" } });
3000
3540
  } catch (e) {
3001
- this.logger.error("Failed to generate OpenAPI YAML", { error: e });
3541
+ this.logger?.error("Failed to generate OpenAPI YAML", { error: e });
3002
3542
  return ctx.text("Internal Server Error", 500);
3003
3543
  }
3004
3544
  });
3005
3545
  if (this.applicationConfig.aiPlugin?.enabled !== false) {
3006
3546
  this.get("/.well-known/ai-plugin.json", async (ctx) => {
3547
+ await this.openApiSpecPromise;
3007
3548
  const config = this.applicationConfig.aiPlugin || {};
3008
3549
  let pkg = {};
3009
3550
  try {
@@ -3031,7 +3572,8 @@ class Shokupan extends ShokupanRouter {
3031
3572
  });
3032
3573
  }
3033
3574
  if (this.applicationConfig.apiCatalog?.enabled !== false) {
3034
- this.get("/.well-known/api-catalog", (ctx) => {
3575
+ this.get("/.well-known/api-catalog", async (ctx) => {
3576
+ await this.openApiSpecPromise;
3035
3577
  const config = this.applicationConfig.apiCatalog || {};
3036
3578
  const catalog = {
3037
3579
  versions: config.versions || [
@@ -3045,110 +3587,57 @@ class Shokupan extends ShokupanRouter {
3045
3587
  return ctx.json(catalog);
3046
3588
  });
3047
3589
  }
3048
- await Promise.all(this.specAvailableHooks.map((hook) => hook(this.openApiSpec)));
3590
+ this.openApiSpecPromise = generateOpenApi(this).then((spec) => {
3591
+ this.openApiSpec = spec;
3592
+ return spec;
3593
+ });
3594
+ const shouldBlock = this.applicationConfig.blockOnOpenApiGen !== false;
3595
+ if (shouldBlock) {
3596
+ await this.openApiSpecPromise;
3597
+ }
3598
+ if (shouldBlock) {
3599
+ await Promise.all(this.specAvailableHooks.map((hook) => hook(this.openApiSpec)));
3600
+ } else {
3601
+ this.openApiSpecPromise.then((spec) => {
3602
+ return Promise.all(this.specAvailableHooks.map((hook) => hook(spec)));
3603
+ }).catch((err) => {
3604
+ this.logger?.error("Error running spec available hooks", { error: err });
3605
+ });
3606
+ }
3049
3607
  }
3050
3608
  if (this.applicationConfig.enableAsyncApiGen) {
3051
3609
  const { generateAsyncApi: generateAsyncApi2 } = await Promise.resolve().then(() => generator);
3052
- this.asyncApiSpec = await generateAsyncApi2(this);
3610
+ this.asyncApiSpecPromise = generateAsyncApi2(this).then((spec) => {
3611
+ this.asyncApiSpec = spec;
3612
+ return spec;
3613
+ });
3614
+ const shouldBlock = this.applicationConfig.blockOnAsyncApiGen !== false;
3615
+ if (shouldBlock) {
3616
+ await this.asyncApiSpecPromise;
3617
+ }
3053
3618
  }
3054
3619
  if (port === 0 && process.platform === "linux") ;
3055
3620
  if (this.applicationConfig.autoBackpressureFeedback === true) {
3056
3621
  this.cpuMonitor = new SystemCpuMonitor();
3057
3622
  this.cpuMonitor.start();
3058
3623
  }
3059
- const self = this;
3060
- const serveOptions = {
3061
- port: finalPort,
3062
- hostname: this.applicationConfig.hostname,
3063
- development: this.applicationConfig.development,
3064
- fetch: this.fetch.bind(this),
3065
- reusePort: this.applicationConfig.reusePort,
3066
- idleTimeout: this.applicationConfig.readTimeout ? this.applicationConfig.readTimeout / 1e3 : void 0,
3067
- websocket: {
3068
- open(ws) {
3069
- ws.data?.handler?.open?.(ws);
3070
- },
3071
- async message(ws, message) {
3072
- if (ws.data?.handler?.message) {
3073
- return ws.data.handler.message(ws, message);
3074
- }
3075
- if (typeof message !== "string") return;
3076
- try {
3077
- const payload = JSON.parse(message);
3078
- if (self.applicationConfig["enableHttpBridge"] && payload.type === "HTTP") {
3079
- const { id, method, path, headers, body } = payload;
3080
- const url = new URL(path, `http://${self.applicationConfig.hostname || "localhost"}:${finalPort}`);
3081
- const req = new Request(url.toString(), {
3082
- method,
3083
- headers,
3084
- body: typeof body === "object" ? JSON.stringify(body) : body
3085
- });
3086
- const res = await self.fetch(req);
3087
- const resBody = await res.json().catch((err) => res.text());
3088
- const resHeaders = {};
3089
- res.headers.forEach((v, k) => resHeaders[k] = v);
3090
- ws.send(JSON.stringify({
3091
- type: "RESPONSE",
3092
- id,
3093
- status: res.status,
3094
- headers: resHeaders,
3095
- body: resBody
3096
- }));
3097
- return;
3098
- }
3099
- const eventName = payload.event || (payload.type === "EVENT" ? payload.name : void 0);
3100
- if (eventName) {
3101
- const handlers = self.findEvent(eventName);
3102
- const handler = handlers?.length == 1 ? handlers[0] : compose(handlers);
3103
- if (handler) {
3104
- const data = payload.data || payload.payload;
3105
- const req = new ShokupanRequest({
3106
- url: `http://${self.applicationConfig.hostname || "localhost"}/event/${eventName}`,
3107
- method: "POST",
3108
- headers: new Headers({ "content-type": "application/json" }),
3109
- body: JSON.stringify(data)
3110
- });
3111
- const ctx = new ShokupanContext(req, self.server);
3112
- ctx[$ws] = ws;
3113
- ws.data ??= {};
3114
- ws.data["ctx"] = ctx;
3115
- try {
3116
- await handler(ctx);
3117
- } catch (err) {
3118
- if (self.applicationConfig["websocketErrorHandler"]) {
3119
- await self.applicationConfig["websocketErrorHandler"](err, ctx);
3120
- } else {
3121
- console.error(`Error in event ${eventName}:`, err);
3122
- }
3123
- }
3124
- }
3125
- }
3126
- } catch (e) {
3127
- }
3128
- },
3129
- drain(ws) {
3130
- ws.data?.handler?.drain?.(ws);
3131
- },
3132
- close(ws, code, reason) {
3133
- ws.data?.handler?.close?.(ws, code, reason);
3134
- const ctx = ws.data?.["ctx"];
3135
- if (ctx && typeof ctx.getDisconnectCallbacks === "function") {
3136
- const callbacks = ctx.getDisconnectCallbacks();
3137
- if (Array.isArray(callbacks) && callbacks.length > 0) {
3138
- Promise.all(callbacks.map((cb) => cb())).catch((err) => {
3139
- console.error("Error executing socket disconnect hook:", err);
3140
- });
3141
- }
3142
- }
3143
- }
3624
+ let adapter = this.applicationConfig.adapter;
3625
+ if (!adapter) {
3626
+ if (typeof Bun !== "undefined") {
3627
+ this.applicationConfig.adapter = "bun";
3628
+ adapter = new BunAdapter();
3629
+ } else {
3630
+ this.applicationConfig.adapter = "node";
3631
+ adapter = new NodeAdapter();
3144
3632
  }
3145
- };
3146
- let factory = this.applicationConfig.serverFactory;
3147
- if (!factory && typeof Bun === "undefined") {
3148
- const { createHttpServer } = await import("./http-server-CCeagTyU.js");
3149
- factory = createHttpServer();
3633
+ } else if (adapter === "bun") {
3634
+ adapter = new BunAdapter();
3635
+ } else if (adapter === "node") {
3636
+ adapter = new NodeAdapter();
3637
+ } else if (adapter === "wintercg") {
3638
+ throw new Error("WinterCG adapter does not support listen(). Use fetch directly.");
3150
3639
  }
3151
- this.server = factory ? await factory(serveOptions) : Bun.serve(serveOptions);
3640
+ this.server = await adapter.listen(finalPort, this);
3152
3641
  return this.server;
3153
3642
  }
3154
3643
  /**
@@ -3201,11 +3690,16 @@ class Shokupan extends ShokupanRouter {
3201
3690
  }
3202
3691
  url = u.toString();
3203
3692
  }
3693
+ const reqBody = options.body && typeof options.body === "object" ? JSON.stringify(options.body) : options.body;
3694
+ const reqHeaders = new Headers(options.headers);
3695
+ if (typeof options.body === "object" && !reqHeaders.has("content-type")) {
3696
+ reqHeaders.set("content-type", "application/json");
3697
+ }
3204
3698
  const req = new ShokupanRequest({
3205
3699
  method: options.method || "GET",
3206
3700
  url,
3207
- headers: options.headers,
3208
- body: options.body && typeof options.body === "object" ? JSON.stringify(options.body) : options.body
3701
+ headers: reqHeaders,
3702
+ body: reqBody
3209
3703
  });
3210
3704
  const res = await this.fetch(req);
3211
3705
  const status = res.status;
@@ -3265,19 +3759,22 @@ class Shokupan extends ShokupanRouter {
3265
3759
  if (this.cpuMonitor && this.cpuMonitor.getUsage() > (this.applicationConfig.autoBackpressureLevel ?? 60)) {
3266
3760
  const msg = "Too Many Requests (CPU Backpressure)";
3267
3761
  const res = ctx.text(msg, 429);
3268
- await this.runHooks("onResponseEnd", ctx, res);
3762
+ if (this.hasOnResponseEndHook) await this.runHooks("onResponseEnd", ctx, res);
3269
3763
  return res;
3270
3764
  }
3271
3765
  try {
3272
- await this.runHooks("onRequestStart", ctx);
3766
+ if (this.hasOnRequestStartHook) await this.runHooks("onRequestStart", ctx);
3273
3767
  const fn = this.composedMiddleware ??= compose(this.middleware);
3274
3768
  const result = await fn(ctx, async () => {
3275
- const bodyParsing = ["POST", "PUT", "PATCH", "DELETE"].includes(req.method) ? ctx.parseBody() : Promise.resolve();
3769
+ let bodyParsing;
3770
+ if (req.method !== "GET" && req.method !== "HEAD") {
3771
+ bodyParsing = ctx.parseBody();
3772
+ }
3276
3773
  const match = this.find(req.method, ctx.path);
3277
3774
  if (match) {
3278
3775
  ctx[$routeMatched] = true;
3279
3776
  ctx.params = match.params;
3280
- await bodyParsing;
3777
+ if (bodyParsing) await bodyParsing;
3281
3778
  return match.handler(ctx);
3282
3779
  }
3283
3780
  return null;
@@ -3309,19 +3806,22 @@ class Shokupan extends ShokupanRouter {
3309
3806
  } else {
3310
3807
  response = ctx.text(String(result));
3311
3808
  }
3312
- await this.runHooks("onRequestEnd", ctx);
3809
+ if (this.hasOnRequestEndHook) await this.runHooks("onRequestEnd", ctx);
3313
3810
  if (response instanceof Promise) {
3314
3811
  response = await response;
3315
3812
  }
3316
- await this.runHooks("onResponseStart", ctx, response);
3813
+ if (this.hasOnResponseStartHook) await this.runHooks("onResponseStart", ctx, response);
3317
3814
  return response;
3318
3815
  } catch (err) {
3319
3816
  const span = asyncContext.getStore()?.span;
3320
3817
  if (span) span.setStatus({ code: 2 });
3321
- const status = getErrorStatus(err);
3818
+ let status = getErrorStatus(err);
3819
+ if (err instanceof SyntaxError && err.message.includes("JSON")) {
3820
+ status = 400;
3821
+ }
3322
3822
  const body = { error: err.message || "Internal Server Error" };
3323
3823
  if (err.errors) body.errors = err.errors;
3324
- await this.runHooks("onError", ctx, err);
3824
+ if (this.hasOnErrorHook) await this.runHooks("onError", ctx, err);
3325
3825
  return ctx.json(body, status);
3326
3826
  }
3327
3827
  };
@@ -3550,7 +4050,25 @@ function ApiExplorerApp({ spec, asyncSpec, config }) {
3550
4050
  }
3551
4051
  return "Ungrouped";
3552
4052
  };
3553
- const createSubgroups = (routes, depth = 0) => {
4053
+ const findCommonPrefix = (routes) => {
4054
+ if (routes.length === 0) return [];
4055
+ const allSegments = routes.map((r) => {
4056
+ const cleaned = r.path.replace(/^\/|\/$/g, "");
4057
+ return cleaned.split("/");
4058
+ });
4059
+ const minLength = Math.min(...allSegments.map((s) => s.length));
4060
+ const commonPrefix = [];
4061
+ for (let i = 0; i < minLength; i++) {
4062
+ const segment = allSegments[0][i];
4063
+ if (allSegments.every((segments) => segments[i] === segment)) {
4064
+ commonPrefix.push(segment);
4065
+ } else {
4066
+ break;
4067
+ }
4068
+ }
4069
+ return commonPrefix;
4070
+ };
4071
+ const createSubgroups = (routes, depth = 0, commonPrefixLength = 0) => {
3554
4072
  if (routes.length < 3 || depth > 5) {
3555
4073
  return routes.map((route) => ({
3556
4074
  name: route.path,
@@ -3561,7 +4079,8 @@ function ApiExplorerApp({ spec, asyncSpec, config }) {
3561
4079
  }
3562
4080
  const pathSegments = routes.map((r) => {
3563
4081
  const cleaned = r.path.replace(/^\/|\/$/g, "");
3564
- return cleaned.split("/");
4082
+ const segments = cleaned.split("/");
4083
+ return segments.slice(commonPrefixLength);
3565
4084
  });
3566
4085
  const prefixGroups = /* @__PURE__ */ new Map();
3567
4086
  const ungrouped = [];
@@ -3580,13 +4099,30 @@ function ApiExplorerApp({ spec, asyncSpec, config }) {
3580
4099
  const result = [];
3581
4100
  prefixGroups.forEach((groupRoutes, prefix) => {
3582
4101
  if (groupRoutes.length >= 3) {
3583
- const prefixName = prefix.split("/").pop() || prefix;
3584
- result.push({
3585
- name: prefixName,
3586
- type: "subgroup",
3587
- path: "/" + prefix,
3588
- children: createSubgroups(groupRoutes, depth + 1)
4102
+ const nextSegments = /* @__PURE__ */ new Set();
4103
+ groupRoutes.forEach((route, idx) => {
4104
+ const routeIdx = routes.indexOf(route);
4105
+ const segments = pathSegments[routeIdx];
4106
+ if (segments.length > depth + 1) {
4107
+ nextSegments.add(segments[depth + 1]);
4108
+ }
3589
4109
  });
4110
+ const hasDivergingPaths = nextSegments.size >= 2;
4111
+ const allTerminal = groupRoutes.every((route, idx) => {
4112
+ const routeIdx = routes.indexOf(route);
4113
+ return pathSegments[routeIdx].length === depth + 1;
4114
+ });
4115
+ if (hasDivergingPaths || allTerminal) {
4116
+ const prefixName = prefix.split("/").pop() || prefix;
4117
+ result.push({
4118
+ name: prefixName,
4119
+ type: "subgroup",
4120
+ path: "/" + prefix,
4121
+ children: createSubgroups(groupRoutes, depth + 1, commonPrefixLength)
4122
+ });
4123
+ } else {
4124
+ result.push(...createSubgroups(groupRoutes, depth + 1, commonPrefixLength));
4125
+ }
3590
4126
  } else {
3591
4127
  ungrouped.push(...groupRoutes);
3592
4128
  }
@@ -3617,29 +4153,53 @@ function ApiExplorerApp({ spec, asyncSpec, config }) {
3617
4153
  addRoute(groupKey, route);
3618
4154
  });
3619
4155
  });
3620
- Object.entries(asyncSpec?.channels || {}).forEach(([name, ch]) => {
3621
- const operations = [];
3622
- if (ch.publish) operations.push({ method: "recv", op: ch.publish });
3623
- if (ch.subscribe) operations.push({ method: "send", op: ch.subscribe });
3624
- operations.forEach(({ method, op }) => {
3625
- if (!op.operationId) op.operationId = `${method}-${name.replace(/[^a-zA-Z0-9]/g, "-")}`;
3626
- const route = { method, path: name, op };
3627
- const source = op["x-shokupan-source"] || op["x-source-info"];
3628
- const groupKey = getGroupKey(op, source);
3629
- addRoute(groupKey, route);
3630
- });
3631
- });
3632
4156
  const hierarchicalGroups = Array.from(hierarchy.entries()).map(([name, routes]) => {
3633
4157
  routes.sort((a, b) => a.path.localeCompare(b.path));
3634
- const children = createSubgroups(routes);
4158
+ const commonPrefix = findCommonPrefix(routes);
4159
+ const commonPrefixPath = "/" + commonPrefix.join("/");
4160
+ const children = createSubgroups(routes, 0, commonPrefix.length);
4161
+ const groupMiddleware = [];
4162
+ if (spec["x-middleware-registry"]) {
4163
+ Object.entries(spec["x-middleware-registry"]).forEach(([id, mw]) => {
4164
+ const firstRoute = routes[0];
4165
+ const routeSource = firstRoute?.op?.["x-shokupan-source"];
4166
+ const mwFile = mw.file?.split("/").pop();
4167
+ const routeFile = routeSource?.file?.split("/").pop();
4168
+ if (mwFile && routeFile && mwFile === routeFile && mw.scope !== "global") {
4169
+ groupMiddleware.push({ ...mw, id, type: "middleware" });
4170
+ }
4171
+ });
4172
+ }
4173
+ const isBuiltin = routes.some((r) => r.op["x-shokupan-builtin"] === true);
3635
4174
  return {
3636
4175
  name,
3637
4176
  type: "group",
3638
- children
4177
+ children,
4178
+ middleware: groupMiddleware,
4179
+ commonPrefixPath,
4180
+ // Store for display stripping
4181
+ isBuiltin
3639
4182
  };
3640
- }).sort((a, b) => {
4183
+ });
4184
+ if (spec["x-middleware-registry"]) {
4185
+ const allGroupMiddleware = hierarchicalGroups.flatMap((g) => g.middleware || []).map((m) => m.id);
4186
+ const globalMiddleware = Object.entries(spec["x-middleware-registry"]).filter(([id]) => !allGroupMiddleware.includes(id)).map(([id, mw]) => ({ ...mw, id, type: "middleware" }));
4187
+ if (globalMiddleware.length > 0) {
4188
+ hierarchicalGroups.push({
4189
+ name: "Global Middleware",
4190
+ type: "group",
4191
+ children: [],
4192
+ middleware: globalMiddleware,
4193
+ commonPrefixPath: "",
4194
+ isBuiltin: false
4195
+ });
4196
+ }
4197
+ }
4198
+ hierarchicalGroups.sort((a, b) => {
3641
4199
  if (a.name === "Ungrouped") return 1;
3642
4200
  if (b.name === "Ungrouped") return -1;
4201
+ if (a.name === "Global Middleware") return 1;
4202
+ if (b.name === "Global Middleware") return -1;
3643
4203
  return a.name.localeCompare(b.name);
3644
4204
  });
3645
4205
  const allRoutes = Array.from(hierarchy.values()).flat();
@@ -3648,6 +4208,9 @@ function ApiExplorerApp({ spec, asyncSpec, config }) {
3648
4208
  /* @__PURE__ */ jsx("meta", { charSet: "UTF-8" }),
3649
4209
  /* @__PURE__ */ jsx("meta", { name: "viewport", content: "width=device-width, initial-scale=1.0" }),
3650
4210
  /* @__PURE__ */ jsx("title", { children: spec.info?.title || "API Explorer" }),
4211
+ /* @__PURE__ */ jsx("link", { rel: "preconnect", href: "https://fonts.googleapis.com" }),
4212
+ /* @__PURE__ */ jsx("link", { rel: "preconnect", href: "https://fonts.gstatic.com", crossorigin: "anonymous" }),
4213
+ /* @__PURE__ */ jsx("link", { href: "https://fonts.googleapis.com/css2?family=Google+Sans+Code:ital,wght@0,300..800;1,300..800&family=Vend+Sans:ital,wght@0,300..700;1,300..700&display=swap", rel: "stylesheet" }),
3651
4214
  /* @__PURE__ */ jsx("link", { rel: "stylesheet", href: "style.css" }),
3652
4215
  /* @__PURE__ */ jsx("link", { rel: "stylesheet", href: "theme.css" }),
3653
4216
  /* @__PURE__ */ jsx("script", { src: "https://cdn.jsdelivr.net/npm/marked@4.3.0/marked.min.js" }),
@@ -3674,11 +4237,25 @@ function ApiExplorerApp({ spec, asyncSpec, config }) {
3674
4237
  ] });
3675
4238
  }
3676
4239
  function Sidebar$1({ spec, hierarchicalGroups }) {
3677
- const renderNavNode = (node, depth = 0) => {
4240
+ const stripPrefix = (path, prefix) => {
4241
+ if (!prefix || prefix === "/") return path;
4242
+ if (path.startsWith(prefix)) {
4243
+ const stripped = path.substring(prefix.length);
4244
+ return stripped || "/";
4245
+ }
4246
+ return path;
4247
+ };
4248
+ const formatAndHighlightPath = (path) => {
4249
+ const converted = path.replace(/\{([^}]+)\}/g, ":$1");
4250
+ return converted.replace(/:([a-zA-Z0-9_]+)/g, '<span class="param-highlight">:$1</span>');
4251
+ };
4252
+ const renderNavNode = (node, depth = 0, commonPrefix = "") => {
3678
4253
  if (node.type === "route") {
3679
4254
  const route = node.routes[0];
3680
4255
  const source = route.op["x-shokupan-source"] || route.op["x-source-info"];
3681
4256
  const isRuntime = route.op["x-source-info"]?.isRuntime;
4257
+ const displayPath = stripPrefix(route.path, commonPrefix);
4258
+ const highlightedPath = formatAndHighlightPath(displayPath);
3682
4259
  return /* @__PURE__ */ jsxs("div", { class: "nav-item-wrapper", style: `padding-left: ${depth * 12}px;`, children: [
3683
4260
  /* @__PURE__ */ jsxs(
3684
4261
  "a",
@@ -3689,7 +4266,7 @@ function Sidebar$1({ spec, hierarchicalGroups }) {
3689
4266
  title: route.path,
3690
4267
  children: [
3691
4268
  /* @__PURE__ */ jsx("span", { class: `badge badge-${route.method.toUpperCase()}`, children: route.method.toUpperCase() }),
3692
- /* @__PURE__ */ jsx("span", { class: "nav-label", children: node.name }),
4269
+ /* @__PURE__ */ jsx("span", { class: "nav-label", dangerouslySetInnerHTML: { __html: highlightedPath } }),
3693
4270
  isRuntime && /* @__PURE__ */ jsx("span", { class: "nav-warning", title: "Static Analysis Failed", children: /* @__PURE__ */ jsxs("svg", { width: "12", height: "12", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round", children: [
3694
4271
  /* @__PURE__ */ jsx("path", { d: "M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z" }),
3695
4272
  /* @__PURE__ */ jsx("line", { x1: "12", y1: "9", x2: "12", y2: "13" }),
@@ -3718,7 +4295,7 @@ function Sidebar$1({ spec, hierarchicalGroups }) {
3718
4295
  /* @__PURE__ */ jsx("span", { class: "chevron", children: /* @__PURE__ */ jsx("svg", { width: "10", height: "10", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round", children: /* @__PURE__ */ jsx("polyline", { points: "9 18 15 12 9 6" }) }) }),
3719
4296
  /* @__PURE__ */ jsx("span", { children: node.name })
3720
4297
  ] }),
3721
- /* @__PURE__ */ jsx("div", { class: "nav-subgroup-items", children: node.children?.map((child) => renderNavNode(child, depth + 1)) })
4298
+ /* @__PURE__ */ jsx("div", { class: "nav-subgroup-items", children: node.children?.map((child) => renderNavNode(child, depth + 1, commonPrefix)) })
3722
4299
  ] });
3723
4300
  }
3724
4301
  };
@@ -3730,13 +4307,46 @@ function Sidebar$1({ spec, hierarchicalGroups }) {
3730
4307
  /* @__PURE__ */ jsx("div", { class: "version", children: spec.info?.version })
3731
4308
  ] }),
3732
4309
  /* @__PURE__ */ jsx("div", { class: "sidebar-collapse-trigger", children: "➔" }),
3733
- /* @__PURE__ */ jsx("nav", { class: "nav-groups", children: hierarchicalGroups.map((group) => /* @__PURE__ */ jsxs("div", { class: "nav-group collapsed", children: [
4310
+ /* @__PURE__ */ jsx("nav", { class: "nav-groups", children: hierarchicalGroups.map((group) => /* @__PURE__ */ jsxs("div", { class: `nav-group collapsed ${group.isBuiltin ? "builtin-group" : ""}`, children: [
3734
4311
  /* @__PURE__ */ jsxs("div", { class: "nav-group-title", children: [
3735
4312
  /* @__PURE__ */ jsx("span", { class: "chevron", children: /* @__PURE__ */ jsx("svg", { width: "12", height: "12", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round", children: /* @__PURE__ */ jsx("polyline", { points: "9 18 15 12 9 6" }) }) }),
3736
- " ",
4313
+ group.isBuiltin && /* @__PURE__ */ jsx("span", { class: "builtin-icon", title: "Built-in Plugin", children: /* @__PURE__ */ jsxs("svg", { width: "12", height: "12", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round", children: [
4314
+ /* @__PURE__ */ jsx("rect", { x: "2", y: "2", width: "20", height: "20", rx: "5", ry: "5" }),
4315
+ /* @__PURE__ */ jsx("path", { d: "M16 11.37A4 4 0 1 1 12.63 8 4 4 0 0 1 16 11.37z" }),
4316
+ /* @__PURE__ */ jsx("line", { x1: "17.5", y1: "6.5", x2: "17.51", y2: "6.5" })
4317
+ ] }) }),
3737
4318
  group.name
3738
4319
  ] }),
3739
- /* @__PURE__ */ jsx("div", { class: "nav-items", children: group.children?.map((child) => renderNavNode(child, 0)) })
4320
+ /* @__PURE__ */ jsxs("div", { class: "nav-items", children: [
4321
+ group.middleware && group.middleware.length > 0 && /* @__PURE__ */ jsx("div", { class: "group-middleware", children: group.middleware.map((mw) => /* @__PURE__ */ jsxs(
4322
+ "a",
4323
+ {
4324
+ href: `#middleware-${mw.id}`,
4325
+ class: "nav-item middleware-nav-item",
4326
+ "data-middleware-id": mw.id,
4327
+ title: mw.name,
4328
+ children: [
4329
+ /* @__PURE__ */ jsx("span", { class: "middleware-icon", children: "⚙" }),
4330
+ /* @__PURE__ */ jsx("span", { class: "nav-label", children: mw.name }),
4331
+ mw.usedBy && mw.usedBy.length > 0 && /* @__PURE__ */ jsx("span", { class: "middleware-badge", title: `Used by ${mw.usedBy.length} routes`, children: mw.usedBy.length }),
4332
+ mw.file && /* @__PURE__ */ jsx(
4333
+ "a",
4334
+ {
4335
+ href: `vscode://file/${mw.file}:${mw.startLine || 1}`,
4336
+ class: "nav-source-link",
4337
+ title: `${mw.file}:${mw.startLine || 1}`,
4338
+ children: /* @__PURE__ */ jsxs("svg", { width: "10", height: "10", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", children: [
4339
+ /* @__PURE__ */ jsx("polyline", { points: "16 18 22 12 16 6" }),
4340
+ /* @__PURE__ */ jsx("polyline", { points: "8 6 2 12 8 18" })
4341
+ ] })
4342
+ }
4343
+ )
4344
+ ]
4345
+ },
4346
+ mw.id
4347
+ )) }),
4348
+ group.children?.map((child) => renderNavNode(child, 0, group.commonPrefixPath || ""))
4349
+ ] })
3740
4350
  ] }, group.name)) })
3741
4351
  ] });
3742
4352
  }
@@ -3744,7 +4354,8 @@ function MainContent$1({ allRoutes, config, spec }) {
3744
4354
  const explorerData = JSON.stringify({
3745
4355
  routes: allRoutes,
3746
4356
  config,
3747
- info: spec.info
4357
+ info: spec.info,
4358
+ middlewareRegistry: spec["x-middleware-registry"] || {}
3748
4359
  });
3749
4360
  const safeJson = explorerData.replace(/<\/script>/g, "<\\/script>");
3750
4361
  return /* @__PURE__ */ jsxs("main", { class: "content", id: "main-content", children: [
@@ -3754,9 +4365,16 @@ function MainContent$1({ allRoutes, config, spec }) {
3754
4365
  }
3755
4366
  class ApiExplorerPlugin extends ShokupanRouter {
3756
4367
  constructor(pluginOptions = {}) {
4368
+ console.log("ApiExplorerPlugin: CONSTRUCTOR CALLED");
3757
4369
  super({ renderer: renderToString });
3758
4370
  this.pluginOptions = pluginOptions;
3759
4371
  pluginOptions.path ??= "/explorer";
4372
+ this.metadata = {
4373
+ file: import.meta.file,
4374
+ line: 1,
4375
+ name: "ApiExplorerPlugin",
4376
+ pluginName: "ApiExplorer"
4377
+ };
3760
4378
  this.init();
3761
4379
  }
3762
4380
  onInit(app, options) {
@@ -3787,6 +4405,7 @@ class ApiExplorerPlugin extends ShokupanRouter {
3787
4405
  delete op["x-source-info"].snippet;
3788
4406
  }
3789
4407
  if (op["x-shokupan-source"]?.code) {
4408
+ console.log("Deleting x-shokupan-source.code");
3790
4409
  delete op["x-shokupan-source"].code;
3791
4410
  }
3792
4411
  });
@@ -3813,7 +4432,10 @@ class ApiExplorerPlugin extends ShokupanRouter {
3813
4432
  this.get("/", async (ctx) => {
3814
4433
  const spec = this.root.openApiSpec ? structuredClone(this.root.openApiSpec) : await (this.root || this).generateApiSpec();
3815
4434
  const asyncSpec = ctx.app.asyncApiSpec;
3816
- return ctx.jsx(ApiExplorerApp({ spec: stripSourceCode(spec), asyncSpec }));
4435
+ const element = ApiExplorerApp({ spec: stripSourceCode(spec), asyncSpec });
4436
+ const html = renderToString(element);
4437
+ if (html.length === 0) throw new Error("ApiExplorerPlugin: rendered page is blank.");
4438
+ return ctx.html(html);
3817
4439
  });
3818
4440
  }
3819
4441
  }
@@ -3824,8 +4446,8 @@ function AsyncApiApp({ spec, serverUrl, base, disableSourceView, navTree }) {
3824
4446
  /* @__PURE__ */ jsx("meta", { name: "viewport", content: "width=device-width, initial-scale=1.0" }),
3825
4447
  /* @__PURE__ */ jsx("title", { children: "Shokupan AsyncAPI" }),
3826
4448
  /* @__PURE__ */ jsx("link", { rel: "preconnect", href: "https://fonts.googleapis.com" }),
3827
- /* @__PURE__ */ jsx("link", { rel: "preconnect", href: "https://fonts.gstatic.com", crossOrigin: "anonymous" }),
3828
- /* @__PURE__ */ jsx("link", { href: "https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap", rel: "stylesheet" }),
4449
+ /* @__PURE__ */ jsx("link", { rel: "preconnect", href: "https://fonts.gstatic.com", crossorigin: "anonymous" }),
4450
+ /* @__PURE__ */ jsx("link", { href: "https://fonts.googleapis.com/css2?family=Google+Sans+Code:ital,wght@0,300..800;1,300..800&family=Vend+Sans:ital,wght@0,300..700;1,300..700&display=swap", rel: "stylesheet" }),
3829
4451
  /* @__PURE__ */ jsx("link", { rel: "stylesheet", href: `${base}/theme.css` }),
3830
4452
  /* @__PURE__ */ jsx("link", { rel: "stylesheet", href: `${base}/style.css` }),
3831
4453
  /* @__PURE__ */ jsx("script", { dangerouslySetInnerHTML: {
@@ -3833,6 +4455,7 @@ function AsyncApiApp({ spec, serverUrl, base, disableSourceView, navTree }) {
3833
4455
  window.INITIAL_SPEC = ${JSON.stringify(spec)};
3834
4456
  window.INITIAL_SERVER_URL = "${serverUrl}";
3835
4457
  window.DISABLE_SOURCE_VIEW = ${JSON.stringify(disableSourceView)};
4458
+ window.BASE_PATH = "${base}";
3836
4459
  `
3837
4460
  } })
3838
4461
  ] }),
@@ -3903,8 +4526,14 @@ function LeafNode({ item, label, disableSourceView }) {
3903
4526
  ] });
3904
4527
  } else {
3905
4528
  const badgeText = item.data.type === "publish" ? "SEND" : "RECV";
4529
+ const isPlugin = item.data.op?.["x-shokupan-plugin-name"] || sourceInfo?.pluginName;
3906
4530
  content = /* @__PURE__ */ jsxs(Fragment, { children: [
3907
4531
  /* @__PURE__ */ jsx("span", { class: `badge badge-${badgeText}`, children: badgeText }),
4532
+ isPlugin && /* @__PURE__ */ jsx("span", { class: "builtin-icon", title: "Built-in Plugin", children: /* @__PURE__ */ jsxs("svg", { width: "12", height: "12", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round", children: [
4533
+ /* @__PURE__ */ jsx("rect", { x: "2", y: "2", width: "20", height: "20", rx: "5", ry: "5" }),
4534
+ /* @__PURE__ */ jsx("path", { d: "M16 11.37A4 4 0 1 1 12.63 8 4 4 0 0 1 16 11.37z" }),
4535
+ /* @__PURE__ */ jsx("line", { x1: "17.5", y1: "6.5", x2: "17.51", y2: "6.5" })
4536
+ ] }) }),
3908
4537
  /* @__PURE__ */ jsx("span", { class: "tree-label", children: label })
3909
4538
  ] });
3910
4539
  }
@@ -4001,45 +4630,56 @@ function buildNavTree(spec) {
4001
4630
  });
4002
4631
  return root;
4003
4632
  }
4004
- async function getAstRoutes(applications) {
4005
- const astRoutes = [];
4006
- const getExpandedRoutes = (app, prefix = "", seen = /* @__PURE__ */ new Set()) => {
4007
- if (seen.has(app.name)) return [];
4008
- const newSeen = new Set(seen);
4009
- newSeen.add(app.name);
4010
- const expanded = [];
4011
- for (const route of app.routes) {
4012
- expanded.push({
4013
- ...route,
4014
- // For events, path is the event name
4015
- path: route.path.startsWith("/") ? route.path.slice(1) : route.path
4016
- });
4017
- }
4018
- if (app.mounted) {
4019
- for (const mount of app.mounted) {
4020
- const targetApp = applications.find((a) => a.name === mount.target || a.className === mount.target);
4021
- if (targetApp) {
4022
- expanded.push(...getExpandedRoutes(targetApp, "", newSeen));
4023
- }
4024
- }
4025
- }
4026
- return expanded;
4027
- };
4028
- applications.forEach((app) => {
4029
- astRoutes.push(...getExpandedRoutes(app));
4030
- });
4031
- return astRoutes;
4633
+ function hasUnknownFields(schema) {
4634
+ if (!schema) return false;
4635
+ if (schema["x-unknown"]) return true;
4636
+ if (schema.type === "object" && schema.properties) {
4637
+ return Object.values(schema.properties).some(
4638
+ (prop) => hasUnknownFields(prop)
4639
+ );
4640
+ }
4641
+ if (schema.type === "array" && schema.items) {
4642
+ return hasUnknownFields(schema.items);
4643
+ }
4644
+ return false;
4032
4645
  }
4033
4646
  async function generateAsyncApi(rootRouter, options = {}) {
4034
4647
  const channels = {};
4035
4648
  let astRoutes = [];
4649
+ let astMiddlewareRegistry = {};
4650
+ let applications = [];
4036
4651
  try {
4037
- const { OpenAPIAnalyzer: OpenAPIAnalyzer2 } = await import("./analyzer-BqIe1p0R.js");
4652
+ const { OpenAPIAnalyzer: OpenAPIAnalyzer2 } = await import("./analyzer-CnKnQ5KV.js");
4038
4653
  const entrypoint = globalThis.Bun?.main || require.main?.filename || process.argv[1];
4039
4654
  const analyzer = new OpenAPIAnalyzer2(process.cwd(), entrypoint);
4040
- const { applications } = await analyzer.analyze();
4041
- astRoutes = await getAstRoutes(applications);
4655
+ const analysisResult = await analyzer.analyze();
4656
+ applications = analysisResult.applications;
4657
+ astRoutes = await getAstRoutes(applications, {
4658
+ includePrefix: false,
4659
+ pathTransform: (p) => p.startsWith("/") ? p.slice(1) : p
4660
+ });
4661
+ let middlewareId = 0;
4662
+ for (const app of applications) {
4663
+ if (app.middleware && app.middleware.length > 0) {
4664
+ for (const mw of app.middleware) {
4665
+ const id = `middleware-${middlewareId++}`;
4666
+ astMiddlewareRegistry[id] = {
4667
+ ...mw,
4668
+ id,
4669
+ usedBy: []
4670
+ // Will be populated when processing events
4671
+ };
4672
+ }
4673
+ }
4674
+ }
4042
4675
  } catch (e) {
4676
+ if (options.warnings) {
4677
+ options.warnings.push({
4678
+ type: "ast-analysis-failed",
4679
+ message: "AST Analysis failed or skipped",
4680
+ detail: e.message
4681
+ });
4682
+ }
4043
4683
  }
4044
4684
  const matchedAstRoutes = /* @__PURE__ */ new Set();
4045
4685
  const collect = async (router, prefix = "") => {
@@ -4083,23 +4723,45 @@ async function generateAsyncApi(rootRouter, options = {}) {
4083
4723
  endLine: astMatch?.sourceContext?.endLine,
4084
4724
  highlightLines: astMatch?.sourceContext ? [astMatch.sourceContext.startLine, astMatch.sourceContext.endLine] : void 0
4085
4725
  } : void 0;
4726
+ const message = {
4727
+ ...userSpec?.message || {}
4728
+ };
4729
+ let inferenceFailed = false;
4730
+ if (!message.payload) {
4731
+ if (astMatch) {
4732
+ if (astMatch.requestTypes?.body) {
4733
+ message.payload = astMatch.requestTypes.body;
4734
+ if (message.payload.type === "object" && !message.payload.properties && !message.payload.additionalProperties && Object.keys(message.payload).length === 1) {
4735
+ inferenceFailed = true;
4736
+ }
4737
+ }
4738
+ } else {
4739
+ message.payload = { type: "object" };
4740
+ inferenceFailed = true;
4741
+ }
4742
+ }
4086
4743
  if (!channels[eventName]) {
4087
- channels[eventName] = {
4088
- publish: {
4089
- operationId: `on${eventName.charAt(0).toUpperCase() + eventName.slice(1)}`,
4090
- tags,
4091
- message: {
4092
- payload: { type: "object" },
4093
- ...userSpec?.message ? userSpec.message : {}
4094
- },
4095
- ...userSpec?.type === "publish" ? userSpec : {},
4096
- "x-source-info": sourceInfo ? [sourceInfo] : [],
4097
- "x-shokupan-source": sourceInfo
4098
- // Simplified
4744
+ const publishOp = {
4745
+ operationId: `on${eventName.charAt(0).toUpperCase() + eventName.slice(1)}`,
4746
+ tags,
4747
+ message,
4748
+ ...userSpec?.type === "publish" ? userSpec : {},
4749
+ "x-source-info": sourceInfo ? [sourceInfo] : [],
4750
+ "x-shokupan-source": {
4751
+ ...sourceInfo,
4752
+ pluginName: handler.pluginName
4099
4753
  }
4100
4754
  };
4101
- if (userSpec?.summary) channels[eventName].publish.summary = userSpec.summary;
4102
- if (userSpec?.description) channels[eventName].publish.description = userSpec.description;
4755
+ if (inferenceFailed) {
4756
+ publishOp["x-warning"] = true;
4757
+ if (!publishOp.summary) publishOp.summary = "Payload Inference Failed";
4758
+ if (!publishOp.description) publishOp.description = "The payload format could not be statically inferred from the source code. Please add a type assertion or @Spec decorator.";
4759
+ }
4760
+ if (userSpec?.summary && !publishOp.summary) publishOp.summary = userSpec.summary;
4761
+ if (userSpec?.description && !publishOp.description) publishOp.description = userSpec.description;
4762
+ channels[eventName] = {
4763
+ publish: publishOp
4764
+ };
4103
4765
  } else {
4104
4766
  if (sourceInfo) {
4105
4767
  if (!channels[eventName].publish["x-source-info"]) {
@@ -4117,6 +4779,14 @@ async function generateAsyncApi(rootRouter, options = {}) {
4117
4779
  for (const emit of emits) {
4118
4780
  if (emit.event === "__DYNAMIC_EMIT__") {
4119
4781
  const warningKey = `${eventName}/Dynamic Emit`;
4782
+ if (options.warnings) {
4783
+ options.warnings.push({
4784
+ type: "dynamic-emit",
4785
+ message: "Dynamic emit detected",
4786
+ detail: `Event: ${eventName}`,
4787
+ location: { file: astMatch?.sourceContext?.file, line: emit.location?.startLine }
4788
+ });
4789
+ }
4120
4790
  channels[warningKey] = {
4121
4791
  subscribe: {
4122
4792
  operationId: `dynamicEmitWarning${eventName}`,
@@ -4151,17 +4821,24 @@ async function generateAsyncApi(rootRouter, options = {}) {
4151
4821
  emitHighlightLines: [emitStart, emitEnd]
4152
4822
  } : void 0;
4153
4823
  if (!channels[emit.event]) {
4824
+ const payload = emit.payload || { type: "object" };
4825
+ const warning = hasUnknownFields(payload);
4154
4826
  channels[emit.event] = {
4155
4827
  subscribe: {
4156
4828
  operationId: `emit${emit.event.charAt(0).toUpperCase() + emit.event.slice(1)}`,
4157
4829
  tags,
4158
4830
  message: {
4159
- payload: emit.payload || { type: "object" }
4831
+ payload
4160
4832
  },
4833
+ ...warning ? {
4834
+ "x-warning": true,
4835
+ "x-warning-reason": "Payload contains fields with unknown types that could not be statically analyzed"
4836
+ } : {},
4161
4837
  "x-source-info": newSourceInfo ? [newSourceInfo] : [],
4162
4838
  "x-shokupan-source": sourceInfo && emitStart ? {
4163
4839
  file: sourceInfo.file,
4164
- line: emitStart
4840
+ line: emitStart,
4841
+ pluginName: handler.pluginName
4165
4842
  } : void 0
4166
4843
  }
4167
4844
  };
@@ -4225,13 +4902,19 @@ async function generateAsyncApi(rootRouter, options = {}) {
4225
4902
  emitHighlightLines: [emitStart, emitEnd]
4226
4903
  } : void 0;
4227
4904
  if (!channels[emit.event]) {
4905
+ const payload = emit.payload || { type: "object" };
4906
+ const warning = hasUnknownFields(payload);
4228
4907
  channels[emit.event] = {
4229
4908
  subscribe: {
4230
4909
  operationId: `emit${emit.event.charAt(0).toUpperCase() + emit.event.slice(1)}`,
4231
4910
  tags,
4232
4911
  message: {
4233
- payload: emit.payload || { type: "object" }
4912
+ payload
4234
4913
  },
4914
+ ...warning ? {
4915
+ "x-warning": true,
4916
+ "x-warning-reason": "Payload contains fields with unknown types that could not be statically analyzed"
4917
+ } : {},
4235
4918
  "x-source-info": newSourceInfo ? [newSourceInfo] : [],
4236
4919
  "x-shokupan-source": sourceInfo && emitStart ? {
4237
4920
  file: sourceInfo.file,
@@ -4270,6 +4953,14 @@ async function generateAsyncApi(rootRouter, options = {}) {
4270
4953
  if (parts.length > 0) prefix = parts[0];
4271
4954
  }
4272
4955
  const key = `${prefix}.Dynamic Event ${i + 1}`;
4956
+ if (options.warnings) {
4957
+ options.warnings.push({
4958
+ type: "dynamic-event",
4959
+ message: "Dynamic event listener detected",
4960
+ detail: `Event listener with dynamic name`,
4961
+ location: { file: r.sourceContext?.file, line: r.sourceContext?.startLine }
4962
+ });
4963
+ }
4273
4964
  channels[key] = {
4274
4965
  publish: {
4275
4966
  operationId: `dynamicEventWarning${i}`,
@@ -4295,7 +4986,8 @@ async function generateAsyncApi(rootRouter, options = {}) {
4295
4986
  return {
4296
4987
  asyncapi: "3.0.0",
4297
4988
  info: { title: "Shokupan AsyncAPI", version: "1.0.0", ...options.info },
4298
- channels
4989
+ channels,
4990
+ "x-middleware-registry": astMiddlewareRegistry
4299
4991
  };
4300
4992
  }
4301
4993
  const generator = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
@@ -4307,6 +4999,12 @@ class AsyncApiPlugin extends ShokupanRouter {
4307
4999
  super({ renderer: renderToString });
4308
5000
  this.pluginOptions = pluginOptions;
4309
5001
  this.pluginOptions.path ??= "/asyncapi";
5002
+ this.metadata = {
5003
+ file: import.meta.file,
5004
+ line: 1,
5005
+ name: "AsyncApiPlugin",
5006
+ pluginName: "AsyncAPI"
5007
+ };
4310
5008
  this.init();
4311
5009
  }
4312
5010
  static getBasePath() {
@@ -4719,12 +5417,15 @@ class ClusterPlugin {
4719
5417
  }
4720
5418
  }
4721
5419
  }
4722
- function DashboardApp({ metrics, uptime, integrations, base, getRequestHeadersSource }) {
5420
+ function DashboardApp({ metrics, uptime, integrations, base, getRequestHeadersSource, rootPath, linkPattern }) {
4723
5421
  return /* @__PURE__ */ jsxs("html", { lang: "en", children: [
4724
5422
  /* @__PURE__ */ jsxs("head", { children: [
4725
5423
  /* @__PURE__ */ jsx("meta", { charSet: "UTF-8" }),
4726
5424
  /* @__PURE__ */ jsx("meta", { name: "viewport", content: "width=device-width, initial-scale=1.0" }),
4727
5425
  /* @__PURE__ */ jsx("title", { children: "Shokupan Debug Dashboard" }),
5426
+ /* @__PURE__ */ jsx("link", { rel: "preconnect", href: "https://fonts.googleapis.com" }),
5427
+ /* @__PURE__ */ jsx("link", { rel: "preconnect", href: "https://fonts.gstatic.com", crossorigin: "anonymous" }),
5428
+ /* @__PURE__ */ jsx("link", { href: "https://fonts.googleapis.com/css2?family=Google+Sans+Code:ital,wght@0,300..800;1,300..800&family=Vend+Sans:ital,wght@0,300..700;1,300..700&display=swap", rel: "stylesheet" }),
4728
5429
  /* @__PURE__ */ jsx("link", { href: "https://unpkg.com/tabulator-tables@5.5.0/dist/css/tabulator_bootstrap5.min.css", rel: "stylesheet" }),
4729
5430
  /* @__PURE__ */ jsx("link", { rel: "stylesheet", href: "https://esm.sh/@xyflow/react@12.3.6/dist/style.css" }),
4730
5431
  /* @__PURE__ */ jsx("link", { rel: "stylesheet", href: `${base}/theme.css` }),
@@ -4733,104 +5434,134 @@ function DashboardApp({ metrics, uptime, integrations, base, getRequestHeadersSo
4733
5434
  /* @__PURE__ */ jsx("link", { rel: "stylesheet", href: `${base}/registry.css` }),
4734
5435
  /* @__PURE__ */ jsx("link", { rel: "stylesheet", href: `${base}/tabulator.css` }),
4735
5436
  /* @__PURE__ */ jsx("script", { src: "https://cdn.jsdelivr.net/npm/chart.js" }),
4736
- /* @__PURE__ */ jsx("script", { type: "text/javascript", src: "https://unpkg.com/tabulator-tables@5.5.0/dist/js/tabulator.min.js" })
5437
+ /* @__PURE__ */ jsx("script", { type: "text/javascript", src: "https://unpkg.com/tabulator-tables@5.5.0/dist/js/tabulator.min.js" }),
5438
+ /* @__PURE__ */ jsx("script", { src: "https://cdn.jsdelivr.net/npm/monaco-editor@0.44.0/min/vs/loader.js" })
4737
5439
  ] }),
4738
5440
  /* @__PURE__ */ jsxs("body", { children: [
4739
5441
  /* @__PURE__ */ jsxs("div", { class: "container", children: [
4740
5442
  /* @__PURE__ */ jsxs("header", { children: [
4741
- /* @__PURE__ */ jsxs("div", { children: [
4742
- /* @__PURE__ */ jsx("h1", { children: "Dashboard" }),
4743
- /* @__PURE__ */ jsxs("div", { style: "color: var(--text-secondary)", children: [
5443
+ /* @__PURE__ */ jsx("div", { children: /* @__PURE__ */ jsx("h1", { children: "Shokupan" }) }),
5444
+ /* @__PURE__ */ jsxs("div", { style: "margin-left: 8px", children: [
5445
+ /* @__PURE__ */ jsxs("span", { style: "color: var(--text-secondary)", children: [
4744
5446
  "Uptime: ",
4745
5447
  /* @__PURE__ */ jsx("span", { id: "uptime", children: uptime })
4746
- ] })
5448
+ ] }),
5449
+ /* @__PURE__ */ jsx("span", { id: "ws-status", title: "WebSocket: Disconnected", style: "width: 10px; height: 10px; border-radius: 50%; background: #6b7280; display: inline-block; margin-left: 10px;" })
4747
5450
  ] }),
5451
+ /* @__PURE__ */ jsx("div", { style: "flex: 1;" }),
4748
5452
  /* @__PURE__ */ jsxs("div", { class: "tabs", children: [
4749
5453
  /* @__PURE__ */ jsx("button", { class: "tab-btn active", onclick: "switchTab('overview')", children: "Overview" }),
4750
- /* @__PURE__ */ jsx("button", { class: "tab-btn", onclick: "switchTab('registry')", children: "Registry" }),
4751
- /* @__PURE__ */ jsx("button", { class: "tab-btn", onclick: "switchTab('graph')", children: "Graph" }),
4752
- /* @__PURE__ */ jsx("button", { class: "tab-btn", onclick: "switchTab('requests')", children: "Requests" }),
4753
- /* @__PURE__ */ jsx("button", { class: "tab-btn", onclick: "switchTab('failures')", children: "Failures" }),
4754
- integrations.scalar && /* @__PURE__ */ jsx("button", { class: "tab-btn", onclick: "switchTab('scalar')", children: "API Reference" }),
4755
- integrations.asyncapi && /* @__PURE__ */ jsx("button", { class: "tab-btn", onclick: "switchTab('asyncapi')", children: "AsyncAPI" })
5454
+ /* @__PURE__ */ jsx("button", { class: "tab-btn", onclick: "switchTab('application')", children: "Application" }),
5455
+ /* @__PURE__ */ jsx("button", { class: "tab-btn", onclick: "switchTab('network')", children: "Network" }),
5456
+ integrations.scalar && /* @__PURE__ */ jsx("button", { class: "tab-btn", onclick: "switchTab('scalar')", children: "Scalar" }),
5457
+ integrations.apiExplorer && /* @__PURE__ */ jsx("button", { class: "tab-btn", onclick: "switchTab('api-explorer')", children: "REST API" }),
5458
+ integrations.asyncapi && /* @__PURE__ */ jsx("button", { class: "tab-btn", onclick: "switchTab('asyncapi')", children: "WS API" })
4756
5459
  ] })
4757
5460
  ] }),
4758
- /* @__PURE__ */ jsxs("div", { id: "tab-overview", class: "tab-content active", children: [
4759
- /* @__PURE__ */ jsx(MetricsGrid, { metrics }),
4760
- /* @__PURE__ */ jsxs("div", { id: "chart-container", style: "display: flex; flex-direction: column; gap: 1rem;", children: [
4761
- /* @__PURE__ */ jsx("div", { style: "display: flex; justify-content: flex-end;", children: /* @__PURE__ */ jsxs("select", { id: "time-range-selector", onchange: "updateCharts(); updateDashboard(); fetchTopStats();", style: "background: var(--bg-primary); color: var(--text-primary); border: 1px solid var(--card-border); padding: 5px; border-radius: 4px;", children: [
4762
- /* @__PURE__ */ jsx("option", { value: "1m", children: "1 Minute" }),
4763
- /* @__PURE__ */ jsx("option", { value: "5m", children: "5 Minutes" }),
4764
- /* @__PURE__ */ jsx("option", { value: "30m", children: "30 Minutes" }),
4765
- /* @__PURE__ */ jsx("option", { value: "1h", children: "1 Hour" }),
4766
- /* @__PURE__ */ jsx("option", { value: "2h", children: "2 Hours" }),
4767
- /* @__PURE__ */ jsx("option", { value: "6h", children: "6 Hours" }),
4768
- /* @__PURE__ */ jsx("option", { value: "12h", children: "12 Hours" }),
4769
- /* @__PURE__ */ jsx("option", { value: "1d", children: "1 Day" }),
4770
- /* @__PURE__ */ jsx("option", { value: "3d", children: "3 Days" }),
4771
- /* @__PURE__ */ jsx("option", { value: "7d", children: "7 Days" }),
4772
- /* @__PURE__ */ jsx("option", { value: "30d", children: "30 Days" })
4773
- ] }) }),
4774
- /* @__PURE__ */ jsxs("div", { class: "card-container", children: [
4775
- /* @__PURE__ */ jsx(ChartCard, { title: "Response Time", id: "latencyChart" }),
4776
- /* @__PURE__ */ jsx(ChartCard, { title: "Requests / Second", id: "rpsChart" }),
4777
- /* @__PURE__ */ jsx(ChartCard, { title: "CPU & Load", id: "cpuChart" }),
4778
- /* @__PURE__ */ jsx(ChartCard, { title: "Memory", id: "memoryChart" }),
4779
- /* @__PURE__ */ jsx(ChartCard, { title: "Heap Usage", id: "heapChart" }),
4780
- /* @__PURE__ */ jsx(ChartCard, { title: "Event Loop Latency", id: "eventLoopChart" }),
4781
- /* @__PURE__ */ jsx(ChartCard, { title: "Error Rate", id: "errorRateChart" })
5461
+ /* @__PURE__ */ jsxs("div", { class: "contents", children: [
5462
+ /* @__PURE__ */ jsxs("div", { id: "tab-overview", class: "tab-content active", children: [
5463
+ /* @__PURE__ */ jsx(MetricsGrid, { metrics }),
5464
+ /* @__PURE__ */ jsxs("div", { id: "chart-container", style: "display: flex; flex-direction: column; gap: 1rem;", children: [
5465
+ /* @__PURE__ */ jsx("div", { style: "display: flex; justify-content: flex-end;", children: /* @__PURE__ */ jsxs("select", { id: "time-range-selector", onchange: "updateCharts(); updateDashboard(); fetchTopStats();", style: "background: var(--bg-primary); color: var(--text-primary); border: 1px solid var(--card-border); padding: 5px; border-radius: 4px;", children: [
5466
+ /* @__PURE__ */ jsx("option", { value: "1m", children: "1 Minute" }),
5467
+ /* @__PURE__ */ jsx("option", { value: "5m", children: "5 Minutes" }),
5468
+ /* @__PURE__ */ jsx("option", { value: "30m", children: "30 Minutes" }),
5469
+ /* @__PURE__ */ jsx("option", { value: "1h", children: "1 Hour" }),
5470
+ /* @__PURE__ */ jsx("option", { value: "2h", children: "2 Hours" }),
5471
+ /* @__PURE__ */ jsx("option", { value: "6h", children: "6 Hours" }),
5472
+ /* @__PURE__ */ jsx("option", { value: "12h", children: "12 Hours" }),
5473
+ /* @__PURE__ */ jsx("option", { value: "1d", children: "1 Day" }),
5474
+ /* @__PURE__ */ jsx("option", { value: "3d", children: "3 Days" }),
5475
+ /* @__PURE__ */ jsx("option", { value: "7d", children: "7 Days" }),
5476
+ /* @__PURE__ */ jsx("option", { value: "30d", children: "30 Days" })
5477
+ ] }) }),
5478
+ /* @__PURE__ */ jsxs("div", { class: "card-container", children: [
5479
+ /* @__PURE__ */ jsx(ChartCard, { title: "Response Time", id: "latencyChart" }),
5480
+ /* @__PURE__ */ jsx(ChartCard, { title: "Requests / Second", id: "rpsChart" }),
5481
+ /* @__PURE__ */ jsx(ChartCard, { title: "CPU & Load", id: "cpuChart" }),
5482
+ /* @__PURE__ */ jsx(ChartCard, { title: "Memory", id: "memoryChart" }),
5483
+ /* @__PURE__ */ jsx(ChartCard, { title: "Heap Usage", id: "heapChart" }),
5484
+ /* @__PURE__ */ jsx(ChartCard, { title: "Event Loop Latency", id: "eventLoopChart" }),
5485
+ /* @__PURE__ */ jsx(ChartCard, { title: "Error Rate", id: "errorRateChart" })
5486
+ ] }),
5487
+ /* @__PURE__ */ jsx("div", { class: "card-title", style: "margin-top: 1rem;", children: "Top Statistics" }),
5488
+ /* @__PURE__ */ jsxs("div", { class: "card-container", children: [
5489
+ /* @__PURE__ */ jsx(Card, { title: "Top Requests", contentId: "top-requests-table" }),
5490
+ /* @__PURE__ */ jsx(Card, { title: "Top Errors", contentId: "top-errors-table" }),
5491
+ /* @__PURE__ */ jsx(Card, { title: "Most Frequent Failures", contentId: "failing-requests-table" }),
5492
+ /* @__PURE__ */ jsx(Card, { title: "Slowest Requests", contentId: "slowest-requests-table" })
5493
+ ] }),
5494
+ /* @__PURE__ */ jsx("div", { id: "table-container", style: "padding: 0; margin-top: 1rem;", children: /* @__PURE__ */ jsx("div", { id: "requests-table", class: "table-dark" }) })
4782
5495
  ] }),
4783
- /* @__PURE__ */ jsx("div", { class: "card-title", style: "margin-top: 1rem;", children: "Top Statistics" }),
4784
- /* @__PURE__ */ jsxs("div", { class: "card-container", children: [
4785
- /* @__PURE__ */ jsx(Card, { title: "Top Requests", contentId: "top-requests-table" }),
4786
- /* @__PURE__ */ jsx(Card, { title: "Top Errors", contentId: "top-errors-table" }),
4787
- /* @__PURE__ */ jsx(Card, { title: "Most Frequent Failures", contentId: "failing-requests-table" }),
4788
- /* @__PURE__ */ jsx(Card, { title: "Slowest Requests", contentId: "slowest-requests-table" })
4789
- ] }),
4790
- /* @__PURE__ */ jsx("div", { id: "table-container", style: "padding: 0; margin-top: 1rem;", children: /* @__PURE__ */ jsx("div", { id: "requests-table", class: "table-dark" }) })
4791
- ] })
4792
- ] }),
4793
- /* @__PURE__ */ jsx("div", { id: "tab-registry", class: "tab-content", children: /* @__PURE__ */ jsxs("div", { id: "registry-container", class: "card", style: "margin-top: 2rem;", children: [
4794
- /* @__PURE__ */ jsx("div", { class: "card-title", children: "Component Registry" }),
4795
- /* @__PURE__ */ jsx("div", { id: "registry-tree", style: "padding: 0 1rem 1rem 1rem; font-family: monospace; font-size: 0.9rem;" })
4796
- ] }) }),
4797
- /* @__PURE__ */ jsxs("div", { id: "tab-graph", class: "tab-content", children: [
4798
- /* @__PURE__ */ jsx("div", { class: "card", style: "margin-bottom: 1rem;", children: /* @__PURE__ */ jsx("div", { style: "display: flex; gap: 1rem;", children: /* @__PURE__ */ jsx("input", { type: "text", id: "graph-search", placeholder: "Search routes or middleware...", "aria-label": "Search routes or middleware", style: "flex:1; padding: 0.5rem; border-radius: 0.5rem; background: var(--bg-primary); border: 1px solid var(--card-border); color: var(--text-primary);" }) }) }),
4799
- /* @__PURE__ */ jsx("div", { id: "cy" })
4800
- ] }),
4801
- /* @__PURE__ */ jsxs("div", { id: "tab-requests", class: "tab-content", children: [
4802
- /* @__PURE__ */ jsxs("div", { class: "card", style: "margin-bottom: 1rem; display: flex; justify-content: space-between; align-items: center;", children: [
4803
- /* @__PURE__ */ jsx("div", { class: "card-title", children: "Recent Requests (Last 100)" }),
4804
- /* @__PURE__ */ jsx("div", { children: /* @__PURE__ */ jsx("button", { onclick: "fetchRequests()", style: "background: var(--bg-primary); color: var(--text-primary); border: 1px solid var(--card-border); padding: 5px 10px; border-radius: 4px; cursor: pointer;", children: "Refresh" }) })
5496
+ /* @__PURE__ */ jsx("div", { style: "height: 2rem" })
4805
5497
  ] }),
4806
- /* @__PURE__ */ jsx("div", { id: "requests-list-container", style: "height: calc(100vh - 300px); margin-bottom: 2rem;" }),
4807
- /* @__PURE__ */ jsxs("div", { id: "request-details-container", class: "card", style: "display: none;", children: [
4808
- /* @__PURE__ */ jsx("div", { class: "card-title", children: "Request Details" }),
4809
- /* @__PURE__ */ jsx("div", { id: "request-details-content" }),
4810
- /* @__PURE__ */ jsx("div", { class: "card-title", style: "margin-top: 1rem;", children: "Middleware Trace" }),
4811
- /* @__PURE__ */ jsx("div", { id: "middleware-trace-container" })
4812
- ] })
4813
- ] }),
4814
- /* @__PURE__ */ jsxs("div", { id: "tab-failures", class: "tab-content", children: [
4815
- /* @__PURE__ */ jsxs("div", { class: "card", style: "margin-bottom: 1rem; display: flex; justify-content: space-between; align-items: center;", children: [
4816
- /* @__PURE__ */ jsx("div", { class: "card-title", children: "Failed Requests (Last 50)" }),
4817
- /* @__PURE__ */ jsxs("div", { children: [
4818
- /* @__PURE__ */ jsx("button", { onclick: "importFailure()", style: "background: var(--bg-primary); color: var(--text-primary); border: 1px solid var(--card-border); padding: 5px 10px; border-radius: 4px; cursor: pointer; margin-right: 8px;", children: "Import" }),
4819
- /* @__PURE__ */ jsx("button", { onclick: "fetchFailures()", style: "background: var(--bg-primary); color: var(--text-primary); border: 1px solid var(--card-border); padding: 5px 10px; border-radius: 4px; cursor: pointer;", children: "Refresh" })
5498
+ /* @__PURE__ */ jsxs("div", { id: "tab-application", class: "tab-content", children: [
5499
+ /* @__PURE__ */ jsx("div", { style: "margin: 2rem 2rem 0 2rem; display: flex; gap: 1rem; align-items: center;", children: /* @__PURE__ */ jsxs("div", { class: "button-group", children: [
5500
+ /* @__PURE__ */ jsx("button", { class: "view-btn active", onclick: "switchApplicationView('registry')", children: "Registry" }),
5501
+ /* @__PURE__ */ jsx("button", { class: "view-btn", onclick: "switchApplicationView('graph')", children: "Graph" })
5502
+ ] }) }),
5503
+ /* @__PURE__ */ jsxs("div", { id: "app-view-registry", class: "app-view active", style: "max-width: 1200px; align-self: center; margin: 0 auto", children: [
5504
+ /* @__PURE__ */ jsxs("div", { id: "registry-container", class: "card", style: "margin: 2rem; margin-top: 1rem;", children: [
5505
+ /* @__PURE__ */ jsx("div", { class: "card-title", children: "Component Registry" }),
5506
+ /* @__PURE__ */ jsx("div", { id: "registry-tree", style: "padding: 0 1rem 1rem 1rem; font-family: monospace; font-size: 0.9rem;" })
5507
+ ] }),
5508
+ /* @__PURE__ */ jsx("div", { style: "height: .1px" })
5509
+ ] }),
5510
+ /* @__PURE__ */ jsxs("div", { id: "app-view-graph", class: "app-view", style: "height: 100%;", children: [
5511
+ /* @__PURE__ */ jsx("div", { class: "card", style: "margin: 1rem 2rem;", children: /* @__PURE__ */ jsx("div", { style: "display: flex; gap: 1rem;", children: /* @__PURE__ */ jsx("input", { type: "text", id: "graph-search", placeholder: "Search routes or middleware...", "aria-label": "Search routes or middleware", style: "flex:1; padding: 0.5rem; border-radius: 0.5rem; background: var(--bg-primary); border: 1px solid var(--card-border); color: var(--text-primary);" }) }) }),
5512
+ /* @__PURE__ */ jsx("div", { id: "cy", style: "margin: 0 2rem; height: calc(100% - 10rem);" })
4820
5513
  ] })
4821
5514
  ] }),
4822
- /* @__PURE__ */ jsx("div", { id: "failures-table-container" })
4823
- ] }),
4824
- integrations.scalar && /* @__PURE__ */ jsx("div", { id: "tab-scalar", class: "tab-content", style: "margin: 0; overflow: hidden; height: 100%; max-width: unset", children: /* @__PURE__ */ jsx("iframe", { src: integrations.scalar, style: "width: 100%; height: 100%; border: none;" }) }),
4825
- integrations.asyncapi && /* @__PURE__ */ jsx("div", { id: "tab-asyncapi", class: "tab-content", style: "margin: 0; overflow: hidden; height: 100%; max-width: unset", children: /* @__PURE__ */ jsx("iframe", { src: integrations.asyncapi, style: "width: 100%; height: 100%; border: none;" }) })
5515
+ /* @__PURE__ */ jsxs("div", { id: "tab-network", class: "tab-content", children: [
5516
+ /* @__PURE__ */ jsx("div", { style: "margin: 1rem 2rem 0 2rem;", children: /* @__PURE__ */ jsxs("div", { id: "network-filter-bar", class: "card", style: "margin-bottom: 1rem; padding: 0.5rem; display: flex; gap: 0.5rem; flex-wrap: wrap; flex-direction: row", children: [
5517
+ /* @__PURE__ */ jsxs("div", { style: "display: flex; background: var(--bg-secondary); border: 1px solid var(--card-border); border-radius: 4px; overflow: hidden;", children: [
5518
+ /* @__PURE__ */ jsx("button", { class: "filter-direction active", "data-value": "all", style: "padding: 4px 12px; border: none; background: var(--bg-primary); color: var(--text-primary); cursor: pointer; border-right: 1px solid var(--card-border);", children: "All" }),
5519
+ /* @__PURE__ */ jsx("button", { class: "filter-direction", "data-value": "inbound", style: "padding: 4px 12px; border: none; background: transparent; color: var(--text-secondary); cursor: pointer; border-right: 1px solid var(--card-border);", children: "Inbound" }),
5520
+ /* @__PURE__ */ jsx("button", { class: "filter-direction", "data-value": "outbound", style: "padding: 4px 12px; border: none; background: transparent; color: var(--text-secondary); cursor: pointer;", children: "Outbound" })
5521
+ ] }),
5522
+ /* @__PURE__ */ jsx("input", { type: "text", id: "network-filter-text", placeholder: "Filter...", style: "background: var(--bg-primary); color: var(--text-primary); border: 1px solid var(--card-border); padding: 4px 8px; border-radius: 4px; flex: 1;" }),
5523
+ /* @__PURE__ */ jsxs("select", { id: "network-filter-type", style: "background: var(--bg-primary); color: var(--text-primary); border: 1px solid var(--card-border); borderRadius: 4px;", children: [
5524
+ /* @__PURE__ */ jsx("option", { value: "all", children: "All Types" }),
5525
+ /* @__PURE__ */ jsx("option", { value: "xhr", children: "XHR/Fetch" }),
5526
+ /* @__PURE__ */ jsx("option", { value: "fetch", children: "Outbound" }),
5527
+ /* @__PURE__ */ jsx("option", { value: "ws", children: "WS" }),
5528
+ /* @__PURE__ */ jsx("option", { value: "other", children: "Other" })
5529
+ ] }),
5530
+ /* @__PURE__ */ jsx("button", { onclick: "fetchRequests()", style: "background: var(--bg-primary); color: var(--text-primary); border: 1px solid var(--card-border); padding: 4px 8px; border-radius: 4px; cursor: pointer;", children: "Refresh" }),
5531
+ /* @__PURE__ */ jsx("button", { onclick: "purgeRequests()", style: "background: var(--bg-primary); color: var(--color-error, #ef4444); border: 1px solid var(--card-border); padding: 4px 8px; border-radius: 4px; cursor: pointer;", children: "Purge" })
5532
+ ] }) }),
5533
+ /* @__PURE__ */ jsx("div", { id: "network-view", class: "active", style: "display: block; height: calc(100vh - 170px);", children: /* @__PURE__ */ jsxs("div", { style: "margin: 0 2rem; display: flex; gap: 1rem; height: 100%;", children: [
5534
+ /* @__PURE__ */ jsx("div", { id: "requests-list-container", style: "flex: 1; height: 100%; border-radius: 6px; overflow: hidden; border: 1px solid var(--card-border);" }),
5535
+ /* @__PURE__ */ jsxs("div", { id: "request-details-container", class: "card", style: "display: none; width: 500px; height: 100%; overflow-y: auto; flex-shrink: 0; background: var(--bg-secondary); border: 1px solid var(--card-border); position: relative;", children: [
5536
+ /* @__PURE__ */ jsx("div", { id: "details-drag-handle", style: "position: absolute; left: 0; top: 0; bottom: 0; width: 5px; cursor: col-resize; z-index: 11; background: transparent;" }),
5537
+ /* @__PURE__ */ jsxs("div", { style: "display: flex; justify-content: space-between; align-items: center; position: sticky; top: 0; background: var(--bg-secondary); padding: 0.5rem 1rem; border-bottom: 1px solid var(--border-color); z-index: 10;", children: [
5538
+ /* @__PURE__ */ jsx("div", { class: "card-title", style: "margin: 0;", children: "Request Details" }),
5539
+ /* @__PURE__ */ jsx("button", { onclick: "closeRequestDetails()", style: "background: transparent; border: none; color: var(--text-secondary); cursor: pointer; font-size: 1.2rem;", children: "×" })
5540
+ ] }),
5541
+ /* @__PURE__ */ jsxs("div", { style: "padding: 1rem;", children: [
5542
+ /* @__PURE__ */ jsx("div", { id: "request-details-content" }),
5543
+ /* @__PURE__ */ jsx("div", { class: "card-title", style: "margin-top: 1rem;", children: "Middleware Trace" }),
5544
+ /* @__PURE__ */ jsx("div", { id: "middleware-trace-container" })
5545
+ ] })
5546
+ ] })
5547
+ ] }) })
5548
+ ] }),
5549
+ integrations.scalar && /* @__PURE__ */ jsx("div", { id: "tab-scalar", class: "tab-content", style: "overflow: hidden; height: 100%; width: 100%", children: /* @__PURE__ */ jsx("iframe", { src: integrations.scalar, style: "width: 100%; height: 100%; border: none;" }) }),
5550
+ integrations.apiExplorer && /* @__PURE__ */ jsx("div", { id: "tab-api-explorer", class: "tab-content", style: "overflow: hidden; height: 100%; width: 100%", children: /* @__PURE__ */ jsx("iframe", { src: integrations.apiExplorer, style: "width: 100%; height: 100%; border: none;" }) }),
5551
+ integrations.asyncapi && /* @__PURE__ */ jsx("div", { id: "tab-asyncapi", class: "tab-content", style: "overflow: hidden; height: 100%; width: 100%", children: /* @__PURE__ */ jsx("iframe", { src: integrations.asyncapi, style: "width: 100%; height: 100%; border: none;" }) })
5552
+ ] })
4826
5553
  ] }),
4827
5554
  /* @__PURE__ */ jsx("script", { dangerouslySetInnerHTML: {
4828
5555
  __html: `
4829
5556
  // Injected function from server config
4830
5557
  const getRequestHeaders = ${getRequestHeadersSource};
5558
+ window.SHOKUPAN_CONFIG = {
5559
+ rootPath: "${rootPath || ""}",
5560
+ linkPattern: "${linkPattern || ""}"
5561
+ };
4831
5562
  `
4832
5563
  } }),
4833
- /* @__PURE__ */ jsx("script", { src: `${base}/poll.js` }),
5564
+ /* @__PURE__ */ jsx("script", { src: `${base}/client.js` }),
4834
5565
  /* @__PURE__ */ jsx("script", { src: `${base}/graph.mjs`, type: "module" }),
4835
5566
  /* @__PURE__ */ jsx("script", { src: `${base}/charts.js` }),
4836
5567
  /* @__PURE__ */ jsx("script", { src: `${base}/tables.js` }),
@@ -4900,6 +5631,264 @@ function Card({ title, contentId }) {
4900
5631
  /* @__PURE__ */ jsx("div", { id: contentId })
4901
5632
  ] });
4902
5633
  }
5634
+ const require$1 = createRequire(import.meta.url);
5635
+ const http = require$1("node:http");
5636
+ const https = require$1("node:https");
5637
+ class FetchInterceptor {
5638
+ originalFetch;
5639
+ originalHttpRequest;
5640
+ originalHttpsRequest;
5641
+ callbacks = [];
5642
+ isPatched = false;
5643
+ constructor() {
5644
+ this.originalFetch = global.fetch;
5645
+ this.originalHttpRequest = http.request;
5646
+ this.originalHttpsRequest = https.request;
5647
+ }
5648
+ /**
5649
+ * Patches the global `fetch` function to intercept requests.
5650
+ * If already patched, this method does nothing.
5651
+ */
5652
+ patch() {
5653
+ if (this.isPatched) return;
5654
+ this.patchGlobalFetch();
5655
+ this.patchNodeRequests();
5656
+ this.isPatched = true;
5657
+ console.log("[FetchInterceptor] Network layer patched.");
5658
+ }
5659
+ patchGlobalFetch() {
5660
+ const self = this;
5661
+ const newFetch = async function(input, init) {
5662
+ const startTime = performance.now();
5663
+ const timestamp = Date.now();
5664
+ let method = "GET";
5665
+ let url = "";
5666
+ let requestHeaders = {};
5667
+ let requestBody = void 0;
5668
+ try {
5669
+ if (input instanceof URL$1) {
5670
+ url = input.toString();
5671
+ } else if (typeof input === "string") {
5672
+ url = input;
5673
+ } else if (typeof input === "object" && "url" in input) {
5674
+ url = input.url;
5675
+ method = input.method;
5676
+ }
5677
+ if (init) {
5678
+ if (init.method) method = init.method;
5679
+ if (init.headers) {
5680
+ if (init.headers instanceof Headers) {
5681
+ init.headers.forEach((v, k) => requestHeaders[k] = v);
5682
+ } else if (Array.isArray(init.headers)) {
5683
+ init.headers.forEach(([k, v]) => requestHeaders[k] = v);
5684
+ } else {
5685
+ Object.assign(requestHeaders, init.headers);
5686
+ }
5687
+ }
5688
+ if (init.body) requestBody = init.body;
5689
+ }
5690
+ } catch (e) {
5691
+ console.warn("[FetchInterceptor] Failed to parse request arguments", e);
5692
+ }
5693
+ try {
5694
+ const response = await self.originalFetch.apply(global, [input, init]);
5695
+ const clone = response.clone();
5696
+ const duration = performance.now() - startTime;
5697
+ self.processResponse(clone, {
5698
+ method,
5699
+ url,
5700
+ requestHeaders,
5701
+ requestBody,
5702
+ status: response.status,
5703
+ startTime: timestamp,
5704
+ duration,
5705
+ ...self.extractRequestMeta(url, requestHeaders),
5706
+ protocol: "1.1"
5707
+ // native fetch doesn't expose this easily, assume 1.1/2
5708
+ });
5709
+ return response;
5710
+ } catch (error) {
5711
+ const duration = performance.now() - startTime;
5712
+ self.notify({
5713
+ method,
5714
+ url,
5715
+ requestHeaders,
5716
+ requestBody,
5717
+ status: 0,
5718
+ responseHeaders: {},
5719
+ responseBody: `Network Error: ${String(error)}`,
5720
+ startTime: timestamp,
5721
+ duration
5722
+ });
5723
+ throw error;
5724
+ }
5725
+ };
5726
+ Object.assign(newFetch, this.originalFetch);
5727
+ global.fetch = newFetch;
5728
+ }
5729
+ patchNodeRequests() {
5730
+ const self = this;
5731
+ const intercept = (module, original, defaultScheme) => {
5732
+ module.request = function(...args) {
5733
+ const startTime = performance.now();
5734
+ const timestamp = Date.now();
5735
+ let options = {};
5736
+ let urlObj;
5737
+ if (typeof args[0] === "string" || args[0] instanceof URL$1) {
5738
+ try {
5739
+ urlObj = new URL$1(args[0]);
5740
+ options = typeof args[1] === "object" ? args[1] : {};
5741
+ } catch (e) {
5742
+ }
5743
+ } else {
5744
+ options = args[0] || {};
5745
+ try {
5746
+ const protocol = options.protocol || defaultScheme + ":";
5747
+ const host = options.hostname || options.host || "localhost";
5748
+ const port = options.port ? ":" + options.port : "";
5749
+ const path = options.path || "/";
5750
+ urlObj = new URL$1(`${protocol}//${host}${port}${path}`);
5751
+ } catch (e) {
5752
+ }
5753
+ }
5754
+ const method = (options.method || "GET").toUpperCase();
5755
+ const url = urlObj ? urlObj.toString() : "unknown";
5756
+ const req = original.apply(this, args);
5757
+ const getReqHeaders = () => {
5758
+ try {
5759
+ const h = req.getHeaders();
5760
+ const normalized = {};
5761
+ for (const k in h) {
5762
+ const v = h[k];
5763
+ normalized[k] = Array.isArray(v) ? v.join(", ") : String(v);
5764
+ }
5765
+ return normalized;
5766
+ } catch (e) {
5767
+ return {};
5768
+ }
5769
+ };
5770
+ req.on("response", (res) => {
5771
+ const duration = performance.now() - startTime;
5772
+ const resHeaders = {};
5773
+ if (res.headers) {
5774
+ for (const k in res.headers) {
5775
+ const v = res.headers[k];
5776
+ resHeaders[k] = Array.isArray(v) ? v.join(", ") : String(v || "");
5777
+ }
5778
+ }
5779
+ self.notify({
5780
+ method,
5781
+ url,
5782
+ requestHeaders: getReqHeaders(),
5783
+ status: res.statusCode || 0,
5784
+ responseHeaders: resHeaders,
5785
+ startTime: timestamp,
5786
+ duration,
5787
+ ...self.extractRequestMeta(url, getReqHeaders()),
5788
+ protocol: req.httpVersion
5789
+ });
5790
+ });
5791
+ req.on("error", (err) => {
5792
+ const duration = performance.now() - startTime;
5793
+ self.notify({
5794
+ method,
5795
+ url,
5796
+ requestHeaders: getReqHeaders(),
5797
+ status: 0,
5798
+ responseHeaders: {},
5799
+ responseBody: `Error: ${err.message}`,
5800
+ // Capture error
5801
+ startTime: timestamp,
5802
+ duration
5803
+ });
5804
+ });
5805
+ return req;
5806
+ };
5807
+ };
5808
+ intercept(http, this.originalHttpRequest, "http");
5809
+ intercept(https, this.originalHttpsRequest, "https");
5810
+ }
5811
+ /**
5812
+ * Restores the original functions.
5813
+ */
5814
+ unpatch() {
5815
+ if (!this.isPatched) return;
5816
+ global.fetch = this.originalFetch;
5817
+ http.request = this.originalHttpRequest;
5818
+ https.request = this.originalHttpsRequest;
5819
+ this.isPatched = false;
5820
+ console.log("[FetchInterceptor] Network layer restored.");
5821
+ }
5822
+ /**
5823
+ * Adds a callback to be notified of outbound requests.
5824
+ * @param callback The callback function.
5825
+ */
5826
+ on(callback) {
5827
+ this.callbacks.push(callback);
5828
+ }
5829
+ extractRequestMeta(urlStr, headers) {
5830
+ try {
5831
+ const url = new URL$1(urlStr);
5832
+ const cookiesHeader = headers["cookie"] || headers["Cookie"];
5833
+ const cookies = cookiesHeader ? cookiesHeader.split(";").length : 0;
5834
+ return {
5835
+ domain: url.hostname,
5836
+ path: url.pathname,
5837
+ scheme: url.protocol.replace(":", ""),
5838
+ cookies,
5839
+ remoteIP: void 0
5840
+ // Not easily accessible via fetch
5841
+ };
5842
+ } catch (e) {
5843
+ return {};
5844
+ }
5845
+ }
5846
+ async processResponse(response, meta) {
5847
+ const responseHeaders = {};
5848
+ response.headers.forEach((v, k) => responseHeaders[k] = v);
5849
+ let responseBody;
5850
+ let transferred = 0;
5851
+ try {
5852
+ const contentType = response.headers.get("content-type") || "";
5853
+ let bodyText = "";
5854
+ if (contentType.includes("application/json") || contentType.includes("text/")) {
5855
+ bodyText = await response.text();
5856
+ if (bodyText.length > 524288) {
5857
+ responseBody = bodyText.substring(0, 524288) + "... (truncated)";
5858
+ } else {
5859
+ responseBody = bodyText;
5860
+ }
5861
+ } else {
5862
+ responseBody = "[Binary Content]";
5863
+ const cl = response.headers.get("content-length");
5864
+ if (cl) transferred = parseInt(cl, 10);
5865
+ }
5866
+ const headersSize = Object.entries(responseHeaders).reduce((acc, [k, v]) => acc + k.length + v.length + 2, 0);
5867
+ if (!transferred && bodyText) {
5868
+ transferred = headersSize + bodyText.length;
5869
+ } else if (!transferred) {
5870
+ transferred = headersSize;
5871
+ }
5872
+ } catch (e) {
5873
+ responseBody = "[Failed to read response body]";
5874
+ }
5875
+ this.notify({
5876
+ ...meta,
5877
+ responseHeaders,
5878
+ responseBody,
5879
+ transferred
5880
+ });
5881
+ }
5882
+ notify(log) {
5883
+ this.callbacks.forEach((cb) => {
5884
+ try {
5885
+ cb(log);
5886
+ } catch (e) {
5887
+ console.error("[FetchInterceptor] Callback failed", e);
5888
+ }
5889
+ });
5890
+ }
5891
+ }
4903
5892
  const INTERVALS = [
4904
5893
  { label: "10s", ms: 10 * 1e3 },
4905
5894
  { label: "1m", ms: 60 * 1e3 },
@@ -4914,7 +5903,8 @@ const INTERVALS = [
4914
5903
  { label: "30d", ms: 30 * 24 * 60 * 60 * 1e3 }
4915
5904
  ];
4916
5905
  class MetricsCollector {
4917
- constructor(db) {
5906
+ constructor(db, onCollect) {
5907
+ this.onCollect = onCollect;
4918
5908
  this.db = db;
4919
5909
  this.eventLoopHistogram.enable();
4920
5910
  const now = Date.now();
@@ -4922,11 +5912,13 @@ class MetricsCollector {
4922
5912
  this.currentIntervalStart[int.label] = this.alignTimestamp(now, int.ms);
4923
5913
  this.pendingDetails[int.label] = [];
4924
5914
  });
5915
+ this.timer = setInterval(() => this.collect(), 1e4);
4925
5916
  }
4926
5917
  currentIntervalStart = {};
4927
5918
  pendingDetails = {};
4928
5919
  eventLoopHistogram = monitorEventLoopDelay({ resolution: 10 });
4929
5920
  timer = null;
5921
+ db;
4930
5922
  recordRequest(duration, isError) {
4931
5923
  INTERVALS.forEach((int) => {
4932
5924
  this.pendingDetails[int.label].push({ duration, isError });
@@ -5002,14 +5994,17 @@ class MetricsCollector {
5002
5994
  p99: getP(0.99)
5003
5995
  }
5004
5996
  };
5997
+ if (!this.db) {
5998
+ return;
5999
+ }
5005
6000
  try {
5006
- const recordId = new RecordId("metrics", timestamp);
5007
- await this.db.upsert(recordId, metric);
5008
- const test = await this.db.select(recordId);
5009
- const queryTest = await this.db.query("SELECT * FROM metrics WHERE id = $id", { id: recordId });
6001
+ await this.db.upsert(new RecordId("metric", timestamp), metric);
5010
6002
  } catch (e) {
5011
6003
  console.error(`[MetricsCollector] ✗ Failed to save metrics for ${label}:`, e);
5012
6004
  }
6005
+ if (this.onCollect) {
6006
+ this.onCollect(metric);
6007
+ }
5013
6008
  }
5014
6009
  // Cleanup if needed
5015
6010
  stop() {
@@ -5055,8 +6050,13 @@ class Dashboard {
5055
6050
  nodeMetrics: {},
5056
6051
  edgeMetrics: {}
5057
6052
  };
6053
+ clients = /* @__PURE__ */ new Set();
6054
+ broadcastTimer;
6055
+ requestPushTimer;
6056
+ requestsBuffer = [];
5058
6057
  startTime = Date.now();
5059
6058
  instrumented = false;
6059
+ mountPath = "/dashboard";
5060
6060
  metricsCollector;
5061
6061
  get db() {
5062
6062
  return this[$appRoot].db;
@@ -5064,8 +6064,69 @@ class Dashboard {
5064
6064
  // ShokupanPlugin interface implementation
5065
6065
  onInit(app, options) {
5066
6066
  this[$appRoot] = app;
5067
- this.metricsCollector = new MetricsCollector(this.db);
5068
- const mountPath = options?.path || this.dashboardConfig.path || "/dashboard";
6067
+ const onCollect = (metric) => {
6068
+ this.broadcastMetricUpdate(metric);
6069
+ };
6070
+ this.metricsCollector = new MetricsCollector(this.db, onCollect);
6071
+ const fetchInterceptor = new FetchInterceptor();
6072
+ fetchInterceptor.patch();
6073
+ fetchInterceptor.on((log) => {
6074
+ if (log.url.includes("/rpc")) return;
6075
+ try {
6076
+ const u = new URL(log.url);
6077
+ if (u.pathname.startsWith(this.mountPath)) return;
6078
+ } catch (e) {
6079
+ }
6080
+ const requestData = {
6081
+ method: log.method,
6082
+ url: log.url,
6083
+ status: log.status,
6084
+ duration: log.duration,
6085
+ timestamp: log.startTime,
6086
+ // Use startTime as timestamp
6087
+ type: "fetch",
6088
+ direction: "outbound",
6089
+ size: log.responseBody ? String(log.responseBody).length : 0,
6090
+ contentType: log.responseHeaders["content-type"] || log.responseHeaders["Content-Type"],
6091
+ body: log.responseBody,
6092
+ requestBody: log.requestBody,
6093
+ domain: log.domain,
6094
+ path: log.path,
6095
+ scheme: log.scheme,
6096
+ protocol: log.protocol,
6097
+ remoteIP: log.remoteIP,
6098
+ cookies: log.cookies,
6099
+ transferred: log.transferred,
6100
+ requestHeaders: log.requestHeaders,
6101
+ responseHeaders: log.responseHeaders
6102
+ // No handler stack for outbound
6103
+ };
6104
+ this.metrics.logs.push(requestData);
6105
+ const recordId = new RecordId("request", nanoid());
6106
+ const idString = recordId.toString();
6107
+ this.db.query("UPSERT $id CONTENT $data", {
6108
+ id: recordId,
6109
+ data: requestData
6110
+ }).catch((e) => console.error("Failed to save outbound request", e));
6111
+ const strategy2 = this.dashboardConfig.updateStrategy || "immediate";
6112
+ if (strategy2 === "immediate") {
6113
+ this.broadcastRequestUpdates([{ ...requestData, id: idString }]);
6114
+ } else {
6115
+ this.requestsBuffer.push({ ...requestData, id: idString });
6116
+ }
6117
+ });
6118
+ if (app.onStart) {
6119
+ app.onStart(async () => {
6120
+ if (app.dbPromise) {
6121
+ await app.dbPromise;
6122
+ if (app.db) {
6123
+ this.metricsCollector.db = app.db;
6124
+ console.log("[Dashboard] Attached datastore to MetricsCollector");
6125
+ }
6126
+ }
6127
+ });
6128
+ }
6129
+ this.mountPath = options?.path || this.dashboardConfig.path || "/dashboard";
5069
6130
  const hooks = this.getHooks();
5070
6131
  if (!app.middleware) {
5071
6132
  app.middleware = [];
@@ -5074,15 +6135,25 @@ class Dashboard {
5074
6135
  if (hooks.onRequestStart) {
5075
6136
  await hooks.onRequestStart(ctx);
5076
6137
  }
6138
+ ctx._startTime = performance.now();
5077
6139
  await next();
5078
- if (hooks.onResponseEnd) {
5079
- const effectiveResponse = ctx._finalResponse || ctx.response || {};
5080
- await hooks.onResponseEnd(ctx, effectiveResponse);
5081
- }
5082
6140
  };
5083
6141
  app.use(hooksMiddleware);
5084
- app.mount(mountPath, this.router);
6142
+ if (hooks.onResponseEnd) {
6143
+ app.hook("onResponseEnd", hooks.onResponseEnd);
6144
+ }
6145
+ app.mount(this.mountPath, this.router);
6146
+ this.router.metadata = {
6147
+ file: import.meta.file,
6148
+ line: 1,
6149
+ name: "DashboardPlugin",
6150
+ pluginName: "Dashboard"
6151
+ };
5085
6152
  this.setupRoutes();
6153
+ const strategy = this.dashboardConfig.updateStrategy || "immediate";
6154
+ if (strategy === "batched") {
6155
+ this.startRequestPushTimer();
6156
+ }
5086
6157
  }
5087
6158
  detectIntegrations() {
5088
6159
  const integrations = {};
@@ -5115,6 +6186,17 @@ class Dashboard {
5115
6186
  }
5116
6187
  }
5117
6188
  }
6189
+ const apiExplorerConf = checkConfig("apiExplorer");
6190
+ if (apiExplorerConf.enabled) {
6191
+ if (apiExplorerConf.path) {
6192
+ integrations["apiExplorer"] = apiExplorerConf.path;
6193
+ } else {
6194
+ const plugin = routers.find((r) => r.constructor.name === "ApiExplorerPlugin");
6195
+ if (plugin) {
6196
+ integrations["apiExplorer"] = plugin[$mountPath];
6197
+ }
6198
+ }
6199
+ }
5118
6200
  return integrations;
5119
6201
  }
5120
6202
  // Get base path for dashboard files - works in both dev (src/) and production (dist/)
@@ -5126,9 +6208,36 @@ class Dashboard {
5126
6208
  return dir;
5127
6209
  }
5128
6210
  setupRoutes() {
6211
+ this.router.get("/ws", (ctx) => {
6212
+ const success = ctx.upgrade({
6213
+ data: {
6214
+ handler: {
6215
+ open: (ws) => {
6216
+ this.clients.add(ws);
6217
+ console.log(`[Dashboard] Client connected. Total clients: ${this.clients.size}`);
6218
+ this.sendHistory(ws, "1m");
6219
+ },
6220
+ close: (ws) => {
6221
+ this.clients.delete(ws);
6222
+ console.log(`[Dashboard] Client disconnected. Total clients: ${this.clients.size}`);
6223
+ },
6224
+ message: (ws, message) => {
6225
+ try {
6226
+ const msg = JSON.parse(message);
6227
+ if (msg.type === "get-history") {
6228
+ this.sendHistory(ws, msg.interval || "1m");
6229
+ }
6230
+ } catch (e) {
6231
+ }
6232
+ }
6233
+ }
6234
+ }
6235
+ });
6236
+ if (success) return void 0;
6237
+ return ctx.text("WebSocket upgrade failed", 400);
6238
+ });
5129
6239
  this.router.get("/metrics", async (ctx) => {
5130
- const uptimeSeconds = Math.floor((Date.now() - this.startTime) / 1e3);
5131
- const uptime = `${Math.floor(uptimeSeconds / 3600)}h ${Math.floor(uptimeSeconds % 3600 / 60)}m ${uptimeSeconds % 60}s`;
6240
+ const uptime = this.getUptime();
5132
6241
  const interval = ctx.query["interval"];
5133
6242
  if (interval) {
5134
6243
  const intervalMap = {
@@ -5155,13 +6264,15 @@ class Dashboard {
5155
6264
  count(IF status < 400 THEN 1 END) as success,
5156
6265
  count(IF status >= 400 THEN 1 END) as failed,
5157
6266
  math::mean(duration) as avg_latency
5158
- FROM requests
6267
+ FROM request
5159
6268
  WHERE timestamp >= $start
5160
6269
  GROUP ALL
5161
6270
  `, { start: startTime });
5162
6271
  } catch (error) {
5163
6272
  console.error("[Dashboard] Query failed at plugin.ts:180-191", {
5164
6273
  error,
6274
+ errorMessage: error.message,
6275
+ errorStack: error.stack,
5165
6276
  interval,
5166
6277
  startTime,
5167
6278
  query: "metrics interval stats",
@@ -5169,12 +6280,12 @@ class Dashboard {
5169
6280
  });
5170
6281
  throw error;
5171
6282
  }
5172
- const s = stats[0] || { total: 0, success: 0, failed: 0, avg_latency: 0 };
6283
+ const s = stats[0] || { avg_latency: 0 };
5173
6284
  return ctx.json({
5174
6285
  metrics: {
5175
- totalRequests: s.total || 0,
5176
- successfulRequests: s.success || 0,
5177
- failedRequests: s.failed || 0,
6286
+ totalRequests: this.metrics.totalRequests,
6287
+ successfulRequests: this.metrics.successfulRequests,
6288
+ failedRequests: this.metrics.failedRequests,
5178
6289
  activeRequests: this.metrics.activeRequests,
5179
6290
  averageTotalTime_ms: s.avg_latency || 0,
5180
6291
  recentTimings: this.metrics.recentTimings,
@@ -5240,7 +6351,7 @@ class Dashboard {
5240
6351
  this.router.get("/requests/top", async (ctx) => {
5241
6352
  const startTime = getIntervalStartTime(ctx.query["interval"]);
5242
6353
  const result = await this.db.query(
5243
- "SELECT method, url, count() as count FROM requests WHERE timestamp >= $start GROUP BY method, url ORDER BY count DESC LIMIT 10",
6354
+ "SELECT method, url, count() as count FROM request WHERE timestamp >= $start GROUP BY method, url ORDER BY count DESC LIMIT 10",
5244
6355
  { start: startTime }
5245
6356
  );
5246
6357
  return ctx.json({ top: result[0] || [] });
@@ -5248,7 +6359,7 @@ class Dashboard {
5248
6359
  this.router.get("/errors/top", async (ctx) => {
5249
6360
  const startTime = getIntervalStartTime(ctx.query["interval"]);
5250
6361
  const result = await this.db.query(
5251
- "SELECT status, count() as count FROM failed_requests WHERE timestamp >= $start GROUP BY status ORDER BY count DESC LIMIT 10",
6362
+ "SELECT status, count() as count FROM failed_request WHERE timestamp >= $start GROUP BY status ORDER BY count DESC LIMIT 10",
5252
6363
  { start: startTime }
5253
6364
  );
5254
6365
  return ctx.json({ top: result[0] || [] });
@@ -5256,7 +6367,7 @@ class Dashboard {
5256
6367
  this.router.get("/requests/failing", async (ctx) => {
5257
6368
  const startTime = getIntervalStartTime(ctx.query["interval"]);
5258
6369
  const result = await this.db.query(
5259
- "SELECT method, url, count() as count FROM failed_requests WHERE timestamp >= $start GROUP BY method, url ORDER BY count DESC LIMIT 10",
6370
+ "SELECT method, url, count() as count FROM failed_request WHERE timestamp >= $start GROUP BY method, url ORDER BY count DESC LIMIT 10",
5260
6371
  { start: startTime }
5261
6372
  );
5262
6373
  return ctx.json({ top: result[0] || [] });
@@ -5264,7 +6375,7 @@ class Dashboard {
5264
6375
  this.router.get("/requests/slowest", async (ctx) => {
5265
6376
  const startTime = getIntervalStartTime(ctx.query["interval"]);
5266
6377
  const result = await this.db.query(
5267
- "SELECT method, url, duration, status, timestamp FROM requests WHERE timestamp >= $start ORDER BY duration DESC LIMIT 10",
6378
+ "SELECT method, url, duration, status, timestamp FROM request WHERE timestamp >= $start ORDER BY duration DESC LIMIT 10",
5268
6379
  { start: startTime }
5269
6380
  );
5270
6381
  return ctx.json({ slowest: result[0] || [] });
@@ -5281,15 +6392,32 @@ class Dashboard {
5281
6392
  return ctx.json({ registry: registry || {} });
5282
6393
  });
5283
6394
  this.router.get("/requests", async (ctx) => {
5284
- const result = await this.db.query("SELECT * FROM requests ORDER BY timestamp DESC LIMIT 100");
5285
- return ctx.json({ requests: result[0] || [] });
6395
+ console.log(`[Dashboard] Handling /requests from ${ctx.ip} ${ctx.get("User-Agent")}`);
6396
+ const result = await this.db.query("SELECT * FROM request ORDER BY timestamp DESC LIMIT 100");
6397
+ const items = result[0] || [];
6398
+ console.log(`[Dashboard] /requests returning ${items.length} items`);
6399
+ return ctx.json({ requests: items });
6400
+ });
6401
+ this.router.delete("/requests", async (ctx) => {
6402
+ console.log(`[Dashboard] Purging all requests`);
6403
+ await this.db.query("DELETE request; DELETE failed_request;");
6404
+ this.metrics.logs = [];
6405
+ this.metrics.totalRequests = 0;
6406
+ this.metrics.activeRequests = 0;
6407
+ this.metrics.successfulRequests = 0;
6408
+ this.metrics.failedRequests = 0;
6409
+ this.metrics.recentTimings = [];
6410
+ this.metrics.rateLimitedCounts = {};
6411
+ this.metrics.nodeMetrics = {};
6412
+ this.metrics.edgeMetrics = {};
6413
+ return ctx.json({ success: true });
5286
6414
  });
5287
6415
  this.router.get("/requests/:id", async (ctx) => {
5288
- const result = await this.db.query("SELECT * FROM requests WHERE id = $id", { id: ctx.params["id"] });
6416
+ const result = await this.db.query("SELECT * FROM request WHERE id = $id", { id: ctx.params["id"] });
5289
6417
  return ctx.json({ request: result[0]?.[0] });
5290
6418
  });
5291
6419
  this.router.get("/failures", async (ctx) => {
5292
- const result = await this.db.query("SELECT * FROM failed_requests ORDER BY timestamp DESC LIMIT 50");
6420
+ const result = await this.db.query("SELECT * FROM failed_request ORDER BY timestamp DESC LIMIT 50");
5293
6421
  return ctx.json({ failures: result[0] });
5294
6422
  });
5295
6423
  this.router.post("/replay", async (ctx) => {
@@ -5327,7 +6455,7 @@ class Dashboard {
5327
6455
  "charts.js",
5328
6456
  "failures.js",
5329
6457
  "graph.mjs",
5330
- "poll.js",
6458
+ "client.js",
5331
6459
  "reactflow.css",
5332
6460
  "registry.css",
5333
6461
  "registry.js",
@@ -5344,15 +6472,15 @@ class Dashboard {
5344
6472
  else if (path.endsWith(".js") || path.endsWith(".mjs")) ctx.set("Content-Type", "application/javascript");
5345
6473
  return ctx.send(content);
5346
6474
  }
5347
- const uptimeSeconds = Math.floor((Date.now() - this.startTime) / 1e3);
5348
- const uptime = `${Math.floor(uptimeSeconds / 3600)}h ${Math.floor(uptimeSeconds % 3600 / 60)}m ${uptimeSeconds % 60}s`;
5349
- this.getLinkPattern();
6475
+ const uptime = this.getUptime();
6476
+ const linkPattern = this.getLinkPattern();
5350
6477
  const integrations = this.detectIntegrations();
5351
6478
  const getRequestHeadersSource = this.dashboardConfig.getRequestHeaders ? this.dashboardConfig.getRequestHeaders.toString() : "undefined";
5352
6479
  const html = renderToString(DashboardApp({
5353
6480
  metrics: this.metrics,
5354
6481
  uptime,
5355
6482
  rootPath: process.cwd(),
6483
+ linkPattern,
5356
6484
  integrations,
5357
6485
  base: mountPath,
5358
6486
  getRequestHeadersSource
@@ -5360,6 +6488,82 @@ class Dashboard {
5360
6488
  return ctx.html(`<!DOCTYPE html>${html}`);
5361
6489
  });
5362
6490
  }
6491
+ getUptime() {
6492
+ const uptimeSeconds = Math.floor((Date.now() - this.startTime) / 1e3);
6493
+ return `${Math.floor(uptimeSeconds / 3600)}h ${Math.floor(uptimeSeconds % 3600 / 60)}m ${uptimeSeconds % 60}s`;
6494
+ }
6495
+ getPublicMetrics() {
6496
+ return {
6497
+ totalRequests: this.metrics.totalRequests,
6498
+ successfulRequests: this.metrics.successfulRequests,
6499
+ failedRequests: this.metrics.failedRequests,
6500
+ activeRequests: this.metrics.activeRequests,
6501
+ averageTotalTime_ms: this.metrics.averageTotalTime_ms,
6502
+ recentTimings: this.metrics.recentTimings,
6503
+ logs: [],
6504
+ // Don't broadcast logs for now to save bandwidth
6505
+ rateLimitedCounts: this.metrics.rateLimitedCounts,
6506
+ nodeMetrics: this.metrics.nodeMetrics,
6507
+ edgeMetrics: this.metrics.edgeMetrics
6508
+ };
6509
+ }
6510
+ broadcastMetricUpdate(metric) {
6511
+ if (this.clients.size === 0) return;
6512
+ const data = JSON.stringify({
6513
+ type: "metric-update",
6514
+ metric
6515
+ });
6516
+ for (const client of this.clients) {
6517
+ client.send(data);
6518
+ }
6519
+ }
6520
+ async sendHistory(ws, interval) {
6521
+ const intervalMap = {
6522
+ "10s": 10 * 1e3,
6523
+ "1m": 60 * 1e3,
6524
+ "5m": 5 * 60 * 1e3,
6525
+ "30m": 30 * 60 * 1e3,
6526
+ "1h": 60 * 60 * 1e3,
6527
+ "2h": 2 * 60 * 60 * 1e3,
6528
+ "6h": 6 * 60 * 60 * 1e3,
6529
+ "12h": 12 * 60 * 60 * 1e3,
6530
+ "1d": 24 * 60 * 60 * 1e3,
6531
+ "3d": 3 * 24 * 60 * 60 * 1e3,
6532
+ "7d": 7 * 24 * 60 * 60 * 1e3,
6533
+ "30d": 30 * 24 * 60 * 60 * 1e3
6534
+ };
6535
+ const periodMs = intervalMap[interval] || 60 * 1e3;
6536
+ const startTime = Date.now() - periodMs * 30;
6537
+ const endTime = Date.now();
6538
+ let history = [];
6539
+ try {
6540
+ const result = await this.db.query(
6541
+ "SELECT * FROM metric WHERE timestamp >= $start AND timestamp <= $end AND interval = $interval ORDER BY timestamp ASC",
6542
+ { start: startTime, end: endTime, interval }
6543
+ );
6544
+ history = result[0] || [];
6545
+ } catch (e) {
6546
+ console.error("[Dashboard] Failed to fetch history for WS", e);
6547
+ }
6548
+ ws.send(JSON.stringify({
6549
+ type: "init",
6550
+ metrics: { ...this.metrics, logs: [] },
6551
+ uptime: this.getUptime(),
6552
+ history
6553
+ }));
6554
+ }
6555
+ broadcastMetrics() {
6556
+ if (this.clients.size === 0) return;
6557
+ console.log(`[Dashboard] Broadcasting metrics to ${this.clients.size} clients`);
6558
+ const data = JSON.stringify({
6559
+ type: "metrics",
6560
+ metrics: this.getPublicMetrics(),
6561
+ uptime: this.getUptime()
6562
+ });
6563
+ for (const client of this.clients) {
6564
+ client.send(data);
6565
+ }
6566
+ }
5363
6567
  instrumentApp(app) {
5364
6568
  if (!app.getComponentRegistry) return;
5365
6569
  const registry = app.getComponentRegistry();
@@ -5389,6 +6593,11 @@ class Dashboard {
5389
6593
  r.id = id;
5390
6594
  this.assignIdsToRegistry(r.children, id);
5391
6595
  });
6596
+ node.events?.forEach((e, idx) => {
6597
+ const id = makeId("event", parentId, idx, e.name);
6598
+ e.id = id;
6599
+ if (e._fn) e._fn._debugId = id;
6600
+ });
5392
6601
  }
5393
6602
  recordNodeMetric(id, type, duration, isError) {
5394
6603
  if (!this.metrics.nodeMetrics[id]) {
@@ -5416,7 +6625,7 @@ class Dashboard {
5416
6625
  if (["vscode", "cursor", "antigravity"].some((t) => term.includes(t))) {
5417
6626
  return "vscode://file/{{absolute}}:{{line}}";
5418
6627
  }
5419
- return "file:///{{absolute}}:{{line}}";
6628
+ return "vscode://file/{{absolute}}:{{line}}";
5420
6629
  }
5421
6630
  getHooks() {
5422
6631
  return {
@@ -5427,19 +6636,36 @@ class Dashboard {
5427
6636
  }
5428
6637
  this.metrics.totalRequests++;
5429
6638
  this.metrics.activeRequests++;
5430
- ctx._debugStartTime = performance.now();
5431
6639
  ctx[$debug] = new Collector(this);
6640
+ if (!this.broadcastTimer) {
6641
+ this.broadcastTimer = setTimeout(() => {
6642
+ this.broadcastMetrics();
6643
+ this.broadcastTimer = void 0;
6644
+ }, 100);
6645
+ }
5432
6646
  },
5433
6647
  onResponseEnd: async (ctx, response) => {
6648
+ if (ctx.path.startsWith(this.mountPath)) return;
5434
6649
  this.metrics.activeRequests = Math.max(0, this.metrics.activeRequests - 1);
5435
- const start = ctx._debugStartTime;
5436
- let duration = 0;
5437
- if (start) {
5438
- duration = performance.now() - start;
5439
- this.updateTiming(duration);
6650
+ const duration = performance.now() - ctx._startTime || 0;
6651
+ if (!response) {
6652
+ if (ctx.isUpgraded) {
6653
+ response = {
6654
+ status: 101,
6655
+ headers: {}
6656
+ };
6657
+ } else {
6658
+ return;
6659
+ }
5440
6660
  }
5441
6661
  const isError = response.status >= 400;
5442
6662
  this.metricsCollector.recordRequest(duration, isError);
6663
+ if (!this.broadcastTimer) {
6664
+ this.broadcastTimer = setTimeout(() => {
6665
+ this.broadcastMetrics();
6666
+ this.broadcastTimer = void 0;
6667
+ }, 100);
6668
+ }
5443
6669
  if (response.status >= 400) {
5444
6670
  this.metrics.failedRequests++;
5445
6671
  if (response.status === 429) {
@@ -5447,20 +6673,28 @@ class Dashboard {
5447
6673
  this.metrics.rateLimitedCounts[path] = (this.metrics.rateLimitedCounts[path] || 0) + 1;
5448
6674
  }
5449
6675
  try {
5450
- const headers = {};
6676
+ const headers2 = {};
5451
6677
  if (ctx.request.headers && typeof ctx.request.headers.forEach === "function") {
5452
6678
  ctx.request.headers.forEach((v, k) => {
5453
- headers[k] = v;
6679
+ headers2[k] = v;
6680
+ });
6681
+ }
6682
+ const resHeaders2 = {};
6683
+ if (response.headers && typeof response.headers.forEach === "function") {
6684
+ response.headers.forEach((v, k) => {
6685
+ resHeaders2[k] = v;
5454
6686
  });
5455
6687
  }
5456
- await this.db.upsert(new RecordId("failed_requests", ctx.requestId), {
6688
+ await this.db.upsert(new RecordId(`failed_request`, ctx.requestId), {
5457
6689
  method: ctx.method,
5458
6690
  url: ctx.url.toString(),
5459
- headers,
6691
+ headers: headers2,
5460
6692
  status: response.status,
5461
6693
  timestamp: Date.now(),
5462
- state: ctx.state
5463
- // body?
6694
+ state: ctx.state,
6695
+ body: this.serializeBody(ctx.bodyData || ctx.requestBody),
6696
+ responseHeaders: resHeaders2,
6697
+ responseBody: this.serializeBody(ctx.responseBody)
5464
6698
  });
5465
6699
  } catch (e) {
5466
6700
  console.error("Failed to record failed request", e);
@@ -5468,17 +6702,58 @@ class Dashboard {
5468
6702
  } else {
5469
6703
  this.metrics.successfulRequests++;
5470
6704
  }
6705
+ const urlObj = new URL(ctx.url.toString());
6706
+ const cookieHeader = ctx.request.headers.get("cookie") || "";
6707
+ const cookiesCount = cookieHeader ? cookieHeader.split(";").length : 0;
6708
+ const headers = {};
6709
+ if (ctx.request.headers && typeof ctx.request.headers.forEach === "function") {
6710
+ ctx.request.headers.forEach((v, k) => {
6711
+ headers[k] = v;
6712
+ });
6713
+ }
6714
+ const resHeaders = {};
6715
+ if (response.headers && typeof response.headers.forEach === "function") {
6716
+ response.headers.forEach((v, k) => {
6717
+ resHeaders[k] = v;
6718
+ });
6719
+ }
6720
+ const responseHeadersSize = Object.entries(response.headers || {}).reduce((acc, [k, v]) => acc + k.length + String(v).length + 2, 0);
6721
+ const responseSize = ctx.responseBody ? String(ctx.responseBody).length : 0;
6722
+ const remoteIP = ctx.request.headers.get("x-forwarded-for") || ctx.req?.socket?.remoteAddress;
5471
6723
  const logEntry = {
5472
6724
  method: ctx.method,
5473
6725
  url: ctx.url.toString(),
5474
6726
  status: response.status,
5475
6727
  duration,
5476
6728
  timestamp: Date.now(),
5477
- handlerStack: ctx.handlerStack
6729
+ handlerStack: this.serializeHandlerStack(ctx.handlerStack),
6730
+ body: this.serializeBody(ctx.responseBody),
6731
+ requestBody: ctx.bodyData || ctx.requestBody,
6732
+ // ShokupanContext usually stores parsed body here if parsed
6733
+ contentType: response.headers["content-type"] || response.headers["Content-Type"],
6734
+ type: "xhr",
6735
+ direction: "inbound",
6736
+ size: responseSize,
6737
+ protocol: ctx.req?.httpVersion,
6738
+ // Try to get protocol from raw request if available, Bun might expose it
6739
+ domain: urlObj.hostname,
6740
+ path: urlObj.pathname,
6741
+ scheme: urlObj.protocol.replace(":", ""),
6742
+ cookies: cookiesCount,
6743
+ transferred: responseSize + responseHeadersSize,
6744
+ remoteIP,
6745
+ requestHeaders: headers,
6746
+ responseHeaders: resHeaders
5478
6747
  };
5479
6748
  this.metrics.logs.push(logEntry);
5480
6749
  try {
5481
- await this.db.upsert(new RecordId("requests", ctx.requestId), logEntry);
6750
+ await this.db.query("UPSERT $id CONTENT $data", {
6751
+ id: new RecordId("request", ctx.requestId),
6752
+ data: {
6753
+ ...logEntry,
6754
+ direction: "inbound"
6755
+ }
6756
+ });
5482
6757
  } catch (e) {
5483
6758
  console.error("Failed to record request log", e);
5484
6759
  }
@@ -5487,9 +6762,49 @@ class Dashboard {
5487
6762
  if (this.metrics.logs.length > 0 && this.metrics.logs[0].timestamp < cutoff) {
5488
6763
  this.metrics.logs = this.metrics.logs.filter((log) => log.timestamp >= cutoff);
5489
6764
  }
6765
+ const requestData = {
6766
+ id: ctx.requestId,
6767
+ ...logEntry
6768
+ };
6769
+ const strategy = this.dashboardConfig.updateStrategy || "immediate";
6770
+ if (strategy === "immediate") {
6771
+ this.broadcastRequestUpdates([requestData]);
6772
+ } else {
6773
+ this.requestsBuffer.push(requestData);
6774
+ }
5490
6775
  }
5491
6776
  };
5492
6777
  }
6778
+ startRequestPushTimer() {
6779
+ const interval = this.dashboardConfig.updateInterval || 1e4;
6780
+ this.requestPushTimer = setInterval(() => {
6781
+ if (this.requestsBuffer.length > 0) {
6782
+ this.broadcastRequestUpdates();
6783
+ }
6784
+ }, interval);
6785
+ }
6786
+ broadcastRequestUpdates(requestsOverride) {
6787
+ if (this.clients.size === 0) {
6788
+ if (!requestsOverride) this.requestsBuffer = [];
6789
+ return;
6790
+ }
6791
+ let requests;
6792
+ if (requestsOverride) {
6793
+ requests = requestsOverride;
6794
+ } else {
6795
+ requests = [...this.requestsBuffer];
6796
+ this.requestsBuffer = [];
6797
+ }
6798
+ if (requests.length === 0) return;
6799
+ console.log(`[Dashboard] Broadcasting ${requests.length} requests. Sample ID: ${requests[0].id}`);
6800
+ const data = JSON.stringify({
6801
+ type: "requests-update",
6802
+ requests
6803
+ });
6804
+ for (const client of this.clients) {
6805
+ client.send(data);
6806
+ }
6807
+ }
5493
6808
  updateTiming(duration) {
5494
6809
  const alpha = 0.1;
5495
6810
  if (this.metrics.averageTotalTime_ms === 0) {
@@ -5502,6 +6817,39 @@ class Dashboard {
5502
6817
  this.metrics.recentTimings.shift();
5503
6818
  }
5504
6819
  }
6820
+ serializeHandlerStack(stack) {
6821
+ if (!stack || !Array.isArray(stack)) return [];
6822
+ return stack.map((item) => ({
6823
+ name: item.name,
6824
+ file: item.file,
6825
+ line: item.line,
6826
+ duration: item.duration,
6827
+ startTime: item.startTime,
6828
+ isBuiltin: item.isBuiltin
6829
+ // stateChanges: item.stateChanges // Exclude complex objects for now
6830
+ }));
6831
+ }
6832
+ serializeBody(body) {
6833
+ if (!body) return void 0;
6834
+ if (typeof body === "string") {
6835
+ if (body.length > 524288) {
6836
+ return body.substring(0, 524288) + "... (truncated)";
6837
+ }
6838
+ return body;
6839
+ }
6840
+ if (typeof body === "object") {
6841
+ try {
6842
+ const str = JSON.stringify(body);
6843
+ if (str.length > 524288) {
6844
+ return str.substring(0, 524288) + "... (truncated)";
6845
+ }
6846
+ return body;
6847
+ } catch (e) {
6848
+ return "[Circular or Non-Serializable Body]";
6849
+ }
6850
+ }
6851
+ return "[Binary or Unreadable Body]";
6852
+ }
5505
6853
  }
5506
6854
  function unknownError(ctx) {
5507
6855
  return ctx.json({ error: "Unknown Error" }, 500);
@@ -5585,6 +6933,12 @@ class ScalarPlugin extends ShokupanRouter {
5585
6933
  pluginOptions.config ??= {};
5586
6934
  super();
5587
6935
  this.pluginOptions = pluginOptions;
6936
+ this.metadata = {
6937
+ file: import.meta.file,
6938
+ line: 1,
6939
+ name: "ScalarPlugin",
6940
+ pluginName: "Scalar"
6941
+ };
5588
6942
  this.initRoutes();
5589
6943
  }
5590
6944
  eta;
@@ -5603,34 +6957,73 @@ class ScalarPlugin extends ShokupanRouter {
5603
6957
  }
5604
6958
  initRoutes() {
5605
6959
  const bootId = Date.now().toString();
5606
- this.get("/_lifecycle", (ctx) => ctx.json({ boot: bootId }));
6960
+ this.get("/_lifecycle", (ctx) => {
6961
+ const success = ctx.upgrade({
6962
+ data: {
6963
+ bootId,
6964
+ handler: {
6965
+ open: (ws) => {
6966
+ ws.send(JSON.stringify({ type: "hello", bootId }));
6967
+ }
6968
+ }
6969
+ }
6970
+ });
6971
+ if (success) return void 0;
6972
+ return ctx.json({ boot: bootId });
6973
+ });
5607
6974
  this.get("/", async (ctx) => {
5608
6975
  await this.ensureEta();
5609
- let path = ctx.url.toString();
6976
+ let path = ctx.path;
5610
6977
  if (!path.endsWith("/")) path += "/";
5611
6978
  const devScript = ctx.app?.applicationConfig.development ? `
5612
6979
  <script>
5613
6980
  (function() {
5614
6981
  const bootId = "${bootId}";
5615
- let isDown = false;
6982
+ let ws;
6983
+ let reconnectTimer;
5616
6984
 
5617
- setInterval(async () => {
5618
- try {
5619
- const res = await fetch('${path}_lifecycle');
5620
- if (!res.ok) throw new Error('Down');
5621
- const data = await res.json();
5622
- if (data.boot !== bootId) {
5623
- console.log('Server restarted, reloading...');
5624
- window.location.reload();
5625
- }
5626
- else if (isDown) {
5627
- isDown = false;
6985
+ function connect() {
6986
+ const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
6987
+ const wsUrl = protocol + '//' + window.location.host + '${path}_lifecycle';
6988
+
6989
+ ws = new WebSocket(wsUrl);
6990
+
6991
+ ws.onopen = () => {
6992
+ console.log('[Scalar] Connected to lifecycle monitor');
6993
+ if (reconnectTimer) {
6994
+ clearTimeout(reconnectTimer);
6995
+ reconnectTimer = undefined;
5628
6996
  }
5629
- } catch (e) {
5630
- isDown = true;
5631
- console.log('Connection lost...');
5632
- }
5633
- }, 2000);
6997
+ };
6998
+
6999
+ ws.onmessage = (event) => {
7000
+ try {
7001
+ const data = JSON.parse(event.data);
7002
+ if (data.type === 'hello') {
7003
+ if (data.bootId !== bootId) {
7004
+ console.log('[Scalar] Server restarted (timestamp change), reloading...');
7005
+ window.location.reload();
7006
+ }
7007
+ }
7008
+ } catch (e) {}
7009
+ };
7010
+
7011
+ ws.onclose = () => {
7012
+ console.log('[Scalar] Lifecycle connection lost');
7013
+ ws = undefined;
7014
+ scheduleReconnect();
7015
+ };
7016
+ }
7017
+
7018
+ function scheduleReconnect() {
7019
+ if (reconnectTimer) return;
7020
+ reconnectTimer = setTimeout(() => {
7021
+ reconnectTimer = undefined;
7022
+ connect();
7023
+ }, 2000);
7024
+ }
7025
+
7026
+ connect();
5634
7027
  })();
5635
7028
  <\/script>
5636
7029
  ` : "";
@@ -5824,10 +7217,13 @@ function Cors(options = {}) {
5824
7217
  };
5825
7218
  const opts = { ...defaults2, ...options };
5826
7219
  const corsMiddleware = async function CorsMiddleware(ctx, next) {
5827
- const headers = new Headers();
7220
+ const headers = {};
5828
7221
  const origin = ctx.headers.get("origin");
5829
- const set = (k, v) => headers.set(k, v);
5830
- const append = (k, v) => headers.append(k, v);
7222
+ const set = (k, v) => headers[k] = v;
7223
+ const append = (k, v) => {
7224
+ const current = headers[k];
7225
+ headers[k] = current ? current + "," + v : v;
7226
+ };
5831
7227
  if (origin === "null" && opts.origin !== "null") {
5832
7228
  return next();
5833
7229
  }
@@ -5886,10 +7282,10 @@ function Cors(options = {}) {
5886
7282
  }
5887
7283
  const response = await next();
5888
7284
  if (response instanceof Response) {
5889
- const headerEntries = Object.entries(headers);
5890
- for (let i = 0; i < headerEntries.length; i++) {
5891
- const [key, value] = headerEntries[i];
5892
- response.headers.set(key, value);
7285
+ const keys = Object.keys(headers);
7286
+ for (let i = 0; i < keys.length; i++) {
7287
+ const key = keys[i];
7288
+ response.headers.set(key, headers[key]);
5893
7289
  }
5894
7290
  }
5895
7291
  return response;