shokupan 0.10.4 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/dist/{analyzer-CKLGLFtx.cjs → analyzer-BAhvpNY_.cjs} +2 -7
  2. package/dist/{analyzer-CKLGLFtx.cjs.map → analyzer-BAhvpNY_.cjs.map} +1 -1
  3. package/dist/{analyzer-BqIe1p0R.js → analyzer-CnKnQ5KV.js} +3 -8
  4. package/dist/{analyzer-BqIe1p0R.js.map → analyzer-CnKnQ5KV.js.map} +1 -1
  5. package/dist/{analyzer.impl-D9Yi1Hax.cjs → analyzer.impl-CfpMu4-g.cjs} +586 -40
  6. package/dist/analyzer.impl-CfpMu4-g.cjs.map +1 -0
  7. package/dist/{analyzer.impl-CV6W1Eq7.js → analyzer.impl-DCiqlXI5.js} +586 -40
  8. package/dist/analyzer.impl-DCiqlXI5.js.map +1 -0
  9. package/dist/cli.cjs +206 -18
  10. package/dist/cli.cjs.map +1 -1
  11. package/dist/cli.js +206 -18
  12. package/dist/cli.js.map +1 -1
  13. package/dist/context.d.ts +6 -1
  14. package/dist/index.cjs +2405 -1008
  15. package/dist/index.cjs.map +1 -1
  16. package/dist/index.js +2402 -1006
  17. package/dist/index.js.map +1 -1
  18. package/dist/plugins/application/api-explorer/static/explorer-client.mjs +423 -30
  19. package/dist/plugins/application/api-explorer/static/style.css +351 -10
  20. package/dist/plugins/application/api-explorer/static/theme.css +7 -2
  21. package/dist/plugins/application/asyncapi/generator.d.ts +4 -0
  22. package/dist/plugins/application/asyncapi/static/asyncapi-client.mjs +154 -22
  23. package/dist/plugins/application/asyncapi/static/style.css +24 -8
  24. package/dist/plugins/application/dashboard/fetch-interceptor.d.ts +107 -0
  25. package/dist/plugins/application/dashboard/metrics-collector.d.ts +38 -2
  26. package/dist/plugins/application/dashboard/plugin.d.ts +44 -1
  27. package/dist/plugins/application/dashboard/static/charts.js +127 -62
  28. package/dist/plugins/application/dashboard/static/client.js +160 -0
  29. package/dist/plugins/application/dashboard/static/graph.mjs +167 -56
  30. package/dist/plugins/application/dashboard/static/reactflow.css +20 -10
  31. package/dist/plugins/application/dashboard/static/registry.js +112 -8
  32. package/dist/plugins/application/dashboard/static/requests.js +868 -58
  33. package/dist/plugins/application/dashboard/static/styles.css +186 -14
  34. package/dist/plugins/application/dashboard/static/tabs.js +44 -9
  35. package/dist/plugins/application/dashboard/static/theme.css +7 -2
  36. package/dist/plugins/application/openapi/analyzer.impl.d.ts +61 -1
  37. package/dist/plugins/application/openapi/openapi.d.ts +3 -0
  38. package/dist/plugins/application/shared/ast-utils.d.ts +7 -0
  39. package/dist/router.d.ts +55 -16
  40. package/dist/shokupan.d.ts +7 -2
  41. package/dist/util/adapter/adapters.d.ts +19 -0
  42. package/dist/util/adapter/filesystem.d.ts +20 -0
  43. package/dist/util/controller-scanner.d.ts +4 -0
  44. package/dist/util/cpu-monitor.d.ts +2 -0
  45. package/dist/util/middleware-tracker.d.ts +10 -0
  46. package/dist/util/types.d.ts +37 -0
  47. package/package.json +5 -5
  48. package/dist/analyzer.impl-CV6W1Eq7.js.map +0 -1
  49. package/dist/analyzer.impl-D9Yi1Hax.cjs.map +0 -1
  50. package/dist/http-server-BEMPIs33.cjs +0 -85
  51. package/dist/http-server-BEMPIs33.cjs.map +0 -1
  52. package/dist/http-server-CCeagTyU.js +0 -68
  53. package/dist/http-server-CCeagTyU.js.map +0 -1
  54. package/dist/plugins/application/dashboard/static/poll.js +0 -146
package/dist/index.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,92 +1145,45 @@ 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
- const collect = (router, prefix = "", currentGroup = defaultTagGroup, defaultTag = defaultTagName, inheritedMiddleware = []) => {
1186
+ const collect = (router, prefix = "", currentGroup = defaultTagGroup, defaultTag = defaultTagName, inheritedMiddleware = [], isRootLevel = true) => {
1129
1187
  let group = currentGroup;
1130
1188
  let tag = defaultTag;
1131
1189
  if (router.config?.group) group = router.config.group;
@@ -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 (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,
@@ -1340,7 +1479,7 @@ async function generateOpenApi(rootRouter, options = {}) {
1340
1479
  const cleanPrefix = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
1341
1480
  const cleanMount = mountPath.startsWith("/") ? mountPath : "/" + mountPath;
1342
1481
  const nextPrefix = cleanPrefix + cleanMount || "/";
1343
- collect(child, nextPrefix, group, tag, [...inheritedMiddleware, ...routerMiddleware]);
1482
+ collect(child, nextPrefix, group, tag, [...inheritedMiddleware, ...routerMiddleware], false);
1344
1483
  }
1345
1484
  };
1346
1485
  collect(rootRouter);
@@ -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(":")) {
@@ -1752,16 +2249,6 @@ class RouterTrie {
1752
2249
  return s.split("/");
1753
2250
  }
1754
2251
  }
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 || {});
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
  }
@@ -2052,290 +2628,49 @@ class ShokupanRouter {
2052
2628
  let previousNode;
2053
2629
  if (debug) {
2054
2630
  debugId = originalHandler._debugId || originalHandler.name || "handler";
2055
- previousNode = debug.getCurrentNode();
2056
- debug.trackEdge(previousNode, debugId);
2057
- debug.setNode(debugId);
2058
- }
2059
- const start = performance.now();
2060
- try {
2061
- const res = await originalHandler(ctx);
2062
- debug?.trackStep(debugId, "handler", performance.now() - start, "success");
2063
- await this.runHooks("onRequestEnd", ctx);
2064
- return res;
2065
- } catch (err) {
2066
- debug?.trackStep(debugId, "handler", performance.now() - start, "error", err);
2067
- await this.runHooks("onError", ctx, err);
2068
- throw err;
2069
- } finally {
2070
- if (debug && previousNode) debug.setNode(previousNode);
2071
- }
2072
- };
2073
- wrapped.originalHandler = originalHandler.originalHandler ?? originalHandler;
2074
- return wrapped;
2075
- }
2076
- mountRouter(prefix, router) {
2077
- if (router[$isMounted]) {
2078
- throw new Error("Router is already mounted");
2079
- }
2080
- 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);
2631
+ previousNode = debug.getCurrentNode();
2632
+ debug.trackEdge(previousNode, debugId);
2633
+ debug.setNode(debugId);
2634
+ }
2635
+ const start = performance.now();
2636
+ try {
2637
+ const res = await originalHandler(ctx);
2638
+ debug?.trackStep(debugId, "handler", performance.now() - start, "success");
2639
+ await this.runHooks("onRequestEnd", ctx);
2640
+ return res;
2641
+ } catch (err) {
2642
+ debug?.trackStep(debugId, "handler", performance.now() - start, "error", err);
2643
+ await this.runHooks("onError", ctx, err);
2644
+ throw err;
2645
+ } finally {
2646
+ if (debug && previousNode) debug.setNode(previousNode);
2333
2647
  }
