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