shokupan 0.10.5 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/dist/{analyzer-CKLGLFtx.cjs → analyzer-BAhvpNY_.cjs} +2 -7
  2. package/dist/{analyzer-CKLGLFtx.cjs.map → analyzer-BAhvpNY_.cjs.map} +1 -1
  3. package/dist/{analyzer-BqIe1p0R.js → analyzer-CnKnQ5KV.js} +3 -8
  4. package/dist/{analyzer-BqIe1p0R.js.map → analyzer-CnKnQ5KV.js.map} +1 -1
  5. package/dist/{analyzer.impl-D9Yi1Hax.cjs → analyzer.impl-CfpMu4-g.cjs} +586 -40
  6. package/dist/analyzer.impl-CfpMu4-g.cjs.map +1 -0
  7. package/dist/{analyzer.impl-CV6W1Eq7.js → analyzer.impl-DCiqlXI5.js} +586 -40
  8. package/dist/analyzer.impl-DCiqlXI5.js.map +1 -0
  9. package/dist/cli.cjs +206 -18
  10. package/dist/cli.cjs.map +1 -1
  11. package/dist/cli.js +206 -18
  12. package/dist/cli.js.map +1 -1
  13. package/dist/context.d.ts +6 -1
  14. package/dist/index.cjs +2339 -984
  15. package/dist/index.cjs.map +1 -1
  16. package/dist/index.js +2336 -982
  17. package/dist/index.js.map +1 -1
  18. package/dist/plugins/application/api-explorer/static/explorer-client.mjs +375 -29
  19. package/dist/plugins/application/api-explorer/static/style.css +327 -8
  20. package/dist/plugins/application/api-explorer/static/theme.css +7 -2
  21. package/dist/plugins/application/asyncapi/generator.d.ts +4 -0
  22. package/dist/plugins/application/asyncapi/static/asyncapi-client.mjs +154 -22
  23. package/dist/plugins/application/asyncapi/static/style.css +24 -8
  24. package/dist/plugins/application/dashboard/fetch-interceptor.d.ts +107 -0
  25. package/dist/plugins/application/dashboard/metrics-collector.d.ts +38 -2
  26. package/dist/plugins/application/dashboard/plugin.d.ts +44 -1
  27. package/dist/plugins/application/dashboard/static/charts.js +127 -62
  28. package/dist/plugins/application/dashboard/static/client.js +160 -0
  29. package/dist/plugins/application/dashboard/static/graph.mjs +167 -56
  30. package/dist/plugins/application/dashboard/static/reactflow.css +20 -10
  31. package/dist/plugins/application/dashboard/static/registry.js +112 -8
  32. package/dist/plugins/application/dashboard/static/requests.js +868 -58
  33. package/dist/plugins/application/dashboard/static/styles.css +186 -14
  34. package/dist/plugins/application/dashboard/static/tabs.js +44 -9
  35. package/dist/plugins/application/dashboard/static/theme.css +7 -2
  36. package/dist/plugins/application/openapi/analyzer.impl.d.ts +61 -1
  37. package/dist/plugins/application/openapi/openapi.d.ts +3 -0
  38. package/dist/plugins/application/shared/ast-utils.d.ts +7 -0
  39. package/dist/router.d.ts +55 -16
  40. package/dist/shokupan.d.ts +7 -2
  41. package/dist/util/adapter/adapters.d.ts +19 -0
  42. package/dist/util/adapter/filesystem.d.ts +20 -0
  43. package/dist/util/controller-scanner.d.ts +4 -0
  44. package/dist/util/cpu-monitor.d.ts +2 -0
  45. package/dist/util/middleware-tracker.d.ts +10 -0
  46. package/dist/util/types.d.ts +37 -0
  47. package/package.json +5 -5
  48. package/dist/analyzer.impl-CV6W1Eq7.js.map +0 -1
  49. package/dist/analyzer.impl-D9Yi1Hax.cjs.map +0 -1
  50. package/dist/http-server-BEMPIs33.cjs +0 -85
  51. package/dist/http-server-BEMPIs33.cjs.map +0 -1
  52. package/dist/http-server-CCeagTyU.js +0 -68
  53. package/dist/http-server-CCeagTyU.js.map +0 -1
  54. package/dist/plugins/application/dashboard/static/poll.js +0 -146
package/dist/index.cjs CHANGED
@@ -25,23 +25,26 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
25
25
  const nanoid = require("nanoid");
26
26
  const promises = require("node:fs/promises");
27
27
  const node_util = require("node:util");
28
- const surrealdb = require("surrealdb");
29
28
  const eta$1 = require("eta");
30
29
  const promises$1 = require("fs/promises");
31
30
  const path = require("path");
32
31
  const api = require("@opentelemetry/api");
32
+ const surrealdb = require("surrealdb");
33
33
  const jsYaml = require("js-yaml");
34
+ const http$1 = require("node:http");
35
+ require("node:https");
34
36
  const node_async_hooks = require("node:async_hooks");
35
- const os = require("node:os");
36
37
  const path$1 = require("node:path");
37
38
  const node_url = require("node:url");
38
39
  const renderToString = require("preact-render-to-string");
39
40
  const jsxRuntime = require("preact/jsx-runtime");
40
41
  const cluster = require("node:cluster");
41
42
  const net = require("node:net");
43
+ const os = require("node:os");
44
+ const node_module = require("node:module");
42
45
  const node_perf_hooks = require("node:perf_hooks");
43
- const fs = require("node:fs");
44
- const analyzer = require("./analyzer-CKLGLFtx.cjs");
46
+ const fs$1 = require("node:fs");
47
+ const analyzer = require("./analyzer-BAhvpNY_.cjs");
45
48
  const zlib = require("node:zlib");
46
49
  const Ajv = require("ajv");
47
50
  const addFormats = require("ajv-formats");
@@ -64,6 +67,7 @@ function _interopNamespaceDefault(e) {
64
67
  n.default = e;
65
68
  return Object.freeze(n);
66
69
  }
70
+ const http__namespace = /* @__PURE__ */ _interopNamespaceDefault(http$1);
67
71
  const os__namespace = /* @__PURE__ */ _interopNamespaceDefault(os);
68
72
  const zlib__namespace = /* @__PURE__ */ _interopNamespaceDefault(zlib);