2648
+ };
2649
+ wrapped.originalHandler = originalHandler.originalHandler ?? originalHandler;
2650
+ return wrapped;
2651
+ }
2652
+ mountRouter(prefix, router) {
2653
+ if (router[$isMounted]) {
2654
+ throw new Error("Router is already mounted");
2334
2655
  }
2335
- if (routesAttached === 0) {
2336
- console.warn(`No routes attached to controller ${instance.constructor.name}`);
2656
+ router[$mountPath] = prefix;
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
  }
@@ -2673,88 +2946,351 @@ class ShokupanRouter {
2673
2946
  handlers = args;
2674
2947
  }
2675
2948
  }
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);
2683
- }
2684
- this.add({
2685
- method,
2686
- path: path2,
2687
- spec,
2688
- handler: finalHandler,
2689
- middleware: handlers.slice(0, handlers.length - 1)
2690
- });
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;
3239
+ }
3240
+ async stop() {
3241
+ if (this.server) {
3242
+ this.server.stop();
3243
+ }
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
  };
@@ -3592,7 +4093,25 @@ function ApiExplorerApp({ spec, asyncSpec, config }) {
3592
4093
  }
3593
4094
  return "Ungrouped";
3594
4095
  };
3595
- const createSubgroups = (routes, depth = 0) => {
4096
+ const findCommonPrefix = (routes) => {
4097
+ if (routes.length === 0) return [];
4098
+ const allSegments = routes.map((r) => {
4099
+ const cleaned = r.path.replace(/^\/|\/$/g, "");
4100
+ return cleaned.split("/");
4101
+ });
4102
+ const minLength = Math.min(...allSegments.map((s) => s.length));
4103
+ const commonPrefix = [];
4104
+ for (let i = 0; i < minLength; i++) {
4105
+ const segment = allSegments[0][i];
4106
+ if (allSegments.every((segments) => segments[i] === segment)) {
4107
+ commonPrefix.push(segment);
4108
+ } else {
4109
+ break;
4110
+ }
4111
+ }
4112
+ return commonPrefix;
4113
+ };
4114
+ const createSubgroups = (routes, depth = 0, commonPrefixLength = 0) => {
3596
4115
  if (routes.length < 3 || depth > 5) {
3597
4116
  return routes.map((route) => ({
3598
4117
  name: route.path,
@@ -3603,7 +4122,8 @@ function ApiExplorerApp({ spec, asyncSpec, config }) {
3603
4122
  }
3604
4123
  const pathSegments = routes.map((r) => {
3605
4124
  const cleaned = r.path.replace(/^\/|\/$/g, "");
3606
- return cleaned.split("/");
4125
+ const segments = cleaned.split("/");
4126
+ return segments.slice(commonPrefixLength);
3607
4127
  });
3608
4128
  const prefixGroups = /* @__PURE__ */ new Map();
3609
4129
  const ungrouped = [];
@@ -3622,13 +4142,30 @@ function ApiExplorerApp({ spec, asyncSpec, config }) {
3622
4142
  const result = [];
3623
4143
  prefixGroups.forEach((groupRoutes, prefix) => {
3624
4144
  if (groupRoutes.length >= 3) {
3625
- const prefixName = prefix.split("/").pop() || prefix;
3626
- result.push({
3627
- name: prefixName,
3628
- type: "subgroup",
3629
- path: "/" + prefix,
3630
- children: createSubgroups(groupRoutes, depth + 1)
4145
+ const nextSegments = /* @__PURE__ */ new Set();
4146
+ groupRoutes.forEach((route, idx) => {
4147
+ const routeIdx = routes.indexOf(route);
4148
+ const segments = pathSegments[routeIdx];
4149
+ if (segments.length > depth + 1) {
4150
+ nextSegments.add(segments[depth + 1]);
4151
+ }
3631
4152
  });
4153
+ const hasDivergingPaths = nextSegments.size >= 2;
4154
+ const allTerminal = groupRoutes.every((route, idx) => {
4155
+ const routeIdx = routes.indexOf(route);
4156
+ return pathSegments[routeIdx].length === depth + 1;
4157
+ });
4158
+ if (hasDivergingPaths || allTerminal) {
4159
+ const prefixName = prefix.split("/").pop() || prefix;
4160
+ result.push({
4161
+ name: prefixName,
4162
+ type: "subgroup",
4163
+ path: "/" + prefix,
4164
+ children: createSubgroups(groupRoutes, depth + 1, commonPrefixLength)
4165
+ });
4166
+ } else {
4167
+ result.push(...createSubgroups(groupRoutes, depth + 1, commonPrefixLength));
4168
+ }
3632
4169
  } else {
3633
4170
  ungrouped.push(...groupRoutes);
3634
4171
  }
@@ -3659,29 +4196,53 @@ function ApiExplorerApp({ spec, asyncSpec, config }) {
3659
4196
  addRoute(groupKey, route);
3660
4197
  });
3661
4198
  });
3662
- Object.entries(asyncSpec?.channels || {}).forEach(([name, ch]) => {
3663
- const operations = [];
3664
- if (ch.publish) operations.push({ method: "recv", op: ch.publish });
3665
- if (ch.subscribe) operations.push({ method: "send", op: ch.subscribe });
3666
- operations.forEach(({ method, op }) => {
3667
- if (!op.operationId) op.operationId = `${method}-${name.replace(/[^a-zA-Z0-9]/g, "-")}`;
3668
- const route = { method, path: name, op };
3669
- const source = op["x-shokupan-source"] || op["x-source-info"];
3670
- const groupKey = getGroupKey(op, source);
3671
- addRoute(groupKey, route);
3672
- });
3673
- });
3674
4199
  const hierarchicalGroups = Array.from(hierarchy.entries()).map(([name, routes]) => {
3675
4200
  routes.sort((a, b) => a.path.localeCompare(b.path));
3676
- const children = createSubgroups(routes);
4201
+ const commonPrefix = findCommonPrefix(routes);
4202
+ const commonPrefixPath = "/" + commonPrefix.join("/");
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);
3677
4217
  return {
3678
4218
  name,
3679
4219
  type: "group",
3680
- children
4220
+ children,
4221
+ middleware: groupMiddleware,
4222
+ commonPrefixPath,
4223
+ // Store for display stripping
4224
+ isBuiltin
3681
4225
  };
3682
- }).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) => {
3683
4242
  if (a.name === "Ungrouped") return 1;
3684
4243
  if (b.name === "Ungrouped") return -1;
4244
+ if (a.name === "Global Middleware") return 1;
4245
+ if (b.name === "Global Middleware") return -1;
3685
4246
  return a.name.localeCompare(b.name);
3686
4247
  });
3687
4248
  const allRoutes = Array.from(hierarchy.values()).flat();
@@ -3690,6 +4251,9 @@ function ApiExplorerApp({ spec, asyncSpec, config }) {
3690
4251
  /* @__PURE__ */ jsxRuntime.jsx("meta", { charSet: "UTF-8" }),
3691
4252
  /* @__PURE__ */ jsxRuntime.jsx("meta", { name: "viewport", content: "width=device-width, initial-scale=1.0" }),
3692
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" }),
3693
4257
  /* @__PURE__ */ jsxRuntime.jsx("link", { rel: "stylesheet", href: "style.css" }),
3694
4258
  /* @__PURE__ */ jsxRuntime.jsx("link", { rel: "stylesheet", href: "theme.css" }),
3695
4259
  /* @__PURE__ */ jsxRuntime.jsx("script", { src: "https://cdn.jsdelivr.net/npm/marked@4.3.0/marked.min.js" }),
@@ -3716,11 +4280,25 @@ function ApiExplorerApp({ spec, asyncSpec, config }) {
3716
4280
  ] });
3717
4281
  }
3718
4282
  function Sidebar$1({ spec, hierarchicalGroups }) {
3719
- const renderNavNode = (node, depth = 0) => {
4283
+ const stripPrefix = (path2, prefix) => {
4284
+ if (!prefix || prefix === "/") return path2;
4285
+ if (path2.startsWith(prefix)) {
4286
+ const stripped = path2.substring(prefix.length);
4287
+ return stripped || "/";
4288
+ }
4289
+ return path2;
4290
+ };
4291
+ const formatAndHighlightPath = (path2) => {
4292
+ const converted = path2.replace(/\{([^}]+)\}/g, ":$1");
4293
+ return converted.replace(/:([a-zA-Z0-9_]+)/g, '<span class="param-highlight">:$1</span>');
4294
+ };
4295
+ const renderNavNode = (node, depth = 0, commonPrefix = "") => {
3720
4296
  if (node.type === "route") {
3721
4297
  const route = node.routes[0];
3722
4298
  const source = route.op["x-shokupan-source"] || route.op["x-source-info"];
3723
4299
  const isRuntime = route.op["x-source-info"]?.isRuntime;
4300
+ const displayPath = stripPrefix(route.path, commonPrefix);
4301
+ const highlightedPath = formatAndHighlightPath(displayPath);
3724
4302
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { class: "nav-item-wrapper", style: `padding-left: ${depth * 12}px;`, children: [
3725
4303
  /* @__PURE__ */ jsxRuntime.jsxs(
3726
4304
  "a",
@@ -3731,7 +4309,7 @@ function Sidebar$1({ spec, hierarchicalGroups }) {
3731
4309
  title: route.path,
3732
4310
  children: [
3733
4311
  /* @__PURE__ */ jsxRuntime.jsx("span", { class: `badge badge-${route.method.toUpperCase()}`, children: route.method.toUpperCase() }),
3734
- /* @__PURE__ */ jsxRuntime.jsx("span", { class: "nav-label", children: node.name }),
4312
+ /* @__PURE__ */ jsxRuntime.jsx("span", { class: "nav-label", dangerouslySetInnerHTML: { __html: highlightedPath } }),
3735
4313
  isRuntime && /* @__PURE__ */ jsxRuntime.jsx("span", { class: "nav-warning", title: "Static Analysis Failed", 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: [
3736
4314
  /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z" }),
3737
4315
  /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "12", y1: "9", x2: "12", y2: "13" }),
@@ -3760,7 +4338,7 @@ function Sidebar$1({ spec, hierarchicalGroups }) {
3760
4338
  /* @__PURE__ */ jsxRuntime.jsx("span", { class: "chevron", children: /* @__PURE__ */ jsxRuntime.jsx("svg", { width: "10", height: "10", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round", children: /* @__PURE__ */ jsxRuntime.jsx("polyline", { points: "9 18 15 12 9 6" }) }) }),
3761
4339
  /* @__PURE__ */ jsxRuntime.jsx("span", { children: node.name })
3762
4340
  ] }),
3763
- /* @__PURE__ */ jsxRuntime.jsx("div", { class: "nav-subgroup-items", children: node.children?.map((child) => renderNavNode(child, depth + 1)) })
4341
+ /* @__PURE__ */ jsxRuntime.jsx("div", { class: "nav-subgroup-items", children: node.children?.map((child) => renderNavNode(child, depth + 1, commonPrefix)) })
3764
4342
  ] });
3765
4343
  }
3766
4344
  };
@@ -3772,13 +4350,46 @@ function Sidebar$1({ spec, hierarchicalGroups }) {
3772
4350
  /* @__PURE__ */ jsxRuntime.jsx("div", { class: "version", children: spec.info?.version })
3773
4351
  ] }),
3774
4352
  /* @__PURE__ */ jsxRuntime.jsx("div", { class: "sidebar-collapse-trigger", children: "➔" }),
3775
- /* @__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: [
3776
4354
  /* @__PURE__ */ jsxRuntime.jsxs("div", { class: "nav-group-title", children: [
3777
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" }) }) }),
3778
- " ",
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
+ ] }) }),
3779
4361
  group.name
3780
4362
  ] }),
3781
- /* @__PURE__ */ jsxRuntime.jsx("div", { class: "nav-items", children: group.children?.map((child) => renderNavNode(child, 0)) })
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
+ ] })
3782
4393
  ] }, group.name)) })
