shokupan 0.10.5 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +2339 -984
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +2336 -982
- package/dist/index.js.map +1 -1
- package/dist/plugins/application/api-explorer/static/explorer-client.mjs +375 -29
- package/dist/plugins/application/api-explorer/static/style.css +327 -8
- 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,90 +1145,43 @@ function analyzeHandler(handler) {
|
|
|
1040
1145
|
}
|
|
1041
1146
|
return { inferredSpec };
|
|
1042
1147
|
}
|
|
1043
|
-
async function getAstRoutes$1(applications) {
|
|
1044
|
-
const astRoutes = [];
|
|
1045
|
-
const getExpandedRoutes = (app, prefix = "", seen = /* @__PURE__ */ new Set(), sourceOverride) => {
|
|
1046
|
-
if (seen.has(app.name)) return [];
|
|
1047
|
-
const newSeen = new Set(seen);
|
|
1048
|
-
newSeen.add(app.name);
|
|
1049
|
-
const expanded = [];
|
|
1050
|
-
let currentPrefix = prefix;
|
|
1051
|
-
if (app.controllerPrefix) {
|
|
1052
|
-
const cleanPrefix = currentPrefix.endsWith("/") ? currentPrefix.slice(0, -1) : currentPrefix;
|
|
1053
|
-
const cleanCont = app.controllerPrefix.startsWith("/") ? app.controllerPrefix : "/" + app.controllerPrefix;
|
|
1054
|
-
currentPrefix = cleanPrefix + cleanCont;
|
|
1055
|
-
}
|
|
1056
|
-
for (const route of app.routes) {
|
|
1057
|
-
const cleanPrefix = currentPrefix.endsWith("/") ? currentPrefix.slice(0, -1) : currentPrefix;
|
|
1058
|
-
const cleanPath = route.path.startsWith("/") ? route.path : "/" + route.path;
|
|
1059
|
-
let joined = cleanPrefix + cleanPath;
|
|
1060
|
-
if (joined.length > 1 && joined.endsWith("/")) {
|
|
1061
|
-
joined = joined.slice(0, -1);
|
|
1062
|
-
}
|
|
1063
|
-
const expandedRoute = {
|
|
1064
|
-
...route,
|
|
1065
|
-
path: joined || "/"
|
|
1066
|
-
};
|
|
1067
|
-
if (sourceOverride) {
|
|
1068
|
-
expandedRoute.sourceContext = sourceOverride;
|
|
1069
|
-
}
|
|
1070
|
-
expanded.push(expandedRoute);
|
|
1071
|
-
}
|
|
1072
|
-
if (app.mounted) {
|
|
1073
|
-
for (const mount of app.mounted) {
|
|
1074
|
-
const targetApp = applications.find((a) => a.name === mount.target || a.className === mount.target);
|
|
1075
|
-
if (targetApp) {
|
|
1076
|
-
const cleanPrefix = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
|
|
1077
|
-
const mountPrefix = mount.prefix.startsWith("/") ? mount.prefix : "/" + mount.prefix;
|
|
1078
|
-
let nextSourceOverride = sourceOverride;
|
|
1079
|
-
if (mount.dependency || mount.targetFilePath && mount.targetFilePath.includes("node_modules")) {
|
|
1080
|
-
if (mount.sourceContext) {
|
|
1081
|
-
nextSourceOverride = {
|
|
1082
|
-
...mount.sourceContext,
|
|
1083
|
-
// Add highlight for the mount line to make it clear
|
|
1084
|
-
highlightLines: [mount.sourceContext.startLine, mount.sourceContext.endLine],
|
|
1085
|
-
highlights: [{
|
|
1086
|
-
startLine: mount.sourceContext.startLine,
|
|
1087
|
-
endLine: mount.sourceContext.endLine,
|
|
1088
|
-
type: "return-success"
|
|
1089
|
-
// Use the success color (cyan) for the mount point
|
|
1090
|
-
}]
|
|
1091
|
-
};
|
|
1092
|
-
}
|
|
1093
|
-
}
|
|
1094
|
-
expanded.push(...getExpandedRoutes(targetApp, cleanPrefix + mountPrefix, newSeen, nextSourceOverride));
|
|
1095
|
-
}
|
|
1096
|
-
}
|
|
1097
|
-
}
|
|
1098
|
-
return expanded;
|
|
1099
|
-
};
|
|
1100
|
-
applications.forEach((app) => {
|
|
1101
|
-
astRoutes.push(...getExpandedRoutes(app));
|
|
1102
|
-
});
|
|
1103
|
-
const dedupedRoutes = /* @__PURE__ */ new Map();
|
|
1104
|
-
for (const route of astRoutes) {
|
|
1105
|
-
const key = `${route.method.toUpperCase()}:${route.path}`;
|
|
1106
|
-
let score = 0;
|
|
1107
|
-
if (route.responseSchema) score += 10;
|
|
1108
|
-
if (route.handlerSource) score += 5;
|
|
1109
|
-
if (!dedupedRoutes.has(key) || score > dedupedRoutes.get(key).score) {
|
|
1110
|
-
dedupedRoutes.set(key, { route, score });
|
|
1111
|
-
}
|
|
1112
|
-
}
|
|
1113
|
-
return Array.from(dedupedRoutes.values()).map((v) => v.route);
|
|
1114
|
-
}
|
|
1115
1148
|
async function generateOpenApi(rootRouter, options = {}) {
|
|
1116
1149
|
const paths = {};
|
|
1117
1150
|
const tagGroups = /* @__PURE__ */ new Map();
|
|
1118
1151
|
const defaultTagGroup = options.defaultTagGroup || "General";
|
|
1119
1152
|
const defaultTagName = options.defaultTag || "Application";
|
|
1120
1153
|
let astRoutes = [];
|
|
1154
|
+
let astMiddlewareRegistry = {};
|
|
1155
|
+
let applications = [];
|
|
1121
1156
|
try {
|
|
1122
|
-
const { OpenAPIAnalyzer } = await Promise.resolve().then(() => require("./analyzer-
|
|
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
1186
|
const collect = (router, prefix = "", currentGroup = defaultTagGroup, defaultTag = defaultTagName, inheritedMiddleware = [], isRootLevel = true) => {
|
|
1129
1187
|
let group = currentGroup;
|
|
@@ -1133,7 +1191,8 @@ async function generateOpenApi(rootRouter, options = {}) {
|
|
|
1133
1191
|
tag = router.config.name;
|
|
1134
1192
|
} else {
|
|
1135
1193
|
const mountPath = router[$mountPath];
|
|
1136
|
-
|
|
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,
|
|
@@ -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(":")) {
|
|
@@ -1749,19 +2246,9 @@ class RouterTrie {
|
|
|
1749
2246
|
if (path2 === "/" || path2 === "") return [];
|
|
1750
2247
|
const s = path2.startsWith("/") ? path2.slice(1) : path2;
|
|
1751
2248
|
if (s === "") return [];
|
|
1752
|
-
return s.split("/");
|
|
1753
|
-
}
|
|
1754
|
-
}
|
|
1755
|
-
const HTTPMethods = ["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS", "ALL"];
|
|
1756
|
-
var RouteParamType = /* @__PURE__ */ ((RouteParamType2) => {
|
|
1757
|
-
RouteParamType2["BODY"] = "BODY";
|
|
1758
|
-
RouteParamType2["PARAM"] = "PARAM";
|
|
1759
|
-
RouteParamType2["QUERY"] = "QUERY";
|
|
1760
|
-
RouteParamType2["HEADER"] = "HEADER";
|
|
1761
|
-
RouteParamType2["REQUEST"] = "REQUEST";
|
|
1762
|
-
RouteParamType2["CONTEXT"] = "CONTEXT";
|
|
1763
|
-
return RouteParamType2;
|
|
1764
|
-
})(RouteParamType || {});
|
|
2249
|
+
return s.split("/");
|
|
2250
|
+
}
|
|
2251
|
+
}
|
|
1765
2252
|
const RouterRegistry = /* @__PURE__ */ new Map();
|
|
1766
2253
|
const ShokupanApplicationTree = {};
|
|
1767
2254
|
class ShokupanRouter {
|
|
@@ -1770,6 +2257,9 @@ class ShokupanRouter {
|
|
|
1770
2257
|
if (config?.requestTimeout) {
|
|
1771
2258
|
this.requestTimeout = config.requestTimeout;
|
|
1772
2259
|
}
|
|
2260
|
+
if (config?.hooks) {
|
|
2261
|
+
this.ensureHooksInitialized();
|
|
2262
|
+
}
|
|
1773
2263
|
}
|
|
1774
2264
|
// Internal marker to identify Router vs. Application
|
|
1775
2265
|
[$isApplication] = false;
|
|
@@ -1781,6 +2271,47 @@ class ShokupanRouter {
|
|
|
1781
2271
|
[$parent] = null;
|
|
1782
2272
|
[$childRouters] = [];
|
|
1783
2273
|
[$childControllers] = [];
|
|
2274
|
+
_hasOnResponseEndHook;
|
|
2275
|
+
_hasOnRequestStartHook;
|
|
2276
|
+
_hasOnRequestEndHook;
|
|
2277
|
+
_hasOnResponseStartHook;
|
|
2278
|
+
_hasOnErrorHook;
|
|
2279
|
+
_hasOnRequestTimeoutHook;
|
|
2280
|
+
_hasOnReadTimeoutHook;
|
|
2281
|
+
_hasOnWriteTimeoutHook;
|
|
2282
|
+
_hasBeforeValidateHook;
|
|
2283
|
+
_hasAfterValidateHook;
|
|
2284
|
+
get hasOnResponseEndHook() {
|
|
2285
|
+
return this._hasOnResponseEndHook;
|
|
2286
|
+
}
|
|
2287
|
+
get hasOnRequestStartHook() {
|
|
2288
|
+
return this._hasOnRequestStartHook;
|
|
2289
|
+
}
|
|
2290
|
+
get hasOnRequestEndHook() {
|
|
2291
|
+
return this._hasOnRequestEndHook;
|
|
2292
|
+
}
|
|
2293
|
+
get hasOnResponseStartHook() {
|
|
2294
|
+
return this._hasOnResponseStartHook;
|
|
2295
|
+
}
|
|
2296
|
+
get hasOnErrorHook() {
|
|
2297
|
+
return this._hasOnErrorHook;
|
|
2298
|
+
}
|
|
2299
|
+
get hasOnRequestTimeoutHook() {
|
|
2300
|
+
return this._hasOnRequestTimeoutHook;
|
|
2301
|
+
}
|
|
2302
|
+
get hasOnReadTimeoutHook() {
|
|
2303
|
+
return this._hasOnReadTimeoutHook;
|
|
2304
|
+
}
|
|
2305
|
+
get hasOnWriteTimeoutHook() {
|
|
2306
|
+
return this._hasOnWriteTimeoutHook;
|
|
2307
|
+
}
|
|
2308
|
+
get hasBeforeValidateHook() {
|
|
2309
|
+
return this._hasBeforeValidateHook;
|
|
2310
|
+
}
|
|
2311
|
+
get hasAfterValidateHook() {
|
|
2312
|
+
return this._hasAfterValidateHook;
|
|
2313
|
+
}
|
|
2314
|
+
requestTimeout;
|
|
1784
2315
|
get db() {
|
|
1785
2316
|
return this.root?.db;
|
|
1786
2317
|
}
|
|
@@ -1816,7 +2347,7 @@ class ShokupanRouter {
|
|
|
1816
2347
|
const r = this[$routes][i];
|
|
1817
2348
|
const entry = {
|
|
1818
2349
|
type: "route",
|
|
1819
|
-
path: r.path,
|
|
2350
|
+
path: r.path.startsWith("/") ? r.path : "/" + r.path,
|
|
1820
2351
|
method: r.method,
|
|
1821
2352
|
metadata: r.metadata,
|
|
1822
2353
|
handlerName: r.handler.name,
|
|
@@ -1843,7 +2374,7 @@ class ShokupanRouter {
|
|
|
1843
2374
|
})) : [];
|
|
1844
2375
|
const routers = this[$childRouters].map((r) => ({
|
|
1845
2376
|
type: "router",
|
|
1846
|
-
path: r[$mountPath],
|
|
2377
|
+
path: r[$mountPath].startsWith("/") ? r[$mountPath] : "/" + r[$mountPath],
|
|
1847
2378
|
metadata: r.metadata,
|
|
1848
2379
|
children: r.getComponentRegistry()
|
|
1849
2380
|
}));
|
|
@@ -1857,12 +2388,25 @@ class ShokupanRouter {
|
|
|
1857
2388
|
children: { routes }
|
|
1858
2389
|
};
|
|
1859
2390
|
});
|
|
2391
|
+
const events2 = [];
|
|
2392
|
+
this.eventHandlers.forEach((handlers, name) => {
|
|
2393
|
+
handlers.forEach((h) => {
|
|
2394
|
+
events2.push({
|
|
2395
|
+
type: "event",
|
|
2396
|
+
name,
|
|
2397
|
+
handlerName: h.name,
|
|
2398
|
+
metadata: h.source ? { file: h.source.file, line: h.source.line } : void 0,
|
|
2399
|
+
_fn: h
|
|
2400
|
+
});
|
|
2401
|
+
});
|
|
2402
|
+
});
|
|
1860
2403
|
return {
|
|
1861
2404
|
metadata: this.metadata,
|
|
1862
2405
|
middleware,
|
|
1863
2406
|
routes: localRoutes,
|
|
1864
2407
|
routers,
|
|
1865
|
-
controllers
|
|
2408
|
+
controllers,
|
|
2409
|
+
events: events2
|
|
1866
2410
|
};
|
|
1867
2411
|
}
|
|
1868
2412
|
isRouterInstance(target) {
|
|
@@ -1885,12 +2429,38 @@ class ShokupanRouter {
|
|
|
1885
2429
|
}
|
|
1886
2430
|
return this;
|
|
1887
2431
|
}
|
|
2432
|
+
/**
|
|
2433
|
+
* Registers a lifecycle hook dynamically.
|
|
2434
|
+
*/
|
|
2435
|
+
hook(name, handler) {
|
|
2436
|
+
if (!this.hooksInitialized) {
|
|
2437
|
+
this.ensureHooksInitialized();
|
|
2438
|
+
}
|
|
2439
|
+
let handlers = this.hookCache.get(name);
|
|
2440
|
+
if (!handlers) {
|
|
2441
|
+
handlers = [];
|
|
2442
|
+
this.hookCache.set(name, handlers);
|
|
2443
|
+
this._hasOnErrorHook ||= name === "onError";
|
|
2444
|
+
this._hasOnRequestStartHook ||= name === "onRequestStart";
|
|
2445
|
+
this._hasOnRequestEndHook ||= name === "onRequestEnd";
|
|
2446
|
+
this._hasOnResponseStartHook ||= name === "onResponseStart";
|
|
2447
|
+
this._hasOnResponseEndHook ||= name === "onResponseEnd";
|
|
2448
|
+
this._hasOnRequestTimeoutHook ||= name === "onRequestTimeout";
|
|
2449
|
+
this._hasOnReadTimeoutHook ||= name === "onReadTimeout";
|
|
2450
|
+
this._hasOnWriteTimeoutHook ||= name === "onWriteTimeout";
|
|
2451
|
+
this._hasBeforeValidateHook ||= name === "beforeValidate";
|
|
2452
|
+
this._hasAfterValidateHook ||= name === "afterValidate";
|
|
2453
|
+
}
|
|
2454
|
+
handlers.push(handler);
|
|
2455
|
+
return this;
|
|
2456
|
+
}
|
|
1888
2457
|
/**
|
|
1889
2458
|
* Finds an event handler(s) by name.
|
|
1890
2459
|
*/
|
|
1891
2460
|
findEvent(name) {
|
|
1892
|
-
|
|
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
|
}
|
|
@@ -2078,264 +2654,23 @@ class ShokupanRouter {
|
|
|
2078
2654
|
throw new Error("Router is already mounted");
|
|
2079
2655
|
}
|
|
2080
2656
|
router[$mountPath] = prefix;
|
|
2081
|
-
if (!router.metadata) {
|
|
2082
|
-
const info = getCallerInfo();
|
|
2083
|
-
router.metadata = {
|
|
2084
|
-
file: info.file,
|
|
2085
|
-
line: info.line,
|
|
2086
|
-
name: "MountedRouter"
|
|
2087
|
-
};
|
|
2088
|
-
}
|
|
2089
|
-
this[$childRouters].push(router);
|
|
2090
|
-
router[$parent] = this;
|
|
2091
|
-
const setRouterContext = (router2) => {
|
|
2092
|
-
router2[$appRoot] = this.root;
|
|
2093
|
-
router2[$childRouters].forEach((child) => setRouterContext(child));
|
|
2094
|
-
};
|
|
2095
|
-
setRouterContext(router);
|
|
2096
|
-
router[$appRoot] = this.root;
|
|
2097
|
-
router[$isMounted] = true;
|
|
2098
|
-
}
|
|
2099
|
-
scanControllerRoutes(prefix, controller) {
|
|
2100
|
-
let instance = controller;
|
|
2101
|
-
if (typeof controller === "function") {
|
|
2102
|
-
instance = Container.resolve(controller);
|
|
2103
|
-
const controllerPath = controller[$controllerPath];
|
|
2104
|
-
if (controllerPath) {
|
|
2105
|
-
const p1 = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
|
|
2106
|
-
const p2 = controllerPath.startsWith("/") ? controllerPath : "/" + controllerPath;
|
|
2107
|
-
prefix = p1 + p2;
|
|
2108
|
-
if (!prefix) prefix = "/";
|
|
2109
|
-
}
|
|
2110
|
-
} else {
|
|
2111
|
-
const ctor = instance.constructor;
|
|
2112
|
-
const controllerPath = ctor[$controllerPath];
|
|
2113
|
-
if (controllerPath) {
|
|
2114
|
-
const p1 = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
|
|
2115
|
-
const p2 = controllerPath.startsWith("/") ? controllerPath : "/" + controllerPath;
|
|
2116
|
-
prefix = p1 + p2;
|
|
2117
|
-
if (!prefix) prefix = "/";
|
|
2118
|
-
}
|
|
2119
|
-
}
|
|
2120
|
-
instance[$mountPath] = prefix;
|
|
2121
|
-
const info = getCallerInfo();
|
|
2122
|
-
instance.metadata = {
|
|
2123
|
-
file: info.file,
|
|
2124
|
-
line: info.line,
|
|
2125
|
-
name: instance.constructor.name
|
|
2126
|
-
};
|
|
2127
|
-
this[$childControllers].push(instance);
|
|
2128
|
-
const controllerMiddleware = (typeof controller === "function" ? controller[$middleware] : instance[$middleware]) || [];
|
|
2129
|
-
const proto = Object.getPrototypeOf(instance);
|
|
2130
|
-
const methods = /* @__PURE__ */ new Set();
|
|
2131
|
-
let current = proto;
|
|
2132
|
-
while (current && current !== Object.prototype) {
|
|
2133
|
-
Object.getOwnPropertyNames(current).forEach((name) => methods.add(name));
|
|
2134
|
-
current = Object.getPrototypeOf(current);
|
|
2135
|
-
}
|
|
2136
|
-
Object.getOwnPropertyNames(instance).forEach((name) => methods.add(name));
|
|
2137
|
-
const decoratedRoutes = instance[$routeMethods] || proto && proto[$routeMethods];
|
|
2138
|
-
const decoratedArgs = instance[$routeArgs] || proto && proto[$routeArgs];
|
|
2139
|
-
const methodMiddlewareMap = instance[$middleware] || proto && proto[$middleware];
|
|
2140
|
-
const decoratedEvents = instance[$eventMethods] || proto && proto[$eventMethods];
|
|
2141
|
-
let routesAttached = 0;
|
|
2142
|
-
for (let i = 0; i < Array.from(methods).length; i++) {
|
|
2143
|
-
const name = Array.from(methods)[i];
|
|
2144
|
-
if (name === "constructor") continue;
|
|
2145
|
-
if (["arguments", "caller", "callee"].includes(name)) continue;
|
|
2146
|
-
const originalHandler = instance[name];
|
|
2147
|
-
if (typeof originalHandler !== "function") continue;
|
|
2148
|
-
let method;
|
|
2149
|
-
let subPath = "";
|
|
2150
|
-
let methodSource;
|
|
2151
|
-
if (decoratedRoutes && decoratedRoutes.has(name)) {
|
|
2152
|
-
const config = decoratedRoutes.get(name);
|
|
2153
|
-
method = config.method;
|
|
2154
|
-
subPath = config.path;
|
|
2155
|
-
methodSource = config.source;
|
|
2156
|
-
} else {
|
|
2157
|
-
for (let j = 0; j < HTTPMethods.length; j++) {
|
|
2158
|
-
const m = HTTPMethods[j];
|
|
2159
|
-
if (name.toUpperCase().startsWith(m)) {
|
|
2160
|
-
method = m;
|
|
2161
|
-
const rest = name.slice(m.length);
|
|
2162
|
-
if (rest.length === 0) {
|
|
2163
|
-
subPath = "/";
|
|
2164
|
-
} else {
|
|
2165
|
-
subPath = "";
|
|
2166
|
-
let buffer = "";
|
|
2167
|
-
const flush = () => {
|
|
2168
|
-
if (buffer.length > 0) {
|
|
2169
|
-
subPath += "/" + buffer.toLowerCase();
|
|
2170
|
-
buffer = "";
|
|
2171
|
-
}
|
|
2172
|
-
};
|
|
2173
|
-
for (let i2 = 0; i2 < rest.length; i2++) {
|
|
2174
|
-
const char = rest[i2];
|
|
2175
|
-
if (char === "$") {
|
|
2176
|
-
flush();
|
|
2177
|
-
subPath += "/:";
|
|
2178
|
-
continue;
|
|
2179
|
-
}
|
|
2180
|
-
buffer += char;
|
|
2181
|
-
}
|
|
2182
|
-
if (buffer.length > 0) flush();
|
|
2183
|
-
subPath = rest.replace(/\$/g, "/:").replace(/([a-z0-9])([A-Z])/g, "$1/$2").toLowerCase();
|
|
2184
|
-
if (!subPath.startsWith("/")) {
|
|
2185
|
-
subPath = "/" + subPath;
|
|
2186
|
-
}
|
|
2187
|
-
}
|
|
2188
|
-
break;
|
|
2189
|
-
}
|
|
2190
|
-
}
|
|
2191
|
-
}
|
|
2192
|
-
if (method) {
|
|
2193
|
-
routesAttached++;
|
|
2194
|
-
const cleanPrefix = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
|
|
2195
|
-
const cleanSubPath = subPath === "/" ? "" : subPath;
|
|
2196
|
-
let joined;
|
|
2197
|
-
if (cleanSubPath.length === 0) {
|
|
2198
|
-
joined = cleanPrefix;
|
|
2199
|
-
} else if (cleanSubPath.startsWith("/")) {
|
|
2200
|
-
joined = cleanPrefix + cleanSubPath;
|
|
2201
|
-
} else {
|
|
2202
|
-
joined = cleanPrefix + "/" + cleanSubPath;
|
|
2203
|
-
}
|
|
2204
|
-
const fullPath = joined || "/";
|
|
2205
|
-
const normalizedPath = fullPath.replace(/\/+/g, "/");
|
|
2206
|
-
const methodMw = methodMiddlewareMap instanceof Map ? methodMiddlewareMap.get(name) || [] : [];
|
|
2207
|
-
const allMiddleware = [...controllerMiddleware, ...methodMw];
|
|
2208
|
-
const routeArgs = decoratedArgs && decoratedArgs.get(name);
|
|
2209
|
-
const wrappedHandler = async (ctx) => {
|
|
2210
|
-
let args = [ctx];
|
|
2211
|
-
if (routeArgs?.length > 0) {
|
|
2212
|
-
args = [];
|
|
2213
|
-
const sortedArgs = [...routeArgs].sort((a, b) => a.index - b.index);
|
|
2214
|
-
for (let k = 0; k < sortedArgs.length; k++) {
|
|
2215
|
-
const arg = sortedArgs[k];
|
|
2216
|
-
switch (arg.type) {
|
|
2217
|
-
case RouteParamType.BODY:
|
|
2218
|
-
try {
|
|
2219
|
-
if (ctx.req.headers.get("content-type")?.includes("application/json")) {
|
|
2220
|
-
args[arg.index] = await ctx.req.json();
|
|
2221
|
-
} else {
|
|
2222
|
-
const text = await ctx.req.text();
|
|
2223
|
-
if (!text) {
|
|
2224
|
-
args[arg.index] = {};
|
|
2225
|
-
} else {
|
|
2226
|
-
args[arg.index] = JSON.parse(text);
|
|
2227
|
-
}
|
|
2228
|
-
}
|
|
2229
|
-
} catch (e) {
|
|
2230
|
-
const err = new Error("Invalid JSON body");
|
|
2231
|
-
err.status = 400;
|
|
2232
|
-
throw err;
|
|
2233
|
-
}
|
|
2234
|
-
break;
|
|
2235
|
-
case RouteParamType.PARAM:
|
|
2236
|
-
args[arg.index] = arg.name ? ctx.params[arg.name] : ctx.params;
|
|
2237
|
-
break;
|
|
2238
|
-
case RouteParamType.QUERY: {
|
|
2239
|
-
const url = new URL(ctx.req.url);
|
|
2240
|
-
if (arg.name) {
|
|
2241
|
-
const vals = url.searchParams.getAll(arg.name);
|
|
2242
|
-
args[arg.index] = vals.length > 1 ? vals : vals[0];
|
|
2243
|
-
} else {
|
|
2244
|
-
const query = {};
|
|
2245
|
-
const keys = Object.keys(url.searchParams);
|
|
2246
|
-
for (let k2 = 0; k2 < keys.length; k2++) {
|
|
2247
|
-
const key = keys[k2];
|
|
2248
|
-
const vals = url.searchParams.getAll(key);
|
|
2249
|
-
query[key] = vals.length > 1 ? vals : vals[0];
|
|
2250
|
-
}
|
|
2251
|
-
args[arg.index] = query;
|
|
2252
|
-
}
|
|
2253
|
-
break;
|
|
2254
|
-
}
|
|
2255
|
-
case RouteParamType.HEADER:
|
|
2256
|
-
args[arg.index] = arg.name ? ctx.req.headers.get(arg.name) : ctx.req.headers;
|
|
2257
|
-
break;
|
|
2258
|
-
case RouteParamType.REQUEST:
|
|
2259
|
-
args[arg.index] = ctx.req;
|
|
2260
|
-
break;
|
|
2261
|
-
case RouteParamType.CONTEXT:
|
|
2262
|
-
args[arg.index] = ctx;
|
|
2263
|
-
break;
|
|
2264
|
-
}
|
|
2265
|
-
}
|
|
2266
|
-
}
|
|
2267
|
-
const tracedOriginalHandler = ctx.app?.applicationConfig.enableTracing ? traceHandler(originalHandler, normalizedPath) : originalHandler;
|
|
2268
|
-
return tracedOriginalHandler.apply(instance, args);
|
|
2269
|
-
};
|
|
2270
|
-
let finalHandler = wrappedHandler;
|
|
2271
|
-
if (allMiddleware.length > 0) {
|
|
2272
|
-
const composed = compose(allMiddleware);
|
|
2273
|
-
finalHandler = async (ctx) => {
|
|
2274
|
-
return composed(ctx, () => wrappedHandler(ctx));
|
|
2275
|
-
};
|
|
2276
|
-
}
|
|
2277
|
-
finalHandler.originalHandler = originalHandler;
|
|
2278
|
-
if (finalHandler !== wrappedHandler) {
|
|
2279
|
-
wrappedHandler.originalHandler = originalHandler;
|
|
2280
|
-
}
|
|
2281
|
-
const tagName = instance.constructor.name;
|
|
2282
|
-
const decoratedSpecs = instance[$routeSpec] || proto && proto[$routeSpec];
|
|
2283
|
-
const userSpec = decoratedSpecs && decoratedSpecs.get(name);
|
|
2284
|
-
const spec = { tags: [tagName], ...userSpec };
|
|
2285
|
-
this.add({
|
|
2286
|
-
method,
|
|
2287
|
-
path: normalizedPath,
|
|
2288
|
-
handler: finalHandler,
|
|
2289
|
-
spec,
|
|
2290
|
-
controller: instance,
|
|
2291
|
-
metadata: methodSource || instance.metadata,
|
|
2292
|
-
middleware: allMiddleware
|
|
2293
|
-
// Capture all resolved middleware
|
|
2294
|
-
});
|
|
2295
|
-
}
|
|
2296
|
-
if (decoratedEvents?.has(name)) {
|
|
2297
|
-
routesAttached++;
|
|
2298
|
-
const config = decoratedEvents.get(name);
|
|
2299
|
-
const routeArgs = decoratedArgs?.get(name);
|
|
2300
|
-
const wrappedHandler = async (ctx) => {
|
|
2301
|
-
let args = [ctx];
|
|
2302
|
-
if (routeArgs?.length > 0) {
|
|
2303
|
-
args = [];
|
|
2304
|
-
const sortedArgs = [...routeArgs].sort((a, b) => a.index - b.index);
|
|
2305
|
-
for (let k = 0; k < sortedArgs.length; k++) {
|
|
2306
|
-
const arg = sortedArgs[k];
|
|
2307
|
-
switch (arg.type) {
|
|
2308
|
-
case RouteParamType.BODY:
|
|
2309
|
-
args[arg.index] = await ctx.body();
|
|
2310
|
-
break;
|
|
2311
|
-
case RouteParamType.CONTEXT:
|
|
2312
|
-
args[arg.index] = ctx;
|
|
2313
|
-
break;
|
|
2314
|
-
case RouteParamType.REQUEST:
|
|
2315
|
-
args[arg.index] = ctx.req;
|
|
2316
|
-
break;
|
|
2317
|
-
case RouteParamType.HEADER:
|
|
2318
|
-
args[arg.index] = arg.name ? ctx.req.headers.get(arg.name) : ctx.req.headers;
|
|
2319
|
-
break;
|
|
2320
|
-
default:
|
|
2321
|
-
args[arg.index] = void 0;
|
|
2322
|
-
}
|
|
2323
|
-
}
|
|
2324
|
-
}
|
|
2325
|
-
return originalHandler.apply(instance, args);
|
|
2326
|
-
};
|
|
2327
|
-
const decoratedSpecs = instance[$routeSpec] || proto && proto[$routeSpec];
|
|
2328
|
-
const userSpec = decoratedSpecs && decoratedSpecs.get(name);
|
|
2329
|
-
const spec = { tags: [{ name: instance.constructor.name }], ...userSpec };
|
|
2330
|
-
wrappedHandler.spec = spec;
|
|
2331
|
-
wrappedHandler.originalHandler = originalHandler;
|
|
2332
|
-
this.event(config.eventName, wrappedHandler);
|
|
2333
|
-
}
|
|
2334
|
-
}
|
|
2335
|
-
if (routesAttached === 0) {
|
|
2336
|
-
console.warn(`No routes attached to controller ${instance.constructor.name}`);
|
|
2657
|
+
if (!router.metadata) {
|
|
2658
|
+
const info = getCallerInfo();
|
|
2659
|
+
router.metadata = {
|
|
2660
|
+
file: info.file,
|
|
2661
|
+
line: info.line,
|
|
2662
|
+
name: "MountedRouter"
|
|
2663
|
+
};
|
|
2337
2664
|
}
|
|
2338
|
-
|
|
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
|
}
|
|
@@ -2648,113 +2921,376 @@ class ShokupanRouter {
|
|
|
2648
2921
|
description: "Serves static files from " + normalizedPrefix,
|
|
2649
2922
|
tags: [groupName]
|
|
2650
2923
|
};
|
|
2651
|
-
const spec = config.openapi ? config.openapi : defaultSpec;
|
|
2652
|
-
if (!spec.tags) spec.tags = [groupName];
|
|
2653
|
-
else if (!spec.tags.includes(groupName)) spec.tags.push(groupName);
|
|
2654
|
-
const pattern = `^${normalizedPrefix}(/.*)?$`;
|
|
2655
|
-
const regex = new RegExp(pattern);
|
|
2656
|
-
const displayPath = normalizedPrefix === "/" ? "/*" : normalizedPrefix + "/*";
|
|
2657
|
-
this.add({ method: "GET", path: displayPath, handler: routeHandler, spec, regex });
|
|
2658
|
-
this.add({ method: "HEAD", path: displayPath, handler: routeHandler, spec, regex });
|
|
2659
|
-
return this;
|
|
2924
|
+
const spec = config.openapi ? config.openapi : defaultSpec;
|
|
2925
|
+
if (!spec.tags) spec.tags = [groupName];
|
|
2926
|
+
else if (!spec.tags.includes(groupName)) spec.tags.push(groupName);
|
|
2927
|
+
const pattern = `^${normalizedPrefix}(/.*)?$`;
|
|
2928
|
+
const regex = new RegExp(pattern);
|
|
2929
|
+
const displayPath = normalizedPrefix === "/" ? "/*" : normalizedPrefix + "/*";
|
|
2930
|
+
this.add({ method: "GET", path: displayPath, handler: routeHandler, spec, regex });
|
|
2931
|
+
this.add({ method: "HEAD", path: displayPath, handler: routeHandler, spec, regex });
|
|
2932
|
+
return this;
|
|
2933
|
+
}
|
|
2934
|
+
/**
|
|
2935
|
+
* Attach the verb routes with their overload signatures.
|
|
2936
|
+
* Use compose to handle multiple handlers (middleware).
|
|
2937
|
+
*/
|
|
2938
|
+
attachVerb(method, path2, ...args) {
|
|
2939
|
+
let spec;
|
|
2940
|
+
let handlers = [];
|
|
2941
|
+
if (args.length > 0) {
|
|
2942
|
+
if (typeof args[0] === "object" && args[0] !== null) {
|
|
2943
|
+
spec = args[0];
|
|
2944
|
+
handlers = args.slice(1);
|
|
2945
|
+
} else {
|
|
2946
|
+
handlers = args;
|
|
2947
|
+
}
|
|
2948
|
+
}
|
|
2949
|
+
if (handlers.length === 0) {
|
|
2950
|
+
return;
|
|
2951
|
+
}
|
|
2952
|
+
let finalHandler = handlers[handlers.length - 1];
|
|
2953
|
+
if (handlers.length > 1) {
|
|
2954
|
+
const fn = compose(handlers);
|
|
2955
|
+
finalHandler = (ctx) => fn(ctx);
|
|
2956
|
+
}
|
|
2957
|
+
this.add({
|
|
2958
|
+
method,
|
|
2959
|
+
path: path2,
|
|
2960
|
+
spec,
|
|
2961
|
+
handler: finalHandler,
|
|
2962
|
+
middleware: handlers.slice(0, handlers.length - 1)
|
|
2963
|
+
});
|
|
2964
|
+
}
|
|
2965
|
+
/**
|
|
2966
|
+
* Generates an OpenAPI 3.1 Document by recursing through the router and its descendants.
|
|
2967
|
+
* Now includes runtime analysis of handler functions to infer request/response types.
|
|
2968
|
+
*/
|
|
2969
|
+
generateApiSpec(options = {}) {
|
|
2970
|
+
return generateOpenApi(this, options);
|
|
2971
|
+
}
|
|
2972
|
+
hasHooks(name) {
|
|
2973
|
+
if (!this.hooksInitialized) {
|
|
2974
|
+
this.ensureHooksInitialized();
|
|
2975
|
+
}
|
|
2976
|
+
const hooks = this.hookCache.get(name);
|
|
2977
|
+
return hooks !== void 0 && hooks.length > 0;
|
|
2978
|
+
}
|
|
2979
|
+
ensureHooksInitialized() {
|
|
2980
|
+
const hooks = this.config?.hooks;
|
|
2981
|
+
if (hooks) {
|
|
2982
|
+
const hookList = Array.isArray(hooks) ? hooks : [hooks];
|
|
2983
|
+
const hookTypes = [
|
|
2984
|
+
"onRequestStart",
|
|
2985
|
+
"onRequestEnd",
|
|
2986
|
+
"onResponseStart",
|
|
2987
|
+
"onResponseEnd",
|
|
2988
|
+
"onError",
|
|
2989
|
+
"beforeValidate",
|
|
2990
|
+
"afterValidate",
|
|
2991
|
+
"onRequestTimeout",
|
|
2992
|
+
"onReadTimeout",
|
|
2993
|
+
"onWriteTimeout"
|
|
2994
|
+
];
|
|
2995
|
+
for (let i = 0; i < hookTypes.length; i++) {
|
|
2996
|
+
const type = hookTypes[i];
|
|
2997
|
+
const fns = [];
|
|
2998
|
+
for (let j = 0; j < hookList.length; j++) {
|
|
2999
|
+
const h = hookList[j];
|
|
3000
|
+
if (h[type]) fns.push(h[type]);
|
|
3001
|
+
}
|
|
3002
|
+
if (fns.length > 0) {
|
|
3003
|
+
this._hasOnErrorHook ||= type === "onError";
|
|
3004
|
+
this._hasOnRequestStartHook ||= type === "onRequestStart";
|
|
3005
|
+
this._hasOnRequestEndHook ||= type === "onRequestEnd";
|
|
3006
|
+
this._hasOnResponseStartHook ||= type === "onResponseStart";
|
|
3007
|
+
this._hasOnResponseEndHook ||= type === "onResponseEnd";
|
|
3008
|
+
this._hasOnRequestTimeoutHook ||= type === "onRequestTimeout";
|
|
3009
|
+
this._hasOnReadTimeoutHook ||= type === "onReadTimeout";
|
|
3010
|
+
this._hasOnWriteTimeoutHook ||= type === "onWriteTimeout";
|
|
3011
|
+
this._hasBeforeValidateHook ||= type === "beforeValidate";
|
|
3012
|
+
this._hasAfterValidateHook ||= type === "afterValidate";
|
|
3013
|
+
this.hookCache.set(type, fns);
|
|
3014
|
+
}
|
|
3015
|
+
}
|
|
3016
|
+
}
|
|
3017
|
+
this.hooksInitialized = true;
|
|
3018
|
+
}
|
|
3019
|
+
runHooks(name, ...args) {
|
|
3020
|
+
if (!this.hooksInitialized) {
|
|
3021
|
+
this.ensureHooksInitialized();
|
|
3022
|
+
}
|
|
3023
|
+
const fns = this.hookCache.get(name);
|
|
3024
|
+
if (!fns) return;
|
|
3025
|
+
const ctx = args?.[0] instanceof ShokupanContext ? args[0] : void 0;
|
|
3026
|
+
const debug = ctx?.[$debug];
|
|
3027
|
+
if (debug) {
|
|
3028
|
+
return Promise.all(fns.map(async (fn, index) => {
|
|
3029
|
+
const hookId = `hook_${name}_${fn.name || index}`;
|
|
3030
|
+
const previousNode = debug.getCurrentNode();
|
|
3031
|
+
debug.trackEdge(previousNode, hookId);
|
|
3032
|
+
debug.setNode(hookId);
|
|
3033
|
+
const start = performance.now();
|
|
3034
|
+
try {
|
|
3035
|
+
await fn(...args);
|
|
3036
|
+
const duration = performance.now() - start;
|
|
3037
|
+
debug.trackStep(hookId, "hook", duration, "success");
|
|
3038
|
+
} catch (error) {
|
|
3039
|
+
const duration = performance.now() - start;
|
|
3040
|
+
debug.trackStep(hookId, "hook", duration, "error", error);
|
|
3041
|
+
throw error;
|
|
3042
|
+
} finally {
|
|
3043
|
+
if (previousNode) debug.setNode(previousNode);
|
|
3044
|
+
}
|
|
3045
|
+
}));
|
|
3046
|
+
} else {
|
|
3047
|
+
return Promise.all(fns.map((fn) => fn(...args)));
|
|
3048
|
+
}
|
|
3049
|
+
}
|
|
3050
|
+
}
|
|
3051
|
+
function createHttpServer() {
|
|
3052
|
+
return async (options) => {
|
|
3053
|
+
const server = http__namespace.createServer(async (req, res) => {
|
|
3054
|
+
const url = new URL(req.url, `http://${req.headers.host}`);
|
|
3055
|
+
const request = new Request(url.toString(), {
|
|
3056
|
+
method: req.method,
|
|
3057
|
+
headers: req.headers,
|
|
3058
|
+
body: ["GET", "HEAD"].includes(req.method) ? void 0 : new ReadableStream({
|
|
3059
|
+
start(controller) {
|
|
3060
|
+
req.on("data", (chunk) => controller.enqueue(chunk));
|
|
3061
|
+
req.on("end", () => controller.close());
|
|
3062
|
+
req.on("error", (err) => controller.error(err));
|
|
3063
|
+
}
|
|
3064
|
+
}),
|
|
3065
|
+
// Required for Node.js undici when sending a body
|
|
3066
|
+
duplex: "half"
|
|
3067
|
+
});
|
|
3068
|
+
const response = await options.fetch(request, fauxServer);
|
|
3069
|
+
res.statusCode = response.status;
|
|
3070
|
+
response.headers.forEach((v, k) => res.setHeader(k, v));
|
|
3071
|
+
if (response.body) {
|
|
3072
|
+
const buffer = await response.arrayBuffer();
|
|
3073
|
+
res.end(Buffer.from(buffer));
|
|
3074
|
+
} else {
|
|
3075
|
+
res.end();
|
|
3076
|
+
}
|
|
3077
|
+
});
|
|
3078
|
+
const fauxServer = {
|
|
3079
|
+
stop: () => {
|
|
3080
|
+
server.close();
|
|
3081
|
+
return Promise.resolve();
|
|
3082
|
+
},
|
|
3083
|
+
upgrade(req, options2) {
|
|
3084
|
+
return false;
|
|
3085
|
+
},
|
|
3086
|
+
reload(options2) {
|
|
3087
|
+
return fauxServer;
|
|
3088
|
+
},
|
|
3089
|
+
get port() {
|
|
3090
|
+
const addr = server.address();
|
|
3091
|
+
if (typeof addr === "object" && addr !== null) {
|
|
3092
|
+
return addr.port;
|
|
3093
|
+
}
|
|
3094
|
+
return options.port;
|
|
3095
|
+
},
|
|
3096
|
+
hostname: options.hostname,
|
|
3097
|
+
development: options.development,
|
|
3098
|
+
pendingRequests: 0,
|
|
3099
|
+
requestIP: (req) => null,
|
|
3100
|
+
publish: () => 0,
|
|
3101
|
+
subscriberCount: () => 0,
|
|
3102
|
+
url: new URL(`http://${options.hostname}:${options.port}`),
|
|
3103
|
+
// Expose the raw Node.js server for generic socket/websocket support (e.g. Socket.IO)
|
|
3104
|
+
nodeServer: server
|
|
3105
|
+
};
|
|
3106
|
+
return new Promise((resolve) => {
|
|
3107
|
+
server.listen(options.port, options.hostname, () => {
|
|
3108
|
+
resolve(fauxServer);
|
|
3109
|
+
});
|
|
3110
|
+
});
|
|
3111
|
+
};
|
|
3112
|
+
}
|
|
3113
|
+
class BunAdapter {
|
|
3114
|
+
server;
|
|
3115
|
+
async listen(port, app) {
|
|
3116
|
+
if (typeof Bun === "undefined") {
|
|
3117
|
+
throw new Error("BunAdapter requires the Bun runtime.");
|
|
3118
|
+
}
|
|
3119
|
+
const serveOptions = {
|
|
3120
|
+
port,
|
|
3121
|
+
hostname: app.applicationConfig.hostname,
|
|
3122
|
+
development: app.applicationConfig.development,
|
|
3123
|
+
fetch: app.fetch.bind(app),
|
|
3124
|
+
reusePort: app.applicationConfig.reusePort,
|
|
3125
|
+
idleTimeout: app.applicationConfig.readTimeout ? app.applicationConfig.readTimeout / 1e3 : void 0,
|
|
3126
|
+
websocket: {
|
|
3127
|
+
// @ts-ignore
|
|
3128
|
+
open(ws) {
|
|
3129
|
+
ws.data?.handler?.open?.(ws);
|
|
3130
|
+
},
|
|
3131
|
+
// @ts-ignore
|
|
3132
|
+
async message(ws, message) {
|
|
3133
|
+
if (ws.data?.handler?.message) {
|
|
3134
|
+
return ws.data.handler.message(ws, message);
|
|
3135
|
+
}
|
|
3136
|
+
let msgString = "";
|
|
3137
|
+
if (typeof message === "string") {
|
|
3138
|
+
msgString = message;
|
|
3139
|
+
} else if (message instanceof Uint8Array || message instanceof ArrayBuffer) {
|
|
3140
|
+
msgString = new TextDecoder().decode(message);
|
|
3141
|
+
} else if (typeof Buffer !== "undefined" && message instanceof Buffer) {
|
|
3142
|
+
msgString = message.toString();
|
|
3143
|
+
} else {
|
|
3144
|
+
return;
|
|
3145
|
+
}
|
|
3146
|
+
if (typeof msgString !== "string") return;
|
|
3147
|
+
let payload;
|
|
3148
|
+
let isJSONPayload = false;
|
|
3149
|
+
if (msgString.startsWith("{")) {
|
|
3150
|
+
try {
|
|
3151
|
+
payload = JSON.parse(msgString);
|
|
3152
|
+
isJSONPayload = true;
|
|
3153
|
+
} catch {
|
|
3154
|
+
}
|
|
3155
|
+
}
|
|
3156
|
+
if (payload) {
|
|
3157
|
+
const self = app;
|
|
3158
|
+
if (isJSONPayload && self.applicationConfig["enableHttpBridge"] && payload.type === "HTTP") {
|
|
3159
|
+
const { id, method, path: path2, headers, body } = payload;
|
|
3160
|
+
const url = new URL(path2, `http://${self.applicationConfig.hostname || "localhost"}:${port}`);
|
|
3161
|
+
const req = new Request(url.toString(), {
|
|
3162
|
+
method,
|
|
3163
|
+
headers,
|
|
3164
|
+
body: typeof body === "object" ? JSON.stringify(body) : body
|
|
3165
|
+
});
|
|
3166
|
+
const res = await self.fetch(req);
|
|
3167
|
+
const resBody = await res.json().catch((err) => res.text());
|
|
3168
|
+
const resHeaders = {};
|
|
3169
|
+
res.headers.forEach((v, k) => resHeaders[k] = v);
|
|
3170
|
+
ws.send(JSON.stringify({
|
|
3171
|
+
type: "RESPONSE",
|
|
3172
|
+
id,
|
|
3173
|
+
status: res.status,
|
|
3174
|
+
headers: resHeaders,
|
|
3175
|
+
body: resBody
|
|
3176
|
+
}));
|
|
3177
|
+
return;
|
|
3178
|
+
}
|
|
3179
|
+
const eventName = payload.event || (payload.type === "EVENT" ? payload.name : void 0);
|
|
3180
|
+
if (eventName) {
|
|
3181
|
+
const handlers = self.findEvent(eventName);
|
|
3182
|
+
const handler = handlers?.length == 1 ? handlers[0] : compose(handlers || []);
|
|
3183
|
+
if (handler) {
|
|
3184
|
+
const data = payload.data || payload.body || payload.payload || payload;
|
|
3185
|
+
const req = new ShokupanRequest({
|
|
3186
|
+
url: `http://${self.applicationConfig.hostname || "localhost"}/event/${eventName}`,
|
|
3187
|
+
method: "POST",
|
|
3188
|
+
headers: new Headers({ "content-type": "application/json" }),
|
|
3189
|
+
body: JSON.stringify(data)
|
|
3190
|
+
});
|
|
3191
|
+
const ctx = new ShokupanContext(
|
|
3192
|
+
// @ts-ignore
|
|
3193
|
+
req,
|
|
3194
|
+
// @ts-ignore
|
|
3195
|
+
self.server,
|
|
3196
|
+
{},
|
|
3197
|
+
self,
|
|
3198
|
+
null,
|
|
3199
|
+
self.applicationConfig.enableMiddlewareTracking,
|
|
3200
|
+
payload.id
|
|
3201
|
+
);
|
|
3202
|
+
ctx[$ws] = ws;
|
|
3203
|
+
ws.data ??= {};
|
|
3204
|
+
ws.data["ctx"] = ctx;
|
|
3205
|
+
try {
|
|
3206
|
+
await handler(ctx);
|
|
3207
|
+
} catch (err) {
|
|
3208
|
+
if (self.applicationConfig["websocketErrorHandler"]) {
|
|
3209
|
+
await self.applicationConfig["websocketErrorHandler"](err, ctx);
|
|
3210
|
+
} else {
|
|
3211
|
+
console.error(`Error in event ${eventName}:`, err);
|
|
3212
|
+
}
|
|
3213
|
+
}
|
|
3214
|
+
}
|
|
3215
|
+
}
|
|
3216
|
+
}
|
|
3217
|
+
},
|
|
3218
|
+
// @ts-ignore
|
|
3219
|
+
drain(ws) {
|
|
3220
|
+
ws.data?.handler?.drain?.(ws);
|
|
3221
|
+
},
|
|
3222
|
+
// @ts-ignore
|
|
3223
|
+
close(ws, code, reason) {
|
|
3224
|
+
ws.data?.handler?.close?.(ws, code, reason);
|
|
3225
|
+
const ctx = ws.data?.["ctx"];
|
|
3226
|
+
if (ctx && typeof ctx.getDisconnectCallbacks === "function") {
|
|
3227
|
+
const callbacks = ctx.getDisconnectCallbacks();
|
|
3228
|
+
if (Array.isArray(callbacks) && callbacks.length > 0) {
|
|
3229
|
+
Promise.all(callbacks.map((cb) => cb())).catch((err) => {
|
|
3230
|
+
console.error("Error executing socket disconnect hook:", err);
|
|
3231
|
+
});
|
|
3232
|
+
}
|
|
3233
|
+
}
|
|
3234
|
+
}
|
|
3235
|
+
}
|
|
3236
|
+
};
|
|
3237
|
+
this.server = Bun.serve(serveOptions);
|
|
3238
|
+
return this.server;
|
|
2660
3239
|
}
|
|
2661
|
-
|
|
2662
|
-
|
|
2663
|
-
|
|
2664
|
-
*/
|
|
2665
|
-
attachVerb(method, path2, ...args) {
|
|
2666
|
-
let spec;
|
|
2667
|
-
let handlers = [];
|
|
2668
|
-
if (args.length > 0) {
|
|
2669
|
-
if (typeof args[0] === "object" && args[0] !== null) {
|
|
2670
|
-
spec = args[0];
|
|
2671
|
-
handlers = args.slice(1);
|
|
2672
|
-
} else {
|
|
2673
|
-
handlers = args;
|
|
2674
|
-
}
|
|
2675
|
-
}
|
|
2676
|
-
if (handlers.length === 0) {
|
|
2677
|
-
return;
|
|
2678
|
-
}
|
|
2679
|
-
let finalHandler = handlers[handlers.length - 1];
|
|
2680
|
-
if (handlers.length > 1) {
|
|
2681
|
-
const fn = compose(handlers);
|
|
2682
|
-
finalHandler = (ctx) => fn(ctx);
|
|
3240
|
+
async stop() {
|
|
3241
|
+
if (this.server) {
|
|
3242
|
+
this.server.stop();
|
|
2683
3243
|
}
|
|
2684
|
-
this.add({
|
|
2685
|
-
method,
|
|
2686
|
-
path: path2,
|
|
2687
|
-
spec,
|
|
2688
|
-
handler: finalHandler,
|
|
2689
|
-
middleware: handlers.slice(0, handlers.length - 1)
|
|
2690
|
-
});
|
|
2691
3244
|
}
|
|
2692
|
-
|
|
2693
|
-
|
|
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
|
};
|
|
@@ -3700,16 +4201,48 @@ function ApiExplorerApp({ spec, asyncSpec, config }) {
|
|
|
3700
4201
|
const commonPrefix = findCommonPrefix(routes);
|
|
3701
4202
|
const commonPrefixPath = "/" + commonPrefix.join("/");
|
|
3702
4203
|
const children = createSubgroups(routes, 0, commonPrefix.length);
|
|
4204
|
+
const groupMiddleware = [];
|
|
4205
|
+
if (spec["x-middleware-registry"]) {
|
|
4206
|
+
Object.entries(spec["x-middleware-registry"]).forEach(([id, mw]) => {
|
|
4207
|
+
const firstRoute = routes[0];
|
|
4208
|
+
const routeSource = firstRoute?.op?.["x-shokupan-source"];
|
|
4209
|
+
const mwFile = mw.file?.split("/").pop();
|
|
4210
|
+
const routeFile = routeSource?.file?.split("/").pop();
|
|
4211
|
+
if (mwFile && routeFile && mwFile === routeFile && mw.scope !== "global") {
|
|
4212
|
+
groupMiddleware.push({ ...mw, id, type: "middleware" });
|
|
4213
|
+
}
|
|
4214
|
+
});
|
|
4215
|
+
}
|
|
4216
|
+
const isBuiltin = routes.some((r) => r.op["x-shokupan-builtin"] === true);
|
|
3703
4217
|
return {
|
|
3704
4218
|
name,
|
|
3705
4219
|
type: "group",
|
|
3706
4220
|
children,
|
|
3707
|
-
|
|
4221
|
+
middleware: groupMiddleware,
|
|
4222
|
+
commonPrefixPath,
|
|
3708
4223
|
// Store for display stripping
|
|
4224
|
+
isBuiltin
|
|
3709
4225
|
};
|
|
3710
|
-
})
|
|
4226
|
+
});
|
|
4227
|
+
if (spec["x-middleware-registry"]) {
|
|
4228
|
+
const allGroupMiddleware = hierarchicalGroups.flatMap((g) => g.middleware || []).map((m) => m.id);
|
|
4229
|
+
const globalMiddleware = Object.entries(spec["x-middleware-registry"]).filter(([id]) => !allGroupMiddleware.includes(id)).map(([id, mw]) => ({ ...mw, id, type: "middleware" }));
|
|
4230
|
+
if (globalMiddleware.length > 0) {
|
|
4231
|
+
hierarchicalGroups.push({
|
|
4232
|
+
name: "Global Middleware",
|
|
4233
|
+
type: "group",
|
|
4234
|
+
children: [],
|
|
4235
|
+
middleware: globalMiddleware,
|
|
4236
|
+
commonPrefixPath: "",
|
|
4237
|
+
isBuiltin: false
|
|
4238
|
+
});
|
|
4239
|
+
}
|
|
4240
|
+
}
|
|
4241
|
+
hierarchicalGroups.sort((a, b) => {
|
|
3711
4242
|
if (a.name === "Ungrouped") return 1;
|
|
3712
4243
|
if (b.name === "Ungrouped") return -1;
|
|
4244
|
+
if (a.name === "Global Middleware") return 1;
|
|
4245
|
+
if (b.name === "Global Middleware") return -1;
|
|
3713
4246
|
return a.name.localeCompare(b.name);
|
|
3714
4247
|
});
|
|
3715
4248
|
const allRoutes = Array.from(hierarchy.values()).flat();
|
|
@@ -3718,6 +4251,9 @@ function ApiExplorerApp({ spec, asyncSpec, config }) {
|
|
|
3718
4251
|
/* @__PURE__ */ jsxRuntime.jsx("meta", { charSet: "UTF-8" }),
|
|
3719
4252
|
/* @__PURE__ */ jsxRuntime.jsx("meta", { name: "viewport", content: "width=device-width, initial-scale=1.0" }),
|
|
3720
4253
|
/* @__PURE__ */ jsxRuntime.jsx("title", { children: spec.info?.title || "API Explorer" }),
|
|
4254
|
+
/* @__PURE__ */ jsxRuntime.jsx("link", { rel: "preconnect", href: "https://fonts.googleapis.com" }),
|
|
4255
|
+
/* @__PURE__ */ jsxRuntime.jsx("link", { rel: "preconnect", href: "https://fonts.gstatic.com", crossorigin: "anonymous" }),
|
|
4256
|
+
/* @__PURE__ */ jsxRuntime.jsx("link", { href: "https://fonts.googleapis.com/css2?family=Google+Sans+Code:ital,wght@0,300..800;1,300..800&family=Vend+Sans:ital,wght@0,300..700;1,300..700&display=swap", rel: "stylesheet" }),
|
|
3721
4257
|
/* @__PURE__ */ jsxRuntime.jsx("link", { rel: "stylesheet", href: "style.css" }),
|
|
3722
4258
|
/* @__PURE__ */ jsxRuntime.jsx("link", { rel: "stylesheet", href: "theme.css" }),
|
|
3723
4259
|
/* @__PURE__ */ jsxRuntime.jsx("script", { src: "https://cdn.jsdelivr.net/npm/marked@4.3.0/marked.min.js" }),
|
|
@@ -3814,13 +4350,46 @@ function Sidebar$1({ spec, hierarchicalGroups }) {
|
|
|
3814
4350
|
/* @__PURE__ */ jsxRuntime.jsx("div", { class: "version", children: spec.info?.version })
|
|
3815
4351
|
] }),
|
|
3816
4352
|
/* @__PURE__ */ jsxRuntime.jsx("div", { class: "sidebar-collapse-trigger", children: "➔" }),
|
|
3817
|
-
/* @__PURE__ */ jsxRuntime.jsx("nav", { class: "nav-groups", children: hierarchicalGroups.map((group) => /* @__PURE__ */ jsxRuntime.jsxs("div", { class:
|
|
4353
|
+
/* @__PURE__ */ jsxRuntime.jsx("nav", { class: "nav-groups", children: hierarchicalGroups.map((group) => /* @__PURE__ */ jsxRuntime.jsxs("div", { class: `nav-group collapsed ${group.isBuiltin ? "builtin-group" : ""}`, children: [
|
|
3818
4354
|
/* @__PURE__ */ jsxRuntime.jsxs("div", { class: "nav-group-title", children: [
|
|
3819
4355
|
/* @__PURE__ */ jsxRuntime.jsx("span", { class: "chevron", children: /* @__PURE__ */ jsxRuntime.jsx("svg", { width: "12", height: "12", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round", children: /* @__PURE__ */ jsxRuntime.jsx("polyline", { points: "9 18 15 12 9 6" }) }) }),
|
|
3820
|
-
" ",
|
|
4356
|
+
group.isBuiltin && /* @__PURE__ */ jsxRuntime.jsx("span", { class: "builtin-icon", title: "Built-in Plugin", children: /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: "12", height: "12", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round", children: [
|
|
4357
|
+
/* @__PURE__ */ jsxRuntime.jsx("rect", { x: "2", y: "2", width: "20", height: "20", rx: "5", ry: "5" }),
|
|
4358
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M16 11.37A4 4 0 1 1 12.63 8 4 4 0 0 1 16 11.37z" }),
|
|
4359
|
+
/* @__PURE__ */ jsxRuntime.jsx("line", { x1: "17.5", y1: "6.5", x2: "17.51", y2: "6.5" })
|
|
4360
|
+
] }) }),
|
|
3821
4361
|
group.name
|
|
3822
4362
|
] }),
|
|
3823
|
-
/* @__PURE__ */ jsxRuntime.
|
|
4363
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { class: "nav-items", children: [
|
|
4364
|
+
group.middleware && group.middleware.length > 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { class: "group-middleware", children: group.middleware.map((mw) => /* @__PURE__ */ jsxRuntime.jsxs(
|
|
4365
|
+
"a",
|
|
4366
|
+
{
|
|
4367
|
+
href: `#middleware-${mw.id}`,
|
|
4368
|
+
class: "nav-item middleware-nav-item",
|
|
4369
|
+
"data-middleware-id": mw.id,
|
|
4370
|
+
title: mw.name,
|
|
4371
|
+
children: [
|
|
4372
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { class: "middleware-icon", children: "⚙" }),
|
|
4373
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { class: "nav-label", children: mw.name }),
|
|
4374
|
+
mw.usedBy && mw.usedBy.length > 0 && /* @__PURE__ */ jsxRuntime.jsx("span", { class: "middleware-badge", title: `Used by ${mw.usedBy.length} routes`, children: mw.usedBy.length }),
|
|
4375
|
+
mw.file && /* @__PURE__ */ jsxRuntime.jsx(
|
|
4376
|
+
"a",
|
|
4377
|
+
{
|
|
4378
|
+
href: `vscode://file/${mw.file}:${mw.startLine || 1}`,
|
|
4379
|
+
class: "nav-source-link",
|
|
4380
|
+
title: `${mw.file}:${mw.startLine || 1}`,
|
|
4381
|
+
children: /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: "10", height: "10", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", children: [
|
|
4382
|
+
/* @__PURE__ */ jsxRuntime.jsx("polyline", { points: "16 18 22 12 16 6" }),
|
|
4383
|
+
/* @__PURE__ */ jsxRuntime.jsx("polyline", { points: "8 6 2 12 8 18" })
|
|
4384
|
+
] })
|
|
4385
|
+
}
|
|
4386
|
+
)
|
|
4387
|
+
]
|
|
4388
|
+
},
|
|
4389
|
+
mw.id
|
|
4390
|
+
)) }),
|
|
4391
|
+
group.children?.map((child) => renderNavNode(child, 0, group.commonPrefixPath || ""))
|
|
4392
|
+
] })
|
|
3824
4393
|
] }, group.name)) })
|
|
3825
4394
|
] });
|
|
3826
4395
|
}
|
|
@@ -3828,7 +4397,8 @@ function MainContent$1({ allRoutes, config, spec }) {
|
|
|
3828
4397
|
const explorerData = JSON.stringify({
|
|
3829
4398
|
routes: allRoutes,
|
|
3830
4399
|
config,
|
|
3831
|
-
info: spec.info
|
|
4400
|
+
info: spec.info,
|
|
4401
|
+
middlewareRegistry: spec["x-middleware-registry"] || {}
|
|
3832
4402
|
});
|
|
3833
4403
|
const safeJson = explorerData.replace(/<\/script>/g, "<\\/script>");
|
|
3834
4404
|
return /* @__PURE__ */ jsxRuntime.jsxs("main", { class: "content", id: "main-content", children: [
|
|
@@ -3838,9 +4408,16 @@ function MainContent$1({ allRoutes, config, spec }) {
|
|
|
3838
4408
|
}
|
|
3839
4409
|
class ApiExplorerPlugin extends ShokupanRouter {
|
|
3840
4410
|
constructor(pluginOptions = {}) {
|
|
4411
|
+
console.log("ApiExplorerPlugin: CONSTRUCTOR CALLED");
|
|
3841
4412
|
super({ renderer: renderToString });
|
|
3842
4413
|
this.pluginOptions = pluginOptions;
|
|
3843
4414
|
pluginOptions.path ??= "/explorer";
|
|
4415
|
+
this.metadata = {
|
|
4416
|
+
file: void 0,
|
|
4417
|
+
line: 1,
|
|
4418
|
+
name: "ApiExplorerPlugin",
|
|
4419
|
+
pluginName: "ApiExplorer"
|
|
4420
|
+
};
|
|
3844
4421
|
this.init();
|
|
3845
4422
|
}
|
|
3846
4423
|
onInit(app, options) {
|
|
@@ -3871,6 +4448,7 @@ class ApiExplorerPlugin extends ShokupanRouter {
|
|
|
3871
4448
|
delete op["x-source-info"].snippet;
|
|
3872
4449
|
}
|
|
3873
4450
|
if (op["x-shokupan-source"]?.code) {
|
|
4451
|
+
console.log("Deleting x-shokupan-source.code");
|
|
3874
4452
|
delete op["x-shokupan-source"].code;
|
|
3875
4453
|
}
|
|
3876
4454
|
});
|
|
@@ -3897,7 +4475,10 @@ class ApiExplorerPlugin extends ShokupanRouter {
|
|
|
3897
4475
|
this.get("/", async (ctx) => {
|
|
3898
4476
|
const spec = this.root.openApiSpec ? structuredClone(this.root.openApiSpec) : await (this.root || this).generateApiSpec();
|
|
3899
4477
|
const asyncSpec = ctx.app.asyncApiSpec;
|
|
3900
|
-
|
|
4478
|
+
const element = ApiExplorerApp({ spec: stripSourceCode(spec), asyncSpec });
|
|
4479
|
+
const html = renderToString(element);
|
|
4480
|
+
if (html.length === 0) throw new Error("ApiExplorerPlugin: rendered page is blank.");
|
|
4481
|
+
return ctx.html(html);
|
|
3901
4482
|
});
|
|
3902
4483
|
}
|
|
3903
4484
|
}
|
|
@@ -3908,8 +4489,8 @@ function AsyncApiApp({ spec, serverUrl, base, disableSourceView, navTree }) {
|
|
|
3908
4489
|
/* @__PURE__ */ jsxRuntime.jsx("meta", { name: "viewport", content: "width=device-width, initial-scale=1.0" }),
|
|
3909
4490
|
/* @__PURE__ */ jsxRuntime.jsx("title", { children: "Shokupan AsyncAPI" }),
|
|
3910
4491
|
/* @__PURE__ */ jsxRuntime.jsx("link", { rel: "preconnect", href: "https://fonts.googleapis.com" }),
|
|
3911
|
-
/* @__PURE__ */ jsxRuntime.jsx("link", { rel: "preconnect", href: "https://fonts.gstatic.com",
|
|
3912
|
-
/* @__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" }),
|
|
3913
4494
|
/* @__PURE__ */ jsxRuntime.jsx("link", { rel: "stylesheet", href: `${base}/theme.css` }),
|
|
3914
4495
|
/* @__PURE__ */ jsxRuntime.jsx("link", { rel: "stylesheet", href: `${base}/style.css` }),
|
|
3915
4496
|
/* @__PURE__ */ jsxRuntime.jsx("script", { dangerouslySetInnerHTML: {
|
|
@@ -3917,6 +4498,7 @@ function AsyncApiApp({ spec, serverUrl, base, disableSourceView, navTree }) {
|
|
|
3917
4498
|
window.INITIAL_SPEC = ${JSON.stringify(spec)};
|
|
3918
4499
|
window.INITIAL_SERVER_URL = "${serverUrl}";
|
|
3919
4500
|
window.DISABLE_SOURCE_VIEW = ${JSON.stringify(disableSourceView)};
|
|
4501
|
+
window.BASE_PATH = "${base}";
|
|
3920
4502
|
`
|
|
3921
4503
|
} })
|
|
3922
4504
|
] }),
|
|
@@ -3987,8 +4569,14 @@ function LeafNode({ item, label, disableSourceView }) {
|
|
|
3987
4569
|
] });
|
|
3988
4570
|
} else {
|
|
3989
4571
|
const badgeText = item.data.type === "publish" ? "SEND" : "RECV";
|
|
4572
|
+
const isPlugin = item.data.op?.["x-shokupan-plugin-name"] || sourceInfo?.pluginName;
|
|
3990
4573
|
content = /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
3991
4574
|
/* @__PURE__ */ jsxRuntime.jsx("span", { class: `badge badge-${badgeText}`, children: badgeText }),
|
|
4575
|
+
isPlugin && /* @__PURE__ */ jsxRuntime.jsx("span", { class: "builtin-icon", title: "Built-in Plugin", children: /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: "12", height: "12", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round", children: [
|
|
4576
|
+
/* @__PURE__ */ jsxRuntime.jsx("rect", { x: "2", y: "2", width: "20", height: "20", rx: "5", ry: "5" }),
|
|
4577
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M16 11.37A4 4 0 1 1 12.63 8 4 4 0 0 1 16 11.37z" }),
|
|
4578
|
+
/* @__PURE__ */ jsxRuntime.jsx("line", { x1: "17.5", y1: "6.5", x2: "17.51", y2: "6.5" })
|
|
4579
|
+
] }) }),
|
|
3992
4580
|
/* @__PURE__ */ jsxRuntime.jsx("span", { class: "tree-label", children: label })
|
|
3993
4581
|
] });
|
|
3994
4582
|
}
|
|
@@ -4085,45 +4673,56 @@ function buildNavTree(spec) {
|
|
|
4085
4673
|
});
|
|
4086
4674
|
return root;
|
|
4087
4675
|
}
|
|
4088
|
-
|
|
4089
|
-
|
|
4090
|
-
|
|
4091
|
-
|
|
4092
|
-
|
|
4093
|
-
|
|
4094
|
-
|
|
4095
|
-
|
|
4096
|
-
|
|
4097
|
-
|
|
4098
|
-
|
|
4099
|
-
|
|
4100
|
-
});
|
|
4101
|
-
}
|
|
4102
|
-
if (app.mounted) {
|
|
4103
|
-
for (const mount of app.mounted) {
|
|
4104
|
-
const targetApp = applications.find((a) => a.name === mount.target || a.className === mount.target);
|
|
4105
|
-
if (targetApp) {
|
|
4106
|
-
expanded.push(...getExpandedRoutes(targetApp, "", newSeen));
|
|
4107
|
-
}
|
|
4108
|
-
}
|
|
4109
|
-
}
|
|
4110
|
-
return expanded;
|
|
4111
|
-
};
|
|
4112
|
-
applications.forEach((app) => {
|
|
4113
|
-
astRoutes.push(...getExpandedRoutes(app));
|
|
4114
|
-
});
|
|
4115
|
-
return astRoutes;
|
|
4676
|
+
function hasUnknownFields(schema) {
|
|
4677
|
+
if (!schema) return false;
|
|
4678
|
+
if (schema["x-unknown"]) return true;
|
|
4679
|
+
if (schema.type === "object" && schema.properties) {
|
|
4680
|
+
return Object.values(schema.properties).some(
|
|
4681
|
+
(prop) => hasUnknownFields(prop)
|
|
4682
|
+
);
|
|
4683
|
+
}
|
|
4684
|
+
if (schema.type === "array" && schema.items) {
|
|
4685
|
+
return hasUnknownFields(schema.items);
|
|
4686
|
+
}
|
|
4687
|
+
return false;
|
|
4116
4688
|
}
|
|
4117
4689
|
async function generateAsyncApi(rootRouter, options = {}) {
|
|
4118
4690
|
const channels = {};
|
|
4119
4691
|
let astRoutes = [];
|
|
4692
|
+
let astMiddlewareRegistry = {};
|
|
4693
|
+
let applications = [];
|
|
4120
4694
|
try {
|
|
4121
|
-
const { OpenAPIAnalyzer } = await Promise.resolve().then(() => require("./analyzer-
|
|
4695
|
+
const { OpenAPIAnalyzer } = await Promise.resolve().then(() => require("./analyzer-BAhvpNY_.cjs"));
|
|
4122
4696
|
const entrypoint = globalThis.Bun?.main || require.main?.filename || process.argv[1];
|
|
4123
4697
|
const analyzer2 = new OpenAPIAnalyzer(process.cwd(), entrypoint);
|
|
4124
|
-
const
|
|
4125
|
-
|
|
4698
|
+
const analysisResult = await analyzer2.analyze();
|
|
4699
|
+
applications = analysisResult.applications;
|
|
4700
|
+
astRoutes = await getAstRoutes(applications, {
|
|
4701
|
+
includePrefix: false,
|
|
4702
|
+
pathTransform: (p) => p.startsWith("/") ? p.slice(1) : p
|
|
4703
|
+
});
|
|
4704
|
+
let middlewareId = 0;
|
|
4705
|
+
for (const app of applications) {
|
|
4706
|
+
if (app.middleware && app.middleware.length > 0) {
|
|
4707
|
+
for (const mw of app.middleware) {
|
|
4708
|
+
const id = `middleware-${middlewareId++}`;
|
|
4709
|
+
astMiddlewareRegistry[id] = {
|
|
4710
|
+
...mw,
|
|
4711
|
+
id,
|
|
4712
|
+
usedBy: []
|
|
4713
|
+
// Will be populated when processing events
|
|
4714
|
+
};
|
|
4715
|
+
}
|
|
4716
|
+
}
|
|
4717
|
+
}
|
|
4126
4718
|
} catch (e) {
|
|
4719
|
+
if (options.warnings) {
|
|
4720
|
+
options.warnings.push({
|
|
4721
|
+
type: "ast-analysis-failed",
|
|
4722
|
+
message: "AST Analysis failed or skipped",
|
|
4723
|
+
detail: e.message
|
|
4724
|
+
});
|
|
4725
|
+
}
|
|
4127
4726
|
}
|
|
4128
4727
|
const matchedAstRoutes = /* @__PURE__ */ new Set();
|
|
4129
4728
|
const collect = async (router, prefix = "") => {
|
|
@@ -4167,23 +4766,45 @@ async function generateAsyncApi(rootRouter, options = {}) {
|
|
|
4167
4766
|
endLine: astMatch?.sourceContext?.endLine,
|
|
4168
4767
|
highlightLines: astMatch?.sourceContext ? [astMatch.sourceContext.startLine, astMatch.sourceContext.endLine] : void 0
|
|
4169
4768
|
} : void 0;
|
|
4769
|
+
const message = {
|
|
4770
|
+
...userSpec?.message || {}
|
|
4771
|
+
};
|
|
4772
|
+
let inferenceFailed = false;
|
|
4773
|
+
if (!message.payload) {
|
|
4774
|
+
if (astMatch) {
|
|
4775
|
+
if (astMatch.requestTypes?.body) {
|
|
4776
|
+
message.payload = astMatch.requestTypes.body;
|
|
4777
|
+
if (message.payload.type === "object" && !message.payload.properties && !message.payload.additionalProperties && Object.keys(message.payload).length === 1) {
|
|
4778
|
+
inferenceFailed = true;
|
|
4779
|
+
}
|
|
4780
|
+
}
|
|
4781
|
+
} else {
|
|
4782
|
+
message.payload = { type: "object" };
|
|
4783
|
+
inferenceFailed = true;
|
|
4784
|
+
}
|
|
4785
|
+
}
|
|
4170
4786
|
if (!channels[eventName]) {
|
|
4171
|
-
|
|
4172
|
-
|
|
4173
|
-
|
|
4174
|
-
|
|
4175
|
-
|
|
4176
|
-
|
|
4177
|
-
|
|
4178
|
-
|
|
4179
|
-
|
|
4180
|
-
"x-source-info": sourceInfo ? [sourceInfo] : [],
|
|
4181
|
-
"x-shokupan-source": sourceInfo
|
|
4182
|
-
// Simplified
|
|
4787
|
+
const publishOp = {
|
|
4788
|
+
operationId: `on${eventName.charAt(0).toUpperCase() + eventName.slice(1)}`,
|
|
4789
|
+
tags,
|
|
4790
|
+
message,
|
|
4791
|
+
...userSpec?.type === "publish" ? userSpec : {},
|
|
4792
|
+
"x-source-info": sourceInfo ? [sourceInfo] : [],
|
|
4793
|
+
"x-shokupan-source": {
|
|
4794
|
+
...sourceInfo,
|
|
4795
|
+
pluginName: handler.pluginName
|
|
4183
4796
|
}
|
|
4184
4797
|
};
|
|
4185
|
-
if (
|
|
4186
|
-
|
|
4798
|
+
if (inferenceFailed) {
|
|
4799
|
+
publishOp["x-warning"] = true;
|
|
4800
|
+
if (!publishOp.summary) publishOp.summary = "Payload Inference Failed";
|
|
4801
|
+
if (!publishOp.description) publishOp.description = "The payload format could not be statically inferred from the source code. Please add a type assertion or @Spec decorator.";
|
|
4802
|
+
}
|
|
4803
|
+
if (userSpec?.summary && !publishOp.summary) publishOp.summary = userSpec.summary;
|
|
4804
|
+
if (userSpec?.description && !publishOp.description) publishOp.description = userSpec.description;
|
|
4805
|
+
channels[eventName] = {
|
|
4806
|
+
publish: publishOp
|
|
4807
|
+
};
|
|
4187
4808
|
} else {
|
|
4188
4809
|
if (sourceInfo) {
|
|
4189
4810
|
if (!channels[eventName].publish["x-source-info"]) {
|
|
@@ -4201,6 +4822,14 @@ async function generateAsyncApi(rootRouter, options = {}) {
|
|
|
4201
4822
|
for (const emit of emits) {
|
|
4202
4823
|
if (emit.event === "__DYNAMIC_EMIT__") {
|
|
4203
4824
|
const warningKey = `${eventName}/Dynamic Emit`;
|
|
4825
|
+
if (options.warnings) {
|
|
4826
|
+
options.warnings.push({
|
|
4827
|
+
type: "dynamic-emit",
|
|
4828
|
+
message: "Dynamic emit detected",
|
|
4829
|
+
detail: `Event: ${eventName}`,
|
|
4830
|
+
location: { file: astMatch?.sourceContext?.file, line: emit.location?.startLine }
|
|
4831
|
+
});
|
|
4832
|
+
}
|
|
4204
4833
|
channels[warningKey] = {
|
|
4205
4834
|
subscribe: {
|
|
4206
4835
|
operationId: `dynamicEmitWarning${eventName}`,
|
|
@@ -4235,17 +4864,24 @@ async function generateAsyncApi(rootRouter, options = {}) {
|
|
|
4235
4864
|
emitHighlightLines: [emitStart, emitEnd]
|
|
4236
4865
|
} : void 0;
|
|
4237
4866
|
if (!channels[emit.event]) {
|
|
4867
|
+
const payload = emit.payload || { type: "object" };
|
|
4868
|
+
const warning = hasUnknownFields(payload);
|
|
4238
4869
|
channels[emit.event] = {
|
|
4239
4870
|
subscribe: {
|
|
4240
4871
|
operationId: `emit${emit.event.charAt(0).toUpperCase() + emit.event.slice(1)}`,
|
|
4241
4872
|
tags,
|
|
4242
4873
|
message: {
|
|
4243
|
-
payload
|
|
4874
|
+
payload
|
|
4244
4875
|
},
|
|
4876
|
+
...warning ? {
|
|
4877
|
+
"x-warning": true,
|
|
4878
|
+
"x-warning-reason": "Payload contains fields with unknown types that could not be statically analyzed"
|
|
4879
|
+
} : {},
|
|
4245
4880
|
"x-source-info": newSourceInfo ? [newSourceInfo] : [],
|
|
4246
4881
|
"x-shokupan-source": sourceInfo && emitStart ? {
|
|
4247
4882
|
file: sourceInfo.file,
|
|
4248
|
-
line: emitStart
|
|
4883
|
+
line: emitStart,
|
|
4884
|
+
pluginName: handler.pluginName
|
|
4249
4885
|
} : void 0
|
|
4250
4886
|
}
|
|
4251
4887
|
};
|
|
@@ -4309,13 +4945,19 @@ async function generateAsyncApi(rootRouter, options = {}) {
|
|
|
4309
4945
|
emitHighlightLines: [emitStart, emitEnd]
|
|
4310
4946
|
} : void 0;
|
|
4311
4947
|
if (!channels[emit.event]) {
|
|
4948
|
+
const payload = emit.payload || { type: "object" };
|
|
4949
|
+
const warning = hasUnknownFields(payload);
|
|
4312
4950
|
channels[emit.event] = {
|
|
4313
4951
|
subscribe: {
|
|
4314
4952
|
operationId: `emit${emit.event.charAt(0).toUpperCase() + emit.event.slice(1)}`,
|
|
4315
4953
|
tags,
|
|
4316
4954
|
message: {
|
|
4317
|
-
payload
|
|
4955
|
+
payload
|
|
4318
4956
|
},
|
|
4957
|
+
...warning ? {
|
|
4958
|
+
"x-warning": true,
|
|
4959
|
+
"x-warning-reason": "Payload contains fields with unknown types that could not be statically analyzed"
|
|
4960
|
+
} : {},
|
|
4319
4961
|
"x-source-info": newSourceInfo ? [newSourceInfo] : [],
|
|
4320
4962
|
"x-shokupan-source": sourceInfo && emitStart ? {
|
|
4321
4963
|
file: sourceInfo.file,
|
|
@@ -4354,6 +4996,14 @@ async function generateAsyncApi(rootRouter, options = {}) {
|
|
|
4354
4996
|
if (parts.length > 0) prefix = parts[0];
|
|
4355
4997
|
}
|
|
4356
4998
|
const key = `${prefix}.Dynamic Event ${i + 1}`;
|
|
4999
|
+
if (options.warnings) {
|
|
5000
|
+
options.warnings.push({
|
|
5001
|
+
type: "dynamic-event",
|
|
5002
|
+
message: "Dynamic event listener detected",
|
|
5003
|
+
detail: `Event listener with dynamic name`,
|
|
5004
|
+
location: { file: r.sourceContext?.file, line: r.sourceContext?.startLine }
|
|
5005
|
+
});
|
|
5006
|
+
}
|
|
4357
5007
|
channels[key] = {
|
|
4358
5008
|
publish: {
|
|
4359
5009
|
operationId: `dynamicEventWarning${i}`,
|
|
@@ -4379,7 +5029,8 @@ async function generateAsyncApi(rootRouter, options = {}) {
|
|
|
4379
5029
|
return {
|
|
4380
5030
|
asyncapi: "3.0.0",
|
|
4381
5031
|
info: { title: "Shokupan AsyncAPI", version: "1.0.0", ...options.info },
|
|
4382
|
-
channels
|
|
5032
|
+
channels,
|
|
5033
|
+
"x-middleware-registry": astMiddlewareRegistry
|
|
4383
5034
|
};
|
|
4384
5035
|
}
|
|
4385
5036
|
const generator = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
|
|
@@ -4391,6 +5042,12 @@ class AsyncApiPlugin extends ShokupanRouter {
|
|
|
4391
5042
|
super({ renderer: renderToString });
|
|
4392
5043
|
this.pluginOptions = pluginOptions;
|
|
4393
5044
|
this.pluginOptions.path ??= "/asyncapi";
|
|
5045
|
+
this.metadata = {
|
|
5046
|
+
file: void 0,
|
|
5047
|
+
line: 1,
|
|
5048
|
+
name: "AsyncApiPlugin",
|
|
5049
|
+
pluginName: "AsyncAPI"
|
|
5050
|
+
};
|
|
4394
5051
|
this.init();
|
|
4395
5052
|
}
|
|
4396
5053
|
static getBasePath() {
|
|
@@ -4803,12 +5460,15 @@ class ClusterPlugin {
|
|
|
4803
5460
|
}
|
|
4804
5461
|
}
|
|
4805
5462
|
}
|
|
4806
|
-
function DashboardApp({ metrics, uptime, integrations, base, getRequestHeadersSource }) {
|
|
5463
|
+
function DashboardApp({ metrics, uptime, integrations, base, getRequestHeadersSource, rootPath, linkPattern }) {
|
|
4807
5464
|
return /* @__PURE__ */ jsxRuntime.jsxs("html", { lang: "en", children: [
|
|
4808
5465
|
/* @__PURE__ */ jsxRuntime.jsxs("head", { children: [
|
|
4809
5466
|
/* @__PURE__ */ jsxRuntime.jsx("meta", { charSet: "UTF-8" }),
|
|
4810
5467
|
/* @__PURE__ */ jsxRuntime.jsx("meta", { name: "viewport", content: "width=device-width, initial-scale=1.0" }),
|
|
4811
5468
|
/* @__PURE__ */ jsxRuntime.jsx("title", { children: "Shokupan Debug Dashboard" }),
|
|
5469
|
+
/* @__PURE__ */ jsxRuntime.jsx("link", { rel: "preconnect", href: "https://fonts.googleapis.com" }),
|
|
5470
|
+
/* @__PURE__ */ jsxRuntime.jsx("link", { rel: "preconnect", href: "https://fonts.gstatic.com", crossorigin: "anonymous" }),
|
|
5471
|
+
/* @__PURE__ */ jsxRuntime.jsx("link", { href: "https://fonts.googleapis.com/css2?family=Google+Sans+Code:ital,wght@0,300..800;1,300..800&family=Vend+Sans:ital,wght@0,300..700;1,300..700&display=swap", rel: "stylesheet" }),
|
|
4812
5472
|
/* @__PURE__ */ jsxRuntime.jsx("link", { href: "https://unpkg.com/tabulator-tables@5.5.0/dist/css/tabulator_bootstrap5.min.css", rel: "stylesheet" }),
|
|
4813
5473
|
/* @__PURE__ */ jsxRuntime.jsx("link", { rel: "stylesheet", href: "https://esm.sh/@xyflow/react@12.3.6/dist/style.css" }),
|
|
4814
5474
|
/* @__PURE__ */ jsxRuntime.jsx("link", { rel: "stylesheet", href: `${base}/theme.css` }),
|
|
@@ -4817,104 +5477,134 @@ function DashboardApp({ metrics, uptime, integrations, base, getRequestHeadersSo
|
|
|
4817
5477
|
/* @__PURE__ */ jsxRuntime.jsx("link", { rel: "stylesheet", href: `${base}/registry.css` }),
|
|
4818
5478
|
/* @__PURE__ */ jsxRuntime.jsx("link", { rel: "stylesheet", href: `${base}/tabulator.css` }),
|
|
4819
5479
|
/* @__PURE__ */ jsxRuntime.jsx("script", { src: "https://cdn.jsdelivr.net/npm/chart.js" }),
|
|
4820
|
-
/* @__PURE__ */ jsxRuntime.jsx("script", { type: "text/javascript", src: "https://unpkg.com/tabulator-tables@5.5.0/dist/js/tabulator.min.js" })
|
|
5480
|
+
/* @__PURE__ */ jsxRuntime.jsx("script", { type: "text/javascript", src: "https://unpkg.com/tabulator-tables@5.5.0/dist/js/tabulator.min.js" }),
|
|
5481
|
+
/* @__PURE__ */ jsxRuntime.jsx("script", { src: "https://cdn.jsdelivr.net/npm/monaco-editor@0.44.0/min/vs/loader.js" })
|
|
4821
5482
|
] }),
|
|
4822
5483
|
/* @__PURE__ */ jsxRuntime.jsxs("body", { children: [
|
|
4823
5484
|
/* @__PURE__ */ jsxRuntime.jsxs("div", { class: "container", children: [
|
|
4824
5485
|
/* @__PURE__ */ jsxRuntime.jsxs("header", { children: [
|
|
4825
|
-
/* @__PURE__ */ jsxRuntime.
|
|
4826
|
-
|
|
4827
|
-
/* @__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: [
|
|
4828
5489
|
"Uptime: ",
|
|
4829
5490
|
/* @__PURE__ */ jsxRuntime.jsx("span", { id: "uptime", children: uptime })
|
|
4830
|
-
] })
|
|
5491
|
+
] }),
|
|
5492
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { id: "ws-status", title: "WebSocket: Disconnected", style: "width: 10px; height: 10px; border-radius: 50%; background: #6b7280; display: inline-block; margin-left: 10px;" })
|
|
4831
5493
|
] }),
|
|
5494
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { style: "flex: 1;" }),
|
|
4832
5495
|
/* @__PURE__ */ jsxRuntime.jsxs("div", { class: "tabs", children: [
|
|
4833
5496
|
/* @__PURE__ */ jsxRuntime.jsx("button", { class: "tab-btn active", onclick: "switchTab('overview')", children: "Overview" }),
|
|
4834
|
-
/* @__PURE__ */ jsxRuntime.jsx("button", { class: "tab-btn", onclick: "switchTab('
|
|
4835
|
-
/* @__PURE__ */ jsxRuntime.jsx("button", { class: "tab-btn", onclick: "switchTab('
|
|
4836
|
-
/* @__PURE__ */ jsxRuntime.jsx("button", { class: "tab-btn", onclick: "switchTab('
|
|
4837
|
-
/* @__PURE__ */ jsxRuntime.jsx("button", { class: "tab-btn", onclick: "switchTab('
|
|
4838
|
-
integrations.
|
|
4839
|
-
integrations.asyncapi && /* @__PURE__ */ jsxRuntime.jsx("button", { class: "tab-btn", onclick: "switchTab('asyncapi')", children: "AsyncAPI" })
|
|
5497
|
+
/* @__PURE__ */ jsxRuntime.jsx("button", { class: "tab-btn", onclick: "switchTab('application')", children: "Application" }),
|
|
5498
|
+
/* @__PURE__ */ jsxRuntime.jsx("button", { class: "tab-btn", onclick: "switchTab('network')", children: "Network" }),
|
|
5499
|
+
integrations.scalar && /* @__PURE__ */ jsxRuntime.jsx("button", { class: "tab-btn", onclick: "switchTab('scalar')", children: "Scalar" }),
|
|
5500
|
+
integrations.apiExplorer && /* @__PURE__ */ jsxRuntime.jsx("button", { class: "tab-btn", onclick: "switchTab('api-explorer')", children: "REST API" }),
|
|
5501
|
+
integrations.asyncapi && /* @__PURE__ */ jsxRuntime.jsx("button", { class: "tab-btn", onclick: "switchTab('asyncapi')", children: "WS API" })
|
|
4840
5502
|
] })
|
|
4841
5503
|
] }),
|
|
4842
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", {
|
|
4843
|
-
/* @__PURE__ */ jsxRuntime.
|
|
4844
|
-
|
|
4845
|
-
/* @__PURE__ */ jsxRuntime.
|
|
4846
|
-
/* @__PURE__ */ jsxRuntime.jsx("
|
|
4847
|
-
|
|
4848
|
-
|
|
4849
|
-
|
|
4850
|
-
|
|
4851
|
-
|
|
4852
|
-
|
|
4853
|
-
|
|
4854
|
-
|
|
4855
|
-
|
|
4856
|
-
|
|
4857
|
-
|
|
4858
|
-
|
|
4859
|
-
/* @__PURE__ */ jsxRuntime.
|
|
4860
|
-
|
|
4861
|
-
|
|
4862
|
-
|
|
4863
|
-
|
|
4864
|
-
|
|
4865
|
-
|
|
5504
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { class: "contents", children: [
|
|
5505
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { id: "tab-overview", class: "tab-content active", children: [
|
|
5506
|
+
/* @__PURE__ */ jsxRuntime.jsx(MetricsGrid, { metrics }),
|
|
5507
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { id: "chart-container", style: "display: flex; flex-direction: column; gap: 1rem;", children: [
|
|
5508
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { style: "display: flex; justify-content: flex-end;", children: /* @__PURE__ */ jsxRuntime.jsxs("select", { id: "time-range-selector", onchange: "updateCharts(); updateDashboard(); fetchTopStats();", style: "background: var(--bg-primary); color: var(--text-primary); border: 1px solid var(--card-border); padding: 5px; border-radius: 4px;", children: [
|
|
5509
|
+
/* @__PURE__ */ jsxRuntime.jsx("option", { value: "1m", children: "1 Minute" }),
|
|
5510
|
+
/* @__PURE__ */ jsxRuntime.jsx("option", { value: "5m", children: "5 Minutes" }),
|
|
5511
|
+
/* @__PURE__ */ jsxRuntime.jsx("option", { value: "30m", children: "30 Minutes" }),
|
|
5512
|
+
/* @__PURE__ */ jsxRuntime.jsx("option", { value: "1h", children: "1 Hour" }),
|
|
5513
|
+
/* @__PURE__ */ jsxRuntime.jsx("option", { value: "2h", children: "2 Hours" }),
|
|
5514
|
+
/* @__PURE__ */ jsxRuntime.jsx("option", { value: "6h", children: "6 Hours" }),
|
|
5515
|
+
/* @__PURE__ */ jsxRuntime.jsx("option", { value: "12h", children: "12 Hours" }),
|
|
5516
|
+
/* @__PURE__ */ jsxRuntime.jsx("option", { value: "1d", children: "1 Day" }),
|
|
5517
|
+
/* @__PURE__ */ jsxRuntime.jsx("option", { value: "3d", children: "3 Days" }),
|
|
5518
|
+
/* @__PURE__ */ jsxRuntime.jsx("option", { value: "7d", children: "7 Days" }),
|
|
5519
|
+
/* @__PURE__ */ jsxRuntime.jsx("option", { value: "30d", children: "30 Days" })
|
|
5520
|
+
] }) }),
|
|
5521
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { class: "card-container", children: [
|
|
5522
|
+
/* @__PURE__ */ jsxRuntime.jsx(ChartCard, { title: "Response Time", id: "latencyChart" }),
|
|
5523
|
+
/* @__PURE__ */ jsxRuntime.jsx(ChartCard, { title: "Requests / Second", id: "rpsChart" }),
|
|
5524
|
+
/* @__PURE__ */ jsxRuntime.jsx(ChartCard, { title: "CPU & Load", id: "cpuChart" }),
|
|
5525
|
+
/* @__PURE__ */ jsxRuntime.jsx(ChartCard, { title: "Memory", id: "memoryChart" }),
|
|
5526
|
+
/* @__PURE__ */ jsxRuntime.jsx(ChartCard, { title: "Heap Usage", id: "heapChart" }),
|
|
5527
|
+
/* @__PURE__ */ jsxRuntime.jsx(ChartCard, { title: "Event Loop Latency", id: "eventLoopChart" }),
|
|
5528
|
+
/* @__PURE__ */ jsxRuntime.jsx(ChartCard, { title: "Error Rate", id: "errorRateChart" })
|
|
5529
|
+
] }),
|
|
5530
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { class: "card-title", style: "margin-top: 1rem;", children: "Top Statistics" }),
|
|
5531
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { class: "card-container", children: [
|
|
5532
|
+
/* @__PURE__ */ jsxRuntime.jsx(Card, { title: "Top Requests", contentId: "top-requests-table" }),
|
|
5533
|
+
/* @__PURE__ */ jsxRuntime.jsx(Card, { title: "Top Errors", contentId: "top-errors-table" }),
|
|
5534
|
+
/* @__PURE__ */ jsxRuntime.jsx(Card, { title: "Most Frequent Failures", contentId: "failing-requests-table" }),
|
|
5535
|
+
/* @__PURE__ */ jsxRuntime.jsx(Card, { title: "Slowest Requests", contentId: "slowest-requests-table" })
|
|
5536
|
+
] }),
|
|
5537
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { id: "table-container", style: "padding: 0; margin-top: 1rem;", children: /* @__PURE__ */ jsxRuntime.jsx("div", { id: "requests-table", class: "table-dark" }) })
|
|
4866
5538
|
] }),
|
|
4867
|
-
/* @__PURE__ */ jsxRuntime.jsx("div", {
|
|
4868
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { class: "card-container", children: [
|
|
4869
|
-
/* @__PURE__ */ jsxRuntime.jsx(Card, { title: "Top Requests", contentId: "top-requests-table" }),
|
|
4870
|
-
/* @__PURE__ */ jsxRuntime.jsx(Card, { title: "Top Errors", contentId: "top-errors-table" }),
|
|
4871
|
-
/* @__PURE__ */ jsxRuntime.jsx(Card, { title: "Most Frequent Failures", contentId: "failing-requests-table" }),
|
|
4872
|
-
/* @__PURE__ */ jsxRuntime.jsx(Card, { title: "Slowest Requests", contentId: "slowest-requests-table" })
|
|
4873
|
-
] }),
|
|
4874
|
-
/* @__PURE__ */ jsxRuntime.jsx("div", { id: "table-container", style: "padding: 0; margin-top: 1rem;", children: /* @__PURE__ */ jsxRuntime.jsx("div", { id: "requests-table", class: "table-dark" }) })
|
|
4875
|
-
] })
|
|
4876
|
-
] }),
|
|
4877
|
-
/* @__PURE__ */ jsxRuntime.jsx("div", { id: "tab-registry", class: "tab-content", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { id: "registry-container", class: "card", style: "margin-top: 2rem;", children: [
|
|
4878
|
-
/* @__PURE__ */ jsxRuntime.jsx("div", { class: "card-title", children: "Component Registry" }),
|
|
4879
|
-
/* @__PURE__ */ jsxRuntime.jsx("div", { id: "registry-tree", style: "padding: 0 1rem 1rem 1rem; font-family: monospace; font-size: 0.9rem;" })
|
|
4880
|
-
] }) }),
|
|
4881
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { id: "tab-graph", class: "tab-content", children: [
|
|
4882
|
-
/* @__PURE__ */ jsxRuntime.jsx("div", { class: "card", style: "margin-bottom: 1rem;", children: /* @__PURE__ */ jsxRuntime.jsx("div", { style: "display: flex; gap: 1rem;", children: /* @__PURE__ */ jsxRuntime.jsx("input", { type: "text", id: "graph-search", placeholder: "Search routes or middleware...", "aria-label": "Search routes or middleware", style: "flex:1; padding: 0.5rem; border-radius: 0.5rem; background: var(--bg-primary); border: 1px solid var(--card-border); color: var(--text-primary);" }) }) }),
|
|
4883
|
-
/* @__PURE__ */ jsxRuntime.jsx("div", { id: "cy" })
|
|
4884
|
-
] }),
|
|
4885
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { id: "tab-requests", class: "tab-content", children: [
|
|
4886
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { class: "card", style: "margin-bottom: 1rem; display: flex; justify-content: space-between; align-items: center;", children: [
|
|
4887
|
-
/* @__PURE__ */ jsxRuntime.jsx("div", { class: "card-title", children: "Recent Requests (Last 100)" }),
|
|
4888
|
-
/* @__PURE__ */ jsxRuntime.jsx("div", { children: /* @__PURE__ */ jsxRuntime.jsx("button", { onclick: "fetchRequests()", style: "background: var(--bg-primary); color: var(--text-primary); border: 1px solid var(--card-border); padding: 5px 10px; border-radius: 4px; cursor: pointer;", children: "Refresh" }) })
|
|
5539
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { style: "height: 2rem" })
|
|
4889
5540
|
] }),
|
|
4890
|
-
/* @__PURE__ */ jsxRuntime.
|
|
4891
|
-
|
|
4892
|
-
|
|
4893
|
-
|
|
4894
|
-
|
|
4895
|
-
/* @__PURE__ */ jsxRuntime.
|
|
4896
|
-
|
|
4897
|
-
|
|
4898
|
-
|
|
4899
|
-
|
|
4900
|
-
|
|
4901
|
-
|
|
4902
|
-
|
|
4903
|
-
/* @__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);" })
|
|
4904
5556
|
] })
|
|
4905
5557
|
] }),
|
|
4906
|
-
/* @__PURE__ */ jsxRuntime.
|
|
4907
|
-
|
|
4908
|
-
|
|
4909
|
-
|
|
5558
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { id: "tab-network", class: "tab-content", children: [
|
|
5559
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { style: "margin: 1rem 2rem 0 2rem;", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { id: "network-filter-bar", class: "card", style: "margin-bottom: 1rem; padding: 0.5rem; display: flex; gap: 0.5rem; flex-wrap: wrap; flex-direction: row", children: [
|
|
5560
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { style: "display: flex; background: var(--bg-secondary); border: 1px solid var(--card-border); border-radius: 4px; overflow: hidden;", children: [
|
|
5561
|
+
/* @__PURE__ */ jsxRuntime.jsx("button", { class: "filter-direction active", "data-value": "all", style: "padding: 4px 12px; border: none; background: var(--bg-primary); color: var(--text-primary); cursor: pointer; border-right: 1px solid var(--card-border);", children: "All" }),
|
|
5562
|
+
/* @__PURE__ */ jsxRuntime.jsx("button", { class: "filter-direction", "data-value": "inbound", style: "padding: 4px 12px; border: none; background: transparent; color: var(--text-secondary); cursor: pointer; border-right: 1px solid var(--card-border);", children: "Inbound" }),
|
|
5563
|
+
/* @__PURE__ */ jsxRuntime.jsx("button", { class: "filter-direction", "data-value": "outbound", style: "padding: 4px 12px; border: none; background: transparent; color: var(--text-secondary); cursor: pointer;", children: "Outbound" })
|
|
5564
|
+
] }),
|
|
5565
|
+
/* @__PURE__ */ jsxRuntime.jsx("input", { type: "text", id: "network-filter-text", placeholder: "Filter...", style: "background: var(--bg-primary); color: var(--text-primary); border: 1px solid var(--card-border); padding: 4px 8px; border-radius: 4px; flex: 1;" }),
|
|
5566
|
+
/* @__PURE__ */ jsxRuntime.jsxs("select", { id: "network-filter-type", style: "background: var(--bg-primary); color: var(--text-primary); border: 1px solid var(--card-border); borderRadius: 4px;", children: [
|
|
5567
|
+
/* @__PURE__ */ jsxRuntime.jsx("option", { value: "all", children: "All Types" }),
|
|
5568
|
+
/* @__PURE__ */ jsxRuntime.jsx("option", { value: "xhr", children: "XHR/Fetch" }),
|
|
5569
|
+
/* @__PURE__ */ jsxRuntime.jsx("option", { value: "fetch", children: "Outbound" }),
|
|
5570
|
+
/* @__PURE__ */ jsxRuntime.jsx("option", { value: "ws", children: "WS" }),
|
|
5571
|
+
/* @__PURE__ */ jsxRuntime.jsx("option", { value: "other", children: "Other" })
|
|
5572
|
+
] }),
|
|
5573
|
+
/* @__PURE__ */ jsxRuntime.jsx("button", { onclick: "fetchRequests()", style: "background: var(--bg-primary); color: var(--text-primary); border: 1px solid var(--card-border); padding: 4px 8px; border-radius: 4px; cursor: pointer;", children: "Refresh" }),
|
|
5574
|
+
/* @__PURE__ */ jsxRuntime.jsx("button", { onclick: "purgeRequests()", style: "background: var(--bg-primary); color: var(--color-error, #ef4444); border: 1px solid var(--card-border); padding: 4px 8px; border-radius: 4px; cursor: pointer;", children: "Purge" })
|
|
5575
|
+
] }) }),
|
|
5576
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { id: "network-view", class: "active", style: "display: block; height: calc(100vh - 170px);", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { style: "margin: 0 2rem; display: flex; gap: 1rem; height: 100%;", children: [
|
|
5577
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { id: "requests-list-container", style: "flex: 1; height: 100%; border-radius: 6px; overflow: hidden; border: 1px solid var(--card-border);" }),
|
|
5578
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { id: "request-details-container", class: "card", style: "display: none; width: 500px; height: 100%; overflow-y: auto; flex-shrink: 0; background: var(--bg-secondary); border: 1px solid var(--card-border); position: relative;", children: [
|
|
5579
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { id: "details-drag-handle", style: "position: absolute; left: 0; top: 0; bottom: 0; width: 5px; cursor: col-resize; z-index: 11; background: transparent;" }),
|
|
5580
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { style: "display: flex; justify-content: space-between; align-items: center; position: sticky; top: 0; background: var(--bg-secondary); padding: 0.5rem 1rem; border-bottom: 1px solid var(--border-color); z-index: 10;", children: [
|
|
5581
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { class: "card-title", style: "margin: 0;", children: "Request Details" }),
|
|
5582
|
+
/* @__PURE__ */ jsxRuntime.jsx("button", { onclick: "closeRequestDetails()", style: "background: transparent; border: none; color: var(--text-secondary); cursor: pointer; font-size: 1.2rem;", children: "×" })
|
|
5583
|
+
] }),
|
|
5584
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { style: "padding: 1rem;", children: [
|
|
5585
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { id: "request-details-content" }),
|
|
5586
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { class: "card-title", style: "margin-top: 1rem;", children: "Middleware Trace" }),
|
|
5587
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { id: "middleware-trace-container" })
|
|
5588
|
+
] })
|
|
5589
|
+
] })
|
|
5590
|
+
] }) })
|
|
5591
|
+
] }),
|
|
5592
|
+
integrations.scalar && /* @__PURE__ */ jsxRuntime.jsx("div", { id: "tab-scalar", class: "tab-content", style: "overflow: hidden; height: 100%; width: 100%", children: /* @__PURE__ */ jsxRuntime.jsx("iframe", { src: integrations.scalar, style: "width: 100%; height: 100%; border: none;" }) }),
|
|
5593
|
+
integrations.apiExplorer && /* @__PURE__ */ jsxRuntime.jsx("div", { id: "tab-api-explorer", class: "tab-content", style: "overflow: hidden; height: 100%; width: 100%", children: /* @__PURE__ */ jsxRuntime.jsx("iframe", { src: integrations.apiExplorer, style: "width: 100%; height: 100%; border: none;" }) }),
|
|
5594
|
+
integrations.asyncapi && /* @__PURE__ */ jsxRuntime.jsx("div", { id: "tab-asyncapi", class: "tab-content", style: "overflow: hidden; height: 100%; width: 100%", children: /* @__PURE__ */ jsxRuntime.jsx("iframe", { src: integrations.asyncapi, style: "width: 100%; height: 100%; border: none;" }) })
|
|
5595
|
+
] })
|
|
4910
5596
|
] }),
|
|
4911
5597
|
/* @__PURE__ */ jsxRuntime.jsx("script", { dangerouslySetInnerHTML: {
|
|
4912
5598
|
__html: `
|
|
4913
5599
|
// Injected function from server config
|
|
4914
5600
|
const getRequestHeaders = ${getRequestHeadersSource};
|
|
5601
|
+
window.SHOKUPAN_CONFIG = {
|
|
5602
|
+
rootPath: "${rootPath || ""}",
|
|
5603
|
+
linkPattern: "${linkPattern || ""}"
|
|
5604
|
+
};
|
|
4915
5605
|
`
|
|
4916
5606
|
} }),
|
|
4917
|
-
/* @__PURE__ */ jsxRuntime.jsx("script", { src: `${base}/
|
|
5607
|
+
/* @__PURE__ */ jsxRuntime.jsx("script", { src: `${base}/client.js` }),
|
|
4918
5608
|
/* @__PURE__ */ jsxRuntime.jsx("script", { src: `${base}/graph.mjs`, type: "module" }),
|
|
4919
5609
|
/* @__PURE__ */ jsxRuntime.jsx("script", { src: `${base}/charts.js` }),
|
|
4920
5610
|
/* @__PURE__ */ jsxRuntime.jsx("script", { src: `${base}/tables.js` }),
|
|
@@ -4984,6 +5674,264 @@ function Card({ title, contentId }) {
|
|
|
4984
5674
|
/* @__PURE__ */ jsxRuntime.jsx("div", { id: contentId })
|
|
4985
5675
|
] });
|
|
4986
5676
|
}
|
|
5677
|
+
const require$1 = node_module.createRequire(typeof document === "undefined" ? require("url").pathToFileURL(__filename).href : _documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === "SCRIPT" && _documentCurrentScript.src || new URL("index.cjs", document.baseURI).href);
|
|
5678
|
+
const http = require$1("node:http");
|
|
5679
|
+
const https = require$1("node:https");
|
|
5680
|
+
class FetchInterceptor {
|
|
5681
|
+
originalFetch;
|
|
5682
|
+
originalHttpRequest;
|
|
5683
|
+
originalHttpsRequest;
|
|
5684
|
+
callbacks = [];
|
|
5685
|
+
isPatched = false;
|
|
5686
|
+
constructor() {
|
|
5687
|
+
this.originalFetch = global.fetch;
|
|
5688
|
+
this.originalHttpRequest = http.request;
|
|
5689
|
+
this.originalHttpsRequest = https.request;
|
|
5690
|
+
}
|
|
5691
|
+
/**
|
|
5692
|
+
* Patches the global `fetch` function to intercept requests.
|
|
5693
|
+
* If already patched, this method does nothing.
|
|
5694
|
+
*/
|
|
5695
|
+
patch() {
|
|
5696
|
+
if (this.isPatched) return;
|
|
5697
|
+
this.patchGlobalFetch();
|
|
5698
|
+
this.patchNodeRequests();
|
|
5699
|
+
this.isPatched = true;
|
|
5700
|
+
console.log("[FetchInterceptor] Network layer patched.");
|
|
5701
|
+
}
|
|
5702
|
+
patchGlobalFetch() {
|
|
5703
|
+
const self = this;
|
|
5704
|
+
const newFetch = async function(input, init) {
|
|
5705
|
+
const startTime = performance.now();
|
|
5706
|
+
const timestamp = Date.now();
|
|
5707
|
+
let method = "GET";
|
|
5708
|
+
let url = "";
|
|
5709
|
+
let requestHeaders = {};
|
|
5710
|
+
let requestBody = void 0;
|
|
5711
|
+
try {
|
|
5712
|
+
if (input instanceof node_url.URL) {
|
|
5713
|
+
url = input.toString();
|
|
5714
|
+
} else if (typeof input === "string") {
|
|
5715
|
+
url = input;
|
|
5716
|
+
} else if (typeof input === "object" && "url" in input) {
|
|
5717
|
+
url = input.url;
|
|
5718
|
+
method = input.method;
|
|
5719
|
+
}
|
|
5720
|
+
if (init) {
|
|
5721
|
+
if (init.method) method = init.method;
|
|
5722
|
+
if (init.headers) {
|
|
5723
|
+
if (init.headers instanceof Headers) {
|
|
5724
|
+
init.headers.forEach((v, k) => requestHeaders[k] = v);
|
|
5725
|
+
} else if (Array.isArray(init.headers)) {
|
|
5726
|
+
init.headers.forEach(([k, v]) => requestHeaders[k] = v);
|
|
5727
|
+
} else {
|
|
5728
|
+
Object.assign(requestHeaders, init.headers);
|
|
5729
|
+
}
|
|
5730
|
+
}
|
|
5731
|
+
if (init.body) requestBody = init.body;
|
|
5732
|
+
}
|
|
5733
|
+
} catch (e) {
|
|
5734
|
+
console.warn("[FetchInterceptor] Failed to parse request arguments", e);
|
|
5735
|
+
}
|
|
5736
|
+
try {
|
|
5737
|
+
const response = await self.originalFetch.apply(global, [input, init]);
|
|
5738
|
+
const clone = response.clone();
|
|
5739
|
+
const duration = performance.now() - startTime;
|
|
5740
|
+
self.processResponse(clone, {
|
|
5741
|
+
method,
|
|
5742
|
+
url,
|
|
5743
|
+
requestHeaders,
|
|
5744
|
+
requestBody,
|
|
5745
|
+
status: response.status,
|
|
5746
|
+
startTime: timestamp,
|
|
5747
|
+
duration,
|
|
5748
|
+
...self.extractRequestMeta(url, requestHeaders),
|
|
5749
|
+
protocol: "1.1"
|
|
5750
|
+
// native fetch doesn't expose this easily, assume 1.1/2
|
|
5751
|
+
});
|
|
5752
|
+
return response;
|
|
5753
|
+
} catch (error) {
|
|
5754
|
+
const duration = performance.now() - startTime;
|
|
5755
|
+
self.notify({
|
|
5756
|
+
method,
|
|
5757
|
+
url,
|
|
5758
|
+
requestHeaders,
|
|
5759
|
+
requestBody,
|
|
5760
|
+
status: 0,
|
|
5761
|
+
responseHeaders: {},
|
|
5762
|
+
responseBody: `Network Error: ${String(error)}`,
|
|
5763
|
+
startTime: timestamp,
|
|
5764
|
+
duration
|
|
5765
|
+
});
|
|
5766
|
+
throw error;
|
|
5767
|
+
}
|
|
5768
|
+
};
|
|
5769
|
+
Object.assign(newFetch, this.originalFetch);
|
|
5770
|
+
global.fetch = newFetch;
|
|
5771
|
+
}
|
|
5772
|
+
patchNodeRequests() {
|
|
5773
|
+
const self = this;
|
|
5774
|
+
const intercept = (module2, original, defaultScheme) => {
|
|
5775
|
+
module2.request = function(...args) {
|
|
5776
|
+
const startTime = performance.now();
|
|
5777
|
+
const timestamp = Date.now();
|
|
5778
|
+
let options = {};
|
|
5779
|
+
let urlObj;
|
|
5780
|
+
if (typeof args[0] === "string" || args[0] instanceof node_url.URL) {
|
|
5781
|
+
try {
|
|
5782
|
+
urlObj = new node_url.URL(args[0]);
|
|
5783
|
+
options = typeof args[1] === "object" ? args[1] : {};
|
|
5784
|
+
} catch (e) {
|
|
5785
|
+
}
|
|
5786
|
+
} else {
|
|
5787
|
+
options = args[0] || {};
|
|
5788
|
+
try {
|
|
5789
|
+
const protocol = options.protocol || defaultScheme + ":";
|
|
5790
|
+
const host = options.hostname || options.host || "localhost";
|
|
5791
|
+
const port = options.port ? ":" + options.port : "";
|
|
5792
|
+
const path2 = options.path || "/";
|
|
5793
|
+
urlObj = new node_url.URL(`${protocol}//${host}${port}${path2}`);
|
|
5794
|
+
} catch (e) {
|
|
5795
|
+
}
|
|
5796
|
+
}
|
|
5797
|
+
const method = (options.method || "GET").toUpperCase();
|
|
5798
|
+
const url = urlObj ? urlObj.toString() : "unknown";
|
|
5799
|
+
const req = original.apply(this, args);
|
|
5800
|
+
const getReqHeaders = () => {
|
|
5801
|
+
try {
|
|
5802
|
+
const h = req.getHeaders();
|
|
5803
|
+
const normalized = {};
|
|
5804
|
+
for (const k in h) {
|
|
5805
|
+
const v = h[k];
|
|
5806
|
+
normalized[k] = Array.isArray(v) ? v.join(", ") : String(v);
|
|
5807
|
+
}
|
|
5808
|
+
return normalized;
|
|
5809
|
+
} catch (e) {
|
|
5810
|
+
return {};
|
|
5811
|
+
}
|
|
5812
|
+
};
|
|
5813
|
+
req.on("response", (res) => {
|
|
5814
|
+
const duration = performance.now() - startTime;
|
|
5815
|
+
const resHeaders = {};
|
|
5816
|
+
if (res.headers) {
|
|
5817
|
+
for (const k in res.headers) {
|
|
5818
|
+
const v = res.headers[k];
|
|
5819
|
+
resHeaders[k] = Array.isArray(v) ? v.join(", ") : String(v || "");
|
|
5820
|
+
}
|
|
5821
|
+
}
|
|
5822
|
+
self.notify({
|
|
5823
|
+
method,
|
|
5824
|
+
url,
|
|
5825
|
+
requestHeaders: getReqHeaders(),
|
|
5826
|
+
status: res.statusCode || 0,
|
|
5827
|
+
responseHeaders: resHeaders,
|
|
5828
|
+
startTime: timestamp,
|
|
5829
|
+
duration,
|
|
5830
|
+
...self.extractRequestMeta(url, getReqHeaders()),
|
|
5831
|
+
protocol: req.httpVersion
|
|
5832
|
+
});
|
|
5833
|
+
});
|
|
5834
|
+
req.on("error", (err) => {
|
|
5835
|
+
const duration = performance.now() - startTime;
|
|
5836
|
+
self.notify({
|
|
5837
|
+
method,
|
|
5838
|
+
url,
|
|
5839
|
+
requestHeaders: getReqHeaders(),
|
|
5840
|
+
status: 0,
|
|
5841
|
+
responseHeaders: {},
|
|
5842
|
+
responseBody: `Error: ${err.message}`,
|
|
5843
|
+
// Capture error
|
|
5844
|
+
startTime: timestamp,
|
|
5845
|
+
duration
|
|
5846
|
+
});
|
|
5847
|
+
});
|
|
5848
|
+
return req;
|
|
5849
|
+
};
|
|
5850
|
+
};
|
|
5851
|
+
intercept(http, this.originalHttpRequest, "http");
|
|
5852
|
+
intercept(https, this.originalHttpsRequest, "https");
|
|
5853
|
+
}
|
|
5854
|
+
/**
|
|
5855
|
+
* Restores the original functions.
|
|
5856
|
+
*/
|
|
5857
|
+
unpatch() {
|
|
5858
|
+
if (!this.isPatched) return;
|
|
5859
|
+
global.fetch = this.originalFetch;
|
|
5860
|
+
http.request = this.originalHttpRequest;
|
|
5861
|
+
https.request = this.originalHttpsRequest;
|
|
5862
|
+
this.isPatched = false;
|
|
5863
|
+
console.log("[FetchInterceptor] Network layer restored.");
|
|
5864
|
+
}
|
|
5865
|
+
/**
|
|
5866
|
+
* Adds a callback to be notified of outbound requests.
|
|
5867
|
+
* @param callback The callback function.
|
|
5868
|
+
*/
|
|
5869
|
+
on(callback) {
|
|
5870
|
+
this.callbacks.push(callback);
|
|
5871
|
+
}
|
|
5872
|
+
extractRequestMeta(urlStr, headers) {
|
|
5873
|
+
try {
|
|
5874
|
+
const url = new node_url.URL(urlStr);
|
|
5875
|
+
const cookiesHeader = headers["cookie"] || headers["Cookie"];
|
|
5876
|
+
const cookies = cookiesHeader ? cookiesHeader.split(";").length : 0;
|
|
5877
|
+
return {
|
|
5878
|
+
domain: url.hostname,
|
|
5879
|
+
path: url.pathname,
|
|
5880
|
+
scheme: url.protocol.replace(":", ""),
|
|
5881
|
+
cookies,
|
|
5882
|
+
remoteIP: void 0
|
|
5883
|
+
// Not easily accessible via fetch
|
|
5884
|
+
};
|
|
5885
|
+
} catch (e) {
|
|
5886
|
+
return {};
|
|
5887
|
+
}
|
|
5888
|
+
}
|
|
5889
|
+
async processResponse(response, meta) {
|
|
5890
|
+
const responseHeaders = {};
|
|
5891
|
+
response.headers.forEach((v, k) => responseHeaders[k] = v);
|
|
5892
|
+
let responseBody;
|
|
5893
|
+
let transferred = 0;
|
|
5894
|
+
try {
|
|
5895
|
+
const contentType = response.headers.get("content-type") || "";
|
|
5896
|
+
let bodyText = "";
|
|
5897
|
+
if (contentType.includes("application/json") || contentType.includes("text/")) {
|
|
5898
|
+
bodyText = await response.text();
|
|
5899
|
+
if (bodyText.length > 524288) {
|
|
5900
|
+
responseBody = bodyText.substring(0, 524288) + "... (truncated)";
|
|
5901
|
+
} else {
|
|
5902
|
+
responseBody = bodyText;
|
|
5903
|
+
}
|
|
5904
|
+
} else {
|
|
5905
|
+
responseBody = "[Binary Content]";
|
|
5906
|
+
const cl = response.headers.get("content-length");
|
|
5907
|
+
if (cl) transferred = parseInt(cl, 10);
|
|
5908
|
+
}
|
|
5909
|
+
const headersSize = Object.entries(responseHeaders).reduce((acc, [k, v]) => acc + k.length + v.length + 2, 0);
|
|
5910
|
+
if (!transferred && bodyText) {
|
|
5911
|
+
transferred = headersSize + bodyText.length;
|
|
5912
|
+
} else if (!transferred) {
|
|
5913
|
+
transferred = headersSize;
|
|
5914
|
+
}
|
|
5915
|
+
} catch (e) {
|
|
5916
|
+
responseBody = "[Failed to read response body]";
|
|
5917
|
+
}
|
|
5918
|
+
this.notify({
|
|
5919
|
+
...meta,
|
|
5920
|
+
responseHeaders,
|
|
5921
|
+
responseBody,
|
|
5922
|
+
transferred
|
|
5923
|
+
});
|
|
5924
|
+
}
|
|
5925
|
+
notify(log) {
|
|
5926
|
+
this.callbacks.forEach((cb) => {
|
|
5927
|
+
try {
|
|
5928
|
+
cb(log);
|
|
5929
|
+
} catch (e) {
|
|
5930
|
+
console.error("[FetchInterceptor] Callback failed", e);
|
|
5931
|
+
}
|
|
5932
|
+
});
|
|
5933
|
+
}
|
|
5934
|
+
}
|
|
4987
5935
|
const INTERVALS = [
|
|
4988
5936
|
{ label: "10s", ms: 10 * 1e3 },
|
|
4989
5937
|
{ label: "1m", ms: 60 * 1e3 },
|
|
@@ -4998,7 +5946,8 @@ const INTERVALS = [
|
|
|
4998
5946
|
{ label: "30d", ms: 30 * 24 * 60 * 60 * 1e3 }
|
|
4999
5947
|
];
|
|
5000
5948
|
class MetricsCollector {
|
|
5001
|
-
constructor(db) {
|
|
5949
|
+
constructor(db, onCollect) {
|
|
5950
|
+
this.onCollect = onCollect;
|
|
5002
5951
|
this.db = db;
|
|
5003
5952
|
this.eventLoopHistogram.enable();
|
|
5004
5953
|
const now = Date.now();
|
|
@@ -5006,11 +5955,13 @@ class MetricsCollector {
|
|
|
5006
5955
|
this.currentIntervalStart[int.label] = this.alignTimestamp(now, int.ms);
|
|
5007
5956
|
this.pendingDetails[int.label] = [];
|
|
5008
5957
|
});
|
|
5958
|
+
this.timer = setInterval(() => this.collect(), 1e4);
|
|
5009
5959
|
}
|
|
5010
5960
|
currentIntervalStart = {};
|
|
5011
5961
|
pendingDetails = {};
|
|
5012
5962
|
eventLoopHistogram = node_perf_hooks.monitorEventLoopDelay({ resolution: 10 });
|
|
5013
5963
|
timer = null;
|
|
5964
|
+
db;
|
|
5014
5965
|
recordRequest(duration, isError) {
|
|
5015
5966
|
INTERVALS.forEach((int) => {
|
|
5016
5967
|
this.pendingDetails[int.label].push({ duration, isError });
|
|
@@ -5086,14 +6037,17 @@ class MetricsCollector {
|
|
|
5086
6037
|
p99: getP(0.99)
|
|
5087
6038
|
}
|
|
5088
6039
|
};
|
|
6040
|
+
if (!this.db) {
|
|
6041
|
+
return;
|
|
6042
|
+
}
|
|
5089
6043
|
try {
|
|
5090
|
-
|
|
5091
|
-
await this.db.upsert(recordId, metric);
|
|
5092
|
-
const test = await this.db.select(recordId);
|
|
5093
|
-
const queryTest = await this.db.query("SELECT * FROM metrics WHERE id = $id", { id: recordId });
|
|
6044
|
+
await this.db.upsert(new surrealdb.RecordId("metric", timestamp), metric);
|
|
5094
6045
|
} catch (e) {
|
|
5095
6046
|
console.error(`[MetricsCollector] ✗ Failed to save metrics for ${label}:`, e);
|
|
5096
6047
|
}
|
|
6048
|
+
if (this.onCollect) {
|
|
6049
|
+
this.onCollect(metric);
|
|
6050
|
+
}
|
|
5097
6051
|
}
|
|
5098
6052
|
// Cleanup if needed
|
|
5099
6053
|
stop() {
|
|
@@ -5139,8 +6093,13 @@ class Dashboard {
|
|
|
5139
6093
|
nodeMetrics: {},
|
|
5140
6094
|
edgeMetrics: {}
|
|
5141
6095
|
};
|
|
6096
|
+
clients = /* @__PURE__ */ new Set();
|
|
6097
|
+
broadcastTimer;
|
|
6098
|
+
requestPushTimer;
|
|
6099
|
+
requestsBuffer = [];
|
|
5142
6100
|
startTime = Date.now();
|
|
5143
6101
|
instrumented = false;
|
|
6102
|
+
mountPath = "/dashboard";
|
|
5144
6103
|
metricsCollector;
|
|
5145
6104
|
get db() {
|
|
5146
6105
|
return this[$appRoot].db;
|
|
@@ -5148,8 +6107,69 @@ class Dashboard {
|
|
|
5148
6107
|
// ShokupanPlugin interface implementation
|
|
5149
6108
|
onInit(app, options) {
|
|
5150
6109
|
this[$appRoot] = app;
|
|
5151
|
-
|
|
5152
|
-
|
|
6110
|
+
const onCollect = (metric) => {
|
|
6111
|
+
this.broadcastMetricUpdate(metric);
|
|
6112
|
+
};
|
|
6113
|
+
this.metricsCollector = new MetricsCollector(this.db, onCollect);
|
|
6114
|
+
const fetchInterceptor = new FetchInterceptor();
|
|
6115
|
+
fetchInterceptor.patch();
|
|
6116
|
+
fetchInterceptor.on((log) => {
|
|
6117
|
+
if (log.url.includes("/rpc")) return;
|
|
6118
|
+
try {
|
|
6119
|
+
const u = new URL(log.url);
|
|
6120
|
+
if (u.pathname.startsWith(this.mountPath)) return;
|
|
6121
|
+
} catch (e) {
|
|
6122
|
+
}
|
|
6123
|
+
const requestData = {
|
|
6124
|
+
method: log.method,
|
|
6125
|
+
url: log.url,
|
|
6126
|
+
status: log.status,
|
|
6127
|
+
duration: log.duration,
|
|
6128
|
+
timestamp: log.startTime,
|
|
6129
|
+
// Use startTime as timestamp
|
|
6130
|
+
type: "fetch",
|
|
6131
|
+
direction: "outbound",
|
|
6132
|
+
size: log.responseBody ? String(log.responseBody).length : 0,
|
|
6133
|
+
contentType: log.responseHeaders["content-type"] || log.responseHeaders["Content-Type"],
|
|
6134
|
+
body: log.responseBody,
|
|
6135
|
+
requestBody: log.requestBody,
|
|
6136
|
+
domain: log.domain,
|
|
6137
|
+
path: log.path,
|
|
6138
|
+
scheme: log.scheme,
|
|
6139
|
+
protocol: log.protocol,
|
|
6140
|
+
remoteIP: log.remoteIP,
|
|
6141
|
+
cookies: log.cookies,
|
|
6142
|
+
transferred: log.transferred,
|
|
6143
|
+
requestHeaders: log.requestHeaders,
|
|
6144
|
+
responseHeaders: log.responseHeaders
|
|
6145
|
+
// No handler stack for outbound
|
|
6146
|
+
};
|
|
6147
|
+
this.metrics.logs.push(requestData);
|
|
6148
|
+
const recordId = new surrealdb.RecordId("request", nanoid.nanoid());
|
|
6149
|
+
const idString = recordId.toString();
|
|
6150
|
+
this.db.query("UPSERT $id CONTENT $data", {
|
|
6151
|
+
id: recordId,
|
|
6152
|
+
data: requestData
|
|
6153
|
+
}).catch((e) => console.error("Failed to save outbound request", e));
|
|
6154
|
+
const strategy2 = this.dashboardConfig.updateStrategy || "immediate";
|
|
6155
|
+
if (strategy2 === "immediate") {
|
|
6156
|
+
this.broadcastRequestUpdates([{ ...requestData, id: idString }]);
|
|
6157
|
+
} else {
|
|
6158
|
+
this.requestsBuffer.push({ ...requestData, id: idString });
|
|
6159
|
+
}
|
|
6160
|
+
});
|
|
6161
|
+
if (app.onStart) {
|
|
6162
|
+
app.onStart(async () => {
|
|
6163
|
+
if (app.dbPromise) {
|
|
6164
|
+
await app.dbPromise;
|
|
6165
|
+
if (app.db) {
|
|
6166
|
+
this.metricsCollector.db = app.db;
|
|
6167
|
+
console.log("[Dashboard] Attached datastore to MetricsCollector");
|
|
6168
|
+
}
|
|
6169
|
+
}
|
|
6170
|
+
});
|
|
6171
|
+
}
|
|
6172
|
+
this.mountPath = options?.path || this.dashboardConfig.path || "/dashboard";
|
|
5153
6173
|
const hooks = this.getHooks();
|
|
5154
6174
|
if (!app.middleware) {
|
|
5155
6175
|
app.middleware = [];
|
|
@@ -5158,15 +6178,25 @@ class Dashboard {
|
|
|
5158
6178
|
if (hooks.onRequestStart) {
|
|
5159
6179
|
await hooks.onRequestStart(ctx);
|
|
5160
6180
|
}
|
|
6181
|
+
ctx._startTime = performance.now();
|
|
5161
6182
|
await next();
|
|
5162
|
-
if (hooks.onResponseEnd) {
|
|
5163
|
-
const effectiveResponse = ctx._finalResponse || ctx.response || {};
|
|
5164
|
-
await hooks.onResponseEnd(ctx, effectiveResponse);
|
|
5165
|
-
}
|
|
5166
6183
|
};
|
|
5167
6184
|
app.use(hooksMiddleware);
|
|
5168
|
-
|
|
6185
|
+
if (hooks.onResponseEnd) {
|
|
6186
|
+
app.hook("onResponseEnd", hooks.onResponseEnd);
|
|
6187
|
+
}
|
|
6188
|
+
app.mount(this.mountPath, this.router);
|
|
6189
|
+
this.router.metadata = {
|
|
6190
|
+
file: void 0,
|
|
6191
|
+
line: 1,
|
|
6192
|
+
name: "DashboardPlugin",
|
|
6193
|
+
pluginName: "Dashboard"
|
|
6194
|
+
};
|
|
5169
6195
|
this.setupRoutes();
|
|
6196
|
+
const strategy = this.dashboardConfig.updateStrategy || "immediate";
|
|
6197
|
+
if (strategy === "batched") {
|
|
6198
|
+
this.startRequestPushTimer();
|
|
6199
|
+
}
|
|
5170
6200
|
}
|
|
5171
6201
|
detectIntegrations() {
|
|
5172
6202
|
const integrations = {};
|
|
@@ -5199,6 +6229,17 @@ class Dashboard {
|
|
|
5199
6229
|
}
|
|
5200
6230
|
}
|
|
5201
6231
|
}
|
|
6232
|
+
const apiExplorerConf = checkConfig("apiExplorer");
|
|
6233
|
+
if (apiExplorerConf.enabled) {
|
|
6234
|
+
if (apiExplorerConf.path) {
|
|
6235
|
+
integrations["apiExplorer"] = apiExplorerConf.path;
|
|
6236
|
+
} else {
|
|
6237
|
+
const plugin = routers.find((r) => r.constructor.name === "ApiExplorerPlugin");
|
|
6238
|
+
if (plugin) {
|
|
6239
|
+
integrations["apiExplorer"] = plugin[$mountPath];
|
|
6240
|
+
}
|
|
6241
|
+
}
|
|
6242
|
+
}
|
|
5202
6243
|
return integrations;
|
|
5203
6244
|
}
|
|
5204
6245
|
// Get base path for dashboard files - works in both dev (src/) and production (dist/)
|
|
@@ -5210,9 +6251,36 @@ class Dashboard {
|
|
|
5210
6251
|
return dir;
|
|
5211
6252
|
}
|
|
5212
6253
|
setupRoutes() {
|
|
6254
|
+
this.router.get("/ws", (ctx) => {
|
|
6255
|
+
const success = ctx.upgrade({
|
|
6256
|
+
data: {
|
|
6257
|
+
handler: {
|
|
6258
|
+
open: (ws) => {
|
|
6259
|
+
this.clients.add(ws);
|
|
6260
|
+
console.log(`[Dashboard] Client connected. Total clients: ${this.clients.size}`);
|
|
6261
|
+
this.sendHistory(ws, "1m");
|
|
6262
|
+
},
|
|
6263
|
+
close: (ws) => {
|
|
6264
|
+
this.clients.delete(ws);
|
|
6265
|
+
console.log(`[Dashboard] Client disconnected. Total clients: ${this.clients.size}`);
|
|
6266
|
+
},
|
|
6267
|
+
message: (ws, message) => {
|
|
6268
|
+
try {
|
|
6269
|
+
const msg = JSON.parse(message);
|
|
6270
|
+
if (msg.type === "get-history") {
|
|
6271
|
+
this.sendHistory(ws, msg.interval || "1m");
|
|
6272
|
+
}
|
|
6273
|
+
} catch (e) {
|
|
6274
|
+
}
|
|
6275
|
+
}
|
|
6276
|
+
}
|
|
6277
|
+
}
|
|
6278
|
+
});
|
|
6279
|
+
if (success) return void 0;
|
|
6280
|
+
return ctx.text("WebSocket upgrade failed", 400);
|
|
6281
|
+
});
|
|
5213
6282
|
this.router.get("/metrics", async (ctx) => {
|
|
5214
|
-
const
|
|
5215
|
-
const uptime = `${Math.floor(uptimeSeconds / 3600)}h ${Math.floor(uptimeSeconds % 3600 / 60)}m ${uptimeSeconds % 60}s`;
|
|
6283
|
+
const uptime = this.getUptime();
|
|
5216
6284
|
const interval = ctx.query["interval"];
|
|
5217
6285
|
if (interval) {
|
|
5218
6286
|
const intervalMap = {
|
|
@@ -5239,13 +6307,15 @@ class Dashboard {
|
|
|
5239
6307
|
count(IF status < 400 THEN 1 END) as success,
|
|
5240
6308
|
count(IF status >= 400 THEN 1 END) as failed,
|
|
5241
6309
|
math::mean(duration) as avg_latency
|
|
5242
|
-
FROM
|
|
6310
|
+
FROM request
|
|
5243
6311
|
WHERE timestamp >= $start
|
|
5244
6312
|
GROUP ALL
|
|
5245
6313
|
`, { start: startTime });
|
|
5246
6314
|
} catch (error) {
|
|
5247
6315
|
console.error("[Dashboard] Query failed at plugin.ts:180-191", {
|
|
5248
6316
|
error,
|
|
6317
|
+
errorMessage: error.message,
|
|
6318
|
+
errorStack: error.stack,
|
|
5249
6319
|
interval,
|
|
5250
6320
|
startTime,
|
|
5251
6321
|
query: "metrics interval stats",
|
|
@@ -5253,12 +6323,12 @@ class Dashboard {
|
|
|
5253
6323
|
});
|
|
5254
6324
|
throw error;
|
|
5255
6325
|
}
|
|
5256
|
-
const s = stats[0] || {
|
|
6326
|
+
const s = stats[0] || { avg_latency: 0 };
|
|
5257
6327
|
return ctx.json({
|
|
5258
6328
|
metrics: {
|
|
5259
|
-
totalRequests:
|
|
5260
|
-
successfulRequests:
|
|
5261
|
-
failedRequests:
|
|
6329
|
+
totalRequests: this.metrics.totalRequests,
|
|
6330
|
+
successfulRequests: this.metrics.successfulRequests,
|
|
6331
|
+
failedRequests: this.metrics.failedRequests,
|
|
5262
6332
|
activeRequests: this.metrics.activeRequests,
|
|
5263
6333
|
averageTotalTime_ms: s.avg_latency || 0,
|
|
5264
6334
|
recentTimings: this.metrics.recentTimings,
|
|
@@ -5324,7 +6394,7 @@ class Dashboard {
|
|
|
5324
6394
|
this.router.get("/requests/top", async (ctx) => {
|
|
5325
6395
|
const startTime = getIntervalStartTime(ctx.query["interval"]);
|
|
5326
6396
|
const result = await this.db.query(
|
|
5327
|
-
"SELECT method, url, count() as count FROM
|
|
6397
|
+
"SELECT method, url, count() as count FROM request WHERE timestamp >= $start GROUP BY method, url ORDER BY count DESC LIMIT 10",
|
|
5328
6398
|
{ start: startTime }
|
|
5329
6399
|
);
|
|
5330
6400
|
return ctx.json({ top: result[0] || [] });
|
|
@@ -5332,7 +6402,7 @@ class Dashboard {
|
|
|
5332
6402
|
this.router.get("/errors/top", async (ctx) => {
|
|
5333
6403
|
const startTime = getIntervalStartTime(ctx.query["interval"]);
|
|
5334
6404
|
const result = await this.db.query(
|
|
5335
|
-
"SELECT status, count() as count FROM
|
|
6405
|
+
"SELECT status, count() as count FROM failed_request WHERE timestamp >= $start GROUP BY status ORDER BY count DESC LIMIT 10",
|
|
5336
6406
|
{ start: startTime }
|
|
5337
6407
|
);
|
|
5338
6408
|
return ctx.json({ top: result[0] || [] });
|
|
@@ -5340,7 +6410,7 @@ class Dashboard {
|
|
|
5340
6410
|
this.router.get("/requests/failing", async (ctx) => {
|
|
5341
6411
|
const startTime = getIntervalStartTime(ctx.query["interval"]);
|
|
5342
6412
|
const result = await this.db.query(
|
|
5343
|
-
"SELECT method, url, count() as count FROM
|
|
6413
|
+
"SELECT method, url, count() as count FROM failed_request WHERE timestamp >= $start GROUP BY method, url ORDER BY count DESC LIMIT 10",
|
|
5344
6414
|
{ start: startTime }
|
|
5345
6415
|
);
|
|
5346
6416
|
return ctx.json({ top: result[0] || [] });
|
|
@@ -5348,7 +6418,7 @@ class Dashboard {
|
|
|
5348
6418
|
this.router.get("/requests/slowest", async (ctx) => {
|
|
5349
6419
|
const startTime = getIntervalStartTime(ctx.query["interval"]);
|
|
5350
6420
|
const result = await this.db.query(
|
|
5351
|
-
"SELECT method, url, duration, status, timestamp FROM
|
|
6421
|
+
"SELECT method, url, duration, status, timestamp FROM request WHERE timestamp >= $start ORDER BY duration DESC LIMIT 10",
|
|
5352
6422
|
{ start: startTime }
|
|
5353
6423
|
);
|
|
5354
6424
|
return ctx.json({ slowest: result[0] || [] });
|
|
@@ -5365,15 +6435,32 @@ class Dashboard {
|
|
|
5365
6435
|
return ctx.json({ registry: registry || {} });
|
|
5366
6436
|
});
|
|
5367
6437
|
this.router.get("/requests", async (ctx) => {
|
|
5368
|
-
|
|
5369
|
-
|
|
6438
|
+
console.log(`[Dashboard] Handling /requests from ${ctx.ip} ${ctx.get("User-Agent")}`);
|
|
6439
|
+
const result = await this.db.query("SELECT * FROM request ORDER BY timestamp DESC LIMIT 100");
|
|
6440
|
+
const items = result[0] || [];
|
|
6441
|
+
console.log(`[Dashboard] /requests returning ${items.length} items`);
|
|
6442
|
+
return ctx.json({ requests: items });
|
|
6443
|
+
});
|
|
6444
|
+
this.router.delete("/requests", async (ctx) => {
|
|
6445
|
+
console.log(`[Dashboard] Purging all requests`);
|
|
6446
|
+
await this.db.query("DELETE request; DELETE failed_request;");
|
|
6447
|
+
this.metrics.logs = [];
|
|
6448
|
+
this.metrics.totalRequests = 0;
|
|
6449
|
+
this.metrics.activeRequests = 0;
|
|
6450
|
+
this.metrics.successfulRequests = 0;
|
|
6451
|
+
this.metrics.failedRequests = 0;
|
|
6452
|
+
this.metrics.recentTimings = [];
|
|
6453
|
+
this.metrics.rateLimitedCounts = {};
|
|
6454
|
+
this.metrics.nodeMetrics = {};
|
|
6455
|
+
this.metrics.edgeMetrics = {};
|
|
6456
|
+
return ctx.json({ success: true });
|
|
5370
6457
|
});
|
|
5371
6458
|
this.router.get("/requests/:id", async (ctx) => {
|
|
5372
|
-
const result = await this.db.query("SELECT * FROM
|
|
6459
|
+
const result = await this.db.query("SELECT * FROM request WHERE id = $id", { id: ctx.params["id"] });
|
|
5373
6460
|
return ctx.json({ request: result[0]?.[0] });
|
|
5374
6461
|
});
|
|
5375
6462
|
this.router.get("/failures", async (ctx) => {
|
|
5376
|
-
const result = await this.db.query("SELECT * FROM
|
|
6463
|
+
const result = await this.db.query("SELECT * FROM failed_request ORDER BY timestamp DESC LIMIT 50");
|
|
5377
6464
|
return ctx.json({ failures: result[0] });
|
|
5378
6465
|
});
|
|
5379
6466
|
this.router.post("/replay", async (ctx) => {
|
|
@@ -5411,7 +6498,7 @@ class Dashboard {
|
|
|
5411
6498
|
"charts.js",
|
|
5412
6499
|
"failures.js",
|
|
5413
6500
|
"graph.mjs",
|
|
5414
|
-
"
|
|
6501
|
+
"client.js",
|
|
5415
6502
|
"reactflow.css",
|
|
5416
6503
|
"registry.css",
|
|
5417
6504
|
"registry.js",
|
|
@@ -5428,15 +6515,15 @@ class Dashboard {
|
|
|
5428
6515
|
else if (path2.endsWith(".js") || path2.endsWith(".mjs")) ctx.set("Content-Type", "application/javascript");
|
|
5429
6516
|
return ctx.send(content);
|
|
5430
6517
|
}
|
|
5431
|
-
const
|
|
5432
|
-
const
|
|
5433
|
-
this.getLinkPattern();
|
|
6518
|
+
const uptime = this.getUptime();
|
|
6519
|
+
const linkPattern = this.getLinkPattern();
|
|
5434
6520
|
const integrations = this.detectIntegrations();
|
|
5435
6521
|
const getRequestHeadersSource = this.dashboardConfig.getRequestHeaders ? this.dashboardConfig.getRequestHeaders.toString() : "undefined";
|
|
5436
6522
|
const html = renderToString(DashboardApp({
|
|
5437
6523
|
metrics: this.metrics,
|
|
5438
6524
|
uptime,
|
|
5439
6525
|
rootPath: process.cwd(),
|
|
6526
|
+
linkPattern,
|
|
5440
6527
|
integrations,
|
|
5441
6528
|
base: mountPath,
|
|
5442
6529
|
getRequestHeadersSource
|
|
@@ -5444,6 +6531,82 @@ class Dashboard {
|
|
|
5444
6531
|
return ctx.html(`<!DOCTYPE html>${html}`);
|
|
5445
6532
|
});
|
|
5446
6533
|
}
|
|
6534
|
+
getUptime() {
|
|
6535
|
+
const uptimeSeconds = Math.floor((Date.now() - this.startTime) / 1e3);
|
|
6536
|
+
return `${Math.floor(uptimeSeconds / 3600)}h ${Math.floor(uptimeSeconds % 3600 / 60)}m ${uptimeSeconds % 60}s`;
|
|
6537
|
+
}
|
|
6538
|
+
getPublicMetrics() {
|
|
6539
|
+
return {
|
|
6540
|
+
totalRequests: this.metrics.totalRequests,
|
|
6541
|
+
successfulRequests: this.metrics.successfulRequests,
|
|
6542
|
+
failedRequests: this.metrics.failedRequests,
|
|
6543
|
+
activeRequests: this.metrics.activeRequests,
|
|
6544
|
+
averageTotalTime_ms: this.metrics.averageTotalTime_ms,
|
|
6545
|
+
recentTimings: this.metrics.recentTimings,
|
|
6546
|
+
logs: [],
|
|
6547
|
+
// Don't broadcast logs for now to save bandwidth
|
|
6548
|
+
rateLimitedCounts: this.metrics.rateLimitedCounts,
|
|
6549
|
+
nodeMetrics: this.metrics.nodeMetrics,
|
|
6550
|
+
edgeMetrics: this.metrics.edgeMetrics
|
|
6551
|
+
};
|
|
6552
|
+
}
|
|
6553
|
+
broadcastMetricUpdate(metric) {
|
|
6554
|
+
if (this.clients.size === 0) return;
|
|
6555
|
+
const data = JSON.stringify({
|
|
6556
|
+
type: "metric-update",
|
|
6557
|
+
metric
|
|
6558
|
+
});
|
|
6559
|
+
for (const client of this.clients) {
|
|
6560
|
+
client.send(data);
|
|
6561
|
+
}
|
|
6562
|
+
}
|
|
6563
|
+
async sendHistory(ws, interval) {
|
|
6564
|
+
const intervalMap = {
|
|
6565
|
+
"10s": 10 * 1e3,
|
|
6566
|
+
"1m": 60 * 1e3,
|
|
6567
|
+
"5m": 5 * 60 * 1e3,
|
|
6568
|
+
"30m": 30 * 60 * 1e3,
|
|
6569
|
+
"1h": 60 * 60 * 1e3,
|
|
6570
|
+
"2h": 2 * 60 * 60 * 1e3,
|
|
6571
|
+
"6h": 6 * 60 * 60 * 1e3,
|
|
6572
|
+
"12h": 12 * 60 * 60 * 1e3,
|
|
6573
|
+
"1d": 24 * 60 * 60 * 1e3,
|
|
6574
|
+
"3d": 3 * 24 * 60 * 60 * 1e3,
|
|
6575
|
+
"7d": 7 * 24 * 60 * 60 * 1e3,
|
|
6576
|
+
"30d": 30 * 24 * 60 * 60 * 1e3
|
|
6577
|
+
};
|
|
6578
|
+
const periodMs = intervalMap[interval] || 60 * 1e3;
|
|
6579
|
+
const startTime = Date.now() - periodMs * 30;
|
|
6580
|
+
const endTime = Date.now();
|
|
6581
|
+
let history = [];
|
|
6582
|
+
try {
|
|
6583
|
+
const result = await this.db.query(
|
|
6584
|
+
"SELECT * FROM metric WHERE timestamp >= $start AND timestamp <= $end AND interval = $interval ORDER BY timestamp ASC",
|
|
6585
|
+
{ start: startTime, end: endTime, interval }
|
|
6586
|
+
);
|
|
6587
|
+
history = result[0] || [];
|
|
6588
|
+
} catch (e) {
|
|
6589
|
+
console.error("[Dashboard] Failed to fetch history for WS", e);
|
|
6590
|
+
}
|
|
6591
|
+
ws.send(JSON.stringify({
|
|
6592
|
+
type: "init",
|
|
6593
|
+
metrics: { ...this.metrics, logs: [] },
|
|
6594
|
+
uptime: this.getUptime(),
|
|
6595
|
+
history
|
|
6596
|
+
}));
|
|
6597
|
+
}
|
|
6598
|
+
broadcastMetrics() {
|
|
6599
|
+
if (this.clients.size === 0) return;
|
|
6600
|
+
console.log(`[Dashboard] Broadcasting metrics to ${this.clients.size} clients`);
|
|
6601
|
+
const data = JSON.stringify({
|
|
6602
|
+
type: "metrics",
|
|
6603
|
+
metrics: this.getPublicMetrics(),
|
|
6604
|
+
uptime: this.getUptime()
|
|
6605
|
+
});
|
|
6606
|
+
for (const client of this.clients) {
|
|
6607
|
+
client.send(data);
|
|
6608
|
+
}
|
|
6609
|
+
}
|
|
5447
6610
|
instrumentApp(app) {
|
|
5448
6611
|
if (!app.getComponentRegistry) return;
|
|
5449
6612
|
const registry = app.getComponentRegistry();
|
|
@@ -5473,6 +6636,11 @@ class Dashboard {
|
|
|
5473
6636
|
r.id = id;
|
|
5474
6637
|
this.assignIdsToRegistry(r.children, id);
|
|
5475
6638
|
});
|
|
6639
|
+
node.events?.forEach((e, idx) => {
|
|
6640
|
+
const id = makeId("event", parentId, idx, e.name);
|
|
6641
|
+
e.id = id;
|
|
6642
|
+
if (e._fn) e._fn._debugId = id;
|
|
6643
|
+
});
|
|
5476
6644
|
}
|
|
5477
6645
|
recordNodeMetric(id, type, duration, isError) {
|
|
5478
6646
|
if (!this.metrics.nodeMetrics[id]) {
|
|
@@ -5500,7 +6668,7 @@ class Dashboard {
|
|
|
5500
6668
|
if (["vscode", "cursor", "antigravity"].some((t) => term.includes(t))) {
|
|
5501
6669
|
return "vscode://file/{{absolute}}:{{line}}";
|
|
5502
6670
|
}
|
|
5503
|
-
return "file
|
|
6671
|
+
return "vscode://file/{{absolute}}:{{line}}";
|
|
5504
6672
|
}
|
|
5505
6673
|
getHooks() {
|
|
5506
6674
|
return {
|
|
@@ -5511,19 +6679,36 @@ class Dashboard {
|
|
|
5511
6679
|
}
|
|
5512
6680
|
this.metrics.totalRequests++;
|
|
5513
6681
|
this.metrics.activeRequests++;
|
|
5514
|
-
ctx._debugStartTime = performance.now();
|
|
5515
6682
|
ctx[$debug] = new Collector(this);
|
|
6683
|
+
if (!this.broadcastTimer) {
|
|
6684
|
+
this.broadcastTimer = setTimeout(() => {
|
|
6685
|
+
this.broadcastMetrics();
|
|
6686
|
+
this.broadcastTimer = void 0;
|
|
6687
|
+
}, 100);
|
|
6688
|
+
}
|
|
5516
6689
|
},
|
|
5517
6690
|
onResponseEnd: async (ctx, response) => {
|
|
6691
|
+
if (ctx.path.startsWith(this.mountPath)) return;
|
|
5518
6692
|
this.metrics.activeRequests = Math.max(0, this.metrics.activeRequests - 1);
|
|
5519
|
-
const
|
|
5520
|
-
|
|
5521
|
-
|
|
5522
|
-
|
|
5523
|
-
|
|
6693
|
+
const duration = performance.now() - ctx._startTime || 0;
|
|
6694
|
+
if (!response) {
|
|
6695
|
+
if (ctx.isUpgraded) {
|
|
6696
|
+
response = {
|
|
6697
|
+
status: 101,
|
|
6698
|
+
headers: {}
|
|
6699
|
+
};
|
|
6700
|
+
} else {
|
|
6701
|
+
return;
|
|
6702
|
+
}
|
|
5524
6703
|
}
|
|
5525
6704
|
const isError = response.status >= 400;
|
|
5526
6705
|
this.metricsCollector.recordRequest(duration, isError);
|
|
6706
|
+
if (!this.broadcastTimer) {
|
|
6707
|
+
this.broadcastTimer = setTimeout(() => {
|
|
6708
|
+
this.broadcastMetrics();
|
|
6709
|
+
this.broadcastTimer = void 0;
|
|
6710
|
+
}, 100);
|
|
6711
|
+
}
|
|
5527
6712
|
if (response.status >= 400) {
|
|
5528
6713
|
this.metrics.failedRequests++;
|
|
5529
6714
|
if (response.status === 429) {
|
|
@@ -5531,20 +6716,28 @@ class Dashboard {
|
|
|
5531
6716
|
this.metrics.rateLimitedCounts[path2] = (this.metrics.rateLimitedCounts[path2] || 0) + 1;
|
|
5532
6717
|
}
|
|
5533
6718
|
try {
|
|
5534
|
-
const
|
|
6719
|
+
const headers2 = {};
|
|
5535
6720
|
if (ctx.request.headers && typeof ctx.request.headers.forEach === "function") {
|
|
5536
6721
|
ctx.request.headers.forEach((v, k) => {
|
|
5537
|
-
|
|
6722
|
+
headers2[k] = v;
|
|
6723
|
+
});
|
|
6724
|
+
}
|
|
6725
|
+
const resHeaders2 = {};
|
|
6726
|
+
if (response.headers && typeof response.headers.forEach === "function") {
|
|
6727
|
+
response.headers.forEach((v, k) => {
|
|
6728
|
+
resHeaders2[k] = v;
|
|
5538
6729
|
});
|
|
5539
6730
|
}
|
|
5540
|
-
await this.db.upsert(new surrealdb.RecordId(
|
|
6731
|
+
await this.db.upsert(new surrealdb.RecordId(`failed_request`, ctx.requestId), {
|
|
5541
6732
|
method: ctx.method,
|
|
5542
6733
|
url: ctx.url.toString(),
|
|
5543
|
-
headers,
|
|
6734
|
+
headers: headers2,
|
|
5544
6735
|
status: response.status,
|
|
5545
6736
|
timestamp: Date.now(),
|
|
5546
|
-
state: ctx.state
|
|
5547
|
-
|
|
6737
|
+
state: ctx.state,
|
|
6738
|
+
body: this.serializeBody(ctx.bodyData || ctx.requestBody),
|
|
6739
|
+
responseHeaders: resHeaders2,
|
|
6740
|
+
responseBody: this.serializeBody(ctx.responseBody)
|
|
5548
6741
|
});
|
|
5549
6742
|
} catch (e) {
|
|
5550
6743
|
console.error("Failed to record failed request", e);
|
|
@@ -5552,17 +6745,58 @@ class Dashboard {
|
|
|
5552
6745
|
} else {
|
|
5553
6746
|
this.metrics.successfulRequests++;
|
|
5554
6747
|
}
|
|
6748
|
+
const urlObj = new URL(ctx.url.toString());
|
|
6749
|
+
const cookieHeader = ctx.request.headers.get("cookie") || "";
|
|
6750
|
+
const cookiesCount = cookieHeader ? cookieHeader.split(";").length : 0;
|
|
6751
|
+
const headers = {};
|
|
6752
|
+
if (ctx.request.headers && typeof ctx.request.headers.forEach === "function") {
|
|
6753
|
+
ctx.request.headers.forEach((v, k) => {
|
|
6754
|
+
headers[k] = v;
|
|
6755
|
+
});
|
|
6756
|
+
}
|
|
6757
|
+
const resHeaders = {};
|
|
6758
|
+
if (response.headers && typeof response.headers.forEach === "function") {
|
|
6759
|
+
response.headers.forEach((v, k) => {
|
|
6760
|
+
resHeaders[k] = v;
|
|
6761
|
+
});
|
|
6762
|
+
}
|
|
6763
|
+
const responseHeadersSize = Object.entries(response.headers || {}).reduce((acc, [k, v]) => acc + k.length + String(v).length + 2, 0);
|
|
6764
|
+
const responseSize = ctx.responseBody ? String(ctx.responseBody).length : 0;
|
|
6765
|
+
const remoteIP = ctx.request.headers.get("x-forwarded-for") || ctx.req?.socket?.remoteAddress;
|
|
5555
6766
|
const logEntry = {
|
|
5556
6767
|
method: ctx.method,
|
|
5557
6768
|
url: ctx.url.toString(),
|
|
5558
6769
|
status: response.status,
|
|
5559
6770
|
duration,
|
|
5560
6771
|
timestamp: Date.now(),
|
|
5561
|
-
handlerStack: ctx.handlerStack
|
|
6772
|
+
handlerStack: this.serializeHandlerStack(ctx.handlerStack),
|
|
6773
|
+
body: this.serializeBody(ctx.responseBody),
|
|
6774
|
+
requestBody: ctx.bodyData || ctx.requestBody,
|
|
6775
|
+
// ShokupanContext usually stores parsed body here if parsed
|
|
6776
|
+
contentType: response.headers["content-type"] || response.headers["Content-Type"],
|
|
6777
|
+
type: "xhr",
|
|
6778
|
+
direction: "inbound",
|
|
6779
|
+
size: responseSize,
|
|
6780
|
+
protocol: ctx.req?.httpVersion,
|
|
6781
|
+
// Try to get protocol from raw request if available, Bun might expose it
|
|
6782
|
+
domain: urlObj.hostname,
|
|
6783
|
+
path: urlObj.pathname,
|
|
6784
|
+
scheme: urlObj.protocol.replace(":", ""),
|
|
6785
|
+
cookies: cookiesCount,
|
|
6786
|
+
transferred: responseSize + responseHeadersSize,
|
|
6787
|
+
remoteIP,
|
|
6788
|
+
requestHeaders: headers,
|
|
6789
|
+
responseHeaders: resHeaders
|
|
5562
6790
|
};
|
|
5563
6791
|
this.metrics.logs.push(logEntry);
|
|
5564
6792
|
try {
|
|
5565
|
-
await this.db.
|
|
6793
|
+
await this.db.query("UPSERT $id CONTENT $data", {
|
|
6794
|
+
id: new surrealdb.RecordId("request", ctx.requestId),
|
|
6795
|
+
data: {
|
|
6796
|
+
...logEntry,
|
|
6797
|
+
direction: "inbound"
|
|
6798
|
+
}
|
|
6799
|
+
});
|
|
5566
6800
|
} catch (e) {
|
|
5567
6801
|
console.error("Failed to record request log", e);
|
|
5568
6802
|
}
|
|
@@ -5571,9 +6805,49 @@ class Dashboard {
|
|
|
5571
6805
|
if (this.metrics.logs.length > 0 && this.metrics.logs[0].timestamp < cutoff) {
|
|
5572
6806
|
this.metrics.logs = this.metrics.logs.filter((log) => log.timestamp >= cutoff);
|
|
5573
6807
|
}
|
|
6808
|
+
const requestData = {
|
|
6809
|
+
id: ctx.requestId,
|
|
6810
|
+
...logEntry
|
|
6811
|
+
};
|
|
6812
|
+
const strategy = this.dashboardConfig.updateStrategy || "immediate";
|
|
6813
|
+
if (strategy === "immediate") {
|
|
6814
|
+
this.broadcastRequestUpdates([requestData]);
|
|
6815
|
+
} else {
|
|
6816
|
+
this.requestsBuffer.push(requestData);
|
|
6817
|
+
}
|
|
5574
6818
|
}
|
|
5575
6819
|
};
|
|
5576
6820
|
}
|
|
6821
|
+
startRequestPushTimer() {
|
|
6822
|
+
const interval = this.dashboardConfig.updateInterval || 1e4;
|
|
6823
|
+
this.requestPushTimer = setInterval(() => {
|
|
6824
|
+
if (this.requestsBuffer.length > 0) {
|
|
6825
|
+
this.broadcastRequestUpdates();
|
|
6826
|
+
}
|
|
6827
|
+
}, interval);
|
|
6828
|
+
}
|
|
6829
|
+
broadcastRequestUpdates(requestsOverride) {
|
|
6830
|
+
if (this.clients.size === 0) {
|
|
6831
|
+
if (!requestsOverride) this.requestsBuffer = [];
|
|
6832
|
+
return;
|
|
6833
|
+
}
|
|
6834
|
+
let requests;
|
|
6835
|
+
if (requestsOverride) {
|
|
6836
|
+
requests = requestsOverride;
|
|
6837
|
+
} else {
|
|
6838
|
+
requests = [...this.requestsBuffer];
|
|
6839
|
+
this.requestsBuffer = [];
|
|
6840
|
+
}
|
|
6841
|
+
if (requests.length === 0) return;
|
|
6842
|
+
console.log(`[Dashboard] Broadcasting ${requests.length} requests. Sample ID: ${requests[0].id}`);
|
|
6843
|
+
const data = JSON.stringify({
|
|
6844
|
+
type: "requests-update",
|
|
6845
|
+
requests
|
|
6846
|
+
});
|
|
6847
|
+
for (const client of this.clients) {
|
|
6848
|
+
client.send(data);
|
|
6849
|
+
}
|
|
6850
|
+
}
|
|
5577
6851
|
updateTiming(duration) {
|
|
5578
6852
|
const alpha = 0.1;
|
|
5579
6853
|
if (this.metrics.averageTotalTime_ms === 0) {
|
|
@@ -5586,6 +6860,39 @@ class Dashboard {
|
|
|
5586
6860
|
this.metrics.recentTimings.shift();
|
|
5587
6861
|
}
|
|
5588
6862
|
}
|
|
6863
|
+
serializeHandlerStack(stack) {
|
|
6864
|
+
if (!stack || !Array.isArray(stack)) return [];
|
|
6865
|
+
return stack.map((item) => ({
|
|
6866
|
+
name: item.name,
|
|
6867
|
+
file: item.file,
|
|
6868
|
+
line: item.line,
|
|
6869
|
+
duration: item.duration,
|
|
6870
|
+
startTime: item.startTime,
|
|
6871
|
+
isBuiltin: item.isBuiltin
|
|
6872
|
+
// stateChanges: item.stateChanges // Exclude complex objects for now
|
|
6873
|
+
}));
|
|
6874
|
+
}
|
|
6875
|
+
serializeBody(body) {
|
|
6876
|
+
if (!body) return void 0;
|
|
6877
|
+
if (typeof body === "string") {
|
|
6878
|
+
if (body.length > 524288) {
|
|
6879
|
+
return body.substring(0, 524288) + "... (truncated)";
|
|
6880
|
+
}
|
|
6881
|
+
return body;
|
|
6882
|
+
}
|
|
6883
|
+
if (typeof body === "object") {
|
|
6884
|
+
try {
|
|
6885
|
+
const str = JSON.stringify(body);
|
|
6886
|
+
if (str.length > 524288) {
|
|
6887
|
+
return str.substring(0, 524288) + "... (truncated)";
|
|
6888
|
+
}
|
|
6889
|
+
return body;
|
|
6890
|
+
} catch (e) {
|
|
6891
|
+
return "[Circular or Non-Serializable Body]";
|
|
6892
|
+
}
|
|
6893
|
+
}
|
|
6894
|
+
return "[Binary or Unreadable Body]";
|
|
6895
|
+
}
|
|
5589
6896
|
}
|
|
5590
6897
|
function unknownError(ctx) {
|
|
5591
6898
|
return ctx.json({ error: "Unknown Error" }, 500);
|
|
@@ -5669,6 +6976,12 @@ class ScalarPlugin extends ShokupanRouter {
|
|
|
5669
6976
|
pluginOptions.config ??= {};
|
|
5670
6977
|
super();
|
|
5671
6978
|
this.pluginOptions = pluginOptions;
|
|
6979
|
+
this.metadata = {
|
|
6980
|
+
file: void 0,
|
|
6981
|
+
line: 1,
|
|
6982
|
+
name: "ScalarPlugin",
|
|
6983
|
+
pluginName: "Scalar"
|
|
6984
|
+
};
|
|
5672
6985
|
this.initRoutes();
|
|
5673
6986
|
}
|
|
5674
6987
|
eta;
|
|
@@ -5687,41 +7000,80 @@ class ScalarPlugin extends ShokupanRouter {
|
|
|
5687
7000
|
}
|
|
5688
7001
|
initRoutes() {
|
|
5689
7002
|
const bootId = Date.now().toString();
|
|
5690
|
-
this.get("/_lifecycle", (ctx) =>
|
|
7003
|
+
this.get("/_lifecycle", (ctx) => {
|
|
7004
|
+
const success = ctx.upgrade({
|
|
7005
|
+
data: {
|
|
7006
|
+
bootId,
|
|
7007
|
+
handler: {
|
|
7008
|
+
open: (ws) => {
|
|
7009
|
+
ws.send(JSON.stringify({ type: "hello", bootId }));
|
|
7010
|
+
}
|
|
7011
|
+
}
|
|
7012
|
+
}
|
|
7013
|
+
});
|
|
7014
|
+
if (success) return void 0;
|
|
7015
|
+
return ctx.json({ boot: bootId });
|
|
7016
|
+
});
|
|
5691
7017
|
this.get("/", async (ctx) => {
|
|
5692
7018
|
await this.ensureEta();
|
|
5693
|
-
let path2 = ctx.
|
|
7019
|
+
let path2 = ctx.path;
|
|
5694
7020
|
if (!path2.endsWith("/")) path2 += "/";
|
|
5695
7021
|
const devScript = ctx.app?.applicationConfig.development ? `
|
|
5696
7022
|
<script>
|
|
5697
7023
|
(function() {
|
|
5698
7024
|
const bootId = "${bootId}";
|
|
5699
|
-
let
|
|
7025
|
+
let ws;
|
|
7026
|
+
let reconnectTimer;
|
|
5700
7027
|
|
|
5701
|
-
|
|
5702
|
-
|
|
5703
|
-
|
|
5704
|
-
|
|
5705
|
-
|
|
5706
|
-
|
|
5707
|
-
|
|
5708
|
-
|
|
5709
|
-
|
|
5710
|
-
|
|
5711
|
-
|
|
7028
|
+
function connect() {
|
|
7029
|
+
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
7030
|
+
const wsUrl = protocol + '//' + window.location.host + '${path2}_lifecycle';
|
|
7031
|
+
|
|
7032
|
+
ws = new WebSocket(wsUrl);
|
|
7033
|
+
|
|
7034
|
+
ws.onopen = () => {
|
|
7035
|
+
console.log('[Scalar] Connected to lifecycle monitor');
|
|
7036
|
+
if (reconnectTimer) {
|
|
7037
|
+
clearTimeout(reconnectTimer);
|
|
7038
|
+
reconnectTimer = undefined;
|
|
5712
7039
|
}
|
|
5713
|
-
}
|
|
5714
|
-
|
|
5715
|
-
|
|
5716
|
-
|
|
5717
|
-
|
|
7040
|
+
};
|
|
7041
|
+
|
|
7042
|
+
ws.onmessage = (event) => {
|
|
7043
|
+
try {
|
|
7044
|
+
const data = JSON.parse(event.data);
|
|
7045
|
+
if (data.type === 'hello') {
|
|
7046
|
+
if (data.bootId !== bootId) {
|
|
7047
|
+
console.log('[Scalar] Server restarted (timestamp change), reloading...');
|
|
7048
|
+
window.location.reload();
|
|
7049
|
+
}
|
|
7050
|
+
}
|
|
7051
|
+
} catch (e) {}
|
|
7052
|
+
};
|
|
7053
|
+
|
|
7054
|
+
ws.onclose = () => {
|
|
7055
|
+
console.log('[Scalar] Lifecycle connection lost');
|
|
7056
|
+
ws = undefined;
|
|
7057
|
+
scheduleReconnect();
|
|
7058
|
+
};
|
|
7059
|
+
}
|
|
7060
|
+
|
|
7061
|
+
function scheduleReconnect() {
|
|
7062
|
+
if (reconnectTimer) return;
|
|
7063
|
+
reconnectTimer = setTimeout(() => {
|
|
7064
|
+
reconnectTimer = undefined;
|
|
7065
|
+
connect();
|
|
7066
|
+
}, 2000);
|
|
7067
|
+
}
|
|
7068
|
+
|
|
7069
|
+
connect();
|
|
5718
7070
|
})();
|
|
5719
7071
|
<\/script>
|
|
5720
7072
|
` : "";
|
|
5721
7073
|
let themeCss = "";
|
|
5722
7074
|
try {
|
|
5723
7075
|
try {
|
|
5724
|
-
themeCss = fs.readFileSync(path$1.join(process.cwd(), "src/theme.css"), "utf-8");
|
|
7076
|
+
themeCss = fs$1.readFileSync(path$1.join(process.cwd(), "src/theme.css"), "utf-8");
|
|
5725
7077
|
} catch {
|
|
5726
7078
|
}
|
|
5727
7079
|
} catch (e) {
|
|
@@ -5908,10 +7260,13 @@ function Cors(options = {}) {
|
|
|
5908
7260
|
};
|
|
5909
7261
|
const opts = { ...defaults2, ...options };
|
|
5910
7262
|
const corsMiddleware = async function CorsMiddleware(ctx, next) {
|
|
5911
|
-
const headers =
|
|
7263
|
+
const headers = {};
|
|
5912
7264
|
const origin = ctx.headers.get("origin");
|
|
5913
|
-
const set = (k, v) => headers
|
|
5914
|
-
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
|
+
};
|
|
5915
7270
|
if (origin === "null" && opts.origin !== "null") {
|
|
5916
7271
|
return next();
|
|
5917
7272
|
}
|
|
@@ -5970,10 +7325,10 @@ function Cors(options = {}) {
|
|
|
5970
7325
|
}
|
|
5971
7326
|
const response = await next();
|
|
5972
7327
|
if (response instanceof Response) {
|
|
5973
|
-
const
|
|
5974
|
-
for (let i = 0; i <
|
|
5975
|
-
const
|
|
5976
|
-
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]);
|
|
5977
7332
|
}
|
|
5978
7333
|
}
|
|
5979
7334
|
return response;
|