69
73
  const HTTP_STATUS = {
@@ -227,12 +231,12 @@ class ShokupanResponse {
227
231
  }
228
232
  const $isApplication = /* @__PURE__ */ Symbol.for("Shokupan.app");
229
233
  const $appRoot = /* @__PURE__ */ Symbol.for("Shokupan.app-root");
230
- const $isMounted = /* @__PURE__ */ Symbol("Shokupan.isMounted");
231
- const $routeMethods = /* @__PURE__ */ Symbol("Shokupan.routeMethods");
232
- const $eventMethods = /* @__PURE__ */ Symbol("Shokupan.eventMethods");
233
- const $routeArgs = /* @__PURE__ */ Symbol("Shokupan.routeArgs");
234
- const $controllerPath = /* @__PURE__ */ Symbol("Shokupan.controllerPath");
235
- const $middleware = /* @__PURE__ */ Symbol("Shokupan.middleware");
234
+ const $isMounted = /* @__PURE__ */ Symbol.for("Shokupan.isMounted");
235
+ const $routeMethods = /* @__PURE__ */ Symbol.for("Shokupan.routeMethods");
236
+ const $eventMethods = /* @__PURE__ */ Symbol.for("Shokupan.eventMethods");
237
+ const $routeArgs = /* @__PURE__ */ Symbol.for("Shokupan.routeArgs");
238
+ const $controllerPath = /* @__PURE__ */ Symbol.for("Shokupan.controllerPath");
239
+ const $middleware = /* @__PURE__ */ Symbol.for("Shokupan.middleware");
236
240
  const $isRouter = /* @__PURE__ */ Symbol.for("Shokupan.router");
237
241
  const $parent = /* @__PURE__ */ Symbol.for("Shokupan.parent");
238
242
  const $childRouters = /* @__PURE__ */ Symbol.for("Shokupan.child-routers");
@@ -269,12 +273,13 @@ function isValidCookieDomain(domain, currentHost) {
269
273
  return false;
270
274
  }
271
275
  class ShokupanContext {
272
- constructor(request, server, state, app, signal, enableMiddlewareTracking = false) {
276
+ constructor(request, server, state, app, signal, enableMiddlewareTracking = false, requestId) {
273
277
  this.request = request;
274
278
  this.server = server;
275
279
  this.app = app;
276
280
  this.signal = signal;
277
281
  this.state = state || {};
282
+ this[$requestId] = requestId;
278
283
  if (enableMiddlewareTracking) {
279
284
  const self = this;
280
285
  this.state = new Proxy(this.state, {
@@ -342,7 +347,10 @@ class ShokupanContext {
342
347
  get requestId() {
343
348
  return this[$requestId] ??= this.app?.applicationConfig?.idGenerator?.() ?? nanoid.nanoid();
344
349
  }
345
- [/* @__PURE__ */ Symbol.for("nodejs.util.inspect.custom")]() {
350
+ [
351
+ // Only apply a custom inspect symbol in Node.js, Deno, or Bun.
352
+ globalThis.navigator?.userAgent?.match(/Node\.js|Deno|Bun/) ? /* @__PURE__ */ Symbol.for("nodejs.util.inspect.custom") : /* @__PURE__ */ Symbol.for("no-op")
353
+ ]() {
346
354
  const innerString = node_util.inspect({
347
355
  method: this.request.method,
348
356
  url: this.request.url,
@@ -477,6 +485,12 @@ class ShokupanContext {
477
485
  get res() {
478
486
  return this.response;
479
487
  }
488
+ /**
489
+ * Get the raw response body content (if available)
490
+ */
491
+ get responseBody() {
492
+ return this[$rawBody];
493
+ }
480
494
  /**
481
495
  * Raw WebSocket connection
482
496
  */
@@ -595,10 +609,10 @@ class ShokupanContext {
595
609
  * The body is only parsed once and cached for subsequent reads.
596
610
  */
597
611
  async body() {
598
- if (this[$bodyParseError]) {
612
+ if (this[$bodyParseError] !== void 0) {
599
613
  throw this[$bodyParseError];
600
614
  }
601
- if (this[$bodyParsed]) {
615
+ if (this[$bodyParsed] === true) {
602
616
  return this[$cachedBody];
603
617
  }
604
618
  const contentType = this.request.headers.get("content-type") || "";
@@ -716,6 +730,7 @@ class ShokupanContext {
716
730
  if (!VALID_HTTP_STATUSES.has(finalStatus)) {
717
731
  throw new Error(`Invalid HTTP status code: ${finalStatus}`);
718
732
  }
733
+ this.response.status = finalStatus;
719
734
  const jsonString = JSON.stringify(data instanceof Promise ? await data : data);
720
735
  this[$rawBody] = jsonString;
721
736
  if (!headers && !this.response.hasPopulatedHeaders) {
@@ -738,6 +753,7 @@ class ShokupanContext {
738
753
  if (this.app.applicationConfig.validateStatusCodes && !VALID_HTTP_STATUSES.has(finalStatus)) {
739
754
  throw new Error(`Invalid HTTP status code: ${finalStatus}`);
740
755
  }
756
+ this.response.status = finalStatus;
741
757
  this[$rawBody] = data instanceof Promise ? await data : data;
742
758
  if (!headers && !this.response.hasPopulatedHeaders) {
743
759
  this[$finalResponse] = new Response(this[$rawBody], {
@@ -759,6 +775,7 @@ class ShokupanContext {
759
775
  if (this.app.applicationConfig.validateStatusCodes && !VALID_HTTP_STATUSES.has(finalStatus)) {
760
776
  throw new Error(`Invalid HTTP status code: ${finalStatus}`);
761
777
  }
778
+ this.response.status = finalStatus;
762
779
  const finalHeaders = this.mergeHeaders(headers);
763
780
  finalHeaders.set("content-type", "text/html; charset=utf-8");
764
781
  this[$rawBody] = html instanceof Promise ? await html : html;
@@ -772,6 +789,7 @@ class ShokupanContext {
772
789
  if (this.app.applicationConfig.validateStatusCodes && !VALID_REDIRECT_STATUSES.has(status)) {
773
790
  throw new Error(`Invalid redirect status code: ${status}`);
774
791
  }
792
+ this.response.status = status;
775
793
  const finalHeaders = this.mergeHeaders();
776
794
  finalHeaders.set("Location", url instanceof Promise ? await url : url);
777
795
  this[$finalResponse] = new Response(null, { status, headers: finalHeaders });
@@ -786,6 +804,7 @@ class ShokupanContext {
786
804
  if (this.app.applicationConfig.validateStatusCodes && !VALID_HTTP_STATUSES.has(status)) {
787
805
  throw new Error(`Invalid HTTP status code: ${status}`);
788
806
  }
807
+ this.response.status = status;
789
808
  const finalHeaders = this.mergeHeaders();
790
809
  this[$finalResponse] = new Response(null, { status, headers: finalHeaders });
791
810
  return this[$finalResponse];
@@ -799,6 +818,7 @@ class ShokupanContext {
799
818
  if (this.app.applicationConfig.validateStatusCodes && !VALID_HTTP_STATUSES.has(status)) {
800
819
  throw new Error(`Invalid HTTP status code: ${status}`);
801
820
  }
821
+ if (status) this.response.status = status;
802
822
  if (typeof Bun !== "undefined") {
803
823
  this[$finalResponse] = new Response(Bun.file(path2, fileOptions), { status, headers: finalHeaders });
804
824
  return this[$finalResponse];
@@ -901,6 +921,91 @@ function deepMerge(target, ...sources) {
901
921
  }
902
922
  return deepMerge(target, ...sources);
903
923
  }
924
+ async function getAstRoutes(applications, options = {}) {
925
+ const { includePrefix = true, pathTransform } = options;
926
+ const astRoutes = [];
927
+ const getExpandedRoutes = (app, prefix = "", seen = /* @__PURE__ */ new Set(), sourceOverride) => {
928
+ if (seen.has(app.name)) return [];
929
+ const newSeen = new Set(seen);
930
+ newSeen.add(app.name);
931
+ const expanded = [];
932
+ let currentPrefix = prefix;
933
+ if (includePrefix && app.controllerPrefix) {
934
+ const cleanPrefix = currentPrefix.endsWith("/") ? currentPrefix.slice(0, -1) : currentPrefix;
935
+ const cleanCont = app.controllerPrefix.startsWith("/") ? app.controllerPrefix : "/" + app.controllerPrefix;
936
+ currentPrefix = cleanPrefix + cleanCont;
937
+ }
938
+ for (const route of app.routes) {
939
+ let path2 = route.path;
940
+ if (includePrefix) {
941
+ const cleanPrefix = currentPrefix.endsWith("/") ? currentPrefix.slice(0, -1) : currentPrefix;
942
+ const cleanPath = path2.startsWith("/") ? path2 : "/" + path2;
943
+ path2 = cleanPrefix + cleanPath;
944
+ if (path2.length > 1 && path2.endsWith("/")) {
945
+ path2 = path2.slice(0, -1);
946
+ }
947
+ }
948
+ if (pathTransform) {
949
+ path2 = pathTransform(path2);
950
+ } else if (includePrefix && !path2.startsWith("/")) {
951
+ path2 = "/" + path2;
952
+ }
953
+ const expandedRoute = {
954
+ ...route,
955
+ path: path2 || "/"
956
+ };
957
+ if (sourceOverride) {
958
+ expandedRoute.sourceContext = sourceOverride;
959
+ }
960
+ expanded.push(expandedRoute);
961
+ }
962
+ if (app.mounted) {
963
+ for (const mount of app.mounted) {
964
+ const targetApp = applications.find((a) => a.name === mount.target || a.className === mount.target);
965
+ if (targetApp) {
966
+ let nextPrefix = "";
967
+ if (includePrefix) {
968
+ const cleanPrefix = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
969
+ const mountPrefix = mount.prefix.startsWith("/") ? mount.prefix : "/" + mount.prefix;
970
+ nextPrefix = cleanPrefix + mountPrefix;
971
+ }
972
+ let nextSourceOverride = sourceOverride;
973
+ if (mount.dependency || mount.targetFilePath && mount.targetFilePath.includes("node_modules")) {
974
+ if (mount.sourceContext) {
975
+ nextSourceOverride = {
976
+ ...mount.sourceContext,
977
+ // Add highlight for the mount line to make it clear
978
+ highlightLines: [mount.sourceContext.startLine, mount.sourceContext.endLine],
979
+ highlights: [{
980
+ startLine: mount.sourceContext.startLine,
981
+ endLine: mount.sourceContext.endLine,
982
+ type: "return-success"
983
+ // Use the success color (cyan) for the mount point
984
+ }]
985
+ };
986
+ }
987
+ }
988
+ expanded.push(...getExpandedRoutes(targetApp, nextPrefix, newSeen, nextSourceOverride));
989
+ }
990
+ }
991
+ }
992
+ return expanded;
993
+ };
994
+ applications.forEach((app) => {
995
+ astRoutes.push(...getExpandedRoutes(app));
996
+ });
997
+ const dedupedRoutes = /* @__PURE__ */ new Map();
998
+ for (const route of astRoutes) {
999
+ const key = `${route.method.toUpperCase()}:${route.path}`;
1000
+ let score = 0;
1001
+ if (route.responseSchema) score += 10;
1002
+ if (route.handlerSource) score += 5;
1003
+ if (!dedupedRoutes.has(key) || score > dedupedRoutes.get(key).score) {
1004
+ dedupedRoutes.set(key, { route, score });
1005
+ }
1006
+ }
1007
+ return Array.from(dedupedRoutes.values()).map((v) => v.route);
1008
+ }
904
1009
  const REGEX_PATTERNS = {
905
1010
  QUERY_INT: /parseInt\(ctx\.query\.(\w+)\)/g,
906
1011
  QUERY_FLOAT: /parseFloat\(ctx\.query\.(\w+)\)/g,
@@ -1040,90 +1145,43 @@ function analyzeHandler(handler) {
1040
1145
  }
1041
1146
  return { inferredSpec };
1042
1147
  }
1043
- async function getAstRoutes$1(applications) {
1044
- const astRoutes = [];
1045
- const getExpandedRoutes = (app, prefix = "", seen = /* @__PURE__ */ new Set(), sourceOverride) => {
1046
- if (seen.has(app.name)) return [];
1047
- const newSeen = new Set(seen);
1048
- newSeen.add(app.name);
1049
- const expanded = [];
1050
- let currentPrefix = prefix;
1051
- if (app.controllerPrefix) {
1052
- const cleanPrefix = currentPrefix.endsWith("/") ? currentPrefix.slice(0, -1) : currentPrefix;
1053
- const cleanCont = app.controllerPrefix.startsWith("/") ? app.controllerPrefix : "/" + app.controllerPrefix;
1054
- currentPrefix = cleanPrefix + cleanCont;
1055
- }
1056
- for (const route of app.routes) {
1057
- const cleanPrefix = currentPrefix.endsWith("/") ? currentPrefix.slice(0, -1) : currentPrefix;
1058
- const cleanPath = route.path.startsWith("/") ? route.path : "/" + route.path;
1059
- let joined = cleanPrefix + cleanPath;
1060
- if (joined.length > 1 && joined.endsWith("/")) {
1061
- joined = joined.slice(0, -1);
1062
- }
1063
- const expandedRoute = {
1064
- ...route,
1065
- path: joined || "/"
1066
- };
1067
- if (sourceOverride) {
1068
- expandedRoute.sourceContext = sourceOverride;
1069
- }
1070
- expanded.push(expandedRoute);
1071
- }
1072
- if (app.mounted) {
1073
- for (const mount of app.mounted) {
1074
- const targetApp = applications.find((a) => a.name === mount.target || a.className === mount.target);
1075
- if (targetApp) {
1076
- const cleanPrefix = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
1077
- const mountPrefix = mount.prefix.startsWith("/") ? mount.prefix : "/" + mount.prefix;
1078
- let nextSourceOverride = sourceOverride;
1079
- if (mount.dependency || mount.targetFilePath && mount.targetFilePath.includes("node_modules")) {
1080
- if (mount.sourceContext) {
1081
- nextSourceOverride = {
1082
- ...mount.sourceContext,
1083
- // Add highlight for the mount line to make it clear
1084
- highlightLines: [mount.sourceContext.startLine, mount.sourceContext.endLine],
1085
- highlights: [{
1086
- startLine: mount.sourceContext.startLine,
1087
- endLine: mount.sourceContext.endLine,
1088
- type: "return-success"
1089
- // Use the success color (cyan) for the mount point
1090
- }]
1091
- };
1092
- }
1093
- }
1094
- expanded.push(...getExpandedRoutes(targetApp, cleanPrefix + mountPrefix, newSeen, nextSourceOverride));
1095
- }
1096
- }
1097
- }
1098
- return expanded;
1099
- };
1100
- applications.forEach((app) => {
1101
- astRoutes.push(...getExpandedRoutes(app));
1102
- });
1103
- const dedupedRoutes = /* @__PURE__ */ new Map();
1104
- for (const route of astRoutes) {
1105
- const key = `${route.method.toUpperCase()}:${route.path}`;
1106
- let score = 0;
1107
- if (route.responseSchema) score += 10;
1108
- if (route.handlerSource) score += 5;
1109
- if (!dedupedRoutes.has(key) || score > dedupedRoutes.get(key).score) {
1110
- dedupedRoutes.set(key, { route, score });
1111
- }
1112
- }
1113
- return Array.from(dedupedRoutes.values()).map((v) => v.route);
1114
- }
1115
1148
  async function generateOpenApi(rootRouter, options = {}) {
1116
1149
  const paths = {};
1117
1150
  const tagGroups = /* @__PURE__ */ new Map();
1118
1151
  const defaultTagGroup = options.defaultTagGroup || "General";
1119
1152
  const defaultTagName = options.defaultTag || "Application";
1120
1153
  let astRoutes = [];
1154
+ let astMiddlewareRegistry = {};
1155
+ let applications = [];
1121
1156
  try {
1122
- const { OpenAPIAnalyzer } = await Promise.resolve().then(() => require("./analyzer-CKLGLFtx.cjs"));
1123
- const analyzer2 = new OpenAPIAnalyzer(process.cwd());
1124
- const { applications } = await analyzer2.analyze();
1125
- astRoutes = await getAstRoutes$1(applications);
1157
+ const { OpenAPIAnalyzer } = await Promise.resolve().then(() => require("./analyzer-BAhvpNY_.cjs"));
1158
+ const entrypoint = rootRouter.metadata?.file;
1159
+ const analyzer2 = new OpenAPIAnalyzer(process.cwd(), entrypoint);
1160
+ const analysisResult = await analyzer2.analyze();
1161
+ applications = analysisResult.applications;
1162
+ astRoutes = await getAstRoutes(applications);
1163
+ let middlewareId = 0;
1164
+ for (const app of applications) {
1165
+ if (app.middleware && app.middleware.length > 0) {
1166
+ for (const mw of app.middleware) {
1167
+ const id = `middleware-${middlewareId++}`;
1168
+ astMiddlewareRegistry[id] = {
1169
+ ...mw,
1170
+ id,
1171
+ usedBy: []
1172
+ // Will be populated when processing routes
1173
+ };
1174
+ }
1175
+ }
1176
+ }
1126
1177
  } catch (e) {
1178
+ if (options.warnings) {
1179
+ options.warnings.push({
1180
+ type: "ast-analysis-failed",
1181
+ message: "AST Analysis failed or skipped",
1182
+ detail: e.message
1183
+ });
1184
+ }
1127
1185
  }
1128
1186
  const collect = (router, prefix = "", currentGroup = defaultTagGroup, defaultTag = defaultTagName, inheritedMiddleware = [], isRootLevel = true) => {
1129
1187
  let group = currentGroup;
@@ -1133,7 +1191,8 @@ async function generateOpenApi(rootRouter, options = {}) {
1133
1191
  tag = router.config.name;
1134
1192
  } else {
1135
1193
  const mountPath = router[$mountPath];
1136
- if (isRootLevel && mountPath && mountPath !== "/") {
1194
+ const isDirectChild = router[$parent] === rootRouter;
1195
+ if ((isRootLevel || isDirectChild) && mountPath && mountPath !== "/") {
1137
1196
  const segments = mountPath.split("/").filter(Boolean);
1138
1197
  if (segments.length > 0) {
1139
1198
  const lastSegment = segments[segments.length - 1];
@@ -1141,6 +1200,20 @@ async function generateOpenApi(rootRouter, options = {}) {
1141
1200
  }
1142
1201
  }
1143
1202
  }
1203
+ let isBuiltinPlugin = false;
1204
+ let pluginName = "";
1205
+ if (router.metadata?.pluginName) {
1206
+ isBuiltinPlugin = true;
1207
+ pluginName = router.metadata.pluginName;
1208
+ tag = pluginName.replace(/[-_]/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
1209
+ } else if (router.metadata?.file && router.metadata.file.includes("plugins/application/")) {
1210
+ isBuiltinPlugin = true;
1211
+ const match = router.metadata.file.match(/plugins\/application\/([^/]+)/);
1212
+ if (match) {
1213
+ pluginName = match[1].replace(/\.(ts|js|mjs|mts|cjs)$/, "");
1214
+ tag = pluginName.replace(/[-_]/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
1215
+ }
1216
+ }
1144
1217
  if (!tagGroups.has(group)) tagGroups.set(group, /* @__PURE__ */ new Set());
1145
1218
  const routerMiddleware = router.middleware || [];
1146
1219
  const routes = router[$routes] || [];
@@ -1163,11 +1236,45 @@ async function generateOpenApi(rootRouter, options = {}) {
1163
1236
  };
1164
1237
  const routeMiddleware = route.middleware || [];
1165
1238
  const allMiddleware = [...inheritedMiddleware, ...routerMiddleware, ...routeMiddleware];
1166
- if (allMiddleware.length > 0) {
1167
- operation["x-shokupan-middleware"] = allMiddleware.map((mw) => ({
1168
- name: mw.name || "middleware",
1169
- metadata: mw.metadata
1170
- }));
1239
+ const astMiddlewareForRoute = [];
1240
+ for (const [mwId, mw] of Object.entries(astMiddlewareRegistry)) {
1241
+ const appForRoute = applications.find(
1242
+ (app) => app.routes?.some((r) => r.path === fullPath && r.method === route.method.toUpperCase())
1243
+ );
1244
+ const appForMiddleware = applications.find(
1245
+ (app) => app.middleware?.some((m) => m.name === mw.name && m.file === mw.file)
1246
+ );
1247
+ if (appForRoute && appForMiddleware && appForRoute.filePath === appForMiddleware.filePath) {
1248
+ astMiddlewareForRoute.push({ ...mw, id: mwId });
1249
+ if (!mw.usedBy.includes(fullPath)) {
1250
+ mw.usedBy.push(fullPath);
1251
+ }
1252
+ }
1253
+ }
1254
+ for (const astMw of astMiddlewareForRoute) {
1255
+ if (astMw.responseTypes) {
1256
+ for (const [statusCode, responseSpec] of Object.entries(astMw.responseTypes)) {
1257
+ if (!operation.responses[statusCode]) {
1258
+ operation.responses[statusCode] = responseSpec;
1259
+ }
1260
+ }
1261
+ }
1262
+ }
1263
+ if (allMiddleware.length > 0 || astMiddlewareForRoute.length > 0) {
1264
+ operation["x-shokupan-middleware"] = [
1265
+ ...allMiddleware.map((mw) => ({
1266
+ name: mw.name || "middleware",
1267
+ metadata: mw.metadata
1268
+ })),
1269
+ ...astMiddlewareForRoute.map((mw) => ({
1270
+ id: mw.id,
1271
+ name: mw.name,
1272
+ responses: mw.responseTypes,
1273
+ headers: mw.headers,
1274
+ file: mw.file,
1275
+ line: mw.startLine
1276
+ }))
1277
+ ];
1171
1278
  }
1172
1279
  if (route.guards) {
1173
1280
  for (const guard of route.guards) {
@@ -1233,6 +1340,18 @@ async function generateOpenApi(rootRouter, options = {}) {
1233
1340
  description: "Successful response",
1234
1341
  content: { "application/json": { schema: astMatch.responseSchema } }
1235
1342
  };
1343
+ if (astMatch.hasUnknownFields) {
1344
+ if (options.warnings) {
1345
+ options.warnings.push({
1346
+ type: "unknown-fields",
1347
+ message: "Response contains fields with unknown types",
1348
+ detail: `Route: ${fullPath} [${route.method}]`,
1349
+ location: { file: astMatch.sourceContext?.file, line: astMatch.sourceContext?.startLine }
1350
+ });
1351
+ }
1352
+ operation["x-warning"] = true;
1353
+ operation["x-warning-reason"] = "Response contains fields with unknown types that could not be statically analyzed";
1354
+ }
1236
1355
  } else if (astMatch.responseType) {
1237
1356
  let contentType = "application/json";
1238
1357
  if (astMatch.responseType === "string") contentType = "text/plain";
@@ -1252,6 +1371,14 @@ async function generateOpenApi(rootRouter, options = {}) {
1252
1371
  operation.parameters = params;
1253
1372
  }
1254
1373
  } else {
1374
+ if (options.warnings) {
1375
+ options.warnings.push({
1376
+ type: "route-not-found",
1377
+ message: "Route could not be statically analyzed",
1378
+ detail: `Route: ${fullPath} [${route.method}]`,
1379
+ location: route.metadata ? { file: route.metadata.file, line: route.metadata.line } : void 0
1380
+ });
1381
+ }
1255
1382
  const runtimeSource = (route.handler.originalHandler || route.handler).toString();
1256
1383
  let file;
1257
1384
  let line;
@@ -1268,10 +1395,22 @@ async function generateOpenApi(rootRouter, options = {}) {
1268
1395
  operation["x-shokupan-source"] = {
1269
1396
  file,
1270
1397
  line: line || 1,
1271
- code: runtimeSource
1398
+ code: runtimeSource,
1399
+ pluginName: route.handler.pluginName
1400
+ // Inject pluginName from handler
1272
1401
  };
1273
1402
  }
1274
1403
  }
1404
+ if (isBuiltinPlugin) {
1405
+ operation["x-shokupan-builtin"] = true;
1406
+ }
1407
+ if (route.handler.pluginName) {
1408
+ operation["x-shokupan-plugin-name"] = route.handler.pluginName;
1409
+ if (!operation["x-shokupan-source"]) operation["x-shokupan-source"] = {};
1410
+ operation["x-shokupan-source"].pluginName = route.handler.pluginName;
1411
+ } else if (pluginName) {
1412
+ operation["x-shokupan-plugin-name"] = pluginName;
1413
+ }
1275
1414
  if (route.keys.length > 0) {
1276
1415
  const pathParams = route.keys.map((key) => ({
1277
1416
  name: key,
@@ -1348,7 +1487,46 @@ async function generateOpenApi(rootRouter, options = {}) {
1348
1487
  for (const [name, tags] of tagGroups.entries()) {
1349
1488
  xTagGroups.push({ name, tags: Array.from(tags).sort() });
1350
1489
  }
1351
- return {
1490
+ if (!options.compliant) {
1491
+ for (const [id, mw] of Object.entries(astMiddlewareRegistry || {})) {
1492
+ const virtualPath = `/_middleware/${id}`;
1493
+ paths[virtualPath] = {
1494
+ get: {
1495
+ tags: ["System", "Middleware"],
1496
+ summary: `Middleware: ${mw.name}`,
1497
+ description: `Virtual endpoint for middleware analysis.
1498
+ **File**: ${mw.file}
1499
+ **Line**: ${mw.startLine}`,
1500
+ operationId: `getMiddleware_${id}`,
1501
+ parameters: [],
1502
+ responses: {
1503
+ "200": {
1504
+ description: "Middleware Analysis",
1505
+ content: {
1506
+ "application/json": {
1507
+ schema: { type: "object" }
1508
+ }
1509
+ }
1510
+ }
1511
+ },
1512
+ "x-middleware-metadata": mw,
1513
+ "x-virtual": true,
1514
+ "x-middleware-detail": true,
1515
+ "x-source-info": {
1516
+ file: mw.file,
1517
+ line: mw.startLine,
1518
+ code: mw.snippet
1519
+ },
1520
+ "x-shokupan-source": {
1521
+ file: mw.file,
1522
+ line: mw.startLine,
1523
+ code: mw.snippet
1524
+ }
1525
+ }
1526
+ };
1527
+ }
1528
+ }
1529
+ const spec = {
1352
1530
  openapi: "3.1.0",
1353
1531
  info: { title: "Shokupan API", version: "1.0.0", ...options.info },
1354
1532
  paths,
@@ -1356,8 +1534,29 @@ async function generateOpenApi(rootRouter, options = {}) {
1356
1534
  servers: options.servers,
1357
1535
  tags: options.tags,
1358
1536
  externalDocs: options.externalDocs,
1359
- "x-tagGroups": xTagGroups
1537
+ "x-tagGroups": xTagGroups,
1538
+ "x-middleware-registry": astMiddlewareRegistry
1360
1539
  };
1540
+ if (options.compliant) {
1541
+ spec["x-tagGroups"] = void 0;
1542
+ spec["x-middleware-registry"] = void 0;
1543
+ const stripExtensions = (obj) => {
1544
+ if (!obj || typeof obj !== "object") return;
1545
+ if (Array.isArray(obj)) {
1546
+ obj.forEach(stripExtensions);
1547
+ return;
1548
+ }
1549
+ for (const key of Object.keys(obj)) {
1550
+ if (key.startsWith("x-")) {
1551
+ delete obj[key];
1552
+ } else {
1553
+ stripExtensions(obj[key]);
1554
+ }
1555
+ }
1556
+ };
1557
+ stripExtensions(spec);
1558
+ }
1559
+ return spec;
1361
1560
  }
1362
1561
  const eta = new eta$1.Eta();
1363
1562
  function serveStatic(config, prefix) {
@@ -1544,35 +1743,6 @@ function Inject(token) {
1544
1743
  });
1545
1744
  };
1546
1745
  }
1547
- class HttpError extends Error {
1548
- status;
1549
- constructor(message, status) {
1550
- super(message);
1551
- this.name = "HttpError";
1552
- this.status = status;
1553
- if (Error.captureStackTrace) {
1554
- Error.captureStackTrace(this, HttpError);
1555
- }
1556
- }
1557
- }
1558
- function getErrorStatus(err) {
1559
- if (!err || typeof err !== "object") {
1560
- return 500;
1561
- }
1562
- if (typeof err.status === "number") {
1563
- return err.status;
1564
- }
1565
- if (typeof err.statusCode === "number") {
1566
- return err.statusCode;
1567
- }
1568
- return 500;
1569
- }
1570
- class EventError extends HttpError {
1571
- constructor(message = "Event Error") {
1572
- super(message, 500);
1573
- this.name = "EventError";
1574
- }
1575
- }
1576
1746
  const tracer = api.trace.getTracer("shokupan.middleware");
1577
1747
  function traceHandler(fn, name) {
1578
1748
  return async function(...args) {
@@ -1596,31 +1766,6 @@ function traceHandler(fn, name) {
1596
1766
  });
1597
1767
  };
1598
1768
  }
1599
- class ShokupanRequestBase {
1600
- method;
1601
- url;
1602
- headers;
1603
- body;
1604
- async json() {
1605
- return JSON.parse(this.body);
1606
- }
1607
- async text() {
1608
- return this.body;
1609
- }
1610
- async formData() {
1611
- if (this.body instanceof FormData) {
1612
- return this.body;
1613
- }
1614
- return new Response(this.body, { headers: this.headers }).formData();
1615
- }
1616
- constructor(props) {
1617
- Object.assign(this, props);
1618
- if (!(this.headers instanceof Headers)) {
1619
- this.headers = new Headers(this.headers);
1620
- }
1621
- }
1622
- }
1623
- const ShokupanRequest = ShokupanRequestBase;
1624
1769
  function getCallerInfo(skipFrames = 1) {
1625
1770
  let file = "unknown";
1626
1771
  let line = 0;
@@ -1652,29 +1797,381 @@ function getCallerInfo(skipFrames = 1) {
1652
1797
  }
1653
1798
  return { file, line };
1654
1799
  }
1655
- class RouterTrie {
1656
- root;
1657
- constructor() {
1658
- this.root = this.createNode();
1659
- }
1660
- createNode() {
1661
- return {
1662
- children: {}
1663
- };
1664
- }
1665
- insert(method, path2, handler) {
1666
- let node = this.root;
1667
- const segments = this.splitPath(path2);
1668
- for (let i = 0; i < segments.length; i++) {
1669
- const segment = segments[i];
1670
- if (segment === "**") {
1671
- if (!node.recursiveChild) {
1672
- node.recursiveChild = this.createNode();
1673
- }
1674
- node = node.recursiveChild;
1675
- } else if (segment === "*") {
1676
- if (!node.wildcardChild) {
1677
- node.wildcardChild = this.createNode();
1800
+ const HTTPMethods = ["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS", "ALL"];
1801
+ var RouteParamType = /* @__PURE__ */ ((RouteParamType2) => {
1802
+ RouteParamType2["BODY"] = "BODY";
1803
+ RouteParamType2["PARAM"] = "PARAM";
1804
+ RouteParamType2["QUERY"] = "QUERY";
1805
+ RouteParamType2["HEADER"] = "HEADER";
1806
+ RouteParamType2["REQUEST"] = "REQUEST";
1807
+ RouteParamType2["CONTEXT"] = "CONTEXT";
1808
+ return RouteParamType2;
1809
+ })(RouteParamType || {});
1810
+ class ControllerScanner {
1811
+ static scan(router, prefix, controller) {
1812
+ let instance = controller;
1813
+ if (typeof controller === "function") {
1814
+ instance = Container.resolve(controller);
1815
+ const controllerPath = controller[$controllerPath];
1816
+ if (controllerPath !== void 0) {
1817
+ const p1 = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
1818
+ const p2 = controllerPath.startsWith("/") ? controllerPath : "/" + controllerPath;
1819
+ prefix = p1 + p2;
1820
+ if (!prefix) prefix = "/";
1821
+ }
1822
+ } else {
1823
+ const ctor = instance.constructor;
1824
+ const controllerPath = ctor[$controllerPath];
1825
+ if (controllerPath !== void 0) {
1826
+ const p1 = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
1827
+ const p2 = controllerPath.startsWith("/") ? controllerPath : "/" + controllerPath;
1828
+ prefix = p1 + p2;
1829
+ if (!prefix) prefix = "/";
1830
+ }
1831
+ }
1832
+ instance[$mountPath] = prefix;
1833
+ const info = getCallerInfo();
1834
+ instance.metadata = {
1835
+ file: info.file,
1836
+ line: info.line,
1837
+ name: instance.constructor.name
1838
+ };
1839
+ router.registerControllerInstance(instance);
1840
+ const controllerMiddleware = (typeof controller === "function" ? controller[$middleware] : instance[$middleware]) || [];
1841
+ const proto = Object.getPrototypeOf(instance);
1842
+ const methods = /* @__PURE__ */ new Set();
1843
+ let current = proto;
1844
+ while (current !== void 0 && current !== Object.prototype) {
1845
+ Object.getOwnPropertyNames(current).forEach((name) => methods.add(name));
1846
+ current = Object.getPrototypeOf(current);
1847
+ }
1848
+ Object.getOwnPropertyNames(instance).forEach((name) => methods.add(name));
1849
+ const decoratedRoutes = instance[$routeMethods] || proto && proto[$routeMethods];
1850
+ const decoratedArgs = instance[$routeArgs] || proto && proto[$routeArgs];
1851
+ const methodMiddlewareMap = instance[$middleware] || proto && proto[$middleware];
1852
+ const decoratedEvents = instance[$eventMethods] || proto && proto[$eventMethods];
1853
+ let routesAttached = 0;
1854
+ for (let i = 0; i < Array.from(methods).length; i++) {
1855
+ const name = Array.from(methods)[i];
1856
+ if (name === "constructor") continue;
1857
+ if (["arguments", "caller", "callee"].includes(name)) continue;
1858
+ const originalHandler = instance[name];
1859
+ if (typeof originalHandler !== "function") continue;
1860
+ let method;
1861
+ let subPath = "";
1862
+ let methodSource;
1863
+ const routeConfig = decoratedRoutes?.get(name);
1864
+ if (routeConfig !== void 0) {
1865
+ method = routeConfig.method;
1866
+ subPath = routeConfig.path;
1867
+ methodSource = routeConfig.source;
1868
+ } else {
1869
+ for (let j = 0; j < HTTPMethods.length; j++) {
1870
+ const m = HTTPMethods[j];
1871
+ if (name.toUpperCase().startsWith(m)) {
1872
+ method = m;
1873
+ const rest = name.slice(m.length);
1874
+ if (rest.length === 0) {
1875
+ subPath = "/";
1876
+ } else {
1877
+ subPath = "";
1878
+ let buffer = "";
1879
+ const flush = () => {
1880
+ if (buffer.length > 0) {
1881
+ subPath += "/" + buffer.toLowerCase();
1882
+ buffer = "";
1883
+ }
1884
+ };
1885
+ for (let i2 = 0; i2 < rest.length; i2++) {
1886
+ const char = rest[i2];
1887
+ if (char === "$") {
1888
+ flush();
1889
+ subPath += "/:";
1890
+ continue;
1891
+ }
1892
+ buffer += char;
1893
+ }
1894
+ if (buffer.length > 0) flush();
1895
+ subPath = rest.replace(/\$/g, "/:").replace(/([a-z0-9])([A-Z])/g, "$1/$2").toLowerCase();
1896
+ if (!subPath.startsWith("/")) {
1897
+ subPath = "/" + subPath;
1898
+ }
1899
+ }
1900
+ break;
1901
+ }
1902
+ }
1903
+ }
1904
+ if (method !== void 0 && method !== "") {
1905
+ routesAttached++;
1906
+ const cleanPrefix = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
1907
+ const cleanSubPath = subPath === "/" ? "" : subPath;
1908
+ let joined;
1909
+ if (cleanSubPath.length === 0) {
1910
+ joined = cleanPrefix;
1911
+ } else if (cleanSubPath.startsWith("/")) {
1912
+ joined = cleanPrefix + cleanSubPath;
1913
+ } else {
1914
+ joined = cleanPrefix + "/" + cleanSubPath;
1915
+ }
1916
+ const fullPath = joined || "/";
1917
+ const normalizedPath = fullPath.replace(/\/+/g, "/");
1918
+ const methodMw = methodMiddlewareMap instanceof Map ? methodMiddlewareMap.get(name) || [] : [];
1919
+ const allMiddleware = [...controllerMiddleware, ...methodMw];
1920
+ const routeArgs = decoratedArgs && decoratedArgs.get(name);
1921
+ const wrappedHandler = async (ctx) => {
1922
+ let args = [ctx];
1923
+ if (routeArgs?.length > 0) {
1924
+ args = [];
1925
+ const sortedArgs = [...routeArgs].sort((a, b) => a.index - b.index);
1926
+ for (let k = 0; k < sortedArgs.length; k++) {
1927
+ const arg = sortedArgs[k];
1928
+ switch (arg.type) {
1929
+ case RouteParamType.BODY:
1930
+ args[arg.index] = await ctx.body();
1931
+ break;
1932
+ case RouteParamType.PARAM:
1933
+ args[arg.index] = arg.name ? ctx.params[arg.name] : ctx.params;
1934
+ break;
1935
+ case RouteParamType.QUERY: {
1936
+ const url = new URL(ctx.req.url);
1937
+ if (arg.name) {
1938
+ const vals = url.searchParams.getAll(arg.name);
1939
+ args[arg.index] = vals.length > 1 ? vals : vals[0];
1940
+ } else {
1941
+ const query = {};
1942
+ const keys = Object.keys(url.searchParams);
1943
+ for (let k2 = 0; k2 < keys.length; k2++) {
1944
+ const key = keys[k2];
1945
+ const vals = url.searchParams.getAll(key);
1946
+ query[key] = vals.length > 1 ? vals : vals[0];
1947
+ }
1948
+ args[arg.index] = query;
1949
+ }
1950
+ break;
1951
+ }
1952
+ case RouteParamType.HEADER:
1953
+ args[arg.index] = arg.name ? ctx.req.headers.get(arg.name) : ctx.req.headers;
1954
+ break;
1955
+ case RouteParamType.REQUEST:
1956
+ args[arg.index] = ctx.req;
1957
+ break;
1958
+ case RouteParamType.CONTEXT:
1959
+ args[arg.index] = ctx;
1960
+ break;
1961
+ }
1962
+ }
1963
+ }
1964
+ const tracedOriginalHandler = ctx.app?.applicationConfig.enableTracing ? traceHandler(originalHandler, normalizedPath) : originalHandler;
1965
+ return tracedOriginalHandler.apply(instance, args);
1966
+ };
1967
+ let finalHandler = wrappedHandler;
1968
+ if (allMiddleware.length > 0) {
1969
+ const composed = compose(allMiddleware);
1970
+ finalHandler = async (ctx) => {
1971
+ return composed(ctx, () => wrappedHandler(ctx));
1972
+ };
1973
+ }
1974
+ finalHandler.originalHandler = originalHandler;
1975
+ if (finalHandler !== wrappedHandler) {
1976
+ wrappedHandler.originalHandler = originalHandler;
1977
+ }
1978
+ const tagName = instance.constructor.name;
1979
+ const decoratedSpecs = instance[$routeSpec] || proto && proto[$routeSpec];
1980
+ const userSpec = decoratedSpecs && decoratedSpecs.get(name);
1981
+ const spec = { tags: [tagName], ...userSpec };
1982
+ router.add({
1983
+ method,
1984
+ path: normalizedPath,
1985
+ handler: finalHandler,
1986
+ spec,
1987
+ controller: instance,
1988
+ metadata: methodSource || instance.metadata,
1989
+ middleware: allMiddleware
1990
+ });
1991
+ }
1992
+ const eventConfig = decoratedEvents?.get(name);
1993
+ if (eventConfig !== void 0) {
1994
+ routesAttached++;
1995
+ const routeArgs = decoratedArgs?.get(name);
1996
+ const wrappedHandler = async (ctx) => {
1997
+ let args = [ctx];
1998
+ if (routeArgs?.length > 0) {
1999
+ args = [];
2000
+ const sortedArgs = [...routeArgs].sort((a, b) => a.index - b.index);
2001
+ for (let k = 0; k < sortedArgs.length; k++) {
2002
+ const arg = sortedArgs[k];
2003
+ switch (arg.type) {
2004
+ case RouteParamType.BODY:
2005
+ args[arg.index] = await ctx.body();
2006
+ break;
2007
+ case RouteParamType.CONTEXT:
2008
+ args[arg.index] = ctx;
2009
+ break;
2010
+ case RouteParamType.REQUEST:
2011
+ args[arg.index] = ctx.req;
2012
+ break;
2013
+ case RouteParamType.HEADER:
2014
+ args[arg.index] = arg.name ? ctx.req.headers.get(arg.name) : ctx.req.headers;
2015
+ break;
2016
+ default:
2017
+ args[arg.index] = void 0;
2018
+ }
2019
+ }
2020
+ }
2021
+ return originalHandler.apply(instance, args);
2022
+ };
2023
+ const decoratedSpecs = instance[$routeSpec] || proto && proto[$routeSpec];
2024
+ const userSpec = decoratedSpecs && decoratedSpecs.get(name);
2025
+ const spec = { tags: [{ name: instance.constructor.name }], ...userSpec };
2026
+ wrappedHandler.spec = spec;
2027
+ wrappedHandler.originalHandler = originalHandler;
2028
+ router.event(eventConfig.eventName, wrappedHandler);
2029
+ }
2030
+ }
2031
+ if (routesAttached === 0) {
2032
+ console.warn(`No routes attached to controller ${instance.constructor.name}`);
2033
+ }
2034
+ instance[$isMounted] = true;
2035
+ }
2036
+ }
2037
+ class HttpError extends Error {
2038
+ status;
2039
+ constructor(message, status) {
2040
+ super(message);
2041
+ this.name = "HttpError";
2042
+ this.status = status;
2043
+ if (Error.captureStackTrace) {
2044
+ Error.captureStackTrace(this, HttpError);
2045
+ }
2046
+ }
2047
+ }
2048
+ function getErrorStatus(err) {
2049
+ if (!err || typeof err !== "object") {
2050
+ return 500;
2051
+ }
2052
+ if (typeof err.status === "number") {
2053
+ return err.status;
2054
+ }
2055
+ if (typeof err.statusCode === "number") {
2056
+ return err.statusCode;
2057
+ }
2058
+ return 500;
2059
+ }
2060
+ class EventError extends HttpError {
2061
+ constructor(message = "Event Error") {
2062
+ super(message, 500);
2063
+ this.name = "EventError";
2064
+ }
2065
+ }
2066
+ class MiddlewareTracker {
2067
+ static wrap(handler, context) {
2068
+ const { file, line, name, isBuiltin, pluginName } = context;
2069
+ const handlerName = name || handler.name || "anonymous";
2070
+ const trackedHandler = async (ctx, next) => {
2071
+ if (!ctx.app?.applicationConfig.enableMiddlewareTracking) {
2072
+ return handler(ctx, next);
2073
+ }
2074
+ const startTime = performance.now();
2075
+ let error = void 0;
2076
+ try {
2077
+ ctx.handlerStack.push({
2078
+ name: handlerName,
2079
+ file,
2080
+ line,
2081
+ isBuiltin,
2082
+ startTime,
2083
+ duration: -1
2084
+ });
2085
+ return await handler(ctx, next);
2086
+ } catch (e) {
2087
+ error = e;
2088
+ throw e;
2089
+ } finally {
2090
+ const duration = performance.now() - startTime;
2091
+ const stackItem = ctx.handlerStack[ctx.handlerStack.length - 1];
2092
+ if (stackItem && stackItem.name === handlerName) {
2093
+ stackItem.duration = duration;
2094
+ }
2095
+ Promise.resolve().then(async () => {
2096
+ try {
2097
+ const db = ctx.app?.db;
2098
+ if (!db) return;
2099
+ const timestamp = Date.now();
2100
+ await db.upsert(new surrealdb.RecordId("middleware_tracking", {
2101
+ timestamp,
2102
+ name: handlerName
2103
+ }), {
2104
+ name: handlerName,
2105
+ path: ctx.path,
2106
+ timestamp,
2107
+ duration,
2108
+ file,
2109
+ line,
2110
+ error: error ? String(error) : void 0,
2111
+ metadata: {
2112
+ isBuiltin,
2113
+ pluginName
2114
+ }
2115
+ });
2116
+ } catch (err) {
2117
+ }
2118
+ });
2119
+ }
2120
+ };
2121
+ trackedHandler.metadata = handler.metadata || context;
2122
+ Object.defineProperty(trackedHandler, "name", { value: handlerName });
2123
+ trackedHandler.originalHandler = handler.originalHandler || handler;
2124
+ return trackedHandler;
2125
+ }
2126
+ }
2127
+ class ShokupanRequestBase {
2128
+ method;
2129
+ url;
2130
+ headers;
2131
+ body;
2132
+ async json() {
2133
+ return JSON.parse(this.body);
2134
+ }
2135
+ async text() {
2136
+ return this.body;
2137
+ }
2138
+ async formData() {
2139
+ if (this.body instanceof FormData) {
2140
+ return this.body;
2141
+ }
2142
+ return new Response(this.body, { headers: this.headers }).formData();
2143
+ }
2144
+ constructor(props) {
2145
+ Object.assign(this, props);
2146
+ if (!(this.headers instanceof Headers)) {
2147
+ this.headers = new Headers(this.headers);
2148
+ }
2149
+ }
2150
+ }
2151
+ const ShokupanRequest = ShokupanRequestBase;
2152
+ class RouterTrie {
2153
+ root;
2154
+ constructor() {
2155
+ this.root = this.createNode();
2156
+ }
2157
+ createNode() {
2158
+ return {
2159
+ children: {}
2160
+ };
2161
+ }
2162
+ insert(method, path2, handler) {
2163
+ let node = this.root;
2164
+ const segments = this.splitPath(path2);
2165
+ for (let i = 0; i < segments.length; i++) {
2166
+ const segment = segments[i];
2167
+ if (segment === "**") {
2168
+ if (!node.recursiveChild) {
2169
+ node.recursiveChild = this.createNode();
2170
+ }
2171
+ node = node.recursiveChild;
2172
+ } else if (segment === "*") {
2173
+ if (!node.wildcardChild) {
2174
+ node.wildcardChild = this.createNode();
1678
2175
  }
1679
2176
  node = node.wildcardChild;
1680
2177
  } else if (segment.startsWith(":")) {
@@ -1749,19 +2246,9 @@ class RouterTrie {
1749
2246
  if (path2 === "/" || path2 === "") return [];
1750
2247
  const s = path2.startsWith("/") ? path2.slice(1) : path2;
1751
2248
  if (s === "") return [];
1752
- return s.split("/");
1753
- }
1754
- }
1755
- const HTTPMethods = ["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS", "ALL"];
1756
- var RouteParamType = /* @__PURE__ */ ((RouteParamType2) => {
1757
- RouteParamType2["BODY"] = "BODY";
1758
- RouteParamType2["PARAM"] = "PARAM";
1759
- RouteParamType2["QUERY"] = "QUERY";
1760
- RouteParamType2["HEADER"] = "HEADER";
1761
- RouteParamType2["REQUEST"] = "REQUEST";
1762
- RouteParamType2["CONTEXT"] = "CONTEXT";
1763
- return RouteParamType2;
1764
- })(RouteParamType || {});
2249
+ return s.split("/");
2250
+ }
2251
+ }
1765
2252
  const RouterRegistry = /* @__PURE__ */ new Map();
1766
2253
  const ShokupanApplicationTree = {};
1767
2254
  class ShokupanRouter {
@@ -1770,6 +2257,9 @@ class ShokupanRouter {
1770
2257
  if (config?.requestTimeout) {
1771
2258
  this.requestTimeout = config.requestTimeout;
1772
2259
  }
2260
+ if (config?.hooks) {
2261
+ this.ensureHooksInitialized();
2262
+ }
1773
2263
  }
1774
2264
  // Internal marker to identify Router vs. Application
1775
2265
  [$isApplication] = false;
@@ -1781,6 +2271,47 @@ class ShokupanRouter {
1781
2271
  [$parent] = null;
1782
2272
  [$childRouters] = [];
1783
2273
  [$childControllers] = [];
2274
+ _hasOnResponseEndHook;
2275
+ _hasOnRequestStartHook;
2276
+ _hasOnRequestEndHook;
2277
+ _hasOnResponseStartHook;
2278
+ _hasOnErrorHook;
2279
+ _hasOnRequestTimeoutHook;
2280
+ _hasOnReadTimeoutHook;
2281
+ _hasOnWriteTimeoutHook;
2282
+ _hasBeforeValidateHook;
2283
+ _hasAfterValidateHook;
2284
+ get hasOnResponseEndHook() {
2285
+ return this._hasOnResponseEndHook;
2286
+ }
2287
+ get hasOnRequestStartHook() {
2288
+ return this._hasOnRequestStartHook;
2289
+ }
2290
+ get hasOnRequestEndHook() {
2291
+ return this._hasOnRequestEndHook;
2292
+ }
2293
+ get hasOnResponseStartHook() {
2294
+ return this._hasOnResponseStartHook;
2295
+ }
2296
+ get hasOnErrorHook() {
2297
+ return this._hasOnErrorHook;
2298
+ }
2299
+ get hasOnRequestTimeoutHook() {
2300
+ return this._hasOnRequestTimeoutHook;
2301
+ }
2302
+ get hasOnReadTimeoutHook() {
2303
+ return this._hasOnReadTimeoutHook;
2304
+ }
2305
+ get hasOnWriteTimeoutHook() {
2306
+ return this._hasOnWriteTimeoutHook;
2307
+ }
2308
+ get hasBeforeValidateHook() {
2309
+ return this._hasBeforeValidateHook;
2310
+ }
2311
+ get hasAfterValidateHook() {
2312
+ return this._hasAfterValidateHook;
2313
+ }
2314
+ requestTimeout;
1784
2315
  get db() {
1785
2316
  return this.root?.db;
1786
2317
  }
@@ -1816,7 +2347,7 @@ class ShokupanRouter {
1816
2347
  const r = this[$routes][i];
1817
2348
  const entry = {
1818
2349
  type: "route",
1819
- path: r.path,
2350
+ path: r.path.startsWith("/") ? r.path : "/" + r.path,
1820
2351
  method: r.method,
1821
2352
  metadata: r.metadata,
1822
2353
  handlerName: r.handler.name,
@@ -1843,7 +2374,7 @@ class ShokupanRouter {
1843
2374
  })) : [];
1844
2375
  const routers = this[$childRouters].map((r) => ({
1845
2376
  type: "router",
1846
- path: r[$mountPath],
2377
+ path: r[$mountPath].startsWith("/") ? r[$mountPath] : "/" + r[$mountPath],
1847
2378
  metadata: r.metadata,
1848
2379
  children: r.getComponentRegistry()
1849
2380
  }));
@@ -1857,12 +2388,25 @@ class ShokupanRouter {
1857
2388
  children: { routes }
1858
2389
  };
1859
2390
  });
2391
+ const events2 = [];
2392
+ this.eventHandlers.forEach((handlers, name) => {
2393
+ handlers.forEach((h) => {
2394
+ events2.push({
2395
+ type: "event",
2396
+ name,
2397
+ handlerName: h.name,
2398
+ metadata: h.source ? { file: h.source.file, line: h.source.line } : void 0,
2399
+ _fn: h
2400
+ });
2401
+ });
2402
+ });
1860
2403
  return {
1861
2404
  metadata: this.metadata,
1862
2405
  middleware,
1863
2406
  routes: localRoutes,
1864
2407
  routers,
1865
- controllers
2408
+ controllers,
2409
+ events: events2
1866
2410
  };
1867
2411
  }
1868
2412
  isRouterInstance(target) {
@@ -1885,12 +2429,38 @@ class ShokupanRouter {
1885
2429
  }
1886
2430
  return this;
1887
2431
  }
2432
+ /**
2433
+ * Registers a lifecycle hook dynamically.
2434
+ */
2435
+ hook(name, handler) {
2436
+ if (!this.hooksInitialized) {
2437
+ this.ensureHooksInitialized();
2438
+ }
2439
+ let handlers = this.hookCache.get(name);
2440
+ if (!handlers) {
2441
+ handlers = [];
2442
+ this.hookCache.set(name, handlers);
2443
+ this._hasOnErrorHook ||= name === "onError";
2444
+ this._hasOnRequestStartHook ||= name === "onRequestStart";
2445
+ this._hasOnRequestEndHook ||= name === "onRequestEnd";
2446
+ this._hasOnResponseStartHook ||= name === "onResponseStart";
2447
+ this._hasOnResponseEndHook ||= name === "onResponseEnd";
2448
+ this._hasOnRequestTimeoutHook ||= name === "onRequestTimeout";
2449
+ this._hasOnReadTimeoutHook ||= name === "onReadTimeout";
2450
+ this._hasOnWriteTimeoutHook ||= name === "onWriteTimeout";
2451
+ this._hasBeforeValidateHook ||= name === "beforeValidate";
2452
+ this._hasAfterValidateHook ||= name === "afterValidate";
2453
+ }
2454
+ handlers.push(handler);
2455
+ return this;
2456
+ }
1888
2457
  /**
1889
2458
  * Finds an event handler(s) by name.
1890
2459
  */
1891
2460
  findEvent(name) {
1892
- if (this.eventHandlers.has(name)) {
1893
- return this.eventHandlers.get(name);
2461
+ const handlers = this.eventHandlers.get(name);
2462
+ if (handlers !== void 0) {
2463
+ return handlers;
1894
2464
  }
1895
2465
  for (const child of this[$childRouters]) {
1896
2466
  const handler = child.findEvent(name);
@@ -1898,6 +2468,12 @@ class ShokupanRouter {
1898
2468
  }
1899
2469
  return null;
1900
2470
  }
2471
+ /**
2472
+ * Registers a controller instance to the router.
2473
+ */
2474
+ registerControllerInstance(controller) {
2475
+ this[$childControllers].push(controller);
2476
+ }
1901
2477
  /**
1902
2478
  * Returns all registered event handlers.
1903
2479
  */
@@ -1924,7 +2500,7 @@ class ShokupanRouter {
1924
2500
  if (this.isRouterInstance(controller)) {
1925
2501
  this.mountRouter(prefix, controller);
1926
2502
  } else {
1927
- this.scanControllerRoutes(prefix, controller);
2503
+ ControllerScanner.scan(this, prefix, controller);
1928
2504
  }
1929
2505
  return this;
1930
2506
  }
@@ -2078,264 +2654,23 @@ class ShokupanRouter {
2078
2654
  throw new Error("Router is already mounted");
2079
2655
  }
2080
2656
  router[$mountPath] = prefix;
2081
- if (!router.metadata) {
2082
- const info = getCallerInfo();
2083
- router.metadata = {
2084
- file: info.file,
2085
- line: info.line,
2086
- name: "MountedRouter"
2087
- };
2088
- }
2089
- this[$childRouters].push(router);
2090
- router[$parent] = this;
2091
- const setRouterContext = (router2) => {
2092
- router2[$appRoot] = this.root;
2093
- router2[$childRouters].forEach((child) => setRouterContext(child));
2094
- };
2095
- setRouterContext(router);
2096
- router[$appRoot] = this.root;
2097
- router[$isMounted] = true;
2098
- }
2099
- scanControllerRoutes(prefix, controller) {
2100
- let instance = controller;
2101
- if (typeof controller === "function") {
2102
- instance = Container.resolve(controller);
2103
- const controllerPath = controller[$controllerPath];
2104
- if (controllerPath) {
2105
- const p1 = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
2106
- const p2 = controllerPath.startsWith("/") ? controllerPath : "/" + controllerPath;
2107
- prefix = p1 + p2;
2108
- if (!prefix) prefix = "/";
2109
- }
2110
- } else {
2111
- const ctor = instance.constructor;
2112
- const controllerPath = ctor[$controllerPath];
2113
- if (controllerPath) {
2114
- const p1 = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
2115
- const p2 = controllerPath.startsWith("/") ? controllerPath : "/" + controllerPath;
2116
- prefix = p1 + p2;
2117
- if (!prefix) prefix = "/";
2118
- }
2119
- }
2120
- instance[$mountPath] = prefix;
2121
- const info = getCallerInfo();
2122
- instance.metadata = {
2123
- file: info.file,
2124
- line: info.line,
2125
- name: instance.constructor.name
2126
- };
2127
- this[$childControllers].push(instance);
2128
- const controllerMiddleware = (typeof controller === "function" ? controller[$middleware] : instance[$middleware]) || [];
2129
- const proto = Object.getPrototypeOf(instance);
2130
- const methods = /* @__PURE__ */ new Set();
2131
- let current = proto;
2132
- while (current && current !== Object.prototype) {
2133
- Object.getOwnPropertyNames(current).forEach((name) => methods.add(name));
2134
- current = Object.getPrototypeOf(current);
2135
- }
2136
- Object.getOwnPropertyNames(instance).forEach((name) => methods.add(name));
2137
- const decoratedRoutes = instance[$routeMethods] || proto && proto[$routeMethods];
2138
- const decoratedArgs = instance[$routeArgs] || proto && proto[$routeArgs];
2139
- const methodMiddlewareMap = instance[$middleware] || proto && proto[$middleware];
2140
- const decoratedEvents = instance[$eventMethods] || proto && proto[$eventMethods];
2141
- let routesAttached = 0;
2142
- for (let i = 0; i < Array.from(methods).length; i++) {
2143
- const name = Array.from(methods)[i];
2144
- if (name === "constructor") continue;
2145
- if (["arguments", "caller", "callee"].includes(name)) continue;
2146
- const originalHandler = instance[name];
2147
- if (typeof originalHandler !== "function") continue;
2148
- let method;
2149
- let subPath = "";
2150
- let methodSource;
2151
- if (decoratedRoutes && decoratedRoutes.has(name)) {
2152
- const config = decoratedRoutes.get(name);
2153
- method = config.method;
2154
- subPath = config.path;
2155
- methodSource = config.source;
2156
- } else {
2157
- for (let j = 0; j < HTTPMethods.length; j++) {
2158
- const m = HTTPMethods[j];
2159
- if (name.toUpperCase().startsWith(m)) {
2160
- method = m;
2161
- const rest = name.slice(m.length);
2162
- if (rest.length === 0) {
2163
- subPath = "/";
2164
- } else {
2165
- subPath = "";
2166
- let buffer = "";
2167
- const flush = () => {
2168
- if (buffer.length > 0) {
2169
- subPath += "/" + buffer.toLowerCase();
2170
- buffer = "";
2171
- }
2172
- };
2173
- for (let i2 = 0; i2 < rest.length; i2++) {
2174
- const char = rest[i2];
2175
- if (char === "$") {
2176
- flush();
2177
- subPath += "/:";
2178
- continue;
2179
- }
2180
- buffer += char;
2181
- }
2182
- if (buffer.length > 0) flush();
2183
- subPath = rest.replace(/\$/g, "/:").replace(/([a-z0-9])([A-Z])/g, "$1/$2").toLowerCase();
2184
- if (!subPath.startsWith("/")) {
2185
- subPath = "/" + subPath;
2186
- }
2187
- }
2188
- break;
2189
- }
2190
- }
2191
- }
2192
- if (method) {
2193
- routesAttached++;
2194
- const cleanPrefix = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
2195
- const cleanSubPath = subPath === "/" ? "" : subPath;
2196
- let joined;
2197
- if (cleanSubPath.length === 0) {
2198
- joined = cleanPrefix;
2199
- } else if (cleanSubPath.startsWith("/")) {
2200
- joined = cleanPrefix + cleanSubPath;
2201
- } else {
2202
- joined = cleanPrefix + "/" + cleanSubPath;
2203
- }
2204
- const fullPath = joined || "/";
2205
- const normalizedPath = fullPath.replace(/\/+/g, "/");
2206
- const methodMw = methodMiddlewareMap instanceof Map ? methodMiddlewareMap.get(name) || [] : [];
2207
- const allMiddleware = [...controllerMiddleware, ...methodMw];
2208
- const routeArgs = decoratedArgs && decoratedArgs.get(name);
2209
- const wrappedHandler = async (ctx) => {
2210
- let args = [ctx];
2211
- if (routeArgs?.length > 0) {
2212
- args = [];
2213
- const sortedArgs = [...routeArgs].sort((a, b) => a.index - b.index);
2214
- for (let k = 0; k < sortedArgs.length; k++) {
2215
- const arg = sortedArgs[k];
2216
- switch (arg.type) {
2217
- case RouteParamType.BODY:
2218
- try {
2219
- if (ctx.req.headers.get("content-type")?.includes("application/json")) {
2220
- args[arg.index] = await ctx.req.json();
2221
- } else {
2222
- const text = await ctx.req.text();
2223
- if (!text) {
2224
- args[arg.index] = {};
2225
- } else {
2226
- args[arg.index] = JSON.parse(text);
2227
- }
2228
- }
2229
- } catch (e) {
2230
- const err = new Error("Invalid JSON body");
2231
- err.status = 400;
2232
- throw err;
2233
- }
2234
- break;
2235
- case RouteParamType.PARAM:
2236
- args[arg.index] = arg.name ? ctx.params[arg.name] : ctx.params;
2237
- break;
2238
- case RouteParamType.QUERY: {
2239
- const url = new URL(ctx.req.url);
2240
- if (arg.name) {
2241
- const vals = url.searchParams.getAll(arg.name);
2242
- args[arg.index] = vals.length > 1 ? vals : vals[0];
2243
- } else {
2244
- const query = {};
2245
- const keys = Object.keys(url.searchParams);
2246
- for (let k2 = 0; k2 < keys.length; k2++) {
2247
- const key = keys[k2];
2248
- const vals = url.searchParams.getAll(key);
2249
- query[key] = vals.length > 1 ? vals : vals[0];
2250
- }
2251
- args[arg.index] = query;
2252
- }
2253
- break;
2254
- }
2255
- case RouteParamType.HEADER:
2256
- args[arg.index] = arg.name ? ctx.req.headers.get(arg.name) : ctx.req.headers;
2257
- break;
2258
- case RouteParamType.REQUEST:
2259
- args[arg.index] = ctx.req;
2260
- break;
2261
- case RouteParamType.CONTEXT:
2262
- args[arg.index] = ctx;
2263
- break;
2264
- }
2265
- }
2266
- }
2267
- const tracedOriginalHandler = ctx.app?.applicationConfig.enableTracing ? traceHandler(originalHandler, normalizedPath) : originalHandler;
2268
- return tracedOriginalHandler.apply(instance, args);
2269
- };
2270
- let finalHandler = wrappedHandler;
2271
- if (allMiddleware.length > 0) {
2272
- const composed = compose(allMiddleware);
2273
- finalHandler = async (ctx) => {
2274
- return composed(ctx, () => wrappedHandler(ctx));
2275
- };
2276
- }
2277
- finalHandler.originalHandler = originalHandler;
2278
- if (finalHandler !== wrappedHandler) {
2279
- wrappedHandler.originalHandler = originalHandler;
2280
- }
2281
- const tagName = instance.constructor.name;
2282
- const decoratedSpecs = instance[$routeSpec] || proto && proto[$routeSpec];
2283
- const userSpec = decoratedSpecs && decoratedSpecs.get(name);
2284
- const spec = { tags: [tagName], ...userSpec };
2285
- this.add({
2286
- method,
2287
- path: normalizedPath,
2288
- handler: finalHandler,
2289
- spec,
2290
- controller: instance,
2291
- metadata: methodSource || instance.metadata,
2292
- middleware: allMiddleware
2293
- // Capture all resolved middleware
2294
- });
2295
- }
2296
- if (decoratedEvents?.has(name)) {
2297
- routesAttached++;
2298
- const config = decoratedEvents.get(name);
2299
- const routeArgs = decoratedArgs?.get(name);
2300
- const wrappedHandler = async (ctx) => {
2301
- let args = [ctx];
2302
- if (routeArgs?.length > 0) {
2303
- args = [];
2304
- const sortedArgs = [...routeArgs].sort((a, b) => a.index - b.index);
2305
- for (let k = 0; k < sortedArgs.length; k++) {
2306
- const arg = sortedArgs[k];
2307
- switch (arg.type) {
2308
- case RouteParamType.BODY:
2309
- args[arg.index] = await ctx.body();
2310
- break;
2311
- case RouteParamType.CONTEXT:
2312
- args[arg.index] = ctx;
2313
- break;
2314
- case RouteParamType.REQUEST:
2315
- args[arg.index] = ctx.req;
2316
- break;
2317
- case RouteParamType.HEADER:
2318
- args[arg.index] = arg.name ? ctx.req.headers.get(arg.name) : ctx.req.headers;
2319
- break;
2320
- default:
2321
- args[arg.index] = void 0;
2322
- }
2323
- }
2324
- }
2325
- return originalHandler.apply(instance, args);
2326
- };
2327
- const decoratedSpecs = instance[$routeSpec] || proto && proto[$routeSpec];
2328
- const userSpec = decoratedSpecs && decoratedSpecs.get(name);
2329
- const spec = { tags: [{ name: instance.constructor.name }], ...userSpec };
2330
- wrappedHandler.spec = spec;
2331
- wrappedHandler.originalHandler = originalHandler;
2332
- this.event(config.eventName, wrappedHandler);
2333
- }
2334
- }
2335
- if (routesAttached === 0) {
2336
- console.warn(`No routes attached to controller ${instance.constructor.name}`);
2657
+ if (!router.metadata) {
2658
+ const info = getCallerInfo();
2659
+ router.metadata = {
2660
+ file: info.file,
2661
+ line: info.line,
2662
+ name: "MountedRouter"
2663
+ };
2337
2664
  }
2338
- instance[$isMounted] = true;
2665
+ this[$childRouters].push(router);
2666
+ router[$parent] = this;
2667
+ const setRouterContext = (router2) => {
2668
+ router2[$appRoot] = this.root;
2669
+ router2[$childRouters].forEach((child) => setRouterContext(child));
2670
+ };
2671
+ setRouterContext(router);
2672
+ router[$appRoot] = this.root;
2673
+ router[$isMounted] = true;
2339
2674
  }
2340
2675
  /**
2341
2676
  * Find a route matching the given method and path.
@@ -2369,6 +2704,9 @@ class ShokupanRouter {
2369
2704
  return null;
2370
2705
  }
2371
2706
  parsePath(path2) {
2707
+ if (typeof path2 !== "string") {
2708
+ throw new Error(`Route path must be a string or regexp, received ${typeof path2 == "function" ? path2["name"] || path2["constructor"]?.["name"] || "function" : typeof path2}. Dynamic paths are **highly** discouraged.`);
2709
+ }
2372
2710
  const keys = [];
2373
2711
  if (path2.length > 2048) {
2374
2712
  throw new Error("Path too long");
@@ -2382,8 +2720,6 @@ class ShokupanRouter {
2382
2720
  keys
2383
2721
  };
2384
2722
  }
2385
- // --- Functional Routing ---
2386
- requestTimeout;
2387
2723
  /**
2388
2724
  * Adds a route to the router.
2389
2725
  *
@@ -2417,9 +2753,6 @@ class ShokupanRouter {
2417
2753
  }
2418
2754
  }
2419
2755
  let wrappedHandler = async (ctx) => {
2420
- if (ctx.upgrade()) {
2421
- return void 0;
2422
- }
2423
2756
  return handler(ctx);
2424
2757
  };
2425
2758
  wrappedHandler.originalHandler = handler.originalHandler || handler;
@@ -2474,67 +2807,13 @@ class ShokupanRouter {
2474
2807
  };
2475
2808
  }
2476
2809
  const { file, line } = metadata || getCallerInfo();
2477
- const trackingHandler = wrappedHandler;
2478
- wrappedHandler = async (ctx) => {
2479
- if (!ctx.app?.applicationConfig.enableMiddlewareTracking) {
2480
- return trackingHandler(ctx);
2481
- }
2482
- const startTime = performance.now();
2483
- let error = void 0;
2484
- try {
2485
- if (ctx.app?.applicationConfig.enableMiddlewareTracking) {
2486
- ctx.handlerStack.push({
2487
- name: handler.name || "anonymous",
2488
- file,
2489
- line
2490
- });
2491
- }
2492
- return await trackingHandler(ctx);
2493
- } catch (e) {
2494
- error = e;
2495
- throw e;
2496
- } finally {
2497
- if (ctx.app?.applicationConfig.enableMiddlewareTracking) {
2498
- const duration = performance.now() - startTime;
2499
- const config = ctx.app.applicationConfig;
2500
- Promise.resolve().then(async () => {
2501
- try {
2502
- const db = ctx.app?.db;
2503
- if (!db) return;
2504
- const timestamp = Date.now();
2505
- await db.upsert(new surrealdb.RecordId("middleware_tracking", {
2506
- timestamp,
2507
- name: handler.name || "anonymous"
2508
- }), {
2509
- name: handler.name || "anonymous",
2510
- path: ctx.path,
2511
- timestamp,
2512
- duration,
2513
- file,
2514
- line,
2515
- error: error ? String(error) : void 0,
2516
- metadata: {
2517
- isBuiltin: handler.isBuiltin,
2518
- pluginName: handler.pluginName
2519
- }
2520
- });
2521
- const ttl = config.middlewareTrackingTTL ?? 864e5;
2522
- const maxCapacity = config.middlewareTrackingMaxCapacity ?? 1e4;
2523
- const cutoff = Date.now() - ttl;
2524
- await db.query(`DELETE middleware_tracking WHERE timestamp < ${cutoff}`);
2525
- const results = await db.query("SELECT count() FROM middleware_tracking GROUP ALL");
2526
- if (results?.[0]?.count > maxCapacity) {
2527
- const toDelete = results[0].count - maxCapacity;
2528
- await db.query(`DELETE middleware_tracking ORDER BY timestamp ASC LIMIT ${toDelete}`);
2529
- }
2530
- } catch (datastoreError) {
2531
- console.error("Failed to store middleware tracking:", datastoreError);
2532
- }
2533
- });
2534
- }
2535
- }
2536
- };
2537
- wrappedHandler.originalHandler = trackingHandler.originalHandler || trackingHandler;
2810
+ wrappedHandler = MiddlewareTracker.wrap(wrappedHandler, {
2811
+ file,
2812
+ line,
2813
+ name: handler.name || "anonymous",
2814
+ isBuiltin: handler.isBuiltin,
2815
+ pluginName: handler.pluginName
2816
+ });
2538
2817
  let bakedHandler = wrappedHandler;
2539
2818
  if (this.config?.hooks) {
2540
2819
  bakedHandler = this.wrapWithHooks(wrappedHandler);
@@ -2609,17 +2888,11 @@ class ShokupanRouter {
2609
2888
  }
2610
2889
  } catch (e) {
2611
2890
  }
2612
- const trackedGuard = async (ctx, next) => {
2613
- if (ctx.app?.applicationConfig.enableMiddlewareTracking) {
2614
- ctx.handlerStack.push({
2615
- name: guardHandler.name || "guard",
2616
- file,
2617
- line
2618
- });
2619
- }
2620
- return guardHandler(ctx, next);
2621
- };
2622
- trackedGuard.originalHandler = guardHandler.originalHandler ?? guardHandler;
2891
+ const trackedGuard = MiddlewareTracker.wrap(guardHandler, {
2892
+ file,
2893
+ line,
2894
+ name: guardHandler.name || "guard"
2895
+ });
2623
2896
  this.currentGuards.push({ handler: trackedGuard, spec });
2624
2897
  return this;
2625
2898
  }
@@ -2648,113 +2921,376 @@ class ShokupanRouter {
2648
2921
  description: "Serves static files from " + normalizedPrefix,
2649
2922
  tags: [groupName]
2650
2923
  };
2651
- const spec = config.openapi ? config.openapi : defaultSpec;
2652
- if (!spec.tags) spec.tags = [groupName];
2653
- else if (!spec.tags.includes(groupName)) spec.tags.push(groupName);
2654
- const pattern = `^${normalizedPrefix}(/.*)?$`;
2655
- const regex = new RegExp(pattern);
2656
- const displayPath = normalizedPrefix === "/" ? "/*" : normalizedPrefix + "/*";
2657
- this.add({ method: "GET", path: displayPath, handler: routeHandler, spec, regex });
2658
- this.add({ method: "HEAD", path: displayPath, handler: routeHandler, spec, regex });
2659
- return this;
2924
+ const spec = config.openapi ? config.openapi : defaultSpec;
2925
+ if (!spec.tags) spec.tags = [groupName];
2926
+ else if (!spec.tags.includes(groupName)) spec.tags.push(groupName);
2927
+ const pattern = `^${normalizedPrefix}(/.*)?$`;
2928
+ const regex = new RegExp(pattern);
2929
+ const displayPath = normalizedPrefix === "/" ? "/*" : normalizedPrefix + "/*";
2930
+ this.add({ method: "GET", path: displayPath, handler: routeHandler, spec, regex });
2931
+ this.add({ method: "HEAD", path: displayPath, handler: routeHandler, spec, regex });
2932
+ return this;
2933
+ }
2934
+ /**
2935
+ * Attach the verb routes with their overload signatures.
2936
+ * Use compose to handle multiple handlers (middleware).
2937
+ */
2938
+ attachVerb(method, path2, ...args) {
2939
+ let spec;
2940
+ let handlers = [];
2941
+ if (args.length > 0) {
2942
+ if (typeof args[0] === "object" && args[0] !== null) {
2943
+ spec = args[0];
2944
+ handlers = args.slice(1);
2945
+ } else {
2946
+ handlers = args;
2947
+ }
2948
+ }
2949
+ if (handlers.length === 0) {
2950
+ return;
2951
+ }
2952
+ let finalHandler = handlers[handlers.length - 1];
2953
+ if (handlers.length > 1) {
2954
+ const fn = compose(handlers);
2955
+ finalHandler = (ctx) => fn(ctx);
2956
+ }
2957
+ this.add({
2958
+ method,
2959
+ path: path2,
2960
+ spec,
2961
+ handler: finalHandler,
2962
+ middleware: handlers.slice(0, handlers.length - 1)
2963
+ });
2964
+ }
2965
+ /**
2966
+ * Generates an OpenAPI 3.1 Document by recursing through the router and its descendants.
2967
+ * Now includes runtime analysis of handler functions to infer request/response types.
2968
+ */
2969
+ generateApiSpec(options = {}) {
2970
+ return generateOpenApi(this, options);
2971
+ }
2972
+ hasHooks(name) {
2973
+ if (!this.hooksInitialized) {
2974
+ this.ensureHooksInitialized();
2975
+ }
2976
+ const hooks = this.hookCache.get(name);
2977
+ return hooks !== void 0 && hooks.length > 0;
2978
+ }
2979
+ ensureHooksInitialized() {
2980
+ const hooks = this.config?.hooks;
2981
+ if (hooks) {
2982
+ const hookList = Array.isArray(hooks) ? hooks : [hooks];
2983
+ const hookTypes = [
2984
+ "onRequestStart",
2985
+ "onRequestEnd",
2986
+ "onResponseStart",
2987
+ "onResponseEnd",
2988
+ "onError",
2989
+ "beforeValidate",
2990
+ "afterValidate",
2991
+ "onRequestTimeout",
2992
+ "onReadTimeout",
2993
+ "onWriteTimeout"
2994
+ ];
2995
+ for (let i = 0; i < hookTypes.length; i++) {
2996
+ const type = hookTypes[i];
2997
+ const fns = [];
2998
+ for (let j = 0; j < hookList.length; j++) {
2999
+ const h = hookList[j];
3000
+ if (h[type]) fns.push(h[type]);
3001
+ }
3002
+ if (fns.length > 0) {
3003
+ this._hasOnErrorHook ||= type === "onError";
3004
+ this._hasOnRequestStartHook ||= type === "onRequestStart";
3005
+ this._hasOnRequestEndHook ||= type === "onRequestEnd";
3006
+ this._hasOnResponseStartHook ||= type === "onResponseStart";
3007
+ this._hasOnResponseEndHook ||= type === "onResponseEnd";
3008
+ this._hasOnRequestTimeoutHook ||= type === "onRequestTimeout";
3009
+ this._hasOnReadTimeoutHook ||= type === "onReadTimeout";
3010
+ this._hasOnWriteTimeoutHook ||= type === "onWriteTimeout";
3011
+ this._hasBeforeValidateHook ||= type === "beforeValidate";
3012
+ this._hasAfterValidateHook ||= type === "afterValidate";
3013
+ this.hookCache.set(type, fns);
3014
+ }
3015
+ }
3016
+ }
3017
+ this.hooksInitialized = true;
3018
+ }
3019
+ runHooks(name, ...args) {
3020
+ if (!this.hooksInitialized) {
3021
+ this.ensureHooksInitialized();
3022
+ }
3023
+ const fns = this.hookCache.get(name);
3024
+ if (!fns) return;
3025
+ const ctx = args?.[0] instanceof ShokupanContext ? args[0] : void 0;
3026
+ const debug = ctx?.[$debug];
3027
+ if (debug) {
3028
+ return Promise.all(fns.map(async (fn, index) => {
3029
+ const hookId = `hook_${name}_${fn.name || index}`;
3030
+ const previousNode = debug.getCurrentNode();
3031
+ debug.trackEdge(previousNode, hookId);
3032
+ debug.setNode(hookId);
3033
+ const start = performance.now();
3034
+ try {
3035
+ await fn(...args);
3036
+ const duration = performance.now() - start;
3037
+ debug.trackStep(hookId, "hook", duration, "success");
3038
+ } catch (error) {
3039
+ const duration = performance.now() - start;
3040
+ debug.trackStep(hookId, "hook", duration, "error", error);
3041
+ throw error;
3042
+ } finally {
3043
+ if (previousNode) debug.setNode(previousNode);
3044
+ }
3045
+ }));
3046
+ } else {
3047
+ return Promise.all(fns.map((fn) => fn(...args)));
3048
+ }
3049
+ }
3050
+ }
3051
+ function createHttpServer() {
3052
+ return async (options) => {
3053
+ const server = http__namespace.createServer(async (req, res) => {
3054
+ const url = new URL(req.url, `http://${req.headers.host}`);
3055
+ const request = new Request(url.toString(), {
3056
+ method: req.method,
3057
+ headers: req.headers,
3058
+ body: ["GET", "HEAD"].includes(req.method) ? void 0 : new ReadableStream({
3059
+ start(controller) {
3060
+ req.on("data", (chunk) => controller.enqueue(chunk));
3061
+ req.on("end", () => controller.close());
3062
+ req.on("error", (err) => controller.error(err));
3063
+ }
3064
+ }),
3065
+ // Required for Node.js undici when sending a body
3066
+ duplex: "half"
3067
+ });
3068
+ const response = await options.fetch(request, fauxServer);
3069
+ res.statusCode = response.status;
3070
+ response.headers.forEach((v, k) => res.setHeader(k, v));
3071
+ if (response.body) {
3072
+ const buffer = await response.arrayBuffer();
3073
+ res.end(Buffer.from(buffer));
3074
+ } else {
3075
+ res.end();
3076
+ }
3077
+ });
3078
+ const fauxServer = {
3079
+ stop: () => {
3080
+ server.close();
3081
+ return Promise.resolve();
3082
+ },
3083
+ upgrade(req, options2) {
3084
+ return false;
3085
+ },
3086
+ reload(options2) {
3087
+ return fauxServer;
3088
+ },
3089
+ get port() {
3090
+ const addr = server.address();
3091
+ if (typeof addr === "object" && addr !== null) {
3092
+ return addr.port;
3093
+ }
3094
+ return options.port;
3095
+ },
3096
+ hostname: options.hostname,
3097
+ development: options.development,
3098
+ pendingRequests: 0,
3099
+ requestIP: (req) => null,
3100
+ publish: () => 0,
3101
+ subscriberCount: () => 0,
3102
+ url: new URL(`http://${options.hostname}:${options.port}`),
3103
+ // Expose the raw Node.js server for generic socket/websocket support (e.g. Socket.IO)
3104
+ nodeServer: server
3105
+ };
3106
+ return new Promise((resolve) => {
3107
+ server.listen(options.port, options.hostname, () => {
3108
+ resolve(fauxServer);
3109
+ });
3110
+ });
3111
+ };
3112
+ }
3113
+ class BunAdapter {
3114
+ server;
3115
+ async listen(port, app) {
3116
+ if (typeof Bun === "undefined") {
3117
+ throw new Error("BunAdapter requires the Bun runtime.");
3118
+ }
3119
+ const serveOptions = {
3120
+ port,
3121
+ hostname: app.applicationConfig.hostname,
3122
+ development: app.applicationConfig.development,
3123
+ fetch: app.fetch.bind(app),
3124
+ reusePort: app.applicationConfig.reusePort,
3125
+ idleTimeout: app.applicationConfig.readTimeout ? app.applicationConfig.readTimeout / 1e3 : void 0,
3126
+ websocket: {
3127
+ // @ts-ignore
3128
+ open(ws) {
3129
+ ws.data?.handler?.open?.(ws);
3130
+ },
3131
+ // @ts-ignore
3132
+ async message(ws, message) {
3133
+ if (ws.data?.handler?.message) {
3134
+ return ws.data.handler.message(ws, message);
3135
+ }
3136
+ let msgString = "";
3137
+ if (typeof message === "string") {
3138
+ msgString = message;
3139
+ } else if (message instanceof Uint8Array || message instanceof ArrayBuffer) {
3140
+ msgString = new TextDecoder().decode(message);
3141
+ } else if (typeof Buffer !== "undefined" && message instanceof Buffer) {
3142
+ msgString = message.toString();
3143
+ } else {
3144
+ return;
3145
+ }
3146
+ if (typeof msgString !== "string") return;
3147
+ let payload;
3148
+ let isJSONPayload = false;
3149
+ if (msgString.startsWith("{")) {
3150
+ try {
3151
+ payload = JSON.parse(msgString);
3152
+ isJSONPayload = true;
3153
+ } catch {
3154
+ }
3155
+ }
3156
+ if (payload) {
3157
+ const self = app;
3158
+ if (isJSONPayload && self.applicationConfig["enableHttpBridge"] && payload.type === "HTTP") {
3159
+ const { id, method, path: path2, headers, body } = payload;
3160
+ const url = new URL(path2, `http://${self.applicationConfig.hostname || "localhost"}:${port}`);
3161
+ const req = new Request(url.toString(), {
3162
+ method,
3163
+ headers,
3164
+ body: typeof body === "object" ? JSON.stringify(body) : body
3165
+ });
3166
+ const res = await self.fetch(req);
3167
+ const resBody = await res.json().catch((err) => res.text());
3168
+ const resHeaders = {};
3169
+ res.headers.forEach((v, k) => resHeaders[k] = v);
3170
+ ws.send(JSON.stringify({
3171
+ type: "RESPONSE",
3172
+ id,
3173
+ status: res.status,
3174
+ headers: resHeaders,
3175
+ body: resBody
3176
+ }));
3177
+ return;
3178
+ }
3179
+ const eventName = payload.event || (payload.type === "EVENT" ? payload.name : void 0);
3180
+ if (eventName) {
3181
+ const handlers = self.findEvent(eventName);
3182
+ const handler = handlers?.length == 1 ? handlers[0] : compose(handlers || []);
3183
+ if (handler) {
3184
+ const data = payload.data || payload.body || payload.payload || payload;
3185
+ const req = new ShokupanRequest({
3186
+ url: `http://${self.applicationConfig.hostname || "localhost"}/event/${eventName}`,
3187
+ method: "POST",
3188
+ headers: new Headers({ "content-type": "application/json" }),
3189
+ body: JSON.stringify(data)
3190
+ });
3191
+ const ctx = new ShokupanContext(
3192
+ // @ts-ignore
3193
+ req,
3194
+ // @ts-ignore
3195
+ self.server,
3196
+ {},
3197
+ self,
3198
+ null,
3199
+ self.applicationConfig.enableMiddlewareTracking,
3200
+ payload.id
3201
+ );
3202
+ ctx[$ws] = ws;
3203
+ ws.data ??= {};
3204
+ ws.data["ctx"] = ctx;
3205
+ try {
3206
+ await handler(ctx);
3207
+ } catch (err) {
3208
+ if (self.applicationConfig["websocketErrorHandler"]) {
3209
+ await self.applicationConfig["websocketErrorHandler"](err, ctx);
3210
+ } else {
3211
+ console.error(`Error in event ${eventName}:`, err);
3212
+ }
3213
+ }
3214
+ }
3215
+ }
3216
+ }
3217
+ },
3218
+ // @ts-ignore
3219
+ drain(ws) {
3220
+ ws.data?.handler?.drain?.(ws);
3221
+ },
3222
+ // @ts-ignore
3223
+ close(ws, code, reason) {
3224
+ ws.data?.handler?.close?.(ws, code, reason);
3225
+ const ctx = ws.data?.["ctx"];
3226
+ if (ctx && typeof ctx.getDisconnectCallbacks === "function") {
3227
+ const callbacks = ctx.getDisconnectCallbacks();
3228
+ if (Array.isArray(callbacks) && callbacks.length > 0) {
3229
+ Promise.all(callbacks.map((cb) => cb())).catch((err) => {
3230
+ console.error("Error executing socket disconnect hook:", err);
3231
+ });
3232
+ }
3233
+ }
3234
+ }
3235
+ }
3236
+ };
3237
+ this.server = Bun.serve(serveOptions);
3238
+ return this.server;
2660
3239
  }
2661
- /**
2662
- * Attach the verb routes with their overload signatures.
2663
- * Use compose to handle multiple handlers (middleware).
2664
- */
2665
- attachVerb(method, path2, ...args) {
2666
- let spec;
2667
- let handlers = [];
2668
- if (args.length > 0) {
2669
- if (typeof args[0] === "object" && args[0] !== null) {
2670
- spec = args[0];
2671
- handlers = args.slice(1);
2672
- } else {
2673
- handlers = args;
2674
- }
2675
- }
2676
- if (handlers.length === 0) {
2677
- return;
2678
- }
2679
- let finalHandler = handlers[handlers.length - 1];
2680
- if (handlers.length > 1) {
2681
- const fn = compose(handlers);
2682
- finalHandler = (ctx) => fn(ctx);
3240
+ async stop() {
3241
+ if (this.server) {
3242
+ this.server.stop();
2683
3243
  }
2684
- this.add({
2685
- method,
2686
- path: path2,
2687
- spec,
2688
- handler: finalHandler,
2689
- middleware: handlers.slice(0, handlers.length - 1)
2690
- });
2691
3244
  }
2692
- /**
2693
- * Generates an OpenAPI 3.1 Document by recursing through the router and its descendants.
2694
- * Now includes runtime analysis of handler functions to infer request/response types.
2695
- */
2696
- generateApiSpec(options = {}) {
2697
- return generateOpenApi(this, options);
3245
+ }
3246
+ class NodeAdapter {
3247
+ server;
3248
+ async listen(port, app) {
3249
+ let factory = app.applicationConfig.serverFactory;
3250
+ if (!factory) {
3251
+ factory = createHttpServer();
3252
+ }
3253
+ const serveOptions = {
3254
+ port,
3255
+ hostname: app.applicationConfig.hostname,
3256
+ development: app.applicationConfig.development,
3257
+ fetch: app.fetch.bind(app),
3258
+ reusePort: app.applicationConfig.reusePort
3259
+ // Node adapter might not support all options exactly the same
3260
+ };
3261
+ this.server = await factory(serveOptions);
3262
+ return this.server;
2698
3263
  }
2699
- ensureHooksInitialized() {
2700
- const hooks = this.config?.hooks;
2701
- if (hooks) {
2702
- const hookList = Array.isArray(hooks) ? hooks : [hooks];
2703
- const hookTypes = [
2704
- "onRequestStart",
2705
- "onRequestEnd",
2706
- "onResponseStart",
2707
- "onResponseEnd",
2708
- "onError",
2709
- "beforeValidate",
2710
- "afterValidate",
2711
- "onRequestTimeout",
2712
- "onReadTimeout",
2713
- "onWriteTimeout"
2714
- ];
2715
- for (let i = 0; i < hookTypes.length; i++) {
2716
- const type = hookTypes[i];
2717
- const fns = [];
2718
- for (let j = 0; j < hookList.length; j++) {
2719
- const h = hookList[j];
2720
- if (h[type]) fns.push(h[type]);
2721
- }
2722
- if (fns.length > 0) {
2723
- this.hookCache.set(type, fns);
2724
- }
2725
- }
3264
+ async stop() {
3265
+ if (this.server?.stop) {
3266
+ await this.server.stop();
2726
3267
  }
2727
- this.hooksInitialized = true;
2728
3268
  }
2729
- runHooks(name, ...args) {
2730
- if (!this.hooksInitialized) {
2731
- this.ensureHooksInitialized();
3269
+ }
3270
+ let fs;
3271
+ class DefaultFileSystemAdapter {
3272
+ async readFile(path2) {
3273
+ if (typeof Bun !== "undefined") {
3274
+ return Bun.file(path2);
3275
+ } else {
3276
+ fs ??= await import("node:fs/promises");
3277
+ return fs.readFile(path2);
2732
3278
  }
2733
- const fns = this.hookCache.get(name);
2734
- if (!fns) return;
2735
- const ctx = args?.[0] instanceof ShokupanContext ? args[0] : void 0;
2736
- const debug = ctx?.[$debug];
2737
- if (debug) {
2738
- return Promise.all(fns.map(async (fn, index) => {
2739
- const hookId = `hook_${name}_${fn.name || index}`;
2740
- const previousNode = debug.getCurrentNode();
2741
- debug.trackEdge(previousNode, hookId);
2742
- debug.setNode(hookId);
2743
- const start = performance.now();
2744
- try {
2745
- await fn(...args);
2746
- const duration = performance.now() - start;
2747
- debug.trackStep(hookId, "hook", duration, "success");
2748
- } catch (error) {
2749
- const duration = performance.now() - start;
2750
- debug.trackStep(hookId, "hook", duration, "error", error);
2751
- throw error;
2752
- } finally {
2753
- if (previousNode) debug.setNode(previousNode);
2754
- }
2755
- }));
3279
+ }
3280
+ async stat(path2) {
3281
+ if (typeof Bun !== "undefined") {
3282
+ const file = Bun.file(path2);
3283
+ return {
3284
+ size: file.size,
3285
+ mtime: new Date(file.lastModified)
3286
+ };
2756
3287
  } else {
2757
- return Promise.all(fns.map((fn) => fn(...args)));
3288
+ fs ??= await import("node:fs/promises");
3289
+ const stats = await fs.stat(path2);
3290
+ return {
3291
+ size: stats.size,
3292
+ mtime: stats.mtime
3293
+ };
2758
3294
  }
2759
3295
  }
2760
3296
  }
@@ -2766,13 +3302,24 @@ const asyncContext = new node_async_hooks.AsyncLocalStorage();
2766
3302
  class SystemCpuMonitor {
2767
3303
  constructor(intervalMs = 1e3) {
2768
3304
  this.intervalMs = intervalMs;
3305
+ this.init();
2769
3306
  }
2770
3307
  interval = null;
2771
3308
  lastCpus = [];
2772
3309
  currentUsage = 0;
3310
+ osStub = null;
3311
+ async init() {
3312
+ try {
3313
+ if (typeof process !== "undefined" && process.versions && process.versions.node) {
3314
+ this.osStub = await import("node:os");
3315
+ }
3316
+ } catch (e) {
3317
+ }
3318
+ }
2773
3319
  start() {
2774
3320
  if (this.interval) return;
2775
- this.lastCpus = os__namespace.cpus();
3321
+ if (!this.osStub) return;
3322
+ this.lastCpus = this.osStub.cpus();
2776
3323
  this.interval = setInterval(() => this.update(), this.intervalMs);
2777
3324
  }
2778
3325
  stop() {
@@ -2785,12 +3332,14 @@ class SystemCpuMonitor {
2785
3332
  return this.currentUsage;
2786
3333
  }
2787
3334
  update() {
2788
- const cpus = os__namespace.cpus();
3335
+ if (!this.osStub) return;
3336
+ const cpus = this.osStub.cpus();
2789
3337
  let idle = 0;
2790
3338
  let total = 0;
2791
3339
  for (let i = 0; i < cpus.length; i++) {
2792
3340
  const cpu = cpus[i];
2793
3341
  const prev = this.lastCpus[i];
3342
+ if (!prev) continue;
2794
3343
  let type;
2795
3344
  for (type in cpu.times) {
2796
3345
  const ticks = cpu.times[type];
@@ -2908,11 +3457,12 @@ const defaults = {
2908
3457
  enableHttpBridge: false,
2909
3458
  reusePort: false
2910
3459
  };
2911
- api.trace.getTracer("shokupan.application");
2912
3460
  class Shokupan extends ShokupanRouter {
2913
3461
  applicationConfig = {};
2914
3462
  openApiSpec;
2915
3463
  asyncApiSpec;
3464
+ openApiSpecPromise;
3465
+ asyncApiSpecPromise;
2916
3466
  composedMiddleware;
2917
3467
  cpuMonitor;
2918
3468
  server;
@@ -2926,6 +3476,7 @@ class Shokupan extends ShokupanRouter {
2926
3476
  }
2927
3477
  constructor(applicationConfig = {}) {
2928
3478
  const config = Object.assign({}, defaults, applicationConfig);
3479
+ config.fileSystem ??= new DefaultFileSystemAdapter();
2929
3480
  const { hooks, ...routerConfig } = config;
2930
3481
  super({ ...routerConfig, hooks });
2931
3482
  this[$isApplication] = true;
@@ -2937,61 +3488,51 @@ class Shokupan extends ShokupanRouter {
2937
3488
  line,
2938
3489
  name: "ShokupanApplication"
2939
3490
  };
2940
- this.dbPromise = this.initDatastore();
3491
+ if (this.applicationConfig.adapter !== "wintercg") {
3492
+ this.dbPromise = this.initDatastore().catch((err) => {
3493
+ this.logger?.debug("Failed to initialize default datastore", { error: err });
3494
+ });
3495
+ }
2941
3496
  }
2942
3497
  async initDatastore() {
2943
- const db = new surrealdb.Surreal({ engines: this.applicationConfig.surreal?.engines ?? (await import("@surrealdb/node")).createNodeEngines() });
3498
+ let engines = this.applicationConfig.surreal?.engines;
3499
+ if (!engines && !this.applicationConfig.surreal?.url?.match(/^(?:wss?|https?):\/\//)) {
3500
+ engines = (await import("@surrealdb/node")).createNodeEngines();
3501
+ }
3502
+ const db = new surrealdb.Surreal({ engines });
2944
3503
  this.datastore = new SurrealDatastore(db);
2945
3504
  await db.connect(
2946
3505
  this.applicationConfig.surreal?.url ?? (process.env.NODE_ENV === "test" ? "mem://" : "surrealkv://database"),
2947
3506
  this.applicationConfig.surreal?.connectOptions
2948
- );
3507
+ ).catch((err) => {
3508
+ this.logger?.error("Failed to connect to SurrealDB", { error: err });
3509
+ });
2949
3510
  await db.use({
2950
3511
  namespace: this.applicationConfig.surreal?.namespace ?? "vendor",
2951
3512
  database: this.applicationConfig.surreal?.database ?? "shokupan"
2952
3513
  });
3514
+ await db.query("DEFINE TABLE OVERWRITE request;");
3515
+ await db.query("DEFINE TABLE OVERWRITE failed_request;");
3516
+ await db.query("DEFINE TABLE OVERWRITE metric;");
2953
3517
  }
3518
+ /**
3519
+ * Adds middleware to the application.
3520
+ */
2954
3521
  /**
2955
3522
  * Adds middleware to the application.
2956
3523
  */
2957
3524
  use(middleware) {
2958
3525
  const { file, line } = getCallerInfo();
2959
- if (!middleware.metadata) {
2960
- middleware.metadata = {
2961
- file,
2962
- line,
2963
- name: middleware.name || "middleware",
2964
- isBuiltin: middleware.isBuiltin,
2965
- pluginName: middleware.pluginName
2966
- };
2967
- }
3526
+ const wrapped = MiddlewareTracker.wrap(middleware, {
3527
+ file,
3528
+ line,
3529
+ name: middleware.name || "middleware",
3530
+ isBuiltin: middleware.isBuiltin,
3531
+ pluginName: middleware.pluginName
3532
+ });
2968
3533
  if (this.applicationConfig.enableMiddlewareTracking) {
2969
- const trackedMiddleware = async (ctx, next) => {
2970
- const c = ctx;
2971
- if (c.handlerStack && c.app?.applicationConfig.enableMiddlewareTracking) {
2972
- const metadata = middleware.metadata || {};
2973
- const start = performance.now();
2974
- const item = {
2975
- name: metadata.pluginName ? `${metadata.pluginName} (${metadata.name})` : metadata.name || middleware.name || "middleware",
2976
- file: metadata.file || file,
2977
- line: metadata.line || line,
2978
- isBuiltin: metadata.isBuiltin,
2979
- startTime: start,
2980
- duration: -1
2981
- };
2982
- c.handlerStack.push(item);
2983
- try {
2984
- return await middleware(ctx, next);
2985
- } finally {
2986
- item.duration = performance.now() - start;
2987
- }
2988
- }
2989
- return middleware(ctx, next);
2990
- };
2991
- trackedMiddleware.metadata = middleware.metadata;
2992
- Object.defineProperty(trackedMiddleware, "name", { value: middleware.name || "middleware" });
2993
- trackedMiddleware.order = this.middleware.length;
2994
- this.middleware.push(trackedMiddleware);
3534
+ wrapped.order = this.middleware.length;
3535
+ this.middleware.push(wrapped);
2995
3536
  } else {
2996
3537
  this.middleware.push(middleware);
2997
3538
  }
@@ -3034,18 +3575,19 @@ class Shokupan extends ShokupanRouter {
3034
3575
  }
3035
3576
  await Promise.all(this.startupHooks.map((hook) => hook()));
3036
3577
  if (this.applicationConfig.enableOpenApiGen) {
3037
- this.openApiSpec = await generateOpenApi(this);
3038
- this.get("/.well-known/openapi.yaml", (ctx) => {
3578
+ this.get("/.well-known/openapi.yaml", async (ctx) => {
3039
3579
  try {
3580
+ await this.openApiSpecPromise;
3040
3581
  const yaml = jsYaml.dump(this.openApiSpec);
3041
3582
  return ctx.send(yaml, { status: 200, headers: { "content-type": "application/yaml" } });
3042
3583
  } catch (e) {
3043
- this.logger.error("Failed to generate OpenAPI YAML", { error: e });
3584
+ this.logger?.error("Failed to generate OpenAPI YAML", { error: e });
3044
3585
  return ctx.text("Internal Server Error", 500);
3045
3586
  }
3046
3587
  });
3047
3588
  if (this.applicationConfig.aiPlugin?.enabled !== false) {
3048
3589
  this.get("/.well-known/ai-plugin.json", async (ctx) => {
3590
+ await this.openApiSpecPromise;
3049
3591
  const config = this.applicationConfig.aiPlugin || {};
3050
3592
  let pkg = {};
3051
3593
  try {
@@ -3073,7 +3615,8 @@ class Shokupan extends ShokupanRouter {
3073
3615
  });
3074
3616
  }
3075
3617
  if (this.applicationConfig.apiCatalog?.enabled !== false) {
3076
- this.get("/.well-known/api-catalog", (ctx) => {
3618
+ this.get("/.well-known/api-catalog", async (ctx) => {
3619
+ await this.openApiSpecPromise;
3077
3620
  const config = this.applicationConfig.apiCatalog || {};
3078
3621
  const catalog = {
3079
3622
  versions: config.versions || [
@@ -3087,110 +3630,57 @@ class Shokupan extends ShokupanRouter {
3087
3630
  return ctx.json(catalog);
3088
3631
  });
3089
3632
  }
3090
- await Promise.all(this.specAvailableHooks.map((hook) => hook(this.openApiSpec)));
3633
+ this.openApiSpecPromise = generateOpenApi(this).then((spec) => {
3634
+ this.openApiSpec = spec;
3635
+ return spec;
3636
+ });
3637
+ const shouldBlock = this.applicationConfig.blockOnOpenApiGen !== false;
3638
+ if (shouldBlock) {
3639
+ await this.openApiSpecPromise;
3640
+ }
3641
+ if (shouldBlock) {
3642
+ await Promise.all(this.specAvailableHooks.map((hook) => hook(this.openApiSpec)));
3643
+ } else {
3644
+ this.openApiSpecPromise.then((spec) => {
3645
+ return Promise.all(this.specAvailableHooks.map((hook) => hook(spec)));
3646
+ }).catch((err) => {
3647
+ this.logger?.error("Error running spec available hooks", { error: err });
3648
+ });
3649
+ }
3091
3650
  }
3092
3651
  if (this.applicationConfig.enableAsyncApiGen) {
3093
3652
  const { generateAsyncApi: generateAsyncApi2 } = await Promise.resolve().then(() => generator);
3094
- this.asyncApiSpec = await generateAsyncApi2(this);
3653
+ this.asyncApiSpecPromise = generateAsyncApi2(this).then((spec) => {
3654
+ this.asyncApiSpec = spec;
3655
+ return spec;
3656
+ });
3657
+ const shouldBlock = this.applicationConfig.blockOnAsyncApiGen !== false;
3658
+ if (shouldBlock) {
3659
+ await this.asyncApiSpecPromise;
3660
+ }
3095
3661
  }
3096
3662
  if (port === 0 && process.platform === "linux") ;
3097
3663
  if (this.applicationConfig.autoBackpressureFeedback === true) {
3098
3664
  this.cpuMonitor = new SystemCpuMonitor();
3099
3665
  this.cpuMonitor.start();
3100
3666
  }
3101
- const self = this;
3102
- const serveOptions = {
3103
- port: finalPort,
3104
- hostname: this.applicationConfig.hostname,
3105
- development: this.applicationConfig.development,
3106
- fetch: this.fetch.bind(this),
3107
- reusePort: this.applicationConfig.reusePort,
3108
- idleTimeout: this.applicationConfig.readTimeout ? this.applicationConfig.readTimeout / 1e3 : void 0,
3109
- websocket: {
3110
- open(ws) {
3111
- ws.data?.handler?.open?.(ws);
3112
- },
3113
- async message(ws, message) {
3114
- if (ws.data?.handler?.message) {
3115
- return ws.data.handler.message(ws, message);
3116
- }
3117
- if (typeof message !== "string") return;
3118
- try {
3119
- const payload = JSON.parse(message);
3120
- if (self.applicationConfig["enableHttpBridge"] && payload.type === "HTTP") {
3121
- const { id, method, path: path2, headers, body } = payload;
3122
- const url = new URL(path2, `http://${self.applicationConfig.hostname || "localhost"}:${finalPort}`);
3123
- const req = new Request(url.toString(), {
3124
- method,
3125
- headers,
3126
- body: typeof body === "object" ? JSON.stringify(body) : body
3127
- });
3128
- const res = await self.fetch(req);
3129
- const resBody = await res.json().catch((err) => res.text());
3130
- const resHeaders = {};
3131
- res.headers.forEach((v, k) => resHeaders[k] = v);
3132
- ws.send(JSON.stringify({
3133
- type: "RESPONSE",
3134
- id,
3135
- status: res.status,
3136
- headers: resHeaders,
3137
- body: resBody
3138
- }));
3139
- return;
3140
- }
3141
- const eventName = payload.event || (payload.type === "EVENT" ? payload.name : void 0);
3142
- if (eventName) {
3143
- const handlers = self.findEvent(eventName);
3144
- const handler = handlers?.length == 1 ? handlers[0] : compose(handlers);
3145
- if (handler) {
3146
- const data = payload.data || payload.payload;
3147
- const req = new ShokupanRequest({
3148
- url: `http://${self.applicationConfig.hostname || "localhost"}/event/${eventName}`,
3149
- method: "POST",
3150
- headers: new Headers({ "content-type": "application/json" }),
3151
- body: JSON.stringify(data)
3152
- });
3153
- const ctx = new ShokupanContext(req, self.server);
3154
- ctx[$ws] = ws;
3155
- ws.data ??= {};
3156
- ws.data["ctx"] = ctx;
3157
- try {
3158
- await handler(ctx);
3159
- } catch (err) {
3160
- if (self.applicationConfig["websocketErrorHandler"]) {
3161
- await self.applicationConfig["websocketErrorHandler"](err, ctx);
3162
- } else {
3163
- console.error(`Error in event ${eventName}:`, err);
3164
- }
3165
- }
3166
- }
3167
- }
3168
- } catch (e) {
3169
- }
3170
- },
3171
- drain(ws) {
3172
- ws.data?.handler?.drain?.(ws);
3173
- },
3174
- close(ws, code, reason) {
3175
- ws.data?.handler?.close?.(ws, code, reason);
3176
- const ctx = ws.data?.["ctx"];
3177
- if (ctx && typeof ctx.getDisconnectCallbacks === "function") {
3178
- const callbacks = ctx.getDisconnectCallbacks();
3179
- if (Array.isArray(callbacks) && callbacks.length > 0) {
3180
- Promise.all(callbacks.map((cb) => cb())).catch((err) => {
3181
- console.error("Error executing socket disconnect hook:", err);
3182
- });
3183
- }
3184
- }
3185
- }
3667
+ let adapter = this.applicationConfig.adapter;
3668
+ if (!adapter) {
3669
+ if (typeof Bun !== "undefined") {
3670
+ this.applicationConfig.adapter = "bun";
3671
+ adapter = new BunAdapter();
3672
+ } else {
3673
+ this.applicationConfig.adapter = "node";
3674
+ adapter = new NodeAdapter();
3186
3675
  }
3187
- };
3188
- let factory = this.applicationConfig.serverFactory;
3189
- if (!factory && typeof Bun === "undefined") {
3190
- const { createHttpServer } = await Promise.resolve().then(() => require("./http-server-BEMPIs33.cjs"));
3191
- factory = createHttpServer();
3676
+ } else if (adapter === "bun") {
3677
+ adapter = new BunAdapter();
3678
+ } else if (adapter === "node") {
3679
+ adapter = new NodeAdapter();
3680
+ } else if (adapter === "wintercg") {
3681
+ throw new Error("WinterCG adapter does not support listen(). Use fetch directly.");
3192
3682
  }
3193
- this.server = factory ? await factory(serveOptions) : Bun.serve(serveOptions);
3683
+ this.server = await adapter.listen(finalPort, this);
3194
3684
  return this.server;
3195
3685
  }
3196
3686
  /**
@@ -3243,11 +3733,16 @@ class Shokupan extends ShokupanRouter {
3243
3733
  }
3244
3734
  url = u.toString();
3245
3735
  }
3736
+ const reqBody = options.body && typeof options.body === "object" ? JSON.stringify(options.body) : options.body;
3737
+ const reqHeaders = new Headers(options.headers);
3738
+ if (typeof options.body === "object" && !reqHeaders.has("content-type")) {
3739
+ reqHeaders.set("content-type", "application/json");
3740
+ }
3246
3741
  const req = new ShokupanRequest({
3247
3742
  method: options.method || "GET",
3248
3743
  url,
3249
- headers: options.headers,
3250
- body: options.body && typeof options.body === "object" ? JSON.stringify(options.body) : options.body
3744
+ headers: reqHeaders,
3745
+ body: reqBody
3251
3746
  });
3252
3747
  const res = await this.fetch(req);
3253
3748
  const status = res.status;
@@ -3307,19 +3802,22 @@ class Shokupan extends ShokupanRouter {
3307
3802
  if (this.cpuMonitor && this.cpuMonitor.getUsage() > (this.applicationConfig.autoBackpressureLevel ?? 60)) {
3308
3803
  const msg = "Too Many Requests (CPU Backpressure)";
3309
3804
  const res = ctx.text(msg, 429);
3310
- await this.runHooks("onResponseEnd", ctx, res);
3805
+ if (this.hasOnResponseEndHook) await this.runHooks("onResponseEnd", ctx, res);
3311
3806
  return res;
3312
3807
  }
3313
3808
  try {
3314
- await this.runHooks("onRequestStart", ctx);
3809
+ if (this.hasOnRequestStartHook) await this.runHooks("onRequestStart", ctx);
3315
3810
  const fn = this.composedMiddleware ??= compose(this.middleware);
3316
3811
  const result = await fn(ctx, async () => {
3317
- const bodyParsing = ["POST", "PUT", "PATCH", "DELETE"].includes(req.method) ? ctx.parseBody() : Promise.resolve();
3812
+ let bodyParsing;
3813
+ if (req.method !== "GET" && req.method !== "HEAD") {
3814
+ bodyParsing = ctx.parseBody();
3815
+ }
3318
3816
  const match = this.find(req.method, ctx.path);
3319
3817
  if (match) {
3320
3818
  ctx[$routeMatched] = true;
3321
3819
  ctx.params = match.params;
3322
- await bodyParsing;
3820
+ if (bodyParsing) await bodyParsing;
3323
3821
  return match.handler(ctx);
3324
3822
  }
3325
3823
  return null;
@@ -3351,19 +3849,22 @@ class Shokupan extends ShokupanRouter {
3351
3849
  } else {
3352
3850
  response = ctx.text(String(result));
3353
3851
  }
3354
- await this.runHooks("onRequestEnd", ctx);
3852
+ if (this.hasOnRequestEndHook) await this.runHooks("onRequestEnd", ctx);
3355
3853
  if (response instanceof Promise) {
3356
3854
  response = await response;
3357
3855
  }
3358
- await this.runHooks("onResponseStart", ctx, response);
3856
+ if (this.hasOnResponseStartHook) await this.runHooks("onResponseStart", ctx, response);
3359
3857
  return response;
3360
3858
  } catch (err) {
3361
3859
  const span = asyncContext.getStore()?.span;
3362
3860
  if (span) span.setStatus({ code: 2 });
3363
- const status = getErrorStatus(err);
3861
+ let status = getErrorStatus(err);
3862
+ if (err instanceof SyntaxError && err.message.includes("JSON")) {
3863
+ status = 400;
3864
+ }
3364
3865
  const body = { error: err.message || "Internal Server Error" };
3365
3866
  if (err.errors) body.errors = err.errors;
3366
- await this.runHooks("onError", ctx, err);
3867
+ if (this.hasOnErrorHook) await this.runHooks("onError", ctx, err);
3367
3868
  return ctx.json(body, status);
3368
3869
  }
3369
3870
  };
@@ -3700,16 +4201,48 @@ function ApiExplorerApp({ spec, asyncSpec, config }) {
3700
4201
  const commonPrefix = findCommonPrefix(routes);
3701
4202
  const commonPrefixPath = "/" + commonPrefix.join("/");
3702
4203
  const children = createSubgroups(routes, 0, commonPrefix.length);
4204
+ const groupMiddleware = [];
4205
+ if (spec["x-middleware-registry"]) {
4206
+ Object.entries(spec["x-middleware-registry"]).forEach(([id, mw]) => {
4207
+ const firstRoute = routes[0];
4208
+ const routeSource = firstRoute?.op?.["x-shokupan-source"];
4209
+ const mwFile = mw.file?.split("/").pop();
4210
+ const routeFile = routeSource?.file?.split("/").pop();
4211
+ if (mwFile && routeFile && mwFile === routeFile && mw.scope !== "global") {
4212
+ groupMiddleware.push({ ...mw, id, type: "middleware" });
4213
+ }
4214
+ });
4215
+ }
4216
+ const isBuiltin = routes.some((r) => r.op["x-shokupan-builtin"] === true);
3703
4217
  return {
3704
4218
  name,
3705
4219
  type: "group",
3706
4220
  children,
3707
- commonPrefixPath
4221
+ middleware: groupMiddleware,
4222
+ commonPrefixPath,
3708
4223
  // Store for display stripping
4224
+ isBuiltin
3709
4225
  };
3710
- }).sort((a, b) => {
4226
+ });
4227
+ if (spec["x-middleware-registry"]) {
4228
+ const allGroupMiddleware = hierarchicalGroups.flatMap((g) => g.middleware || []).map((m) => m.id);
4229
+ const globalMiddleware = Object.entries(spec["x-middleware-registry"]).filter(([id]) => !allGroupMiddleware.includes(id)).map(([id, mw]) => ({ ...mw, id, type: "middleware" }));
4230
+ if (globalMiddleware.length > 0) {
4231
+ hierarchicalGroups.push({
4232
+ name: "Global Middleware",
4233
+ type: "group",
4234
+ children: [],
4235
+ middleware: globalMiddleware,
4236
+ commonPrefixPath: "",
4237
+ isBuiltin: false
4238
+ });
4239
+ }
4240
+ }
4241
+ hierarchicalGroups.sort((a, b) => {
3711
4242
  if (a.name === "Ungrouped") return 1;
3712
4243
  if (b.name === "Ungrouped") return -1;
4244
+ if (a.name === "Global Middleware") return 1;
4245
+ if (b.name === "Global Middleware") return -1;
3713
4246
  return a.name.localeCompare(b.name);
3714
4247
  });
3715
4248
  const allRoutes = Array.from(hierarchy.values()).flat();
@@ -3718,6 +4251,9 @@ function ApiExplorerApp({ spec, asyncSpec, config }) {
3718
4251
  /* @__PURE__ */ jsxRuntime.jsx("meta", { charSet: "UTF-8" }),
3719
4252
  /* @__PURE__ */ jsxRuntime.jsx("meta", { name: "viewport", content: "width=device-width, initial-scale=1.0" }),
3720
4253
  /* @__PURE__ */ jsxRuntime.jsx("title", { children: spec.info?.title || "API Explorer" }),
4254
+ /* @__PURE__ */ jsxRuntime.jsx("link", { rel: "preconnect", href: "https://fonts.googleapis.com" }),
4255
+ /* @__PURE__ */ jsxRuntime.jsx("link", { rel: "preconnect", href: "https://fonts.gstatic.com", crossorigin: "anonymous" }),
4256
+ /* @__PURE__ */ jsxRuntime.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" }),
3721
4257
  /* @__PURE__ */ jsxRuntime.jsx("link", { rel: "stylesheet", href: "style.css" }),
3722
4258
  /* @__PURE__ */ jsxRuntime.jsx("link", { rel: "stylesheet", href: "theme.css" }),
3723
4259
  /* @__PURE__ */ jsxRuntime.jsx("script", { src: "https://cdn.jsdelivr.net/npm/marked@4.3.0/marked.min.js" }),
@@ -3814,13 +4350,46 @@ function Sidebar$1({ spec, hierarchicalGroups }) {
3814
4350
  /* @__PURE__ */ jsxRuntime.jsx("div", { class: "version", children: spec.info?.version })
3815
4351
  ] }),
3816
4352
  /* @__PURE__ */ jsxRuntime.jsx("div", { class: "sidebar-collapse-trigger", children: "➔" }),
3817
- /* @__PURE__ */ jsxRuntime.jsx("nav", { class: "nav-groups", children: hierarchicalGroups.map((group) => /* @__PURE__ */ jsxRuntime.jsxs("div", { class: "nav-group collapsed", children: [
4353
+ /* @__PURE__ */ jsxRuntime.jsx("nav", { class: "nav-groups", children: hierarchicalGroups.map((group) => /* @__PURE__ */ jsxRuntime.jsxs("div", { class: `nav-group collapsed ${group.isBuiltin ? "builtin-group" : ""}`, children: [
3818
4354
  /* @__PURE__ */ jsxRuntime.jsxs("div", { class: "nav-group-title", children: [
3819
4355
  /* @__PURE__ */ jsxRuntime.jsx("span", { class: "chevron", children: /* @__PURE__ */ jsxRuntime.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__ */ jsxRuntime.jsx("polyline", { points: "9 18 15 12 9 6" }) }) }),
3820
- " ",
4356
+ group.isBuiltin && /* @__PURE__ */ jsxRuntime.jsx("span", { class: "builtin-icon", title: "Built-in Plugin", children: /* @__PURE__ */ jsxRuntime.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: [
4357
+ /* @__PURE__ */ jsxRuntime.jsx("rect", { x: "2", y: "2", width: "20", height: "20", rx: "5", ry: "5" }),
4358
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M16 11.37A4 4 0 1 1 12.63 8 4 4 0 0 1 16 11.37z" }),
4359
+ /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "17.5", y1: "6.5", x2: "17.51", y2: "6.5" })
4360
+ ] }) }),
3821
4361
  group.name
3822
4362
  ] }),
3823
- /* @__PURE__ */ jsxRuntime.jsx("div", { class: "nav-items", children: group.children?.map((child) => renderNavNode(child, 0, group.commonPrefixPath || "")) })
4363
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { class: "nav-items", children: [
4364
+ group.middleware && group.middleware.length > 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { class: "group-middleware", children: group.middleware.map((mw) => /* @__PURE__ */ jsxRuntime.jsxs(
4365
+ "a",
4366
+ {
4367
+ href: `#middleware-${mw.id}`,
4368
+ class: "nav-item middleware-nav-item",
4369
+ "data-middleware-id": mw.id,
4370
+ title: mw.name,
4371
+ children: [
4372
+ /* @__PURE__ */ jsxRuntime.jsx("span", { class: "middleware-icon", children: "⚙" }),
4373
+ /* @__PURE__ */ jsxRuntime.jsx("span", { class: "nav-label", children: mw.name }),
4374
+ mw.usedBy && mw.usedBy.length > 0 && /* @__PURE__ */ jsxRuntime.jsx("span", { class: "middleware-badge", title: `Used by ${mw.usedBy.length} routes`, children: mw.usedBy.length }),
4375
+ mw.file && /* @__PURE__ */ jsxRuntime.jsx(
4376
+ "a",
4377
+ {
4378
+ href: `vscode://file/${mw.file}:${mw.startLine || 1}`,
4379
+ class: "nav-source-link",
4380
+ title: `${mw.file}:${mw.startLine || 1}`,
4381
+ children: /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: "10", height: "10", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", children: [
4382
+ /* @__PURE__ */ jsxRuntime.jsx("polyline", { points: "16 18 22 12 16 6" }),
4383
+ /* @__PURE__ */ jsxRuntime.jsx("polyline", { points: "8 6 2 12 8 18" })
4384
+ ] })
4385
+ }
4386
+ )
4387
+ ]
4388
+ },
4389
+ mw.id
4390
+ )) }),
4391
+ group.children?.map((child) => renderNavNode(child, 0, group.commonPrefixPath || ""))
4392
+ ] })
3824
4393
  ] }, group.name)) })
3825
4394
  ] });
3826
4395
  }
@@ -3828,7 +4397,8 @@ function MainContent$1({ allRoutes, config, spec }) {
3828
4397
  const explorerData = JSON.stringify({
3829
4398
  routes: allRoutes,
3830
4399
  config,
3831
- info: spec.info
4400
+ info: spec.info,
4401
+ middlewareRegistry: spec["x-middleware-registry"] || {}
3832
4402
  });
3833
4403
  const safeJson = explorerData.replace(/<\/script>/g, "<\\/script>");
3834
4404
  return /* @__PURE__ */ jsxRuntime.jsxs("main", { class: "content", id: "main-content", children: [
@@ -3838,9 +4408,16 @@ function MainContent$1({ allRoutes, config, spec }) {
3838
4408
  }
3839
4409
  class ApiExplorerPlugin extends ShokupanRouter {
3840
4410
  constructor(pluginOptions = {}) {
4411
+ console.log("ApiExplorerPlugin: CONSTRUCTOR CALLED");
3841
4412
  super({ renderer: renderToString });
3842
4413
  this.pluginOptions = pluginOptions;
3843
4414
  pluginOptions.path ??= "/explorer";
4415
+ this.metadata = {
4416
+ file: void 0,
4417
+ line: 1,
4418
+ name: "ApiExplorerPlugin",
4419
+ pluginName: "ApiExplorer"
4420
+ };
3844
4421
  this.init();
3845
4422
  }
3846
4423
  onInit(app, options) {
@@ -3871,6 +4448,7 @@ class ApiExplorerPlugin extends ShokupanRouter {
3871
4448
  delete op["x-source-info"].snippet;
3872
4449
  }
3873
4450
  if (op["x-shokupan-source"]?.code) {
4451
+ console.log("Deleting x-shokupan-source.code");
3874
4452
  delete op["x-shokupan-source"].code;
3875
4453
  }
3876
4454
  });
@@ -3897,7 +4475,10 @@ class ApiExplorerPlugin extends ShokupanRouter {
3897
4475
  this.get("/", async (ctx) => {
3898
4476
  const spec = this.root.openApiSpec ? structuredClone(this.root.openApiSpec) : await (this.root || this).generateApiSpec();
3899
4477
  const asyncSpec = ctx.app.asyncApiSpec;
3900
- return ctx.jsx(ApiExplorerApp({ spec: stripSourceCode(spec), asyncSpec }));
4478
+ const element = ApiExplorerApp({ spec: stripSourceCode(spec), asyncSpec });
4479
+ const html = renderToString(element);
4480
+ if (html.length === 0) throw new Error("ApiExplorerPlugin: rendered page is blank.");
4481
+ return ctx.html(html);
3901
4482
  });
3902
4483
  }
3903
4484
  }
@@ -3908,8 +4489,8 @@ function AsyncApiApp({ spec, serverUrl, base, disableSourceView, navTree }) {
3908
4489
  /* @__PURE__ */ jsxRuntime.jsx("meta", { name: "viewport", content: "width=device-width, initial-scale=1.0" }),
3909
4490
  /* @__PURE__ */ jsxRuntime.jsx("title", { children: "Shokupan AsyncAPI" }),
3910
4491
  /* @__PURE__ */ jsxRuntime.jsx("link", { rel: "preconnect", href: "https://fonts.googleapis.com" }),
3911
- /* @__PURE__ */ jsxRuntime.jsx("link", { rel: "preconnect", href: "https://fonts.gstatic.com", crossOrigin: "anonymous" }),
3912
- /* @__PURE__ */ jsxRuntime.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" }),
4492
+ /* @__PURE__ */ jsxRuntime.jsx("link", { rel: "preconnect", href: "https://fonts.gstatic.com", crossorigin: "anonymous" }),
4493
+ /* @__PURE__ */ jsxRuntime.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" }),
3913
4494
  /* @__PURE__ */ jsxRuntime.jsx("link", { rel: "stylesheet", href: `${base}/theme.css` }),
3914
4495
  /* @__PURE__ */ jsxRuntime.jsx("link", { rel: "stylesheet", href: `${base}/style.css` }),
3915
4496
  /* @__PURE__ */ jsxRuntime.jsx("script", { dangerouslySetInnerHTML: {
@@ -3917,6 +4498,7 @@ function AsyncApiApp({ spec, serverUrl, base, disableSourceView, navTree }) {
3917
4498
  window.INITIAL_SPEC = ${JSON.stringify(spec)};
3918
4499
  window.INITIAL_SERVER_URL = "${serverUrl}";
3919
4500
  window.DISABLE_SOURCE_VIEW = ${JSON.stringify(disableSourceView)};
4501
+ window.BASE_PATH = "${base}";
3920
4502
  `
3921
4503
  } })
3922
4504
  ] }),
@@ -3987,8 +4569,14 @@ function LeafNode({ item, label, disableSourceView }) {
3987
4569
  ] });
3988
4570
  } else {
3989
4571
  const badgeText = item.data.type === "publish" ? "SEND" : "RECV";
4572
+ const isPlugin = item.data.op?.["x-shokupan-plugin-name"] || sourceInfo?.pluginName;
3990
4573
  content = /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
3991
4574
  /* @__PURE__ */ jsxRuntime.jsx("span", { class: `badge badge-${badgeText}`, children: badgeText }),
4575
+ isPlugin && /* @__PURE__ */ jsxRuntime.jsx("span", { class: "builtin-icon", title: "Built-in Plugin", children: /* @__PURE__ */ jsxRuntime.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: [
4576
+ /* @__PURE__ */ jsxRuntime.jsx("rect", { x: "2", y: "2", width: "20", height: "20", rx: "5", ry: "5" }),
4577
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M16 11.37A4 4 0 1 1 12.63 8 4 4 0 0 1 16 11.37z" }),
4578
+ /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "17.5", y1: "6.5", x2: "17.51", y2: "6.5" })
4579
+ ] }) }),
3992
4580
  /* @__PURE__ */ jsxRuntime.jsx("span", { class: "tree-label", children: label })
3993
4581
  ] });
3994
4582
  }
@@ -4085,45 +4673,56 @@ function buildNavTree(spec) {
4085
4673
  });
4086
4674
  return root;
4087
4675
  }
4088
- async function getAstRoutes(applications) {
4089
- const astRoutes = [];
4090
- const getExpandedRoutes = (app, prefix = "", seen = /* @__PURE__ */ new Set()) => {
4091
- if (seen.has(app.name)) return [];
4092
- const newSeen = new Set(seen);
4093
- newSeen.add(app.name);
4094
- const expanded = [];
4095
- for (const route of app.routes) {
4096
- expanded.push({
4097
- ...route,
4098
- // For events, path is the event name
4099
- path: route.path.startsWith("/") ? route.path.slice(1) : route.path
4100
- });
4101
- }
4102
- if (app.mounted) {
4103
- for (const mount of app.mounted) {
4104
- const targetApp = applications.find((a) => a.name === mount.target || a.className === mount.target);
4105
- if (targetApp) {
4106
- expanded.push(...getExpandedRoutes(targetApp, "", newSeen));
4107
- }
4108
- }
4109
- }
4110
- return expanded;
4111
- };
4112
- applications.forEach((app) => {
4113
- astRoutes.push(...getExpandedRoutes(app));
4114
- });
4115
- return astRoutes;
4676
+ function hasUnknownFields(schema) {
4677
+ if (!schema) return false;
4678
+ if (schema["x-unknown"]) return true;
4679
+ if (schema.type === "object" && schema.properties) {
4680
+ return Object.values(schema.properties).some(
4681
+ (prop) => hasUnknownFields(prop)
4682
+ );
4683
+ }
4684
+ if (schema.type === "array" && schema.items) {
4685
+ return hasUnknownFields(schema.items);
4686
+ }
4687
+ return false;
4116
4688
  }
4117
4689
  async function generateAsyncApi(rootRouter, options = {}) {
4118
4690
  const channels = {};
4119
4691
  let astRoutes = [];
4692
+ let astMiddlewareRegistry = {};
4693
+ let applications = [];
4120
4694
  try {
4121
- const { OpenAPIAnalyzer } = await Promise.resolve().then(() => require("./analyzer-CKLGLFtx.cjs"));
4695
+ const { OpenAPIAnalyzer } = await Promise.resolve().then(() => require("./analyzer-BAhvpNY_.cjs"));
4122
4696
  const entrypoint = globalThis.Bun?.main || require.main?.filename || process.argv[1];
4123
4697
  const analyzer2 = new OpenAPIAnalyzer(process.cwd(), entrypoint);
4124
- const { applications } = await analyzer2.analyze();
4125
- astRoutes = await getAstRoutes(applications);
4698
+ const analysisResult = await analyzer2.analyze();
4699
+ applications = analysisResult.applications;
4700
+ astRoutes = await getAstRoutes(applications, {
4701
+ includePrefix: false,
4702
+ pathTransform: (p) => p.startsWith("/") ? p.slice(1) : p
4703
+ });
4704
+ let middlewareId = 0;
4705
+ for (const app of applications) {
4706
+ if (app.middleware && app.middleware.length > 0) {
4707
+ for (const mw of app.middleware) {
4708
+ const id = `middleware-${middlewareId++}`;
4709
+ astMiddlewareRegistry[id] = {
4710
+ ...mw,
4711
+ id,
4712
+ usedBy: []
4713
+ // Will be populated when processing events
4714
+ };
4715
+ }
4716
+ }
4717
+ }
4126
4718
  } catch (e) {
4719
+ if (options.warnings) {
4720
+ options.warnings.push({
4721
+ type: "ast-analysis-failed",
4722
+ message: "AST Analysis failed or skipped",
4723
+ detail: e.message
4724
+ });
4725
+ }
4127
4726
  }
4128
4727
  const matchedAstRoutes = /* @__PURE__ */ new Set();
4129
4728
  const collect = async (router, prefix = "") => {
@@ -4167,23 +4766,45 @@ async function generateAsyncApi(rootRouter, options = {}) {
4167
4766
  endLine: astMatch?.sourceContext?.endLine,
4168
4767
  highlightLines: astMatch?.sourceContext ? [astMatch.sourceContext.startLine, astMatch.sourceContext.endLine] : void 0
4169
4768
  } : void 0;
4769
+ const message = {
4770
+ ...userSpec?.message || {}
4771
+ };
4772
+ let inferenceFailed = false;
4773
+ if (!message.payload) {
4774
+ if (astMatch) {
4775
+ if (astMatch.requestTypes?.body) {
4776
+ message.payload = astMatch.requestTypes.body;
4777
+ if (message.payload.type === "object" && !message.payload.properties && !message.payload.additionalProperties && Object.keys(message.payload).length === 1) {
4778
+ inferenceFailed = true;
4779
+ }
4780
+ }
4781
+ } else {
4782
+ message.payload = { type: "object" };
4783
+ inferenceFailed = true;
4784
+ }
4785
+ }
4170
4786
  if (!channels[eventName]) {
4171
- channels[eventName] = {
4172
- publish: {
4173
- operationId: `on${eventName.charAt(0).toUpperCase() + eventName.slice(1)}`,
4174
- tags,
4175
- message: {
4176
- payload: { type: "object" },
4177
- ...userSpec?.message ? userSpec.message : {}
4178
- },
4179
- ...userSpec?.type === "publish" ? userSpec : {},
4180
- "x-source-info": sourceInfo ? [sourceInfo] : [],
4181
- "x-shokupan-source": sourceInfo
4182
- // Simplified
4787
+ const publishOp = {
4788
+ operationId: `on${eventName.charAt(0).toUpperCase() + eventName.slice(1)}`,
4789
+ tags,
4790
+ message,
4791
+ ...userSpec?.type === "publish" ? userSpec : {},
4792
+ "x-source-info": sourceInfo ? [sourceInfo] : [],
4793
+ "x-shokupan-source": {
4794
+ ...sourceInfo,
4795
+ pluginName: handler.pluginName
4183
4796
  }
4184
4797
  };
4185
- if (userSpec?.summary) channels[eventName].publish.summary = userSpec.summary;
4186
- if (userSpec?.description) channels[eventName].publish.description = userSpec.description;
4798
+ if (inferenceFailed) {
4799
+ publishOp["x-warning"] = true;
4800
+ if (!publishOp.summary) publishOp.summary = "Payload Inference Failed";
4801
+ 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.";
4802
+ }
4803
+ if (userSpec?.summary && !publishOp.summary) publishOp.summary = userSpec.summary;
4804
+ if (userSpec?.description && !publishOp.description) publishOp.description = userSpec.description;
4805
+ channels[eventName] = {
4806
+ publish: publishOp
4807
+ };
4187
4808
  } else {
4188
4809
  if (sourceInfo) {
4189
4810
  if (!channels[eventName].publish["x-source-info"]) {
@@ -4201,6 +4822,14 @@ async function generateAsyncApi(rootRouter, options = {}) {
4201
4822
  for (const emit of emits) {
4202
4823
  if (emit.event === "__DYNAMIC_EMIT__") {
4203
4824
  const warningKey = `${eventName}/Dynamic Emit`;
4825
+ if (options.warnings) {
4826
+ options.warnings.push({
4827
+ type: "dynamic-emit",
4828
+ message: "Dynamic emit detected",
4829
+ detail: `Event: ${eventName}`,
4830
+ location: { file: astMatch?.sourceContext?.file, line: emit.location?.startLine }
4831
+ });
4832
+ }
4204
4833
  channels[warningKey] = {
4205
4834
  subscribe: {
4206
4835
  operationId: `dynamicEmitWarning${eventName}`,
@@ -4235,17 +4864,24 @@ async function generateAsyncApi(rootRouter, options = {}) {
4235
4864
  emitHighlightLines: [emitStart, emitEnd]
4236
4865
  } : void 0;
4237
4866
  if (!channels[emit.event]) {
4867
+ const payload = emit.payload || { type: "object" };
4868
+ const warning = hasUnknownFields(payload);
4238
4869
  channels[emit.event] = {
4239
4870
  subscribe: {
4240
4871
  operationId: `emit${emit.event.charAt(0).toUpperCase() + emit.event.slice(1)}`,
4241
4872
  tags,
4242
4873
  message: {
4243
- payload: emit.payload || { type: "object" }
4874
+ payload
4244
4875
  },
4876
+ ...warning ? {
4877
+ "x-warning": true,
4878
+ "x-warning-reason": "Payload contains fields with unknown types that could not be statically analyzed"
4879
+ } : {},
4245
4880
  "x-source-info": newSourceInfo ? [newSourceInfo] : [],
4246
4881
  "x-shokupan-source": sourceInfo && emitStart ? {
4247
4882
  file: sourceInfo.file,
4248
- line: emitStart
4883
+ line: emitStart,
4884
+ pluginName: handler.pluginName
4249
4885
  } : void 0
4250
4886
  }
4251
4887
  };
@@ -4309,13 +4945,19 @@ async function generateAsyncApi(rootRouter, options = {}) {
4309
4945
  emitHighlightLines: [emitStart, emitEnd]
4310
4946
  } : void 0;
4311
4947
  if (!channels[emit.event]) {
4948
+ const payload = emit.payload || { type: "object" };
4949
+ const warning = hasUnknownFields(payload);
4312
4950
  channels[emit.event] = {
4313
4951
  subscribe: {
4314
4952
  operationId: `emit${emit.event.charAt(0).toUpperCase() + emit.event.slice(1)}`,
4315
4953
  tags,
4316
4954
  message: {
4317
- payload: emit.payload || { type: "object" }
4955
+ payload
4318
4956
  },
4957
+ ...warning ? {
4958
+ "x-warning": true,
4959
+ "x-warning-reason": "Payload contains fields with unknown types that could not be statically analyzed"
4960
+ } : {},
4319
4961
  "x-source-info": newSourceInfo ? [newSourceInfo] : [],
4320
4962
  "x-shokupan-source": sourceInfo && emitStart ? {
4321
4963
  file: sourceInfo.file,
@@ -4354,6 +4996,14 @@ async function generateAsyncApi(rootRouter, options = {}) {
4354
4996
  if (parts.length > 0) prefix = parts[0];
4355
4997
  }
4356
4998
  const key = `${prefix}.Dynamic Event ${i + 1}`;
4999
+ if (options.warnings) {
5000
+ options.warnings.push({
5001
+ type: "dynamic-event",
5002
+ message: "Dynamic event listener detected",
5003
+ detail: `Event listener with dynamic name`,
5004
+ location: { file: r.sourceContext?.file, line: r.sourceContext?.startLine }
5005
+ });
5006
+ }
4357
5007
  channels[key] = {
4358
5008
  publish: {
4359
5009
  operationId: `dynamicEventWarning${i}`,
@@ -4379,7 +5029,8 @@ async function generateAsyncApi(rootRouter, options = {}) {
4379
5029
  return {
4380
5030
  asyncapi: "3.0.0",
4381
5031
  info: { title: "Shokupan AsyncAPI", version: "1.0.0", ...options.info },
4382
- channels
5032
+ channels,
5033
+ "x-middleware-registry": astMiddlewareRegistry
4383
5034
  };
4384
5035
  }
4385
5036
  const generator = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
@@ -4391,6 +5042,12 @@ class AsyncApiPlugin extends ShokupanRouter {
4391
5042
  super({ renderer: renderToString });
4392
5043
  this.pluginOptions = pluginOptions;
4393
5044
  this.pluginOptions.path ??= "/asyncapi";
5045
+ this.metadata = {
5046
+ file: void 0,
5047
+ line: 1,
5048
+ name: "AsyncApiPlugin",
5049
+ pluginName: "AsyncAPI"
5050
+ };
4394
5051
  this.init();
4395
5052
  }
4396
5053
  static getBasePath() {
@@ -4803,12 +5460,15 @@ class ClusterPlugin {
4803
5460
  }
4804
5461
  }
4805
5462
  }
4806
- function DashboardApp({ metrics, uptime, integrations, base, getRequestHeadersSource }) {
5463
+ function DashboardApp({ metrics, uptime, integrations, base, getRequestHeadersSource, rootPath, linkPattern }) {
4807
5464
  return /* @__PURE__ */ jsxRuntime.jsxs("html", { lang: "en", children: [
4808
5465
  /* @__PURE__ */ jsxRuntime.jsxs("head", { children: [
4809
5466
  /* @__PURE__ */ jsxRuntime.jsx("meta", { charSet: "UTF-8" }),
4810
5467
  /* @__PURE__ */ jsxRuntime.jsx("meta", { name: "viewport", content: "width=device-width, initial-scale=1.0" }),
4811
5468
  /* @__PURE__ */ jsxRuntime.jsx("title", { children: "Shokupan Debug Dashboard" }),
5469
+ /* @__PURE__ */ jsxRuntime.jsx("link", { rel: "preconnect", href: "https://fonts.googleapis.com" }),
5470
+ /* @__PURE__ */ jsxRuntime.jsx("link", { rel: "preconnect", href: "https://fonts.gstatic.com", crossorigin: "anonymous" }),
5471
+ /* @__PURE__ */ jsxRuntime.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" }),
4812
5472
  /* @__PURE__ */ jsxRuntime.jsx("link", { href: "https://unpkg.com/tabulator-tables@5.5.0/dist/css/tabulator_bootstrap5.min.css", rel: "stylesheet" }),
4813
5473
  /* @__PURE__ */ jsxRuntime.jsx("link", { rel: "stylesheet", href: "https://esm.sh/@xyflow/react@12.3.6/dist/style.css" }),
4814
5474
  /* @__PURE__ */ jsxRuntime.jsx("link", { rel: "stylesheet", href: `${base}/theme.css` }),
@@ -4817,104 +5477,134 @@ function DashboardApp({ metrics, uptime, integrations, base, getRequestHeadersSo
4817
5477
  /* @__PURE__ */ jsxRuntime.jsx("link", { rel: "stylesheet", href: `${base}/registry.css` }),
4818
5478
  /* @__PURE__ */ jsxRuntime.jsx("link", { rel: "stylesheet", href: `${base}/tabulator.css` }),
4819
5479
  /* @__PURE__ */ jsxRuntime.jsx("script", { src: "https://cdn.jsdelivr.net/npm/chart.js" }),
4820
- /* @__PURE__ */ jsxRuntime.jsx("script", { type: "text/javascript", src: "https://unpkg.com/tabulator-tables@5.5.0/dist/js/tabulator.min.js" })
5480
+ /* @__PURE__ */ jsxRuntime.jsx("script", { type: "text/javascript", src: "https://unpkg.com/tabulator-tables@5.5.0/dist/js/tabulator.min.js" }),
5481
+ /* @__PURE__ */ jsxRuntime.jsx("script", { src: "https://cdn.jsdelivr.net/npm/monaco-editor@0.44.0/min/vs/loader.js" })
4821
5482
  ] }),
4822
5483
  /* @__PURE__ */ jsxRuntime.jsxs("body", { children: [
4823
5484
  /* @__PURE__ */ jsxRuntime.jsxs("div", { class: "container", children: [
4824
5485
  /* @__PURE__ */ jsxRuntime.jsxs("header", { children: [
4825
- /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
4826
- /* @__PURE__ */ jsxRuntime.jsx("h1", { children: "Dashboard" }),
4827
- /* @__PURE__ */ jsxRuntime.jsxs("div", { style: "color: var(--text-secondary)", children: [
5486
+ /* @__PURE__ */ jsxRuntime.jsx("div", { children: /* @__PURE__ */ jsxRuntime.jsx("h1", { children: "Shokupan" }) }),
5487
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { style: "margin-left: 8px", children: [
5488
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { style: "color: var(--text-secondary)", children: [
4828
5489
  "Uptime: ",
4829
5490
  /* @__PURE__ */ jsxRuntime.jsx("span", { id: "uptime", children: uptime })
4830
- ] })
5491
+ ] }),
5492
+ /* @__PURE__ */ jsxRuntime.jsx("span", { id: "ws-status", title: "WebSocket: Disconnected", style: "width: 10px; height: 10px; border-radius: 50%; background: #6b7280; display: inline-block; margin-left: 10px;" })
4831
5493
  ] }),
5494
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: "flex: 1;" }),
4832
5495
  /* @__PURE__ */ jsxRuntime.jsxs("div", { class: "tabs", children: [
4833
5496
  /* @__PURE__ */ jsxRuntime.jsx("button", { class: "tab-btn active", onclick: "switchTab('overview')", children: "Overview" }),
4834
- /* @__PURE__ */ jsxRuntime.jsx("button", { class: "tab-btn", onclick: "switchTab('registry')", children: "Registry" }),
4835
- /* @__PURE__ */ jsxRuntime.jsx("button", { class: "tab-btn", onclick: "switchTab('graph')", children: "Graph" }),
4836
- /* @__PURE__ */ jsxRuntime.jsx("button", { class: "tab-btn", onclick: "switchTab('requests')", children: "Requests" }),
4837
- /* @__PURE__ */ jsxRuntime.jsx("button", { class: "tab-btn", onclick: "switchTab('failures')", children: "Failures" }),
4838
- integrations.scalar && /* @__PURE__ */ jsxRuntime.jsx("button", { class: "tab-btn", onclick: "switchTab('scalar')", children: "API Reference" }),
4839
- integrations.asyncapi && /* @__PURE__ */ jsxRuntime.jsx("button", { class: "tab-btn", onclick: "switchTab('asyncapi')", children: "AsyncAPI" })
5497
+ /* @__PURE__ */ jsxRuntime.jsx("button", { class: "tab-btn", onclick: "switchTab('application')", children: "Application" }),
5498
+ /* @__PURE__ */ jsxRuntime.jsx("button", { class: "tab-btn", onclick: "switchTab('network')", children: "Network" }),
5499
+ integrations.scalar && /* @__PURE__ */ jsxRuntime.jsx("button", { class: "tab-btn", onclick: "switchTab('scalar')", children: "Scalar" }),
5500
+ integrations.apiExplorer && /* @__PURE__ */ jsxRuntime.jsx("button", { class: "tab-btn", onclick: "switchTab('api-explorer')", children: "REST API" }),
5501
+ integrations.asyncapi && /* @__PURE__ */ jsxRuntime.jsx("button", { class: "tab-btn", onclick: "switchTab('asyncapi')", children: "WS API" })
4840
5502
  ] })
4841
5503
  ] }),
4842
- /* @__PURE__ */ jsxRuntime.jsxs("div", { id: "tab-overview", class: "tab-content active", children: [
4843
- /* @__PURE__ */ jsxRuntime.jsx(MetricsGrid, { metrics }),
4844
- /* @__PURE__ */ jsxRuntime.jsxs("div", { id: "chart-container", style: "display: flex; flex-direction: column; gap: 1rem;", children: [
4845
- /* @__PURE__ */ jsxRuntime.jsx("div", { style: "display: flex; justify-content: flex-end;", children: /* @__PURE__ */ jsxRuntime.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: [
4846
- /* @__PURE__ */ jsxRuntime.jsx("option", { value: "1m", children: "1 Minute" }),
4847
- /* @__PURE__ */ jsxRuntime.jsx("option", { value: "5m", children: "5 Minutes" }),
4848
- /* @__PURE__ */ jsxRuntime.jsx("option", { value: "30m", children: "30 Minutes" }),
4849
- /* @__PURE__ */ jsxRuntime.jsx("option", { value: "1h", children: "1 Hour" }),
4850
- /* @__PURE__ */ jsxRuntime.jsx("option", { value: "2h", children: "2 Hours" }),
4851
- /* @__PURE__ */ jsxRuntime.jsx("option", { value: "6h", children: "6 Hours" }),
4852
- /* @__PURE__ */ jsxRuntime.jsx("option", { value: "12h", children: "12 Hours" }),
4853
- /* @__PURE__ */ jsxRuntime.jsx("option", { value: "1d", children: "1 Day" }),
4854
- /* @__PURE__ */ jsxRuntime.jsx("option", { value: "3d", children: "3 Days" }),
4855
- /* @__PURE__ */ jsxRuntime.jsx("option", { value: "7d", children: "7 Days" }),
4856
- /* @__PURE__ */ jsxRuntime.jsx("option", { value: "30d", children: "30 Days" })
4857
- ] }) }),
4858
- /* @__PURE__ */ jsxRuntime.jsxs("div", { class: "card-container", children: [
4859
- /* @__PURE__ */ jsxRuntime.jsx(ChartCard, { title: "Response Time", id: "latencyChart" }),
4860
- /* @__PURE__ */ jsxRuntime.jsx(ChartCard, { title: "Requests / Second", id: "rpsChart" }),
4861
- /* @__PURE__ */ jsxRuntime.jsx(ChartCard, { title: "CPU & Load", id: "cpuChart" }),
4862
- /* @__PURE__ */ jsxRuntime.jsx(ChartCard, { title: "Memory", id: "memoryChart" }),
4863
- /* @__PURE__ */ jsxRuntime.jsx(ChartCard, { title: "Heap Usage", id: "heapChart" }),
4864
- /* @__PURE__ */ jsxRuntime.jsx(ChartCard, { title: "Event Loop Latency", id: "eventLoopChart" }),
4865
- /* @__PURE__ */ jsxRuntime.jsx(ChartCard, { title: "Error Rate", id: "errorRateChart" })
5504
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { class: "contents", children: [
5505
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { id: "tab-overview", class: "tab-content active", children: [
5506
+ /* @__PURE__ */ jsxRuntime.jsx(MetricsGrid, { metrics }),
5507
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { id: "chart-container", style: "display: flex; flex-direction: column; gap: 1rem;", children: [
5508
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: "display: flex; justify-content: flex-end;", children: /* @__PURE__ */ jsxRuntime.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: [
5509
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: "1m", children: "1 Minute" }),
5510
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: "5m", children: "5 Minutes" }),
5511
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: "30m", children: "30 Minutes" }),
5512
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: "1h", children: "1 Hour" }),
5513
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: "2h", children: "2 Hours" }),
5514
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: "6h", children: "6 Hours" }),
5515
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: "12h", children: "12 Hours" }),
5516
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: "1d", children: "1 Day" }),
5517
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: "3d", children: "3 Days" }),
5518
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: "7d", children: "7 Days" }),
5519
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: "30d", children: "30 Days" })
5520
+ ] }) }),
5521
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { class: "card-container", children: [
5522
+ /* @__PURE__ */ jsxRuntime.jsx(ChartCard, { title: "Response Time", id: "latencyChart" }),
5523
+ /* @__PURE__ */ jsxRuntime.jsx(ChartCard, { title: "Requests / Second", id: "rpsChart" }),
5524
+ /* @__PURE__ */ jsxRuntime.jsx(ChartCard, { title: "CPU & Load", id: "cpuChart" }),
5525
+ /* @__PURE__ */ jsxRuntime.jsx(ChartCard, { title: "Memory", id: "memoryChart" }),
5526
+ /* @__PURE__ */ jsxRuntime.jsx(ChartCard, { title: "Heap Usage", id: "heapChart" }),
5527
+ /* @__PURE__ */ jsxRuntime.jsx(ChartCard, { title: "Event Loop Latency", id: "eventLoopChart" }),
5528
+ /* @__PURE__ */ jsxRuntime.jsx(ChartCard, { title: "Error Rate", id: "errorRateChart" })
5529
+ ] }),
5530
+ /* @__PURE__ */ jsxRuntime.jsx("div", { class: "card-title", style: "margin-top: 1rem;", children: "Top Statistics" }),
5531
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { class: "card-container", children: [
5532
+ /* @__PURE__ */ jsxRuntime.jsx(Card, { title: "Top Requests", contentId: "top-requests-table" }),
5533
+ /* @__PURE__ */ jsxRuntime.jsx(Card, { title: "Top Errors", contentId: "top-errors-table" }),
5534
+ /* @__PURE__ */ jsxRuntime.jsx(Card, { title: "Most Frequent Failures", contentId: "failing-requests-table" }),
5535
+ /* @__PURE__ */ jsxRuntime.jsx(Card, { title: "Slowest Requests", contentId: "slowest-requests-table" })
5536
+ ] }),
5537
+ /* @__PURE__ */ jsxRuntime.jsx("div", { id: "table-container", style: "padding: 0; margin-top: 1rem;", children: /* @__PURE__ */ jsxRuntime.jsx("div", { id: "requests-table", class: "table-dark" }) })
4866
5538
  ] }),
4867
- /* @__PURE__ */ jsxRuntime.jsx("div", { class: "card-title", style: "margin-top: 1rem;", children: "Top Statistics" }),
4868
- /* @__PURE__ */ jsxRuntime.jsxs("div", { class: "card-container", children: [
4869
- /* @__PURE__ */ jsxRuntime.jsx(Card, { title: "Top Requests", contentId: "top-requests-table" }),
4870
- /* @__PURE__ */ jsxRuntime.jsx(Card, { title: "Top Errors", contentId: "top-errors-table" }),
4871
- /* @__PURE__ */ jsxRuntime.jsx(Card, { title: "Most Frequent Failures", contentId: "failing-requests-table" }),
4872
- /* @__PURE__ */ jsxRuntime.jsx(Card, { title: "Slowest Requests", contentId: "slowest-requests-table" })
4873
- ] }),
4874
- /* @__PURE__ */ jsxRuntime.jsx("div", { id: "table-container", style: "padding: 0; margin-top: 1rem;", children: /* @__PURE__ */ jsxRuntime.jsx("div", { id: "requests-table", class: "table-dark" }) })
4875
- ] })
4876
- ] }),
4877
- /* @__PURE__ */ jsxRuntime.jsx("div", { id: "tab-registry", class: "tab-content", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { id: "registry-container", class: "card", style: "margin-top: 2rem;", children: [
4878
- /* @__PURE__ */ jsxRuntime.jsx("div", { class: "card-title", children: "Component Registry" }),
4879
- /* @__PURE__ */ jsxRuntime.jsx("div", { id: "registry-tree", style: "padding: 0 1rem 1rem 1rem; font-family: monospace; font-size: 0.9rem;" })
4880
- ] }) }),
4881
- /* @__PURE__ */ jsxRuntime.jsxs("div", { id: "tab-graph", class: "tab-content", children: [
4882
- /* @__PURE__ */ jsxRuntime.jsx("div", { class: "card", style: "margin-bottom: 1rem;", children: /* @__PURE__ */ jsxRuntime.jsx("div", { style: "display: flex; gap: 1rem;", children: /* @__PURE__ */ jsxRuntime.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);" }) }) }),
4883
- /* @__PURE__ */ jsxRuntime.jsx("div", { id: "cy" })
4884
- ] }),
4885
- /* @__PURE__ */ jsxRuntime.jsxs("div", { id: "tab-requests", class: "tab-content", children: [
4886
- /* @__PURE__ */ jsxRuntime.jsxs("div", { class: "card", style: "margin-bottom: 1rem; display: flex; justify-content: space-between; align-items: center;", children: [
4887
- /* @__PURE__ */ jsxRuntime.jsx("div", { class: "card-title", children: "Recent Requests (Last 100)" }),
4888
- /* @__PURE__ */ jsxRuntime.jsx("div", { children: /* @__PURE__ */ jsxRuntime.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" }) })
5539
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: "height: 2rem" })
4889
5540
  ] }),
4890
- /* @__PURE__ */ jsxRuntime.jsx("div", { id: "requests-list-container", style: "height: calc(100vh - 300px); margin-bottom: 2rem;" }),
4891
- /* @__PURE__ */ jsxRuntime.jsxs("div", { id: "request-details-container", class: "card", style: "display: none;", children: [
4892
- /* @__PURE__ */ jsxRuntime.jsx("div", { class: "card-title", children: "Request Details" }),
4893
- /* @__PURE__ */ jsxRuntime.jsx("div", { id: "request-details-content" }),
4894
- /* @__PURE__ */ jsxRuntime.jsx("div", { class: "card-title", style: "margin-top: 1rem;", children: "Middleware Trace" }),
4895
- /* @__PURE__ */ jsxRuntime.jsx("div", { id: "middleware-trace-container" })
4896
- ] })
4897
- ] }),
4898
- /* @__PURE__ */ jsxRuntime.jsxs("div", { id: "tab-failures", class: "tab-content", children: [
4899
- /* @__PURE__ */ jsxRuntime.jsxs("div", { class: "card", style: "margin-bottom: 1rem; display: flex; justify-content: space-between; align-items: center;", children: [
4900
- /* @__PURE__ */ jsxRuntime.jsx("div", { class: "card-title", children: "Failed Requests (Last 50)" }),
4901
- /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
4902
- /* @__PURE__ */ jsxRuntime.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" }),
4903
- /* @__PURE__ */ jsxRuntime.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" })
5541
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { id: "tab-application", class: "tab-content", children: [
5542
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: "margin: 2rem 2rem 0 2rem; display: flex; gap: 1rem; align-items: center;", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { class: "button-group", children: [
5543
+ /* @__PURE__ */ jsxRuntime.jsx("button", { class: "view-btn active", onclick: "switchApplicationView('registry')", children: "Registry" }),
5544
+ /* @__PURE__ */ jsxRuntime.jsx("button", { class: "view-btn", onclick: "switchApplicationView('graph')", children: "Graph" })
5545
+ ] }) }),
5546
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { id: "app-view-registry", class: "app-view active", style: "max-width: 1200px; align-self: center; margin: 0 auto", children: [
5547
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { id: "registry-container", class: "card", style: "margin: 2rem; margin-top: 1rem;", children: [
5548
+ /* @__PURE__ */ jsxRuntime.jsx("div", { class: "card-title", children: "Component Registry" }),
5549
+ /* @__PURE__ */ jsxRuntime.jsx("div", { id: "registry-tree", style: "padding: 0 1rem 1rem 1rem; font-family: monospace; font-size: 0.9rem;" })
5550
+ ] }),
5551
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: "height: .1px" })
5552
+ ] }),
5553
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { id: "app-view-graph", class: "app-view", style: "height: 100%;", children: [
5554
+ /* @__PURE__ */ jsxRuntime.jsx("div", { class: "card", style: "margin: 1rem 2rem;", children: /* @__PURE__ */ jsxRuntime.jsx("div", { style: "display: flex; gap: 1rem;", children: /* @__PURE__ */ jsxRuntime.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);" }) }) }),
5555
+ /* @__PURE__ */ jsxRuntime.jsx("div", { id: "cy", style: "margin: 0 2rem; height: calc(100% - 10rem);" })
4904
5556
  ] })
4905
5557
  ] }),
4906
- /* @__PURE__ */ jsxRuntime.jsx("div", { id: "failures-table-container" })
4907
- ] }),
4908
- integrations.scalar && /* @__PURE__ */ jsxRuntime.jsx("div", { id: "tab-scalar", class: "tab-content", style: "margin: 0; overflow: hidden; height: 100%; max-width: unset", children: /* @__PURE__ */ jsxRuntime.jsx("iframe", { src: integrations.scalar, style: "width: 100%; height: 100%; border: none;" }) }),
4909
- integrations.asyncapi && /* @__PURE__ */ jsxRuntime.jsx("div", { id: "tab-asyncapi", class: "tab-content", style: "margin: 0; overflow: hidden; height: 100%; max-width: unset", children: /* @__PURE__ */ jsxRuntime.jsx("iframe", { src: integrations.asyncapi, style: "width: 100%; height: 100%; border: none;" }) })
5558
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { id: "tab-network", class: "tab-content", children: [
5559
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: "margin: 1rem 2rem 0 2rem;", children: /* @__PURE__ */ jsxRuntime.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: [
5560
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { style: "display: flex; background: var(--bg-secondary); border: 1px solid var(--card-border); border-radius: 4px; overflow: hidden;", children: [
5561
+ /* @__PURE__ */ jsxRuntime.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" }),
5562
+ /* @__PURE__ */ jsxRuntime.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" }),
5563
+ /* @__PURE__ */ jsxRuntime.jsx("button", { class: "filter-direction", "data-value": "outbound", style: "padding: 4px 12px; border: none; background: transparent; color: var(--text-secondary); cursor: pointer;", children: "Outbound" })
5564
+ ] }),
5565
+ /* @__PURE__ */ jsxRuntime.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;" }),
5566
+ /* @__PURE__ */ jsxRuntime.jsxs("select", { id: "network-filter-type", style: "background: var(--bg-primary); color: var(--text-primary); border: 1px solid var(--card-border); borderRadius: 4px;", children: [
5567
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: "all", children: "All Types" }),
5568
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: "xhr", children: "XHR/Fetch" }),
5569
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: "fetch", children: "Outbound" }),
5570
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: "ws", children: "WS" }),
5571
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: "other", children: "Other" })
5572
+ ] }),
5573
+ /* @__PURE__ */ jsxRuntime.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" }),
5574
+ /* @__PURE__ */ jsxRuntime.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" })
5575
+ ] }) }),
5576
+ /* @__PURE__ */ jsxRuntime.jsx("div", { id: "network-view", class: "active", style: "display: block; height: calc(100vh - 170px);", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { style: "margin: 0 2rem; display: flex; gap: 1rem; height: 100%;", children: [
5577
+ /* @__PURE__ */ jsxRuntime.jsx("div", { id: "requests-list-container", style: "flex: 1; height: 100%; border-radius: 6px; overflow: hidden; border: 1px solid var(--card-border);" }),
5578
+ /* @__PURE__ */ jsxRuntime.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: [
5579
+ /* @__PURE__ */ jsxRuntime.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;" }),
5580
+ /* @__PURE__ */ jsxRuntime.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: [
5581
+ /* @__PURE__ */ jsxRuntime.jsx("div", { class: "card-title", style: "margin: 0;", children: "Request Details" }),
5582
+ /* @__PURE__ */ jsxRuntime.jsx("button", { onclick: "closeRequestDetails()", style: "background: transparent; border: none; color: var(--text-secondary); cursor: pointer; font-size: 1.2rem;", children: "×" })
5583
+ ] }),
5584
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { style: "padding: 1rem;", children: [
5585
+ /* @__PURE__ */ jsxRuntime.jsx("div", { id: "request-details-content" }),
5586
+ /* @__PURE__ */ jsxRuntime.jsx("div", { class: "card-title", style: "margin-top: 1rem;", children: "Middleware Trace" }),
5587
+ /* @__PURE__ */ jsxRuntime.jsx("div", { id: "middleware-trace-container" })
5588
+ ] })
5589
+ ] })
5590
+ ] }) })
5591
+ ] }),
5592
+ integrations.scalar && /* @__PURE__ */ jsxRuntime.jsx("div", { id: "tab-scalar", class: "tab-content", style: "overflow: hidden; height: 100%; width: 100%", children: /* @__PURE__ */ jsxRuntime.jsx("iframe", { src: integrations.scalar, style: "width: 100%; height: 100%; border: none;" }) }),
5593
+ integrations.apiExplorer && /* @__PURE__ */ jsxRuntime.jsx("div", { id: "tab-api-explorer", class: "tab-content", style: "overflow: hidden; height: 100%; width: 100%", children: /* @__PURE__ */ jsxRuntime.jsx("iframe", { src: integrations.apiExplorer, style: "width: 100%; height: 100%; border: none;" }) }),
5594
+ integrations.asyncapi && /* @__PURE__ */ jsxRuntime.jsx("div", { id: "tab-asyncapi", class: "tab-content", style: "overflow: hidden; height: 100%; width: 100%", children: /* @__PURE__ */ jsxRuntime.jsx("iframe", { src: integrations.asyncapi, style: "width: 100%; height: 100%; border: none;" }) })
5595
+ ] })
4910
5596
  ] }),
4911
5597
  /* @__PURE__ */ jsxRuntime.jsx("script", { dangerouslySetInnerHTML: {
4912
5598
  __html: `
4913
5599
  // Injected function from server config
4914
5600
  const getRequestHeaders = ${getRequestHeadersSource};
5601
+ window.SHOKUPAN_CONFIG = {
5602
+ rootPath: "${rootPath || ""}",
5603
+ linkPattern: "${linkPattern || ""}"
5604
+ };
4915
5605
  `
4916
5606
  } }),
4917
- /* @__PURE__ */ jsxRuntime.jsx("script", { src: `${base}/poll.js` }),
5607
+ /* @__PURE__ */ jsxRuntime.jsx("script", { src: `${base}/client.js` }),
4918
5608
  /* @__PURE__ */ jsxRuntime.jsx("script", { src: `${base}/graph.mjs`, type: "module" }),
4919
5609
  /* @__PURE__ */ jsxRuntime.jsx("script", { src: `${base}/charts.js` }),
4920
5610
  /* @__PURE__ */ jsxRuntime.jsx("script", { src: `${base}/tables.js` }),
@@ -4984,6 +5674,264 @@ function Card({ title, contentId }) {
4984
5674
  /* @__PURE__ */ jsxRuntime.jsx("div", { id: contentId })
4985
5675
  ] });
4986
5676
  }
5677
+ const require$1 = node_module.createRequire(typeof document === "undefined" ? require("url").pathToFileURL(__filename).href : _documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === "SCRIPT" && _documentCurrentScript.src || new URL("index.cjs", document.baseURI).href);
5678
+ const http = require$1("node:http");
5679
+ const https = require$1("node:https");
5680
+ class FetchInterceptor {
5681
+ originalFetch;
5682
+ originalHttpRequest;
5683
+ originalHttpsRequest;
5684
+ callbacks = [];
5685
+ isPatched = false;
5686
+ constructor() {
5687
+ this.originalFetch = global.fetch;
5688
+ this.originalHttpRequest = http.request;
5689
+ this.originalHttpsRequest = https.request;
5690
+ }
5691
+ /**
5692
+ * Patches the global `fetch` function to intercept requests.
5693
+ * If already patched, this method does nothing.
5694
+ */
5695
+ patch() {
5696
+ if (this.isPatched) return;
5697
+ this.patchGlobalFetch();
5698
+ this.patchNodeRequests();
5699
+ this.isPatched = true;
5700
+ console.log("[FetchInterceptor] Network layer patched.");
5701
+ }
5702
+ patchGlobalFetch() {
5703
+ const self = this;
5704
+ const newFetch = async function(input, init) {
5705
+ const startTime = performance.now();
5706
+ const timestamp = Date.now();
5707
+ let method = "GET";
5708
+ let url = "";
5709
+ let requestHeaders = {};
5710
+ let requestBody = void 0;
5711
+ try {
5712
+ if (input instanceof node_url.URL) {
5713
+ url = input.toString();
5714
+ } else if (typeof input === "string") {
5715
+ url = input;
5716
+ } else if (typeof input === "object" && "url" in input) {
5717
+ url = input.url;
5718
+ method = input.method;
5719
+ }
5720
+ if (init) {
5721
+ if (init.method) method = init.method;
5722
+ if (init.headers) {
5723
+ if (init.headers instanceof Headers) {
5724
+ init.headers.forEach((v, k) => requestHeaders[k] = v);
5725
+ } else if (Array.isArray(init.headers)) {
5726
+ init.headers.forEach(([k, v]) => requestHeaders[k] = v);
5727
+ } else {
5728
+ Object.assign(requestHeaders, init.headers);
5729
+ }
5730
+ }
5731
+ if (init.body) requestBody = init.body;
5732
+ }
5733
+ } catch (e) {
5734
+ console.warn("[FetchInterceptor] Failed to parse request arguments", e);
5735
+ }
5736
+ try {
5737
+ const response = await self.originalFetch.apply(global, [input, init]);
5738
+ const clone = response.clone();
5739
+ const duration = performance.now() - startTime;
5740
+ self.processResponse(clone, {
5741
+ method,
5742
+ url,
5743
+ requestHeaders,
5744
+ requestBody,
5745
+ status: response.status,
5746
+ startTime: timestamp,
5747
+ duration,
5748
+ ...self.extractRequestMeta(url, requestHeaders),
5749
+ protocol: "1.1"
5750
+ // native fetch doesn't expose this easily, assume 1.1/2
5751
+ });
5752
+ return response;
5753
+ } catch (error) {
5754
+ const duration = performance.now() - startTime;
5755
+ self.notify({
5756
+ method,
5757
+ url,
5758
+ requestHeaders,
5759
+ requestBody,
5760
+ status: 0,
5761
+ responseHeaders: {},
5762
+ responseBody: `Network Error: ${String(error)}`,
5763
+ startTime: timestamp,
5764
+ duration
5765
+ });
5766
+ throw error;
5767
+ }
5768
+ };
5769
+ Object.assign(newFetch, this.originalFetch);
5770
+ global.fetch = newFetch;
5771
+ }
5772
+ patchNodeRequests() {
5773
+ const self = this;
5774
+ const intercept = (module2, original, defaultScheme) => {
5775
+ module2.request = function(...args) {
5776
+ const startTime = performance.now();
5777
+ const timestamp = Date.now();
5778
+ let options = {};
5779
+ let urlObj;
5780
+ if (typeof args[0] === "string" || args[0] instanceof node_url.URL) {
5781
+ try {
5782
+ urlObj = new node_url.URL(args[0]);
5783
+ options = typeof args[1] === "object" ? args[1] : {};
5784
+ } catch (e) {
5785
+ }
5786
+ } else {
5787
+ options = args[0] || {};
5788
+ try {
5789
+ const protocol = options.protocol || defaultScheme + ":";
5790
+ const host = options.hostname || options.host || "localhost";
5791
+ const port = options.port ? ":" + options.port : "";
5792
+ const path2 = options.path || "/";
5793
+ urlObj = new node_url.URL(`${protocol}//${host}${port}${path2}`);
5794
+ } catch (e) {
5795
+ }
5796
+ }
5797
+ const method = (options.method || "GET").toUpperCase();
5798
+ const url = urlObj ? urlObj.toString() : "unknown";
5799
+ const req = original.apply(this, args);
5800
+ const getReqHeaders = () => {
5801
+ try {
5802
+ const h = req.getHeaders();
5803
+ const normalized = {};
5804
+ for (const k in h) {
5805
+ const v = h[k];
5806
+ normalized[k] = Array.isArray(v) ? v.join(", ") : String(v);
5807
+ }
5808
+ return normalized;
5809
+ } catch (e) {
5810
+ return {};
5811
+ }
5812
+ };
5813
+ req.on("response", (res) => {
5814
+ const duration = performance.now() - startTime;
5815
+ const resHeaders = {};
5816
+ if (res.headers) {
5817
+ for (const k in res.headers) {
5818
+ const v = res.headers[k];
5819
+ resHeaders[k] = Array.isArray(v) ? v.join(", ") : String(v || "");
5820
+ }
5821
+ }
5822
+ self.notify({
5823
+ method,
5824
+ url,
5825
+ requestHeaders: getReqHeaders(),
5826
+ status: res.statusCode || 0,
5827
+ responseHeaders: resHeaders,
5828
+ startTime: timestamp,
5829
+ duration,
5830
+ ...self.extractRequestMeta(url, getReqHeaders()),
5831
+ protocol: req.httpVersion
5832
+ });
5833
+ });
5834
+ req.on("error", (err) => {
5835
+ const duration = performance.now() - startTime;
5836
+ self.notify({
5837
+ method,
5838
+ url,
5839
+ requestHeaders: getReqHeaders(),
5840
+ status: 0,
5841
+ responseHeaders: {},
5842
+ responseBody: `Error: ${err.message}`,
5843
+ // Capture error
5844
+ startTime: timestamp,
5845
+ duration
5846
+ });
5847
+ });
5848
+ return req;
5849
+ };
5850
+ };
5851
+ intercept(http, this.originalHttpRequest, "http");
5852
+ intercept(https, this.originalHttpsRequest, "https");
5853
+ }
5854
+ /**
5855
+ * Restores the original functions.
5856
+ */
5857
+ unpatch() {
5858
+ if (!this.isPatched) return;
5859
+ global.fetch = this.originalFetch;
5860
+ http.request = this.originalHttpRequest;
5861
+ https.request = this.originalHttpsRequest;
5862
+ this.isPatched = false;
5863
+ console.log("[FetchInterceptor] Network layer restored.");
5864
+ }
5865
+ /**
5866
+ * Adds a callback to be notified of outbound requests.
5867
+ * @param callback The callback function.
5868
+ */
5869
+ on(callback) {
5870
+ this.callbacks.push(callback);
5871
+ }
5872
+ extractRequestMeta(urlStr, headers) {
5873
+ try {
5874
+ const url = new node_url.URL(urlStr);
5875
+ const cookiesHeader = headers["cookie"] || headers["Cookie"];
5876
+ const cookies = cookiesHeader ? cookiesHeader.split(";").length : 0;
5877
+ return {
5878
+ domain: url.hostname,
5879
+ path: url.pathname,
5880
+ scheme: url.protocol.replace(":", ""),
5881
+ cookies,
5882
+ remoteIP: void 0
5883
+ // Not easily accessible via fetch
5884
+ };
5885
+ } catch (e) {
5886
+ return {};
5887
+ }
5888
+ }
5889
+ async processResponse(response, meta) {
5890
+ const responseHeaders = {};
5891
+ response.headers.forEach((v, k) => responseHeaders[k] = v);
5892
+ let responseBody;
5893
+ let transferred = 0;
5894
+ try {
5895
+ const contentType = response.headers.get("content-type") || "";
5896
+ let bodyText = "";
5897
+ if (contentType.includes("application/json") || contentType.includes("text/")) {
5898
+ bodyText = await response.text();
5899
+ if (bodyText.length > 524288) {
5900
+ responseBody = bodyText.substring(0, 524288) + "... (truncated)";
5901
+ } else {
5902
+ responseBody = bodyText;
5903
+ }
5904
+ } else {
5905
+ responseBody = "[Binary Content]";
5906
+ const cl = response.headers.get("content-length");
5907
+ if (cl) transferred = parseInt(cl, 10);
5908
+ }
5909
+ const headersSize = Object.entries(responseHeaders).reduce((acc, [k, v]) => acc + k.length + v.length + 2, 0);
5910
+ if (!transferred && bodyText) {
5911
+ transferred = headersSize + bodyText.length;
5912
+ } else if (!transferred) {
5913
+ transferred = headersSize;
5914
+ }
5915
+ } catch (e) {
5916
+ responseBody = "[Failed to read response body]";
5917
+ }
5918
+ this.notify({
5919
+ ...meta,
5920
+ responseHeaders,
5921
+ responseBody,
5922
+ transferred
5923
+ });
5924
+ }
5925
+ notify(log) {
5926
+ this.callbacks.forEach((cb) => {
5927
+ try {
5928
+ cb(log);
5929
+ } catch (e) {
5930
+ console.error("[FetchInterceptor] Callback failed", e);
5931
+ }
5932
+ });
5933
+ }
5934
+ }
4987
5935
  const INTERVALS = [
4988
5936
  { label: "10s", ms: 10 * 1e3 },
4989
5937
  { label: "1m", ms: 60 * 1e3 },
@@ -4998,7 +5946,8 @@ const INTERVALS = [
4998
5946
  { label: "30d", ms: 30 * 24 * 60 * 60 * 1e3 }
4999
5947
  ];
5000
5948
  class MetricsCollector {
5001
- constructor(db) {
5949
+ constructor(db, onCollect) {
5950
+ this.onCollect = onCollect;
5002
5951
  this.db = db;
5003
5952
  this.eventLoopHistogram.enable();
5004
5953
  const now = Date.now();
@@ -5006,11 +5955,13 @@ class MetricsCollector {
5006
5955
  this.currentIntervalStart[int.label] = this.alignTimestamp(now, int.ms);
5007
5956
  this.pendingDetails[int.label] = [];
5008
5957
  });
5958
+ this.timer = setInterval(() => this.collect(), 1e4);
5009
5959
  }
5010
5960
  currentIntervalStart = {};
5011
5961
  pendingDetails = {};
5012
5962
  eventLoopHistogram = node_perf_hooks.monitorEventLoopDelay({ resolution: 10 });
5013
5963
  timer = null;
5964
+ db;
5014
5965
  recordRequest(duration, isError) {
5015
5966
  INTERVALS.forEach((int) => {
5016
5967
  this.pendingDetails[int.label].push({ duration, isError });
@@ -5086,14 +6037,17 @@ class MetricsCollector {
5086
6037
  p99: getP(0.99)
5087
6038
  }
5088
6039
  };
6040
+ if (!this.db) {
6041
+ return;
6042
+ }
5089
6043
  try {
5090
- const recordId = new surrealdb.RecordId("metrics", timestamp);
5091
- await this.db.upsert(recordId, metric);
5092
- const test = await this.db.select(recordId);
5093
- const queryTest = await this.db.query("SELECT * FROM metrics WHERE id = $id", { id: recordId });
6044
+ await this.db.upsert(new surrealdb.RecordId("metric", timestamp), metric);
5094
6045
  } catch (e) {
5095
6046
  console.error(`[MetricsCollector] ✗ Failed to save metrics for ${label}:`, e);
5096
6047
  }
6048
+ if (this.onCollect) {
6049
+ this.onCollect(metric);
6050
+ }
5097
6051
  }
5098
6052
  // Cleanup if needed
5099
6053
  stop() {
@@ -5139,8 +6093,13 @@ class Dashboard {
5139
6093
  nodeMetrics: {},
5140
6094
  edgeMetrics: {}
5141
6095
  };
6096
+ clients = /* @__PURE__ */ new Set();
6097
+ broadcastTimer;
6098
+ requestPushTimer;
6099
+ requestsBuffer = [];
5142
6100
  startTime = Date.now();
5143
6101
  instrumented = false;
6102
+ mountPath = "/dashboard";
5144
6103
  metricsCollector;
5145
6104
  get db() {
5146
6105
  return this[$appRoot].db;
@@ -5148,8 +6107,69 @@ class Dashboard {
5148
6107
  // ShokupanPlugin interface implementation
5149
6108
  onInit(app, options) {
5150
6109
  this[$appRoot] = app;
5151
- this.metricsCollector = new MetricsCollector(this.db);
5152
- const mountPath = options?.path || this.dashboardConfig.path || "/dashboard";
6110
+ const onCollect = (metric) => {
6111
+ this.broadcastMetricUpdate(metric);
6112
+ };
6113
+ this.metricsCollector = new MetricsCollector(this.db, onCollect);
6114
+ const fetchInterceptor = new FetchInterceptor();
6115
+ fetchInterceptor.patch();
6116
+ fetchInterceptor.on((log) => {
6117
+ if (log.url.includes("/rpc")) return;
6118
+ try {
6119
+ const u = new URL(log.url);
6120
+ if (u.pathname.startsWith(this.mountPath)) return;
6121
+ } catch (e) {
6122
+ }
6123
+ const requestData = {
6124
+ method: log.method,
6125
+ url: log.url,
6126
+ status: log.status,
6127
+ duration: log.duration,
6128
+ timestamp: log.startTime,
6129
+ // Use startTime as timestamp
6130
+ type: "fetch",
6131
+ direction: "outbound",
6132
+ size: log.responseBody ? String(log.responseBody).length : 0,
6133
+ contentType: log.responseHeaders["content-type"] || log.responseHeaders["Content-Type"],
6134
+ body: log.responseBody,
6135
+ requestBody: log.requestBody,
6136
+ domain: log.domain,
6137
+ path: log.path,
6138
+ scheme: log.scheme,
6139
+ protocol: log.protocol,
6140
+ remoteIP: log.remoteIP,
6141
+ cookies: log.cookies,
6142
+ transferred: log.transferred,
6143
+ requestHeaders: log.requestHeaders,
6144
+ responseHeaders: log.responseHeaders
6145
+ // No handler stack for outbound
6146
+ };
6147
+ this.metrics.logs.push(requestData);
6148
+ const recordId = new surrealdb.RecordId("request", nanoid.nanoid());
6149
+ const idString = recordId.toString();
6150
+ this.db.query("UPSERT $id CONTENT $data", {
6151
+ id: recordId,
6152
+ data: requestData
6153
+ }).catch((e) => console.error("Failed to save outbound request", e));
6154
+ const strategy2 = this.dashboardConfig.updateStrategy || "immediate";
6155
+ if (strategy2 === "immediate") {
6156
+ this.broadcastRequestUpdates([{ ...requestData, id: idString }]);
6157
+ } else {
6158
+ this.requestsBuffer.push({ ...requestData, id: idString });
6159
+ }
6160
+ });
6161
+ if (app.onStart) {
6162
+ app.onStart(async () => {
6163
+ if (app.dbPromise) {
6164
+ await app.dbPromise;
6165
+ if (app.db) {
6166
+ this.metricsCollector.db = app.db;
6167
+ console.log("[Dashboard] Attached datastore to MetricsCollector");
6168
+ }
6169
+ }
6170
+ });
6171
+ }
6172
+ this.mountPath = options?.path || this.dashboardConfig.path || "/dashboard";
5153
6173
  const hooks = this.getHooks();
5154
6174
  if (!app.middleware) {
5155
6175
  app.middleware = [];
@@ -5158,15 +6178,25 @@ class Dashboard {
5158
6178
  if (hooks.onRequestStart) {
5159
6179
  await hooks.onRequestStart(ctx);
5160
6180
  }
6181
+ ctx._startTime = performance.now();
5161
6182
  await next();
5162
- if (hooks.onResponseEnd) {
5163
- const effectiveResponse = ctx._finalResponse || ctx.response || {};
5164
- await hooks.onResponseEnd(ctx, effectiveResponse);
5165
- }
5166
6183
  };
5167
6184
  app.use(hooksMiddleware);
5168
- app.mount(mountPath, this.router);
6185
+ if (hooks.onResponseEnd) {
6186
+ app.hook("onResponseEnd", hooks.onResponseEnd);
6187
+ }
6188
+ app.mount(this.mountPath, this.router);
6189
+ this.router.metadata = {
6190
+ file: void 0,
6191
+ line: 1,
6192
+ name: "DashboardPlugin",
6193
+ pluginName: "Dashboard"
6194
+ };
5169
6195
  this.setupRoutes();
6196
+ const strategy = this.dashboardConfig.updateStrategy || "immediate";
6197
+ if (strategy === "batched") {
6198
+ this.startRequestPushTimer();
6199
+ }
5170
6200
  }
5171
6201
  detectIntegrations() {
5172
6202
  const integrations = {};
@@ -5199,6 +6229,17 @@ class Dashboard {
5199
6229
  }
5200
6230
  }
5201
6231
  }
6232
+ const apiExplorerConf = checkConfig("apiExplorer");
6233
+ if (apiExplorerConf.enabled) {
6234
+ if (apiExplorerConf.path) {
6235
+ integrations["apiExplorer"] = apiExplorerConf.path;
6236
+ } else {
6237
+ const plugin = routers.find((r) => r.constructor.name === "ApiExplorerPlugin");
6238
+ if (plugin) {
6239
+ integrations["apiExplorer"] = plugin[$mountPath];
6240
+ }
6241
+ }
6242
+ }
5202
6243
  return integrations;
5203
6244
  }
5204
6245
  // Get base path for dashboard files - works in both dev (src/) and production (dist/)
@@ -5210,9 +6251,36 @@ class Dashboard {
5210
6251
  return dir;
5211
6252
  }
5212
6253
  setupRoutes() {
6254
+ this.router.get("/ws", (ctx) => {
6255
+ const success = ctx.upgrade({
6256
+ data: {
6257
+ handler: {
6258
+ open: (ws) => {
6259
+ this.clients.add(ws);
6260
+ console.log(`[Dashboard] Client connected. Total clients: ${this.clients.size}`);
6261
+ this.sendHistory(ws, "1m");
6262
+ },
6263
+ close: (ws) => {
6264
+ this.clients.delete(ws);
6265
+ console.log(`[Dashboard] Client disconnected. Total clients: ${this.clients.size}`);
6266
+ },
6267
+ message: (ws, message) => {
6268
+ try {
6269
+ const msg = JSON.parse(message);
6270
+ if (msg.type === "get-history") {
6271
+ this.sendHistory(ws, msg.interval || "1m");
6272
+ }
6273
+ } catch (e) {
6274
+ }
6275
+ }
6276
+ }
6277
+ }
6278
+ });
6279
+ if (success) return void 0;
6280
+ return ctx.text("WebSocket upgrade failed", 400);
6281
+ });
5213
6282
  this.router.get("/metrics", async (ctx) => {
5214
- const uptimeSeconds = Math.floor((Date.now() - this.startTime) / 1e3);
5215
- const uptime = `${Math.floor(uptimeSeconds / 3600)}h ${Math.floor(uptimeSeconds % 3600 / 60)}m ${uptimeSeconds % 60}s`;
6283
+ const uptime = this.getUptime();
5216
6284
  const interval = ctx.query["interval"];
5217
6285
  if (interval) {
5218
6286
  const intervalMap = {
@@ -5239,13 +6307,15 @@ class Dashboard {
5239
6307
  count(IF status < 400 THEN 1 END) as success,
5240
6308
  count(IF status >= 400 THEN 1 END) as failed,
5241
6309
  math::mean(duration) as avg_latency
5242
- FROM requests
6310
+ FROM request
5243
6311
  WHERE timestamp >= $start
5244
6312
  GROUP ALL
5245
6313
  `, { start: startTime });
5246
6314
  } catch (error) {
5247
6315
  console.error("[Dashboard] Query failed at plugin.ts:180-191", {
5248
6316
  error,
6317
+ errorMessage: error.message,
6318
+ errorStack: error.stack,
5249
6319
  interval,
5250
6320
  startTime,
5251
6321
  query: "metrics interval stats",
@@ -5253,12 +6323,12 @@ class Dashboard {
5253
6323
  });
5254
6324
  throw error;
5255
6325
  }
5256
- const s = stats[0] || { total: 0, success: 0, failed: 0, avg_latency: 0 };
6326
+ const s = stats[0] || { avg_latency: 0 };
5257
6327
  return ctx.json({
5258
6328
  metrics: {
5259
- totalRequests: s.total || 0,
5260
- successfulRequests: s.success || 0,
5261
- failedRequests: s.failed || 0,
6329
+ totalRequests: this.metrics.totalRequests,
6330
+ successfulRequests: this.metrics.successfulRequests,
6331
+ failedRequests: this.metrics.failedRequests,
5262
6332
  activeRequests: this.metrics.activeRequests,
5263
6333
  averageTotalTime_ms: s.avg_latency || 0,
5264
6334
  recentTimings: this.metrics.recentTimings,
@@ -5324,7 +6394,7 @@ class Dashboard {
5324
6394
  this.router.get("/requests/top", async (ctx) => {
5325
6395
  const startTime = getIntervalStartTime(ctx.query["interval"]);
5326
6396
  const result = await this.db.query(
5327
- "SELECT method, url, count() as count FROM requests WHERE timestamp >= $start GROUP BY method, url ORDER BY count DESC LIMIT 10",
6397
+ "SELECT method, url, count() as count FROM request WHERE timestamp >= $start GROUP BY method, url ORDER BY count DESC LIMIT 10",
5328
6398
  { start: startTime }
5329
6399
  );
5330
6400
  return ctx.json({ top: result[0] || [] });
@@ -5332,7 +6402,7 @@ class Dashboard {
5332
6402
  this.router.get("/errors/top", async (ctx) => {
5333
6403
  const startTime = getIntervalStartTime(ctx.query["interval"]);
5334
6404
  const result = await this.db.query(
5335
- "SELECT status, count() as count FROM failed_requests WHERE timestamp >= $start GROUP BY status ORDER BY count DESC LIMIT 10",
6405
+ "SELECT status, count() as count FROM failed_request WHERE timestamp >= $start GROUP BY status ORDER BY count DESC LIMIT 10",
5336
6406
  { start: startTime }
5337
6407
  );
5338
6408
  return ctx.json({ top: result[0] || [] });
@@ -5340,7 +6410,7 @@ class Dashboard {
5340
6410
  this.router.get("/requests/failing", async (ctx) => {
5341
6411
  const startTime = getIntervalStartTime(ctx.query["interval"]);
5342
6412
  const result = await this.db.query(
5343
- "SELECT method, url, count() as count FROM failed_requests WHERE timestamp >= $start GROUP BY method, url ORDER BY count DESC LIMIT 10",
6413
+ "SELECT method, url, count() as count FROM failed_request WHERE timestamp >= $start GROUP BY method, url ORDER BY count DESC LIMIT 10",
5344
6414
  { start: startTime }
5345
6415
  );
5346
6416
  return ctx.json({ top: result[0] || [] });
@@ -5348,7 +6418,7 @@ class Dashboard {
5348
6418
  this.router.get("/requests/slowest", async (ctx) => {
5349
6419
  const startTime = getIntervalStartTime(ctx.query["interval"]);
5350
6420
  const result = await this.db.query(
5351
- "SELECT method, url, duration, status, timestamp FROM requests WHERE timestamp >= $start ORDER BY duration DESC LIMIT 10",
6421
+ "SELECT method, url, duration, status, timestamp FROM request WHERE timestamp >= $start ORDER BY duration DESC LIMIT 10",
5352
6422
  { start: startTime }
5353
6423
  );
5354
6424
  return ctx.json({ slowest: result[0] || [] });
@@ -5365,15 +6435,32 @@ class Dashboard {
5365
6435
  return ctx.json({ registry: registry || {} });
5366
6436
  });
5367
6437
  this.router.get("/requests", async (ctx) => {
5368
- const result = await this.db.query("SELECT * FROM requests ORDER BY timestamp DESC LIMIT 100");
5369
- return ctx.json({ requests: result[0] || [] });
6438
+ console.log(`[Dashboard] Handling /requests from ${ctx.ip} ${ctx.get("User-Agent")}`);
6439
+ const result = await this.db.query("SELECT * FROM request ORDER BY timestamp DESC LIMIT 100");
6440
+ const items = result[0] || [];
6441
+ console.log(`[Dashboard] /requests returning ${items.length} items`);
6442
+ return ctx.json({ requests: items });
6443
+ });
6444
+ this.router.delete("/requests", async (ctx) => {
6445
+ console.log(`[Dashboard] Purging all requests`);
6446
+ await this.db.query("DELETE request; DELETE failed_request;");
6447
+ this.metrics.logs = [];
6448
+ this.metrics.totalRequests = 0;
6449
+ this.metrics.activeRequests = 0;
6450
+ this.metrics.successfulRequests = 0;
6451
+ this.metrics.failedRequests = 0;
6452
+ this.metrics.recentTimings = [];
6453
+ this.metrics.rateLimitedCounts = {};
6454
+ this.metrics.nodeMetrics = {};
6455
+ this.metrics.edgeMetrics = {};
6456
+ return ctx.json({ success: true });
5370
6457
  });
5371
6458
  this.router.get("/requests/:id", async (ctx) => {
5372
- const result = await this.db.query("SELECT * FROM requests WHERE id = $id", { id: ctx.params["id"] });
6459
+ const result = await this.db.query("SELECT * FROM request WHERE id = $id", { id: ctx.params["id"] });
5373
6460
  return ctx.json({ request: result[0]?.[0] });
5374
6461
  });
5375
6462
  this.router.get("/failures", async (ctx) => {
5376
- const result = await this.db.query("SELECT * FROM failed_requests ORDER BY timestamp DESC LIMIT 50");
6463
+ const result = await this.db.query("SELECT * FROM failed_request ORDER BY timestamp DESC LIMIT 50");
5377
6464
  return ctx.json({ failures: result[0] });
5378
6465
  });
5379
6466
  this.router.post("/replay", async (ctx) => {
@@ -5411,7 +6498,7 @@ class Dashboard {
5411
6498
  "charts.js",
5412
6499
  "failures.js",
5413
6500
  "graph.mjs",
5414
- "poll.js",
6501
+ "client.js",
5415
6502
  "reactflow.css",
5416
6503
  "registry.css",
5417
6504
  "registry.js",
@@ -5428,15 +6515,15 @@ class Dashboard {
5428
6515
  else if (path2.endsWith(".js") || path2.endsWith(".mjs")) ctx.set("Content-Type", "application/javascript");
5429
6516
  return ctx.send(content);
5430
6517
  }
5431
- const uptimeSeconds = Math.floor((Date.now() - this.startTime) / 1e3);
5432
- const uptime = `${Math.floor(uptimeSeconds / 3600)}h ${Math.floor(uptimeSeconds % 3600 / 60)}m ${uptimeSeconds % 60}s`;
5433
- this.getLinkPattern();
6518
+ const uptime = this.getUptime();
6519
+ const linkPattern = this.getLinkPattern();
5434
6520
  const integrations = this.detectIntegrations();
5435
6521
  const getRequestHeadersSource = this.dashboardConfig.getRequestHeaders ? this.dashboardConfig.getRequestHeaders.toString() : "undefined";
5436
6522
  const html = renderToString(DashboardApp({
5437
6523
  metrics: this.metrics,
5438
6524
  uptime,
5439
6525
  rootPath: process.cwd(),
6526
+ linkPattern,
5440
6527
  integrations,
5441
6528
  base: mountPath,
5442
6529
  getRequestHeadersSource
@@ -5444,6 +6531,82 @@ class Dashboard {
5444
6531
  return ctx.html(`<!DOCTYPE html>${html}`);
5445
6532
  });
5446
6533
  }
6534
+ getUptime() {
6535
+ const uptimeSeconds = Math.floor((Date.now() - this.startTime) / 1e3);
6536
+ return `${Math.floor(uptimeSeconds / 3600)}h ${Math.floor(uptimeSeconds % 3600 / 60)}m ${uptimeSeconds % 60}s`;
6537
+ }
6538
+ getPublicMetrics() {
6539
+ return {
6540
+ totalRequests: this.metrics.totalRequests,
6541
+ successfulRequests: this.metrics.successfulRequests,
6542
+ failedRequests: this.metrics.failedRequests,
6543
+ activeRequests: this.metrics.activeRequests,
6544
+ averageTotalTime_ms: this.metrics.averageTotalTime_ms,
6545
+ recentTimings: this.metrics.recentTimings,
6546
+ logs: [],
6547
+ // Don't broadcast logs for now to save bandwidth
6548
+ rateLimitedCounts: this.metrics.rateLimitedCounts,
6549
+ nodeMetrics: this.metrics.nodeMetrics,
6550
+ edgeMetrics: this.metrics.edgeMetrics
6551
+ };
6552
+ }
6553
+ broadcastMetricUpdate(metric) {
6554
+ if (this.clients.size === 0) return;
6555
+ const data = JSON.stringify({
6556
+ type: "metric-update",
6557
+ metric
6558
+ });
6559
+ for (const client of this.clients) {
6560
+ client.send(data);
6561
+ }
6562
+ }
6563
+ async sendHistory(ws, interval) {
6564
+ const intervalMap = {
6565
+ "10s": 10 * 1e3,
6566
+ "1m": 60 * 1e3,
6567
+ "5m": 5 * 60 * 1e3,
6568
+ "30m": 30 * 60 * 1e3,
6569
+ "1h": 60 * 60 * 1e3,
6570
+ "2h": 2 * 60 * 60 * 1e3,
6571
+ "6h": 6 * 60 * 60 * 1e3,
6572
+ "12h": 12 * 60 * 60 * 1e3,
6573
+ "1d": 24 * 60 * 60 * 1e3,
6574
+ "3d": 3 * 24 * 60 * 60 * 1e3,
6575
+ "7d": 7 * 24 * 60 * 60 * 1e3,
6576
+ "30d": 30 * 24 * 60 * 60 * 1e3
6577
+ };
6578
+ const periodMs = intervalMap[interval] || 60 * 1e3;
6579
+ const startTime = Date.now() - periodMs * 30;
6580
+ const endTime = Date.now();
6581
+ let history = [];
6582
+ try {
6583
+ const result = await this.db.query(
6584
+ "SELECT * FROM metric WHERE timestamp >= $start AND timestamp <= $end AND interval = $interval ORDER BY timestamp ASC",
6585
+ { start: startTime, end: endTime, interval }
6586
+ );
6587
+ history = result[0] || [];
6588
+ } catch (e) {
6589
+ console.error("[Dashboard] Failed to fetch history for WS", e);
6590
+ }
6591
+ ws.send(JSON.stringify({
6592
+ type: "init",
6593
+ metrics: { ...this.metrics, logs: [] },
6594
+ uptime: this.getUptime(),
6595
+ history
6596
+ }));
6597
+ }
6598
+ broadcastMetrics() {
6599
+ if (this.clients.size === 0) return;
6600
+ console.log(`[Dashboard] Broadcasting metrics to ${this.clients.size} clients`);
6601
+ const data = JSON.stringify({
6602
+ type: "metrics",
6603
+ metrics: this.getPublicMetrics(),
6604
+ uptime: this.getUptime()
6605
+ });
6606
+ for (const client of this.clients) {
6607
+ client.send(data);
6608
+ }
6609
+ }
5447
6610
  instrumentApp(app) {
5448
6611
  if (!app.getComponentRegistry) return;
5449
6612
  const registry = app.getComponentRegistry();
@@ -5473,6 +6636,11 @@ class Dashboard {
5473
6636
  r.id = id;
5474
6637
  this.assignIdsToRegistry(r.children, id);
5475
6638
  });
6639
+ node.events?.forEach((e, idx) => {
6640
+ const id = makeId("event", parentId, idx, e.name);
6641
+ e.id = id;
6642
+ if (e._fn) e._fn._debugId = id;
6643
+ });
5476
6644
  }
5477
6645
  recordNodeMetric(id, type, duration, isError) {
5478
6646
  if (!this.metrics.nodeMetrics[id]) {
@@ -5500,7 +6668,7 @@ class Dashboard {
5500
6668
  if (["vscode", "cursor", "antigravity"].some((t) => term.includes(t))) {
5501
6669
  return "vscode://file/{{absolute}}:{{line}}";
5502
6670
  }
5503
- return "file:///{{absolute}}:{{line}}";
6671
+ return "vscode://file/{{absolute}}:{{line}}";
5504
6672
  }
5505
6673
  getHooks() {
5506
6674
  return {
@@ -5511,19 +6679,36 @@ class Dashboard {
5511
6679
  }
5512
6680
  this.metrics.totalRequests++;
5513
6681
  this.metrics.activeRequests++;
5514
- ctx._debugStartTime = performance.now();
5515
6682
  ctx[$debug] = new Collector(this);
6683
+ if (!this.broadcastTimer) {
6684
+ this.broadcastTimer = setTimeout(() => {
6685
+ this.broadcastMetrics();
6686
+ this.broadcastTimer = void 0;
6687
+ }, 100);
6688
+ }
5516
6689
  },
5517
6690
  onResponseEnd: async (ctx, response) => {
6691
+ if (ctx.path.startsWith(this.mountPath)) return;
5518
6692
  this.metrics.activeRequests = Math.max(0, this.metrics.activeRequests - 1);
5519
- const start = ctx._debugStartTime;
5520
- let duration = 0;
5521
- if (start) {
5522
- duration = performance.now() - start;
5523
- this.updateTiming(duration);
6693
+ const duration = performance.now() - ctx._startTime || 0;
6694
+ if (!response) {
6695
+ if (ctx.isUpgraded) {
6696
+ response = {
6697
+ status: 101,
6698
+ headers: {}
6699
+ };
6700
+ } else {
6701
+ return;
6702
+ }
5524
6703
  }
5525
6704
  const isError = response.status >= 400;
5526
6705
  this.metricsCollector.recordRequest(duration, isError);
6706
+ if (!this.broadcastTimer) {
6707
+ this.broadcastTimer = setTimeout(() => {
6708
+ this.broadcastMetrics();
6709
+ this.broadcastTimer = void 0;
6710
+ }, 100);
6711
+ }
5527
6712
  if (response.status >= 400) {
5528
6713
  this.metrics.failedRequests++;
5529
6714
  if (response.status === 429) {
@@ -5531,20 +6716,28 @@ class Dashboard {
5531
6716
  this.metrics.rateLimitedCounts[path2] = (this.metrics.rateLimitedCounts[path2] || 0) + 1;
5532
6717
  }
5533
6718
  try {
5534
- const headers = {};
6719
+ const headers2 = {};
5535
6720
  if (ctx.request.headers && typeof ctx.request.headers.forEach === "function") {
5536
6721
  ctx.request.headers.forEach((v, k) => {
5537
- headers[k] = v;
6722
+ headers2[k] = v;
6723
+ });
6724
+ }
6725
+ const resHeaders2 = {};
6726
+ if (response.headers && typeof response.headers.forEach === "function") {
6727
+ response.headers.forEach((v, k) => {
6728
+ resHeaders2[k] = v;
5538
6729
  });
5539
6730
  }
5540
- await this.db.upsert(new surrealdb.RecordId("failed_requests", ctx.requestId), {
6731
+ await this.db.upsert(new surrealdb.RecordId(`failed_request`, ctx.requestId), {
5541
6732
  method: ctx.method,
5542
6733
  url: ctx.url.toString(),
5543
- headers,
6734
+ headers: headers2,
5544
6735
  status: response.status,
5545
6736
  timestamp: Date.now(),
5546
- state: ctx.state
5547
- // body?
6737
+ state: ctx.state,
6738
+ body: this.serializeBody(ctx.bodyData || ctx.requestBody),
6739
+ responseHeaders: resHeaders2,
6740
+ responseBody: this.serializeBody(ctx.responseBody)
5548
6741
  });
5549
6742
  } catch (e) {
5550
6743
  console.error("Failed to record failed request", e);
@@ -5552,17 +6745,58 @@ class Dashboard {
5552
6745
  } else {
5553
6746
  this.metrics.successfulRequests++;
5554
6747
  }
6748
+ const urlObj = new URL(ctx.url.toString());
6749
+ const cookieHeader = ctx.request.headers.get("cookie") || "";
6750
+ const cookiesCount = cookieHeader ? cookieHeader.split(";").length : 0;
6751
+ const headers = {};
6752
+ if (ctx.request.headers && typeof ctx.request.headers.forEach === "function") {
6753
+ ctx.request.headers.forEach((v, k) => {
6754
+ headers[k] = v;
6755
+ });
6756
+ }
6757
+ const resHeaders = {};
6758
+ if (response.headers && typeof response.headers.forEach === "function") {
6759
+ response.headers.forEach((v, k) => {
6760
+ resHeaders[k] = v;
6761
+ });
6762
+ }
6763
+ const responseHeadersSize = Object.entries(response.headers || {}).reduce((acc, [k, v]) => acc + k.length + String(v).length + 2, 0);
6764
+ const responseSize = ctx.responseBody ? String(ctx.responseBody).length : 0;
6765
+ const remoteIP = ctx.request.headers.get("x-forwarded-for") || ctx.req?.socket?.remoteAddress;
5555
6766
  const logEntry = {
5556
6767
  method: ctx.method,
5557
6768
  url: ctx.url.toString(),
5558
6769
  status: response.status,
5559
6770
  duration,
5560
6771
  timestamp: Date.now(),
5561
- handlerStack: ctx.handlerStack
6772
+ handlerStack: this.serializeHandlerStack(ctx.handlerStack),
6773
+ body: this.serializeBody(ctx.responseBody),
6774
+ requestBody: ctx.bodyData || ctx.requestBody,
6775
+ // ShokupanContext usually stores parsed body here if parsed
6776
+ contentType: response.headers["content-type"] || response.headers["Content-Type"],
6777
+ type: "xhr",
6778
+ direction: "inbound",
6779
+ size: responseSize,
6780
+ protocol: ctx.req?.httpVersion,
6781
+ // Try to get protocol from raw request if available, Bun might expose it
6782
+ domain: urlObj.hostname,
6783
+ path: urlObj.pathname,
6784
+ scheme: urlObj.protocol.replace(":", ""),
6785
+ cookies: cookiesCount,
6786
+ transferred: responseSize + responseHeadersSize,
6787
+ remoteIP,
6788
+ requestHeaders: headers,
6789
+ responseHeaders: resHeaders
5562
6790
  };
5563
6791
  this.metrics.logs.push(logEntry);
5564
6792
  try {
5565
- await this.db.upsert(new surrealdb.RecordId("requests", ctx.requestId), logEntry);
6793
+ await this.db.query("UPSERT $id CONTENT $data", {
6794
+ id: new surrealdb.RecordId("request", ctx.requestId),
6795
+ data: {
6796
+ ...logEntry,
6797
+ direction: "inbound"
6798
+ }
6799
+ });
5566
6800
  } catch (e) {
5567
6801
  console.error("Failed to record request log", e);
5568
6802
  }
@@ -5571,9 +6805,49 @@ class Dashboard {
5571
6805
  if (this.metrics.logs.length > 0 && this.metrics.logs[0].timestamp < cutoff) {
5572
6806
  this.metrics.logs = this.metrics.logs.filter((log) => log.timestamp >= cutoff);
5573
6807
  }
6808
+ const requestData = {
6809
+ id: ctx.requestId,
6810
+ ...logEntry
6811
+ };
6812
+ const strategy = this.dashboardConfig.updateStrategy || "immediate";
6813
+ if (strategy === "immediate") {
6814
+ this.broadcastRequestUpdates([requestData]);
6815
+ } else {
6816
+ this.requestsBuffer.push(requestData);
6817
+ }
5574
6818
  }
5575
6819
  };
5576
6820
  }
6821
+ startRequestPushTimer() {
6822
+ const interval = this.dashboardConfig.updateInterval || 1e4;
6823
+ this.requestPushTimer = setInterval(() => {
6824
+ if (this.requestsBuffer.length > 0) {
6825
+ this.broadcastRequestUpdates();
6826
+ }
6827
+ }, interval);
6828
+ }
6829
+ broadcastRequestUpdates(requestsOverride) {
6830
+ if (this.clients.size === 0) {
6831
+ if (!requestsOverride) this.requestsBuffer = [];
6832
+ return;
6833
+ }
6834
+ let requests;
6835
+ if (requestsOverride) {
6836
+ requests = requestsOverride;
6837
+ } else {
6838
+ requests = [...this.requestsBuffer];
6839
+ this.requestsBuffer = [];
6840
+ }
6841
+ if (requests.length === 0) return;
6842
+ console.log(`[Dashboard] Broadcasting ${requests.length} requests. Sample ID: ${requests[0].id}`);
6843
+ const data = JSON.stringify({
6844
+ type: "requests-update",
6845
+ requests
6846
+ });
6847
+ for (const client of this.clients) {
6848
+ client.send(data);
6849
+ }
6850
+ }
5577
6851
  updateTiming(duration) {
5578
6852
  const alpha = 0.1;
5579
6853
  if (this.metrics.averageTotalTime_ms === 0) {
@@ -5586,6 +6860,39 @@ class Dashboard {
5586
6860
  this.metrics.recentTimings.shift();
5587
6861
  }
5588
6862
  }
6863
+ serializeHandlerStack(stack) {
6864
+ if (!stack || !Array.isArray(stack)) return [];
6865
+ return stack.map((item) => ({
6866
+ name: item.name,
6867
+ file: item.file,
6868
+ line: item.line,
6869
+ duration: item.duration,
6870
+ startTime: item.startTime,
6871
+ isBuiltin: item.isBuiltin
6872
+ // stateChanges: item.stateChanges // Exclude complex objects for now
6873
+ }));
6874
+ }
6875
+ serializeBody(body) {
6876
+ if (!body) return void 0;
6877
+ if (typeof body === "string") {
6878
+ if (body.length > 524288) {
6879
+ return body.substring(0, 524288) + "... (truncated)";
6880
+ }
6881
+ return body;
6882
+ }
6883
+ if (typeof body === "object") {
6884
+ try {
6885
+ const str = JSON.stringify(body);
6886
+ if (str.length > 524288) {
6887
+ return str.substring(0, 524288) + "... (truncated)";
6888
+ }
6889
+ return body;
6890
+ } catch (e) {
6891
+ return "[Circular or Non-Serializable Body]";
6892
+ }
6893
+ }
6894
+ return "[Binary or Unreadable Body]";
6895
+ }
5589
6896
  }
5590
6897
  function unknownError(ctx) {
5591
6898
  return ctx.json({ error: "Unknown Error" }, 500);
@@ -5669,6 +6976,12 @@ class ScalarPlugin extends ShokupanRouter {
5669
6976
  pluginOptions.config ??= {};
5670
6977
  super();
5671
6978
  this.pluginOptions = pluginOptions;
6979
+ this.metadata = {
6980
+ file: void 0,
6981
+ line: 1,
6982
+ name: "ScalarPlugin",
6983
+ pluginName: "Scalar"
6984
+ };
5672
6985
  this.initRoutes();
5673
6986
  }
5674
6987
  eta;
@@ -5687,41 +7000,80 @@ class ScalarPlugin extends ShokupanRouter {
5687
7000
  }
5688
7001
  initRoutes() {
5689
7002
  const bootId = Date.now().toString();
5690
- this.get("/_lifecycle", (ctx) => ctx.json({ boot: bootId }));
7003
+ this.get("/_lifecycle", (ctx) => {
7004
+ const success = ctx.upgrade({
7005
+ data: {
7006
+ bootId,
7007
+ handler: {
7008
+ open: (ws) => {
7009
+ ws.send(JSON.stringify({ type: "hello", bootId }));
7010
+ }
7011
+ }
7012
+ }
7013
+ });
7014
+ if (success) return void 0;
7015
+ return ctx.json({ boot: bootId });
7016
+ });
5691
7017
  this.get("/", async (ctx) => {
5692
7018
  await this.ensureEta();
5693
- let path2 = ctx.url.toString();
7019
+ let path2 = ctx.path;
5694
7020
  if (!path2.endsWith("/")) path2 += "/";
5695
7021
  const devScript = ctx.app?.applicationConfig.development ? `
5696
7022
  <script>
5697
7023
  (function() {
5698
7024
  const bootId = "${bootId}";
5699
- let isDown = false;
7025
+ let ws;
7026
+ let reconnectTimer;
5700
7027
 
5701
- setInterval(async () => {
5702
- try {
5703
- const res = await fetch('${path2}_lifecycle');
5704
- if (!res.ok) throw new Error('Down');
5705
- const data = await res.json();
5706
- if (data.boot !== bootId) {
5707
- console.log('Server restarted, reloading...');
5708
- window.location.reload();
5709
- }
5710
- else if (isDown) {
5711
- isDown = false;
7028
+ function connect() {
7029
+ const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
7030
+ const wsUrl = protocol + '//' + window.location.host + '${path2}_lifecycle';
7031
+
7032
+ ws = new WebSocket(wsUrl);
7033
+
7034
+ ws.onopen = () => {
7035
+ console.log('[Scalar] Connected to lifecycle monitor');
7036
+ if (reconnectTimer) {
7037
+ clearTimeout(reconnectTimer);
7038
+ reconnectTimer = undefined;
5712
7039
  }
5713
- } catch (e) {
5714
- isDown = true;
5715
- console.log('Connection lost...');
5716
- }
5717
- }, 2000);
7040
+ };
7041
+
7042
+ ws.onmessage = (event) => {
7043
+ try {
7044
+ const data = JSON.parse(event.data);
7045
+ if (data.type === 'hello') {
7046
+ if (data.bootId !== bootId) {
7047
+ console.log('[Scalar] Server restarted (timestamp change), reloading...');
7048
+ window.location.reload();
7049
+ }
7050
+ }
7051
+ } catch (e) {}
7052
+ };
7053
+
7054
+ ws.onclose = () => {
7055
+ console.log('[Scalar] Lifecycle connection lost');
7056
+ ws = undefined;
7057
+ scheduleReconnect();
7058
+ };
7059
+ }
7060
+
7061
+ function scheduleReconnect() {
7062
+ if (reconnectTimer) return;
7063
+ reconnectTimer = setTimeout(() => {
7064
+ reconnectTimer = undefined;
7065
+ connect();
7066
+ }, 2000);
7067
+ }
7068
+
7069
+ connect();
5718
7070
  })();
5719
7071
  <\/script>
5720
7072
  ` : "";
5721
7073
  let themeCss = "";
5722
7074
  try {
5723
7075
  try {
5724
- themeCss = fs.readFileSync(path$1.join(process.cwd(), "src/theme.css"), "utf-8");
7076
+ themeCss = fs$1.readFileSync(path$1.join(process.cwd(), "src/theme.css"), "utf-8");
5725
7077
  } catch {
5726
7078
  }
5727
7079
  } catch (e) {
@@ -5908,10 +7260,13 @@ function Cors(options = {}) {
5908
7260
  };
5909
7261
  const opts = { ...defaults2, ...options };
5910
7262
  const corsMiddleware = async function CorsMiddleware(ctx, next) {
5911
- const headers = new Headers();
7263
+ const headers = {};
5912
7264
  const origin = ctx.headers.get("origin");
5913
- const set = (k, v) => headers.set(k, v);
5914
- const append = (k, v) => headers.append(k, v);
7265
+ const set = (k, v) => headers[k] = v;
7266
+ const append = (k, v) => {
7267
+ const current = headers[k];
7268
+ headers[k] = current ? current + "," + v : v;
7269
+ };
5915
7270
  if (origin === "null" && opts.origin !== "null") {
5916
7271
  return next();
5917
7272
  }
@@ -5970,10 +7325,10 @@ function Cors(options = {}) {
5970
7325
  }
5971
7326
  const response = await next();
5972
7327
  if (response instanceof Response) {
5973
- const headerEntries = Object.entries(headers);
5974
- for (let i = 0; i < headerEntries.length; i++) {
5975
- const [key, value] = headerEntries[i];
5976
- response.headers.set(key, value);
7328
+ const keys = Object.keys(headers);
7329
+ for (let i = 0; i < keys.length; i++) {
7330
+ const key = keys[i];
7331
+ response.headers.set(key, headers[key]);
5977
7332
  }
5978
7333
  }
5979
7334
  return response;