shokupan 0.10.5 → 0.11.0

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