3783
4394
  ] });
3784
4395
  }
@@ -3786,7 +4397,8 @@ function MainContent$1({ allRoutes, config, spec }) {
3786
4397
  const explorerData = JSON.stringify({
3787
4398
  routes: allRoutes,
3788
4399
  config,
3789
- info: spec.info
4400
+ info: spec.info,
4401
+ middlewareRegistry: spec["x-middleware-registry"] || {}
3790
4402
  });
3791
4403
  const safeJson = explorerData.replace(/<\/script>/g, "<\\/script>");
3792
4404
  return /* @__PURE__ */ jsxRuntime.jsxs("main", { class: "content", id: "main-content", children: [
@@ -3796,9 +4408,16 @@ function MainContent$1({ allRoutes, config, spec }) {
3796
4408
  }
3797
4409
  class ApiExplorerPlugin extends ShokupanRouter {
3798
4410
  constructor(pluginOptions = {}) {
4411
+ console.log("ApiExplorerPlugin: CONSTRUCTOR CALLED");
3799
4412
  super({ renderer: renderToString });
3800
4413
  this.pluginOptions = pluginOptions;
3801
4414
  pluginOptions.path ??= "/explorer";
4415
+ this.metadata = {
4416
+ file: void 0,
4417
+ line: 1,
4418
+ name: "ApiExplorerPlugin",
4419
+ pluginName: "ApiExplorer"
4420
+ };
3802
4421
  this.init();
3803
4422
  }
3804
4423
  onInit(app, options) {
@@ -3829,6 +4448,7 @@ class ApiExplorerPlugin extends ShokupanRouter {
3829
4448
  delete op["x-source-info"].snippet;
3830
4449
  }
3831
4450
  if (op["x-shokupan-source"]?.code) {
4451
+ console.log("Deleting x-shokupan-source.code");
3832
4452
  delete op["x-shokupan-source"].code;
3833
4453
  }
3834
4454
  });
@@ -3855,7 +4475,10 @@ class ApiExplorerPlugin extends ShokupanRouter {
3855
4475
  this.get("/", async (ctx) => {
3856
4476
  const spec = this.root.openApiSpec ? structuredClone(this.root.openApiSpec) : await (this.root || this).generateApiSpec();
3857
4477
  const asyncSpec = ctx.app.asyncApiSpec;
3858
- 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);
3859
4482
  });
3860
4483
  }
3861
4484
  }
@@ -3866,8 +4489,8 @@ function AsyncApiApp({ spec, serverUrl, base, disableSourceView, navTree }) {
3866
4489
  /* @__PURE__ */ jsxRuntime.jsx("meta", { name: "viewport", content: "width=device-width, initial-scale=1.0" }),
3867
4490
  /* @__PURE__ */ jsxRuntime.jsx("title", { children: "Shokupan AsyncAPI" }),
3868
4491
  /* @__PURE__ */ jsxRuntime.jsx("link", { rel: "preconnect", href: "https://fonts.googleapis.com" }),
3869
- /* @__PURE__ */ jsxRuntime.jsx("link", { rel: "preconnect", href: "https://fonts.gstatic.com", crossOrigin: "anonymous" }),
3870
- /* @__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" }),
3871
4494
  /* @__PURE__ */ jsxRuntime.jsx("link", { rel: "stylesheet", href: `${base}/theme.css` }),
3872
4495
  /* @__PURE__ */ jsxRuntime.jsx("link", { rel: "stylesheet", href: `${base}/style.css` }),
3873
4496
  /* @__PURE__ */ jsxRuntime.jsx("script", { dangerouslySetInnerHTML: {
@@ -3875,6 +4498,7 @@ function AsyncApiApp({ spec, serverUrl, base, disableSourceView, navTree }) {
3875
4498
  window.INITIAL_SPEC = ${JSON.stringify(spec)};
3876
4499
  window.INITIAL_SERVER_URL = "${serverUrl}";
3877
4500
  window.DISABLE_SOURCE_VIEW = ${JSON.stringify(disableSourceView)};
4501
+ window.BASE_PATH = "${base}";
3878
4502
  `
3879
4503
  } })
3880
4504
  ] }),
@@ -3945,8 +4569,14 @@ function LeafNode({ item, label, disableSourceView }) {
3945
4569
  ] });
3946
4570
  } else {
3947
4571
  const badgeText = item.data.type === "publish" ? "SEND" : "RECV";
4572
+ const isPlugin = item.data.op?.["x-shokupan-plugin-name"] || sourceInfo?.pluginName;
3948
4573
  content = /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
3949
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
+ ] }) }),
3950
4580
  /* @__PURE__ */ jsxRuntime.jsx("span", { class: "tree-label", children: label })
3951
4581
  ] });
3952
4582
  }
@@ -4043,45 +4673,56 @@ function buildNavTree(spec) {
4043
4673
  });
4044
4674
  return root;
4045
4675
  }
4046
- async function getAstRoutes(applications) {
4047
- const astRoutes = [];
4048
- const getExpandedRoutes = (app, prefix = "", seen = /* @__PURE__ */ new Set()) => {
4049
- if (seen.has(app.name)) return [];
4050
- const newSeen = new Set(seen);
4051
- newSeen.add(app.name);
4052
- const expanded = [];
4053
- for (const route of app.routes) {
4054
- expanded.push({
4055
- ...route,
4056
- // For events, path is the event name
4057
- path: route.path.startsWith("/") ? route.path.slice(1) : route.path
4058
- });
4059
- }
4060
- if (app.mounted) {
4061
- for (const mount of app.mounted) {
4062
- const targetApp = applications.find((a) => a.name === mount.target || a.className === mount.target);
4063
- if (targetApp) {
4064
- expanded.push(...getExpandedRoutes(targetApp, "", newSeen));
4065
- }
4066
- }
4067
- }
4068
- return expanded;
4069
- };
4070
- applications.forEach((app) => {
4071
- astRoutes.push(...getExpandedRoutes(app));
4072
- });
4073
- 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;
4074
4688
  }
4075
4689
  async function generateAsyncApi(rootRouter, options = {}) {
4076
4690
  const channels = {};
4077
4691
  let astRoutes = [];
4692
+ let astMiddlewareRegistry = {};
4693
+ let applications = [];
4078
4694
  try {
4079
- const { OpenAPIAnalyzer } = await Promise.resolve().then(() => require("./analyzer-CKLGLFtx.cjs"));
4695
+ const { OpenAPIAnalyzer } = await Promise.resolve().then(() => require("./analyzer-BAhvpNY_.cjs"));
4080
4696
  const entrypoint = globalThis.Bun?.main || require.main?.filename || process.argv[1];
4081
4697
  const analyzer2 = new OpenAPIAnalyzer(process.cwd(), entrypoint);
4082
- const { applications } = await analyzer2.analyze();
4083
- 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
+ }
4084
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
+ }
4085
4726
  }
4086
4727
  const matchedAstRoutes = /* @__PURE__ */ new Set();
4087
4728
  const collect = async (router, prefix = "") => {
@@ -4125,23 +4766,45 @@ async function generateAsyncApi(rootRouter, options = {}) {
4125
4766
  endLine: astMatch?.sourceContext?.endLine,
4126
4767
  highlightLines: astMatch?.sourceContext ? [astMatch.sourceContext.startLine, astMatch.sourceContext.endLine] : void 0
4127
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
+ }
4128
4786
  if (!channels[eventName]) {
4129
- channels[eventName] = {
4130
- publish: {
4131
- operationId: `on${eventName.charAt(0).toUpperCase() + eventName.slice(1)}`,
4132
- tags,
4133
- message: {
4134
- payload: { type: "object" },
4135
- ...userSpec?.message ? userSpec.message : {}
4136
- },
4137
- ...userSpec?.type === "publish" ? userSpec : {},
4138
- "x-source-info": sourceInfo ? [sourceInfo] : [],
4139
- "x-shokupan-source": sourceInfo
4140
- // 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
4141
4796
  }
4142
4797
  };
4143
- if (userSpec?.summary) channels[eventName].publish.summary = userSpec.summary;
4144
- 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
+ };
4145
4808
  } else {
4146
4809
  if (sourceInfo) {
4147
4810
  if (!channels[eventName].publish["x-source-info"]) {
@@ -4159,6 +4822,14 @@ async function generateAsyncApi(rootRouter, options = {}) {
4159
4822
  for (const emit of emits) {
4160
4823
  if (emit.event === "__DYNAMIC_EMIT__") {
4161
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
+ }
4162
4833
  channels[warningKey] = {
4163
4834
  subscribe: {
4164
4835
  operationId: `dynamicEmitWarning${eventName}`,
@@ -4193,17 +4864,24 @@ async function generateAsyncApi(rootRouter, options = {}) {
4193
4864
  emitHighlightLines: [emitStart, emitEnd]
4194
4865
  } : void 0;
4195
4866
  if (!channels[emit.event]) {
4867
+ const payload = emit.payload || { type: "object" };
4868
+ const warning = hasUnknownFields(payload);
4196
4869
  channels[emit.event] = {
4197
4870
  subscribe: {
4198
4871
  operationId: `emit${emit.event.charAt(0).toUpperCase() + emit.event.slice(1)}`,
4199
4872
  tags,
4200
4873
  message: {
4201
- payload: emit.payload || { type: "object" }
4874
+ payload
4202
4875
  },
4876
+ ...warning ? {
4877
+ "x-warning": true,
4878
+ "x-warning-reason": "Payload contains fields with unknown types that could not be statically analyzed"
4879
+ } : {},
4203
4880
  "x-source-info": newSourceInfo ? [newSourceInfo] : [],
4204
4881
  "x-shokupan-source": sourceInfo && emitStart ? {
4205
4882
  file: sourceInfo.file,
4206
- line: emitStart
4883
+ line: emitStart,
4884
+ pluginName: handler.pluginName
4207
4885
  } : void 0
4208
4886
  }
4209
4887
  };
@@ -4267,13 +4945,19 @@ async function generateAsyncApi(rootRouter, options = {}) {
4267
4945
  emitHighlightLines: [emitStart, emitEnd]
4268
4946
  } : void 0;
4269
4947
  if (!channels[emit.event]) {
4948
+ const payload = emit.payload || { type: "object" };
4949
+ const warning = hasUnknownFields(payload);
4270
4950
  channels[emit.event] = {
4271
4951
  subscribe: {
4272
4952
  operationId: `emit${emit.event.charAt(0).toUpperCase() + emit.event.slice(1)}`,
4273
4953
  tags,
4274
4954
  message: {
4275
- payload: emit.payload || { type: "object" }
4955
+ payload
4276
4956
  },
4957
+ ...warning ? {
4958
+ "x-warning": true,
4959
+ "x-warning-reason": "Payload contains fields with unknown types that could not be statically analyzed"
4960
+ } : {},
4277
4961
  "x-source-info": newSourceInfo ? [newSourceInfo] : [],
4278
4962
  "x-shokupan-source": sourceInfo && emitStart ? {
4279
4963
  file: sourceInfo.file,
@@ -4312,6 +4996,14 @@ async function generateAsyncApi(rootRouter, options = {}) {
4312
4996
  if (parts.length > 0) prefix = parts[0];
4313
4997
  }
4314
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
+ }
4315
5007
  channels[key] = {
4316
5008
  publish: {
4317
5009
  operationId: `dynamicEventWarning${i}`,
@@ -4337,7 +5029,8 @@ async function generateAsyncApi(rootRouter, options = {}) {
4337
5029
  return {
4338
5030
  asyncapi: "3.0.0",
4339
5031
  info: { title: "Shokupan AsyncAPI", version: "1.0.0", ...options.info },
4340
- channels
5032
+ channels,
5033
+ "x-middleware-registry": astMiddlewareRegistry
4341
5034
  };
4342
5035
  }
4343
5036
  const generator = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
@@ -4349,6 +5042,12 @@ class AsyncApiPlugin extends ShokupanRouter {
4349
5042
  super({ renderer: renderToString });
4350
5043
  this.pluginOptions = pluginOptions;
4351
5044
  this.pluginOptions.path ??= "/asyncapi";
5045
+ this.metadata = {
5046
+ file: void 0,
5047
+ line: 1,
5048
+ name: "AsyncApiPlugin",
5049
+ pluginName: "AsyncAPI"
5050
+ };
4352
5051
  this.init();
4353
5052
  }
4354
5053
  static getBasePath() {
@@ -4761,12 +5460,15 @@ class ClusterPlugin {
4761
5460
  }
4762
5461
  }
4763
5462
  }
4764
- function DashboardApp({ metrics, uptime, integrations, base, getRequestHeadersSource }) {
5463
+ function DashboardApp({ metrics, uptime, integrations, base, getRequestHeadersSource, rootPath, linkPattern }) {
4765
5464
  return /* @__PURE__ */ jsxRuntime.jsxs("html", { lang: "en", children: [
4766
5465
  /* @__PURE__ */ jsxRuntime.jsxs("head", { children: [
4767
5466
  /* @__PURE__ */ jsxRuntime.jsx("meta", { charSet: "UTF-8" }),
4768
5467
  /* @__PURE__ */ jsxRuntime.jsx("meta", { name: "viewport", content: "width=device-width, initial-scale=1.0" }),
4769
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" }),
4770
5472
  /* @__PURE__ */ jsxRuntime.jsx("link", { href: "https://unpkg.com/tabulator-tables@5.5.0/dist/css/tabulator_bootstrap5.min.css", rel: "stylesheet" }),
4771
5473
  /* @__PURE__ */ jsxRuntime.jsx("link", { rel: "stylesheet", href: "https://esm.sh/@xyflow/react@12.3.6/dist/style.css" }),
4772
5474
  /* @__PURE__ */ jsxRuntime.jsx("link", { rel: "stylesheet", href: `${base}/theme.css` }),
@@ -4775,104 +5477,134 @@ function DashboardApp({ metrics, uptime, integrations, base, getRequestHeadersSo
4775
5477
  /* @__PURE__ */ jsxRuntime.jsx("link", { rel: "stylesheet", href: `${base}/registry.css` }),
4776
5478
  /* @__PURE__ */ jsxRuntime.jsx("link", { rel: "stylesheet", href: `${base}/tabulator.css` }),
4777
5479
  /* @__PURE__ */ jsxRuntime.jsx("script", { src: "https://cdn.jsdelivr.net/npm/chart.js" }),
4778
- /* @__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" })
4779
5482
  ] }),
4780
5483
  /* @__PURE__ */ jsxRuntime.jsxs("body", { children: [
4781
5484
  /* @__PURE__ */ jsxRuntime.jsxs("div", { class: "container", children: [
4782
5485
  /* @__PURE__ */ jsxRuntime.jsxs("header", { children: [
4783
- /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
4784
- /* @__PURE__ */ jsxRuntime.jsx("h1", { children: "Dashboard" }),
4785
- /* @__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: [
4786
5489
  "Uptime: ",
4787
5490
  /* @__PURE__ */ jsxRuntime.jsx("span", { id: "uptime", children: uptime })
4788
- ] })
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;" })
4789
5493
  ] }),
5494
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: "flex: 1;" }),
4790
5495
  /* @__PURE__ */ jsxRuntime.jsxs("div", { class: "tabs", children: [
4791
5496
  /* @__PURE__ */ jsxRuntime.jsx("button", { class: "tab-btn active", onclick: "switchTab('overview')", children: "Overview" }),
4792
- /* @__PURE__ */ jsxRuntime.jsx("button", { class: "tab-btn", onclick: "switchTab('registry')", children: "Registry" }),
4793
- /* @__PURE__ */ jsxRuntime.jsx("button", { class: "tab-btn", onclick: "switchTab('graph')", children: "Graph" }),
4794
- /* @__PURE__ */ jsxRuntime.jsx("button", { class: "tab-btn", onclick: "switchTab('requests')", children: "Requests" }),
4795
- /* @__PURE__ */ jsxRuntime.jsx("button", { class: "tab-btn", onclick: "switchTab('failures')", children: "Failures" }),
4796
- integrations.scalar && /* @__PURE__ */ jsxRuntime.jsx("button", { class: "tab-btn", onclick: "switchTab('scalar')", children: "API Reference" }),
4797
- 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" })
4798
5502
  ] })
4799
5503
  ] }),
4800
- /* @__PURE__ */ jsxRuntime.jsxs("div", { id: "tab-overview", class: "tab-content active", children: [
4801
- /* @__PURE__ */ jsxRuntime.jsx(MetricsGrid, { metrics }),
4802
- /* @__PURE__ */ jsxRuntime.jsxs("div", { id: "chart-container", style: "display: flex; flex-direction: column; gap: 1rem;", children: [
4803
- /* @__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: [
4804
- /* @__PURE__ */ jsxRuntime.jsx("option", { value: "1m", children: "1 Minute" }),
4805
- /* @__PURE__ */ jsxRuntime.jsx("option", { value: "5m", children: "5 Minutes" }),
4806
- /* @__PURE__ */ jsxRuntime.jsx("option", { value: "30m", children: "30 Minutes" }),
4807
- /* @__PURE__ */ jsxRuntime.jsx("option", { value: "1h", children: "1 Hour" }),
4808
- /* @__PURE__ */ jsxRuntime.jsx("option", { value: "2h", children: "2 Hours" }),
4809
- /* @__PURE__ */ jsxRuntime.jsx("option", { value: "6h", children: "6 Hours" }),
4810
- /* @__PURE__ */ jsxRuntime.jsx("option", { value: "12h", children: "12 Hours" }),
4811
- /* @__PURE__ */ jsxRuntime.jsx("option", { value: "1d", children: "1 Day" }),
4812
- /* @__PURE__ */ jsxRuntime.jsx("option", { value: "3d", children: "3 Days" }),
4813
- /* @__PURE__ */ jsxRuntime.jsx("option", { value: "7d", children: "7 Days" }),
4814
- /* @__PURE__ */ jsxRuntime.jsx("option", { value: "30d", children: "30 Days" })
4815
- ] }) }),
4816
- /* @__PURE__ */ jsxRuntime.jsxs("div", { class: "card-container", children: [
4817
- /* @__PURE__ */ jsxRuntime.jsx(ChartCard, { title: "Response Time", id: "latencyChart" }),
4818
- /* @__PURE__ */ jsxRuntime.jsx(ChartCard, { title: "Requests / Second", id: "rpsChart" }),
4819
- /* @__PURE__ */ jsxRuntime.jsx(ChartCard, { title: "CPU & Load", id: "cpuChart" }),
4820
- /* @__PURE__ */ jsxRuntime.jsx(ChartCard, { title: "Memory", id: "memoryChart" }),
4821
- /* @__PURE__ */ jsxRuntime.jsx(ChartCard, { title: "Heap Usage", id: "heapChart" }),
4822
- /* @__PURE__ */ jsxRuntime.jsx(ChartCard, { title: "Event Loop Latency", id: "eventLoopChart" }),
4823
- /* @__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" }) })
4824
5538
  ] }),
4825
- /* @__PURE__ */ jsxRuntime.jsx("div", { class: "card-title", style: "margin-top: 1rem;", children: "Top Statistics" }),
4826
- /* @__PURE__ */ jsxRuntime.jsxs("div", { class: "card-container", children: [
4827
- /* @__PURE__ */ jsxRuntime.jsx(Card, { title: "Top Requests", contentId: "top-requests-table" }),
4828
- /* @__PURE__ */ jsxRuntime.jsx(Card, { title: "Top Errors", contentId: "top-errors-table" }),
4829
- /* @__PURE__ */ jsxRuntime.jsx(Card, { title: "Most Frequent Failures", contentId: "failing-requests-table" }),
4830
- /* @__PURE__ */ jsxRuntime.jsx(Card, { title: "Slowest Requests", contentId: "slowest-requests-table" })
4831
- ] }),
4832
- /* @__PURE__ */ jsxRuntime.jsx("div", { id: "table-container", style: "padding: 0; margin-top: 1rem;", children: /* @__PURE__ */ jsxRuntime.jsx("div", { id: "requests-table", class: "table-dark" }) })
4833
- ] })
4834
- ] }),
4835
- /* @__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: [
4836
- /* @__PURE__ */ jsxRuntime.jsx("div", { class: "card-title", children: "Component Registry" }),
4837
- /* @__PURE__ */ jsxRuntime.jsx("div", { id: "registry-tree", style: "padding: 0 1rem 1rem 1rem; font-family: monospace; font-size: 0.9rem;" })
4838
- ] }) }),
4839
- /* @__PURE__ */ jsxRuntime.jsxs("div", { id: "tab-graph", class: "tab-content", children: [
4840
- /* @__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);" }) }) }),
4841
- /* @__PURE__ */ jsxRuntime.jsx("div", { id: "cy" })
4842
- ] }),
4843
- /* @__PURE__ */ jsxRuntime.jsxs("div", { id: "tab-requests", class: "tab-content", children: [
4844
- /* @__PURE__ */ jsxRuntime.jsxs("div", { class: "card", style: "margin-bottom: 1rem; display: flex; justify-content: space-between; align-items: center;", children: [
4845
- /* @__PURE__ */ jsxRuntime.jsx("div", { class: "card-title", children: "Recent Requests (Last 100)" }),
4846
- /* @__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" })
4847
5540
  ] }),
4848
- /* @__PURE__ */ jsxRuntime.jsx("div", { id: "requests-list-container", style: "height: calc(100vh - 300px); margin-bottom: 2rem;" }),
4849
- /* @__PURE__ */ jsxRuntime.jsxs("div", { id: "request-details-container", class: "card", style: "display: none;", children: [
4850
- /* @__PURE__ */ jsxRuntime.jsx("div", { class: "card-title", children: "Request Details" }),
4851
- /* @__PURE__ */ jsxRuntime.jsx("div", { id: "request-details-content" }),
4852
- /* @__PURE__ */ jsxRuntime.jsx("div", { class: "card-title", style: "margin-top: 1rem;", children: "Middleware Trace" }),
4853
- /* @__PURE__ */ jsxRuntime.jsx("div", { id: "middleware-trace-container" })
4854
- ] })
4855
- ] }),
4856
- /* @__PURE__ */ jsxRuntime.jsxs("div", { id: "tab-failures", class: "tab-content", children: [
4857
- /* @__PURE__ */ jsxRuntime.jsxs("div", { class: "card", style: "margin-bottom: 1rem; display: flex; justify-content: space-between; align-items: center;", children: [
4858
- /* @__PURE__ */ jsxRuntime.jsx("div", { class: "card-title", children: "Failed Requests (Last 50)" }),
4859
- /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
4860
- /* @__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" }),
4861
- /* @__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);" })
4862
5556
  ] })
4863
5557
  ] }),
4864
- /* @__PURE__ */ jsxRuntime.jsx("div", { id: "failures-table-container" })
4865
- ] }),
4866
- 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;" }) }),
4867
- 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
+ ] })
4868
5596
  ] }),
4869
5597
  /* @__PURE__ */ jsxRuntime.jsx("script", { dangerouslySetInnerHTML: {
4870
5598
  __html: `
4871
5599
  // Injected function from server config
4872
5600
  const getRequestHeaders = ${getRequestHeadersSource};
5601
+ window.SHOKUPAN_CONFIG = {
5602
+ rootPath: "${rootPath || ""}",
5603
+ linkPattern: "${linkPattern || ""}"
5604
+ };
4873
5605
  `
4874
5606
  } }),
4875
- /* @__PURE__ */ jsxRuntime.jsx("script", { src: `${base}/poll.js` }),
5607
+ /* @__PURE__ */ jsxRuntime.jsx("script", { src: `${base}/client.js` }),
4876
5608
  /* @__PURE__ */ jsxRuntime.jsx("script", { src: `${base}/graph.mjs`, type: "module" }),
4877
5609
  /* @__PURE__ */ jsxRuntime.jsx("script", { src: `${base}/charts.js` }),
4878
5610
  /* @__PURE__ */ jsxRuntime.jsx("script", { src: `${base}/tables.js` }),
@@ -4942,6 +5674,264 @@ function Card({ title, contentId }) {
4942
5674
  /* @__PURE__ */ jsxRuntime.jsx("div", { id: contentId })
4943
5675
  ] });
4944
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
+ }
4945
5935
  const INTERVALS = [
4946
5936
  { label: "10s", ms: 10 * 1e3 },
4947
5937
  { label: "1m", ms: 60 * 1e3 },
@@ -4956,7 +5946,8 @@ const INTERVALS = [
4956
5946
  { label: "30d", ms: 30 * 24 * 60 * 60 * 1e3 }
4957
5947
  ];
4958
5948
  class MetricsCollector {
4959
- constructor(db) {
5949
+ constructor(db, onCollect) {
5950
+ this.onCollect = onCollect;
4960
5951
  this.db = db;
4961
5952
  this.eventLoopHistogram.enable();
4962
5953
  const now = Date.now();
@@ -4964,11 +5955,13 @@ class MetricsCollector {
4964
5955
  this.currentIntervalStart[int.label] = this.alignTimestamp(now, int.ms);
4965
5956
  this.pendingDetails[int.label] = [];
4966
5957
  });
5958
+ this.timer = setInterval(() => this.collect(), 1e4);
4967
5959
  }
4968
5960
  currentIntervalStart = {};
4969
5961
  pendingDetails = {};
4970
5962
  eventLoopHistogram = node_perf_hooks.monitorEventLoopDelay({ resolution: 10 });
4971
5963
  timer = null;
5964
+ db;
4972
5965
  recordRequest(duration, isError) {
4973
5966
  INTERVALS.forEach((int) => {
4974
5967
  this.pendingDetails[int.label].push({ duration, isError });
@@ -5044,14 +6037,17 @@ class MetricsCollector {
5044
6037
  p99: getP(0.99)
5045
6038
  }
5046
6039
  };
6040
+ if (!this.db) {
6041
+ return;
6042
+ }
5047
6043
  try {
5048
- const recordId = new surrealdb.RecordId("metrics", timestamp);
5049
- await this.db.upsert(recordId, metric);
5050
- const test = await this.db.select(recordId);
5051
- 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);
5052
6045
  } catch (e) {
5053
6046
  console.error(`[MetricsCollector] ✗ Failed to save metrics for ${label}:`, e);
5054
6047
  }
6048
+ if (this.onCollect) {
6049
+ this.onCollect(metric);
6050
+ }
5055
6051
  }
5056
6052
  // Cleanup if needed
5057
6053
  stop() {
@@ -5097,8 +6093,13 @@ class Dashboard {
5097
6093
  nodeMetrics: {},
5098
6094
  edgeMetrics: {}
5099
6095
  };
6096
+ clients = /* @__PURE__ */ new Set();
6097
+ broadcastTimer;
6098
+ requestPushTimer;
6099
+ requestsBuffer = [];
5100
6100
  startTime = Date.now();
5101
6101
  instrumented = false;
6102
+ mountPath = "/dashboard";
5102
6103
  metricsCollector;
5103
6104
  get db() {
5104
6105
  return this[$appRoot].db;
@@ -5106,8 +6107,69 @@ class Dashboard {
5106
6107
  // ShokupanPlugin interface implementation
5107
6108
  onInit(app, options) {
5108
6109
  this[$appRoot] = app;
5109
- this.metricsCollector = new MetricsCollector(this.db);
5110
- 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";
5111
6173
  const hooks = this.getHooks();
5112
6174
  if (!app.middleware) {
5113
6175
  app.middleware = [];
@@ -5116,15 +6178,25 @@ class Dashboard {
5116
6178
  if (hooks.onRequestStart) {
5117
6179
  await hooks.onRequestStart(ctx);
5118
6180
  }
6181
+ ctx._startTime = performance.now();
5119
6182
  await next();
5120
- if (hooks.onResponseEnd) {
5121
- const effectiveResponse = ctx._finalResponse || ctx.response || {};
5122
- await hooks.onResponseEnd(ctx, effectiveResponse);
5123
- }
5124
6183
  };
5125
6184
  app.use(hooksMiddleware);
5126
- 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
+ };
5127
6195
  this.setupRoutes();
6196
+ const strategy = this.dashboardConfig.updateStrategy || "immediate";
6197
+ if (strategy === "batched") {
6198
+ this.startRequestPushTimer();
6199
+ }
5128
6200
  }
5129
6201
  detectIntegrations() {
5130
6202
  const integrations = {};
@@ -5157,6 +6229,17 @@ class Dashboard {
5157
6229
  }
5158
6230
  }
5159
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
+ }
5160
6243
  return integrations;
5161
6244
  }
5162
6245
  // Get base path for dashboard files - works in both dev (src/) and production (dist/)
@@ -5168,9 +6251,36 @@ class Dashboard {
5168
6251
  return dir;
5169
6252
  }
5170
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
+ });
5171
6282
  this.router.get("/metrics", async (ctx) => {
5172
- const uptimeSeconds = Math.floor((Date.now() - this.startTime) / 1e3);
5173
- const uptime = `${Math.floor(uptimeSeconds / 3600)}h ${Math.floor(uptimeSeconds % 3600 / 60)}m ${uptimeSeconds % 60}s`;
6283
+ const uptime = this.getUptime();
5174
6284
  const interval = ctx.query["interval"];
5175
6285
  if (interval) {
5176
6286
  const intervalMap = {
@@ -5197,13 +6307,15 @@ class Dashboard {
5197
6307
  count(IF status < 400 THEN 1 END) as success,
5198
6308
  count(IF status >= 400 THEN 1 END) as failed,
5199
6309
  math::mean(duration) as avg_latency
5200
- FROM requests
6310
+ FROM request
5201
6311
  WHERE timestamp >= $start
5202
6312
  GROUP ALL
5203
6313
  `, { start: startTime });
5204
6314
  } catch (error) {
5205
6315
  console.error("[Dashboard] Query failed at plugin.ts:180-191", {
5206
6316
  error,
6317
+ errorMessage: error.message,
6318
+ errorStack: error.stack,
5207
6319
  interval,
5208
6320
  startTime,
5209
6321
  query: "metrics interval stats",
@@ -5211,12 +6323,12 @@ class Dashboard {
5211
6323
  });
5212
6324
  throw error;
5213
6325
  }
5214
- const s = stats[0] || { total: 0, success: 0, failed: 0, avg_latency: 0 };
6326
+ const s = stats[0] || { avg_latency: 0 };
5215
6327
  return ctx.json({
5216
6328
  metrics: {
5217
- totalRequests: s.total || 0,
5218
- successfulRequests: s.success || 0,
5219
- failedRequests: s.failed || 0,
6329
+ totalRequests: this.metrics.totalRequests,
6330
+ successfulRequests: this.metrics.successfulRequests,
6331
+ failedRequests: this.metrics.failedRequests,
5220
6332
  activeRequests: this.metrics.activeRequests,
5221
6333
  averageTotalTime_ms: s.avg_latency || 0,
5222
6334
  recentTimings: this.metrics.recentTimings,
@@ -5282,7 +6394,7 @@ class Dashboard {
5282
6394
  this.router.get("/requests/top", async (ctx) => {
5283
6395
  const startTime = getIntervalStartTime(ctx.query["interval"]);
5284
6396
  const result = await this.db.query(
5285
- "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",
5286
6398
  { start: startTime }
5287
6399
  );
5288
6400
  return ctx.json({ top: result[0] || [] });
@@ -5290,7 +6402,7 @@ class Dashboard {
5290
6402
  this.router.get("/errors/top", async (ctx) => {
5291
6403
  const startTime = getIntervalStartTime(ctx.query["interval"]);
5292
6404
  const result = await this.db.query(
5293
- "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",
5294
6406
  { start: startTime }
5295
6407
  );
5296
6408
  return ctx.json({ top: result[0] || [] });
@@ -5298,7 +6410,7 @@ class Dashboard {
5298
6410
  this.router.get("/requests/failing", async (ctx) => {
5299
6411
  const startTime = getIntervalStartTime(ctx.query["interval"]);
5300
6412
  const result = await this.db.query(
5301
- "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",
5302
6414
  { start: startTime }
5303
6415
  );
5304
6416
  return ctx.json({ top: result[0] || [] });
@@ -5306,7 +6418,7 @@ class Dashboard {
5306
6418
  this.router.get("/requests/slowest", async (ctx) => {
5307
6419
  const startTime = getIntervalStartTime(ctx.query["interval"]);
5308
6420
  const result = await this.db.query(
5309
- "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",
5310
6422
  { start: startTime }
5311
6423
  );
5312
6424
  return ctx.json({ slowest: result[0] || [] });
@@ -5323,15 +6435,32 @@ class Dashboard {
5323
6435
  return ctx.json({ registry: registry || {} });
5324
6436
  });
5325
6437
  this.router.get("/requests", async (ctx) => {
5326
- const result = await this.db.query("SELECT * FROM requests ORDER BY timestamp DESC LIMIT 100");
5327
- 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 });
5328
6457
  });
5329
6458
  this.router.get("/requests/:id", async (ctx) => {
5330
- 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"] });
5331
6460
  return ctx.json({ request: result[0]?.[0] });
5332
6461
  });
5333
6462
  this.router.get("/failures", async (ctx) => {
5334
- 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");
5335
6464
  return ctx.json({ failures: result[0] });
5336
6465
  });
5337
6466
  this.router.post("/replay", async (ctx) => {
@@ -5369,7 +6498,7 @@ class Dashboard {
5369
6498
  "charts.js",
5370
6499
  "failures.js",
5371
6500
  "graph.mjs",
5372
- "poll.js",
6501
+ "client.js",
5373
6502
  "reactflow.css",
5374
6503
  "registry.css",
5375
6504
  "registry.js",
@@ -5386,15 +6515,15 @@ class Dashboard {
5386
6515
  else if (path2.endsWith(".js") || path2.endsWith(".mjs")) ctx.set("Content-Type", "application/javascript");
5387
6516
  return ctx.send(content);
5388
6517
  }
5389
- const uptimeSeconds = Math.floor((Date.now() - this.startTime) / 1e3);
5390
- const uptime = `${Math.floor(uptimeSeconds / 3600)}h ${Math.floor(uptimeSeconds % 3600 / 60)}m ${uptimeSeconds % 60}s`;
5391
- this.getLinkPattern();
6518
+ const uptime = this.getUptime();
6519
+ const linkPattern = this.getLinkPattern();
5392
6520
  const integrations = this.detectIntegrations();
5393
6521
  const getRequestHeadersSource = this.dashboardConfig.getRequestHeaders ? this.dashboardConfig.getRequestHeaders.toString() : "undefined";
5394
6522
  const html = renderToString(DashboardApp({
5395
6523
  metrics: this.metrics,
5396
6524
  uptime,
5397
6525
  rootPath: process.cwd(),
6526
+ linkPattern,
5398
6527
  integrations,
5399
6528
  base: mountPath,
5400
6529
  getRequestHeadersSource
@@ -5402,6 +6531,82 @@ class Dashboard {
5402
6531
  return ctx.html(`<!DOCTYPE html>${html}`);
5403
6532
  });
5404
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
+ }
5405
6610
  instrumentApp(app) {
5406
6611
  if (!app.getComponentRegistry) return;
5407
6612
  const registry = app.getComponentRegistry();
@@ -5431,6 +6636,11 @@ class Dashboard {
5431
6636
  r.id = id;
5432
6637
  this.assignIdsToRegistry(r.children, id);
5433
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
+ });
5434
6644
  }
5435
6645
  recordNodeMetric(id, type, duration, isError) {
5436
6646
  if (!this.metrics.nodeMetrics[id]) {
@@ -5458,7 +6668,7 @@ class Dashboard {
5458
6668
  if (["vscode", "cursor", "antigravity"].some((t) => term.includes(t))) {
5459
6669
  return "vscode://file/{{absolute}}:{{line}}";
5460
6670
  }
5461
- return "file:///{{absolute}}:{{line}}";
6671
+ return "vscode://file/{{absolute}}:{{line}}";
5462
6672
  }
5463
6673
  getHooks() {
5464
6674
  return {
@@ -5469,19 +6679,36 @@ class Dashboard {
5469
6679
  }
5470
6680
  this.metrics.totalRequests++;
5471
6681
  this.metrics.activeRequests++;
5472
- ctx._debugStartTime = performance.now();
5473
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
+ }
5474
6689
  },
5475
6690
  onResponseEnd: async (ctx, response) => {
6691
+ if (ctx.path.startsWith(this.mountPath)) return;
5476
6692
  this.metrics.activeRequests = Math.max(0, this.metrics.activeRequests - 1);
5477
- const start = ctx._debugStartTime;
5478
- let duration = 0;
5479
- if (start) {
5480
- duration = performance.now() - start;
5481
- 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
+ }
5482
6703
  }
5483
6704
  const isError = response.status >= 400;
5484
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
+ }
5485
6712
  if (response.status >= 400) {
5486
6713
  this.metrics.failedRequests++;
5487
6714
  if (response.status === 429) {
@@ -5489,20 +6716,28 @@ class Dashboard {
5489
6716
  this.metrics.rateLimitedCounts[path2] = (this.metrics.rateLimitedCounts[path2] || 0) + 1;
5490
6717
  }
5491
6718
  try {
5492
- const headers = {};
6719
+ const headers2 = {};
5493
6720
  if (ctx.request.headers && typeof ctx.request.headers.forEach === "function") {
5494
6721
  ctx.request.headers.forEach((v, k) => {
5495
- 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;
5496
6729
  });
5497
6730
  }
5498
- await this.db.upsert(new surrealdb.RecordId("failed_requests", ctx.requestId), {
6731
+ await this.db.upsert(new surrealdb.RecordId(`failed_request`, ctx.requestId), {
5499
6732
  method: ctx.method,
5500
6733
  url: ctx.url.toString(),
5501
- headers,
6734
+ headers: headers2,
5502
6735
  status: response.status,
5503
6736
  timestamp: Date.now(),
5504
- state: ctx.state
5505
- // body?
6737
+ state: ctx.state,
6738
+ body: this.serializeBody(ctx.bodyData || ctx.requestBody),
6739
+ responseHeaders: resHeaders2,
6740
+ responseBody: this.serializeBody(ctx.responseBody)
5506
6741
  });
5507
6742
  } catch (e) {
5508
6743
  console.error("Failed to record failed request", e);
@@ -5510,17 +6745,58 @@ class Dashboard {
5510
6745
  } else {
5511
6746
  this.metrics.successfulRequests++;
5512
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;
5513
6766
  const logEntry = {
5514
6767
  method: ctx.method,
5515
6768
  url: ctx.url.toString(),
5516
6769
  status: response.status,
5517
6770
  duration,
5518
6771
  timestamp: Date.now(),
5519
- 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
5520
6790
  };
5521
6791
  this.metrics.logs.push(logEntry);
5522
6792
  try {
5523
- 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
+ });
5524
6800
  } catch (e) {
5525
6801
  console.error("Failed to record request log", e);
5526
6802
  }
@@ -5529,9 +6805,49 @@ class Dashboard {
5529
6805
  if (this.metrics.logs.length > 0 && this.metrics.logs[0].timestamp < cutoff) {
5530
6806
  this.metrics.logs = this.metrics.logs.filter((log) => log.timestamp >= cutoff);
5531
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
+ }
5532
6818
  }
5533
6819
  };
5534
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
+ }
5535
6851
  updateTiming(duration) {
5536
6852
  const alpha = 0.1;
5537
6853
  if (this.metrics.averageTotalTime_ms === 0) {
@@ -5544,6 +6860,39 @@ class Dashboard {
5544
6860
  this.metrics.recentTimings.shift();
5545
6861
  }
5546
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
+ }
5547
6896
  }
5548
6897
  function unknownError(ctx) {
5549
6898
  return ctx.json({ error: "Unknown Error" }, 500);
@@ -5627,6 +6976,12 @@ class ScalarPlugin extends ShokupanRouter {
5627
6976
  pluginOptions.config ??= {};
5628
6977
  super();
5629
6978
  this.pluginOptions = pluginOptions;
6979
+ this.metadata = {
6980
+ file: void 0,
6981
+ line: 1,
6982
+ name: "ScalarPlugin",
6983
+ pluginName: "Scalar"
6984
+ };
5630
6985
  this.initRoutes();
5631
6986
  }
5632
6987
  eta;
@@ -5645,41 +7000,80 @@ class ScalarPlugin extends ShokupanRouter {
5645
7000
  }
5646
7001
  initRoutes() {
5647
7002
  const bootId = Date.now().toString();
5648
- 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
+ });
5649
7017
  this.get("/", async (ctx) => {
5650
7018
  await this.ensureEta();
5651
- let path2 = ctx.url.toString();
7019
+ let path2 = ctx.path;
5652
7020
  if (!path2.endsWith("/")) path2 += "/";
5653
7021
  const devScript = ctx.app?.applicationConfig.development ? `
5654
7022
  <script>
5655
7023
  (function() {
5656
7024
  const bootId = "${bootId}";
5657
- let isDown = false;
7025
+ let ws;
7026
+ let reconnectTimer;
5658
7027
 
5659
- setInterval(async () => {
5660
- try {
5661
- const res = await fetch('${path2}_lifecycle');
5662
- if (!res.ok) throw new Error('Down');
5663
- const data = await res.json();
5664
- if (data.boot !== bootId) {
5665
- console.log('Server restarted, reloading...');
5666
- window.location.reload();
5667
- }
5668
- else if (isDown) {
5669
- 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;
5670
7039
  }
5671
- } catch (e) {
5672
- isDown = true;
5673
- console.log('Connection lost...');
5674
- }
5675
- }, 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();
5676
7070
  })();
5677
7071
  <\/script>
5678
7072
  ` : "";
5679
7073
  let themeCss = "";
5680
7074
  try {
5681
7075
  try {
5682
- 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");
5683
7077
  } catch {
5684
7078
  }
5685
7079
  } catch (e) {
@@ -5866,10 +7260,13 @@ function Cors(options = {}) {
5866
7260
  };
5867
7261
  const opts = { ...defaults2, ...options };
5868
7262
  const corsMiddleware = async function CorsMiddleware(ctx, next) {
5869
- const headers = new Headers();
7263
+ const headers = {};
5870
7264
  const origin = ctx.headers.get("origin");
5871
- const set = (k, v) => headers.set(k, v);
5872
- 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
+ };
5873
7270
  if (origin === "null" && opts.origin !== "null") {
5874
7271
  return next();
5875
7272
  }
@@ -5928,10 +7325,10 @@ function Cors(options = {}) {
5928
7325
  }
5929
7326
  const response = await next();
5930
7327
  if (response instanceof Response) {
5931
- const headerEntries = Object.entries(headers);
5932
- for (let i = 0; i < headerEntries.length; i++) {
5933
- const [key, value] = headerEntries[i];
5934
- 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]);
5935
7332
  }
5936
7333
  }
5937
7334
  return response;