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