shokupan 0.10.5 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{analyzer-CKLGLFtx.cjs → analyzer-BAhvpNY_.cjs} +2 -7
- package/dist/{analyzer-CKLGLFtx.cjs.map → analyzer-BAhvpNY_.cjs.map} +1 -1
- package/dist/{analyzer-BqIe1p0R.js → analyzer-CnKnQ5KV.js} +3 -8
- package/dist/{analyzer-BqIe1p0R.js.map → analyzer-CnKnQ5KV.js.map} +1 -1
- package/dist/{analyzer.impl-D9Yi1Hax.cjs → analyzer.impl-CfpMu4-g.cjs} +586 -40
- package/dist/analyzer.impl-CfpMu4-g.cjs.map +1 -0
- package/dist/{analyzer.impl-CV6W1Eq7.js → analyzer.impl-DCiqlXI5.js} +586 -40
- package/dist/analyzer.impl-DCiqlXI5.js.map +1 -0
- package/dist/cli.cjs +206 -18
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +206 -18
- package/dist/cli.js.map +1 -1
- package/dist/context.d.ts +6 -1
- package/dist/index.cjs +2339 -984
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +2336 -982
- package/dist/index.js.map +1 -1
- package/dist/plugins/application/api-explorer/static/explorer-client.mjs +375 -29
- package/dist/plugins/application/api-explorer/static/style.css +327 -8
- package/dist/plugins/application/api-explorer/static/theme.css +7 -2
- package/dist/plugins/application/asyncapi/generator.d.ts +4 -0
- package/dist/plugins/application/asyncapi/static/asyncapi-client.mjs +154 -22
- package/dist/plugins/application/asyncapi/static/style.css +24 -8
- package/dist/plugins/application/dashboard/fetch-interceptor.d.ts +107 -0
- package/dist/plugins/application/dashboard/metrics-collector.d.ts +38 -2
- package/dist/plugins/application/dashboard/plugin.d.ts +44 -1
- package/dist/plugins/application/dashboard/static/charts.js +127 -62
- package/dist/plugins/application/dashboard/static/client.js +160 -0
- package/dist/plugins/application/dashboard/static/graph.mjs +167 -56
- package/dist/plugins/application/dashboard/static/reactflow.css +20 -10
- package/dist/plugins/application/dashboard/static/registry.js +112 -8
- package/dist/plugins/application/dashboard/static/requests.js +868 -58
- package/dist/plugins/application/dashboard/static/styles.css +186 -14
- package/dist/plugins/application/dashboard/static/tabs.js +44 -9
- package/dist/plugins/application/dashboard/static/theme.css +7 -2
- package/dist/plugins/application/openapi/analyzer.impl.d.ts +61 -1
- package/dist/plugins/application/openapi/openapi.d.ts +3 -0
- package/dist/plugins/application/shared/ast-utils.d.ts +7 -0
- package/dist/router.d.ts +55 -16
- package/dist/shokupan.d.ts +7 -2
- package/dist/util/adapter/adapters.d.ts +19 -0
- package/dist/util/adapter/filesystem.d.ts +20 -0
- package/dist/util/controller-scanner.d.ts +4 -0
- package/dist/util/cpu-monitor.d.ts +2 -0
- package/dist/util/middleware-tracker.d.ts +10 -0
- package/dist/util/types.d.ts +37 -0
- package/package.json +5 -5
- package/dist/analyzer.impl-CV6W1Eq7.js.map +0 -1
- package/dist/analyzer.impl-D9Yi1Hax.cjs.map +0 -1
- package/dist/http-server-BEMPIs33.cjs +0 -85
- package/dist/http-server-BEMPIs33.cjs.map +0 -1
- package/dist/http-server-CCeagTyU.js +0 -68
- package/dist/http-server-CCeagTyU.js.map +0 -1
- package/dist/plugins/application/dashboard/static/poll.js +0 -146
package/dist/index.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,90 +1102,43 @@ function analyzeHandler(handler) {
|
|
|
998
1102
|
}
|
|
999
1103
|
return { inferredSpec };
|
|
1000
1104
|
}
|
|
1001
|
-
async function getAstRoutes$1(applications) {
|
|
1002
|
-
const astRoutes = [];
|
|
1003
|
-
const getExpandedRoutes = (app, prefix = "", seen = /* @__PURE__ */ new Set(), sourceOverride) => {
|
|
1004
|
-
if (seen.has(app.name)) return [];
|
|
1005
|
-
const newSeen = new Set(seen);
|
|
1006
|
-
newSeen.add(app.name);
|
|
1007
|
-
const expanded = [];
|
|
1008
|
-
let currentPrefix = prefix;
|
|
1009
|
-
if (app.controllerPrefix) {
|
|
1010
|
-
const cleanPrefix = currentPrefix.endsWith("/") ? currentPrefix.slice(0, -1) : currentPrefix;
|
|
1011
|
-
const cleanCont = app.controllerPrefix.startsWith("/") ? app.controllerPrefix : "/" + app.controllerPrefix;
|
|
1012
|
-
currentPrefix = cleanPrefix + cleanCont;
|
|
1013
|
-
}
|
|
1014
|
-
for (const route of app.routes) {
|
|
1015
|
-
const cleanPrefix = currentPrefix.endsWith("/") ? currentPrefix.slice(0, -1) : currentPrefix;
|
|
1016
|
-
const cleanPath = route.path.startsWith("/") ? route.path : "/" + route.path;
|
|
1017
|
-
let joined = cleanPrefix + cleanPath;
|
|
1018
|
-
if (joined.length > 1 && joined.endsWith("/")) {
|
|
1019
|
-
joined = joined.slice(0, -1);
|
|
1020
|
-
}
|
|
1021
|
-
const expandedRoute = {
|
|
1022
|
-
...route,
|
|
1023
|
-
path: joined || "/"
|
|
1024
|
-
};
|
|
1025
|
-
if (sourceOverride) {
|
|
1026
|
-
expandedRoute.sourceContext = sourceOverride;
|
|
1027
|
-
}
|
|
1028
|
-
expanded.push(expandedRoute);
|
|
1029
|
-
}
|
|
1030
|
-
if (app.mounted) {
|
|
1031
|
-
for (const mount of app.mounted) {
|
|
1032
|
-
const targetApp = applications.find((a) => a.name === mount.target || a.className === mount.target);
|
|
1033
|
-
if (targetApp) {
|
|
1034
|
-
const cleanPrefix = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
|
|
1035
|
-
const mountPrefix = mount.prefix.startsWith("/") ? mount.prefix : "/" + mount.prefix;
|
|
1036
|
-
let nextSourceOverride = sourceOverride;
|
|
1037
|
-
if (mount.dependency || mount.targetFilePath && mount.targetFilePath.includes("node_modules")) {
|
|
1038
|
-
if (mount.sourceContext) {
|
|
1039
|
-
nextSourceOverride = {
|
|
1040
|
-
...mount.sourceContext,
|
|
1041
|
-
// Add highlight for the mount line to make it clear
|
|
1042
|
-
highlightLines: [mount.sourceContext.startLine, mount.sourceContext.endLine],
|
|
1043
|
-
highlights: [{
|
|
1044
|
-
startLine: mount.sourceContext.startLine,
|
|
1045
|
-
endLine: mount.sourceContext.endLine,
|
|
1046
|
-
type: "return-success"
|
|
1047
|
-
// Use the success color (cyan) for the mount point
|
|
1048
|
-
}]
|
|
1049
|
-
};
|
|
1050
|
-
}
|
|
1051
|
-
}
|
|
1052
|
-
expanded.push(...getExpandedRoutes(targetApp, cleanPrefix + mountPrefix, newSeen, nextSourceOverride));
|
|
1053
|
-
}
|
|
1054
|
-
}
|
|
1055
|
-
}
|
|
1056
|
-
return expanded;
|
|
1057
|
-
};
|
|
1058
|
-
applications.forEach((app) => {
|
|
1059
|
-
astRoutes.push(...getExpandedRoutes(app));
|
|
1060
|
-
});
|
|
1061
|
-
const dedupedRoutes = /* @__PURE__ */ new Map();
|
|
1062
|
-
for (const route of astRoutes) {
|
|
1063
|
-
const key = `${route.method.toUpperCase()}:${route.path}`;
|
|
1064
|
-
let score = 0;
|
|
1065
|
-
if (route.responseSchema) score += 10;
|
|
1066
|
-
if (route.handlerSource) score += 5;
|
|
1067
|
-
if (!dedupedRoutes.has(key) || score > dedupedRoutes.get(key).score) {
|
|
1068
|
-
dedupedRoutes.set(key, { route, score });
|
|
1069
|
-
}
|
|
1070
|
-
}
|
|
1071
|
-
return Array.from(dedupedRoutes.values()).map((v) => v.route);
|
|
1072
|
-
}
|
|
1073
1105
|
async function generateOpenApi(rootRouter, options = {}) {
|
|
1074
1106
|
const paths = {};
|
|
1075
1107
|
const tagGroups = /* @__PURE__ */ new Map();
|
|
1076
1108
|
const defaultTagGroup = options.defaultTagGroup || "General";
|
|
1077
1109
|
const defaultTagName = options.defaultTag || "Application";
|
|
1078
1110
|
let astRoutes = [];
|
|
1111
|
+
let astMiddlewareRegistry = {};
|
|
1112
|
+
let applications = [];
|
|
1079
1113
|
try {
|
|
1080
|
-
const { OpenAPIAnalyzer: OpenAPIAnalyzer2 } = await import("./analyzer-
|
|
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
1143
|
const collect = (router, prefix = "", currentGroup = defaultTagGroup, defaultTag = defaultTagName, inheritedMiddleware = [], isRootLevel = true) => {
|
|
1087
1144
|
let group = currentGroup;
|
|
@@ -1091,7 +1148,8 @@ async function generateOpenApi(rootRouter, options = {}) {
|
|
|
1091
1148
|
tag = router.config.name;
|
|
1092
1149
|
} else {
|
|
1093
1150
|
const mountPath = router[$mountPath];
|
|
1094
|
-
|
|
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,
|
|
@@ -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
|
}
|
|
@@ -1707,19 +2203,9 @@ class RouterTrie {
|
|
|
1707
2203
|
if (path === "/" || path === "") return [];
|
|
1708
2204
|
const s = path.startsWith("/") ? path.slice(1) : path;
|
|
1709
2205
|
if (s === "") return [];
|
|
1710
|
-
return s.split("/");
|
|
1711
|
-
}
|
|
1712
|
-
}
|
|
1713
|
-
const HTTPMethods = ["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS", "ALL"];
|
|
1714
|
-
var RouteParamType = /* @__PURE__ */ ((RouteParamType2) => {
|
|
1715
|
-
RouteParamType2["BODY"] = "BODY";
|
|
1716
|
-
RouteParamType2["PARAM"] = "PARAM";
|
|
1717
|
-
RouteParamType2["QUERY"] = "QUERY";
|
|
1718
|
-
RouteParamType2["HEADER"] = "HEADER";
|
|
1719
|
-
RouteParamType2["REQUEST"] = "REQUEST";
|
|
1720
|
-
RouteParamType2["CONTEXT"] = "CONTEXT";
|
|
1721
|
-
return RouteParamType2;
|
|
1722
|
-
})(RouteParamType || {});
|
|
2206
|
+
return s.split("/");
|
|
2207
|
+
}
|
|
2208
|
+
}
|
|
1723
2209
|
const RouterRegistry = /* @__PURE__ */ new Map();
|
|
1724
2210
|
const ShokupanApplicationTree = {};
|
|
1725
2211
|
class ShokupanRouter {
|
|
@@ -1728,6 +2214,9 @@ class ShokupanRouter {
|
|
|
1728
2214
|
if (config?.requestTimeout) {
|
|
1729
2215
|
this.requestTimeout = config.requestTimeout;
|
|
1730
2216
|
}
|
|
2217
|
+
if (config?.hooks) {
|
|
2218
|
+
this.ensureHooksInitialized();
|
|
2219
|
+
}
|
|
1731
2220
|
}
|
|
1732
2221
|
// Internal marker to identify Router vs. Application
|
|
1733
2222
|
[$isApplication] = false;
|
|
@@ -1739,6 +2228,47 @@ class ShokupanRouter {
|
|
|
1739
2228
|
[$parent] = null;
|
|
1740
2229
|
[$childRouters] = [];
|
|
1741
2230
|
[$childControllers] = [];
|
|
2231
|
+
_hasOnResponseEndHook;
|
|
2232
|
+
_hasOnRequestStartHook;
|
|
2233
|
+
_hasOnRequestEndHook;
|
|
2234
|
+
_hasOnResponseStartHook;
|
|
2235
|
+
_hasOnErrorHook;
|
|
2236
|
+
_hasOnRequestTimeoutHook;
|
|
2237
|
+
_hasOnReadTimeoutHook;
|
|
2238
|
+
_hasOnWriteTimeoutHook;
|
|
2239
|
+
_hasBeforeValidateHook;
|
|
2240
|
+
_hasAfterValidateHook;
|
|
2241
|
+
get hasOnResponseEndHook() {
|
|
2242
|
+
return this._hasOnResponseEndHook;
|
|
2243
|
+
}
|
|
2244
|
+
get hasOnRequestStartHook() {
|
|
2245
|
+
return this._hasOnRequestStartHook;
|
|
2246
|
+
}
|
|
2247
|
+
get hasOnRequestEndHook() {
|
|
2248
|
+
return this._hasOnRequestEndHook;
|
|
2249
|
+
}
|
|
2250
|
+
get hasOnResponseStartHook() {
|
|
2251
|
+
return this._hasOnResponseStartHook;
|
|
2252
|
+
}
|
|
2253
|
+
get hasOnErrorHook() {
|
|
2254
|
+
return this._hasOnErrorHook;
|
|
2255
|
+
}
|
|
2256
|
+
get hasOnRequestTimeoutHook() {
|
|
2257
|
+
return this._hasOnRequestTimeoutHook;
|
|
2258
|
+
}
|
|
2259
|
+
get hasOnReadTimeoutHook() {
|
|
2260
|
+
return this._hasOnReadTimeoutHook;
|
|
2261
|
+
}
|
|
2262
|
+
get hasOnWriteTimeoutHook() {
|
|
2263
|
+
return this._hasOnWriteTimeoutHook;
|
|
2264
|
+
}
|
|
2265
|
+
get hasBeforeValidateHook() {
|
|
2266
|
+
return this._hasBeforeValidateHook;
|
|
2267
|
+
}
|
|
2268
|
+
get hasAfterValidateHook() {
|
|
2269
|
+
return this._hasAfterValidateHook;
|
|
2270
|
+
}
|
|
2271
|
+
requestTimeout;
|
|
1742
2272
|
get db() {
|
|
1743
2273
|
return this.root?.db;
|
|
1744
2274
|
}
|
|
@@ -1774,7 +2304,7 @@ class ShokupanRouter {
|
|
|
1774
2304
|
const r = this[$routes][i];
|
|
1775
2305
|
const entry = {
|
|
1776
2306
|
type: "route",
|
|
1777
|
-
path: r.path,
|
|
2307
|
+
path: r.path.startsWith("/") ? r.path : "/" + r.path,
|
|
1778
2308
|
method: r.method,
|
|
1779
2309
|
metadata: r.metadata,
|
|
1780
2310
|
handlerName: r.handler.name,
|
|
@@ -1801,7 +2331,7 @@ class ShokupanRouter {
|
|
|
1801
2331
|
})) : [];
|
|
1802
2332
|
const routers = this[$childRouters].map((r) => ({
|
|
1803
2333
|
type: "router",
|
|
1804
|
-
path: r[$mountPath],
|
|
2334
|
+
path: r[$mountPath].startsWith("/") ? r[$mountPath] : "/" + r[$mountPath],
|
|
1805
2335
|
metadata: r.metadata,
|
|
1806
2336
|
children: r.getComponentRegistry()
|
|
1807
2337
|
}));
|
|
@@ -1815,12 +2345,25 @@ class ShokupanRouter {
|
|
|
1815
2345
|
children: { routes }
|
|
1816
2346
|
};
|
|
1817
2347
|
});
|
|
2348
|
+
const events = [];
|
|
2349
|
+
this.eventHandlers.forEach((handlers, name) => {
|
|
2350
|
+
handlers.forEach((h) => {
|
|
2351
|
+
events.push({
|
|
2352
|
+
type: "event",
|
|
2353
|
+
name,
|
|
2354
|
+
handlerName: h.name,
|
|
2355
|
+
metadata: h.source ? { file: h.source.file, line: h.source.line } : void 0,
|
|
2356
|
+
_fn: h
|
|
2357
|
+
});
|
|
2358
|
+
});
|
|
2359
|
+
});
|
|
1818
2360
|
return {
|
|
1819
2361
|
metadata: this.metadata,
|
|
1820
2362
|
middleware,
|
|
1821
2363
|
routes: localRoutes,
|
|
1822
2364
|
routers,
|
|
1823
|
-
controllers
|
|
2365
|
+
controllers,
|
|
2366
|
+
events
|
|
1824
2367
|
};
|
|
1825
2368
|
}
|
|
1826
2369
|
isRouterInstance(target) {
|
|
@@ -1843,12 +2386,38 @@ class ShokupanRouter {
|
|
|
1843
2386
|
}
|
|
1844
2387
|
return this;
|
|
1845
2388
|
}
|
|
2389
|
+
/**
|
|
2390
|
+
* Registers a lifecycle hook dynamically.
|
|
2391
|
+
*/
|
|
2392
|
+
hook(name, handler) {
|
|
2393
|
+
if (!this.hooksInitialized) {
|
|
2394
|
+
this.ensureHooksInitialized();
|
|
2395
|
+
}
|
|
2396
|
+
let handlers = this.hookCache.get(name);
|
|
2397
|
+
if (!handlers) {
|
|
2398
|
+
handlers = [];
|
|
2399
|
+
this.hookCache.set(name, handlers);
|
|
2400
|
+
this._hasOnErrorHook ||= name === "onError";
|
|
2401
|
+
this._hasOnRequestStartHook ||= name === "onRequestStart";
|
|
2402
|
+
this._hasOnRequestEndHook ||= name === "onRequestEnd";
|
|
2403
|
+
this._hasOnResponseStartHook ||= name === "onResponseStart";
|
|
2404
|
+
this._hasOnResponseEndHook ||= name === "onResponseEnd";
|
|
2405
|
+
this._hasOnRequestTimeoutHook ||= name === "onRequestTimeout";
|
|
2406
|
+
this._hasOnReadTimeoutHook ||= name === "onReadTimeout";
|
|
2407
|
+
this._hasOnWriteTimeoutHook ||= name === "onWriteTimeout";
|
|
2408
|
+
this._hasBeforeValidateHook ||= name === "beforeValidate";
|
|
2409
|
+
this._hasAfterValidateHook ||= name === "afterValidate";
|
|
2410
|
+
}
|
|
2411
|
+
handlers.push(handler);
|
|
2412
|
+
return this;
|
|
2413
|
+
}
|
|
1846
2414
|
/**
|
|
1847
2415
|
* Finds an event handler(s) by name.
|
|
1848
2416
|
*/
|
|
1849
2417
|
findEvent(name) {
|
|
1850
|
-
|
|
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
|
}
|
|
@@ -2036,264 +2611,23 @@ class ShokupanRouter {
|
|
|
2036
2611
|
throw new Error("Router is already mounted");
|
|
2037
2612
|
}
|
|
2038
2613
|
router[$mountPath] = prefix;
|
|
2039
|
-
if (!router.metadata) {
|
|
2040
|
-
const info = getCallerInfo();
|
|
2041
|
-
router.metadata = {
|
|
2042
|
-
file: info.file,
|
|
2043
|
-
line: info.line,
|
|
2044
|
-
name: "MountedRouter"
|
|
2045
|
-
};
|
|
2046
|
-
}
|
|
2047
|
-
this[$childRouters].push(router);
|
|
2048
|
-
router[$parent] = this;
|
|
2049
|
-
const setRouterContext = (router2) => {
|
|
2050
|
-
router2[$appRoot] = this.root;
|
|
2051
|
-
router2[$childRouters].forEach((child) => setRouterContext(child));
|
|
2052
|
-
};
|
|
2053
|
-
setRouterContext(router);
|
|
2054
|
-
router[$appRoot] = this.root;
|
|
2055
|
-
router[$isMounted] = true;
|
|
2056
|
-
}
|
|
2057
|
-
scanControllerRoutes(prefix, controller) {
|
|
2058
|
-
let instance = controller;
|
|
2059
|
-
if (typeof controller === "function") {
|
|
2060
|
-
instance = Container.resolve(controller);
|
|
2061
|
-
const controllerPath = controller[$controllerPath];
|
|
2062
|
-
if (controllerPath) {
|
|
2063
|
-
const p1 = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
|
|
2064
|
-
const p2 = controllerPath.startsWith("/") ? controllerPath : "/" + controllerPath;
|
|
2065
|
-
prefix = p1 + p2;
|
|
2066
|
-
if (!prefix) prefix = "/";
|
|
2067
|
-
}
|
|
2068
|
-
} else {
|
|
2069
|
-
const ctor = instance.constructor;
|
|
2070
|
-
const controllerPath = ctor[$controllerPath];
|
|
2071
|
-
if (controllerPath) {
|
|
2072
|
-
const p1 = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
|
|
2073
|
-
const p2 = controllerPath.startsWith("/") ? controllerPath : "/" + controllerPath;
|
|
2074
|
-
prefix = p1 + p2;
|
|
2075
|
-
if (!prefix) prefix = "/";
|
|
2076
|
-
}
|
|
2077
|
-
}
|
|
2078
|
-
instance[$mountPath] = prefix;
|
|
2079
|
-
const info = getCallerInfo();
|
|
2080
|
-
instance.metadata = {
|
|
2081
|
-
file: info.file,
|
|
2082
|
-
line: info.line,
|
|
2083
|
-
name: instance.constructor.name
|
|
2084
|
-
};
|
|
2085
|
-
this[$childControllers].push(instance);
|
|
2086
|
-
const controllerMiddleware = (typeof controller === "function" ? controller[$middleware] : instance[$middleware]) || [];
|
|
2087
|
-
const proto = Object.getPrototypeOf(instance);
|
|
2088
|
-
const methods = /* @__PURE__ */ new Set();
|
|
2089
|
-
let current = proto;
|
|
2090
|
-
while (current && current !== Object.prototype) {
|
|
2091
|
-
Object.getOwnPropertyNames(current).forEach((name) => methods.add(name));
|
|
2092
|
-
current = Object.getPrototypeOf(current);
|
|
2093
|
-
}
|
|
2094
|
-
Object.getOwnPropertyNames(instance).forEach((name) => methods.add(name));
|
|
2095
|
-
const decoratedRoutes = instance[$routeMethods] || proto && proto[$routeMethods];
|
|
2096
|
-
const decoratedArgs = instance[$routeArgs] || proto && proto[$routeArgs];
|
|
2097
|
-
const methodMiddlewareMap = instance[$middleware] || proto && proto[$middleware];
|
|
2098
|
-
const decoratedEvents = instance[$eventMethods] || proto && proto[$eventMethods];
|
|
2099
|
-
let routesAttached = 0;
|
|
2100
|
-
for (let i = 0; i < Array.from(methods).length; i++) {
|
|
2101
|
-
const name = Array.from(methods)[i];
|
|
2102
|
-
if (name === "constructor") continue;
|
|
2103
|
-
if (["arguments", "caller", "callee"].includes(name)) continue;
|
|
2104
|
-
const originalHandler = instance[name];
|
|
2105
|
-
if (typeof originalHandler !== "function") continue;
|
|
2106
|
-
let method;
|
|
2107
|
-
let subPath = "";
|
|
2108
|
-
let methodSource;
|
|
2109
|
-
if (decoratedRoutes && decoratedRoutes.has(name)) {
|
|
2110
|
-
const config = decoratedRoutes.get(name);
|
|
2111
|
-
method = config.method;
|
|
2112
|
-
subPath = config.path;
|
|
2113
|
-
methodSource = config.source;
|
|
2114
|
-
} else {
|
|
2115
|
-
for (let j = 0; j < HTTPMethods.length; j++) {
|
|
2116
|
-
const m = HTTPMethods[j];
|
|
2117
|
-
if (name.toUpperCase().startsWith(m)) {
|
|
2118
|
-
method = m;
|
|
2119
|
-
const rest = name.slice(m.length);
|
|
2120
|
-
if (rest.length === 0) {
|
|
2121
|
-
subPath = "/";
|
|
2122
|
-
} else {
|
|
2123
|
-
subPath = "";
|
|
2124
|
-
let buffer = "";
|
|
2125
|
-
const flush = () => {
|
|
2126
|
-
if (buffer.length > 0) {
|
|
2127
|
-
subPath += "/" + buffer.toLowerCase();
|
|
2128
|
-
buffer = "";
|
|
2129
|
-
}
|
|
2130
|
-
};
|
|
2131
|
-
for (let i2 = 0; i2 < rest.length; i2++) {
|
|
2132
|
-
const char = rest[i2];
|
|
2133
|
-
if (char === "$") {
|
|
2134
|
-
flush();
|
|
2135
|
-
subPath += "/:";
|
|
2136
|
-
continue;
|
|
2137
|
-
}
|
|
2138
|
-
buffer += char;
|
|
2139
|
-
}
|
|
2140
|
-
if (buffer.length > 0) flush();
|
|
2141
|
-
subPath = rest.replace(/\$/g, "/:").replace(/([a-z0-9])([A-Z])/g, "$1/$2").toLowerCase();
|
|
2142
|
-
if (!subPath.startsWith("/")) {
|
|
2143
|
-
subPath = "/" + subPath;
|
|
2144
|
-
}
|
|
2145
|
-
}
|
|
2146
|
-
break;
|
|
2147
|
-
}
|
|
2148
|
-
}
|
|
2149
|
-
}
|
|
2150
|
-
if (method) {
|
|
2151
|
-
routesAttached++;
|
|
2152
|
-
const cleanPrefix = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
|
|
2153
|
-
const cleanSubPath = subPath === "/" ? "" : subPath;
|
|
2154
|
-
let joined;
|
|
2155
|
-
if (cleanSubPath.length === 0) {
|
|
2156
|
-
joined = cleanPrefix;
|
|
2157
|
-
} else if (cleanSubPath.startsWith("/")) {
|
|
2158
|
-
joined = cleanPrefix + cleanSubPath;
|
|
2159
|
-
} else {
|
|
2160
|
-
joined = cleanPrefix + "/" + cleanSubPath;
|
|
2161
|
-
}
|
|
2162
|
-
const fullPath = joined || "/";
|
|
2163
|
-
const normalizedPath = fullPath.replace(/\/+/g, "/");
|
|
2164
|
-
const methodMw = methodMiddlewareMap instanceof Map ? methodMiddlewareMap.get(name) || [] : [];
|
|
2165
|
-
const allMiddleware = [...controllerMiddleware, ...methodMw];
|
|
2166
|
-
const routeArgs = decoratedArgs && decoratedArgs.get(name);
|
|
2167
|
-
const wrappedHandler = async (ctx) => {
|
|
2168
|
-
let args = [ctx];
|
|
2169
|
-
if (routeArgs?.length > 0) {
|
|
2170
|
-
args = [];
|
|
2171
|
-
const sortedArgs = [...routeArgs].sort((a, b) => a.index - b.index);
|
|
2172
|
-
for (let k = 0; k < sortedArgs.length; k++) {
|
|
2173
|
-
const arg = sortedArgs[k];
|
|
2174
|
-
switch (arg.type) {
|
|
2175
|
-
case RouteParamType.BODY:
|
|
2176
|
-
try {
|
|
2177
|
-
if (ctx.req.headers.get("content-type")?.includes("application/json")) {
|
|
2178
|
-
args[arg.index] = await ctx.req.json();
|
|
2179
|
-
} else {
|
|
2180
|
-
const text = await ctx.req.text();
|
|
2181
|
-
if (!text) {
|
|
2182
|
-
args[arg.index] = {};
|
|
2183
|
-
} else {
|
|
2184
|
-
args[arg.index] = JSON.parse(text);
|
|
2185
|
-
}
|
|
2186
|
-
}
|
|
2187
|
-
} catch (e) {
|
|
2188
|
-
const err = new Error("Invalid JSON body");
|
|
2189
|
-
err.status = 400;
|
|
2190
|
-
throw err;
|
|
2191
|
-
}
|
|
2192
|
-
break;
|
|
2193
|
-
case RouteParamType.PARAM:
|
|
2194
|
-
args[arg.index] = arg.name ? ctx.params[arg.name] : ctx.params;
|
|
2195
|
-
break;
|
|
2196
|
-
case RouteParamType.QUERY: {
|
|
2197
|
-
const url = new URL(ctx.req.url);
|
|
2198
|
-
if (arg.name) {
|
|
2199
|
-
const vals = url.searchParams.getAll(arg.name);
|
|
2200
|
-
args[arg.index] = vals.length > 1 ? vals : vals[0];
|
|
2201
|
-
} else {
|
|
2202
|
-
const query = {};
|
|
2203
|
-
const keys = Object.keys(url.searchParams);
|
|
2204
|
-
for (let k2 = 0; k2 < keys.length; k2++) {
|
|
2205
|
-
const key = keys[k2];
|
|
2206
|
-
const vals = url.searchParams.getAll(key);
|
|
2207
|
-
query[key] = vals.length > 1 ? vals : vals[0];
|
|
2208
|
-
}
|
|
2209
|
-
args[arg.index] = query;
|
|
2210
|
-
}
|
|
2211
|
-
break;
|
|
2212
|
-
}
|
|
2213
|
-
case RouteParamType.HEADER:
|
|
2214
|
-
args[arg.index] = arg.name ? ctx.req.headers.get(arg.name) : ctx.req.headers;
|
|
2215
|
-
break;
|
|
2216
|
-
case RouteParamType.REQUEST:
|
|
2217
|
-
args[arg.index] = ctx.req;
|
|
2218
|
-
break;
|
|
2219
|
-
case RouteParamType.CONTEXT:
|
|
2220
|
-
args[arg.index] = ctx;
|
|
2221
|
-
break;
|
|
2222
|
-
}
|
|
2223
|
-
}
|
|
2224
|
-
}
|
|
2225
|
-
const tracedOriginalHandler = ctx.app?.applicationConfig.enableTracing ? traceHandler(originalHandler, normalizedPath) : originalHandler;
|
|
2226
|
-
return tracedOriginalHandler.apply(instance, args);
|
|
2227
|
-
};
|
|
2228
|
-
let finalHandler = wrappedHandler;
|
|
2229
|
-
if (allMiddleware.length > 0) {
|
|
2230
|
-
const composed = compose(allMiddleware);
|
|
2231
|
-
finalHandler = async (ctx) => {
|
|
2232
|
-
return composed(ctx, () => wrappedHandler(ctx));
|
|
2233
|
-
};
|
|
2234
|
-
}
|
|
2235
|
-
finalHandler.originalHandler = originalHandler;
|
|
2236
|
-
if (finalHandler !== wrappedHandler) {
|
|
2237
|
-
wrappedHandler.originalHandler = originalHandler;
|
|
2238
|
-
}
|
|
2239
|
-
const tagName = instance.constructor.name;
|
|
2240
|
-
const decoratedSpecs = instance[$routeSpec] || proto && proto[$routeSpec];
|
|
2241
|
-
const userSpec = decoratedSpecs && decoratedSpecs.get(name);
|
|
2242
|
-
const spec = { tags: [tagName], ...userSpec };
|
|
2243
|
-
this.add({
|
|
2244
|
-
method,
|
|
2245
|
-
path: normalizedPath,
|
|
2246
|
-
handler: finalHandler,
|
|
2247
|
-
spec,
|
|
2248
|
-
controller: instance,
|
|
2249
|
-
metadata: methodSource || instance.metadata,
|
|
2250
|
-
middleware: allMiddleware
|
|
2251
|
-
// Capture all resolved middleware
|
|
2252
|
-
});
|
|
2253
|
-
}
|
|
2254
|
-
if (decoratedEvents?.has(name)) {
|
|
2255
|
-
routesAttached++;
|
|
2256
|
-
const config = decoratedEvents.get(name);
|
|
2257
|
-
const routeArgs = decoratedArgs?.get(name);
|
|
2258
|
-
const wrappedHandler = async (ctx) => {
|
|
2259
|
-
let args = [ctx];
|
|
2260
|
-
if (routeArgs?.length > 0) {
|
|
2261
|
-
args = [];
|
|
2262
|
-
const sortedArgs = [...routeArgs].sort((a, b) => a.index - b.index);
|
|
2263
|
-
for (let k = 0; k < sortedArgs.length; k++) {
|
|
2264
|
-
const arg = sortedArgs[k];
|
|
2265
|
-
switch (arg.type) {
|
|
2266
|
-
case RouteParamType.BODY:
|
|
2267
|
-
args[arg.index] = await ctx.body();
|
|
2268
|
-
break;
|
|
2269
|
-
case RouteParamType.CONTEXT:
|
|
2270
|
-
args[arg.index] = ctx;
|
|
2271
|
-
break;
|
|
2272
|
-
case RouteParamType.REQUEST:
|
|
2273
|
-
args[arg.index] = ctx.req;
|
|
2274
|
-
break;
|
|
2275
|
-
case RouteParamType.HEADER:
|
|
2276
|
-
args[arg.index] = arg.name ? ctx.req.headers.get(arg.name) : ctx.req.headers;
|
|
2277
|
-
break;
|
|
2278
|
-
default:
|
|
2279
|
-
args[arg.index] = void 0;
|
|
2280
|
-
}
|
|
2281
|
-
}
|
|
2282
|
-
}
|
|
2283
|
-
return originalHandler.apply(instance, args);
|
|
2284
|
-
};
|
|
2285
|
-
const decoratedSpecs = instance[$routeSpec] || proto && proto[$routeSpec];
|
|
2286
|
-
const userSpec = decoratedSpecs && decoratedSpecs.get(name);
|
|
2287
|
-
const spec = { tags: [{ name: instance.constructor.name }], ...userSpec };
|
|
2288
|
-
wrappedHandler.spec = spec;
|
|
2289
|
-
wrappedHandler.originalHandler = originalHandler;
|
|
2290
|
-
this.event(config.eventName, wrappedHandler);
|
|
2291
|
-
}
|
|
2292
|
-
}
|
|
2293
|
-
if (routesAttached === 0) {
|
|
2294
|
-
console.warn(`No routes attached to controller ${instance.constructor.name}`);
|
|
2614
|
+
if (!router.metadata) {
|
|
2615
|
+
const info = getCallerInfo();
|
|
2616
|
+
router.metadata = {
|
|
2617
|
+
file: info.file,
|
|
2618
|
+
line: info.line,
|
|
2619
|
+
name: "MountedRouter"
|
|
2620
|
+
};
|
|
2295
2621
|
}
|
|
2296
|
-
|
|
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
|
}
|
|
@@ -2606,113 +2878,376 @@ class ShokupanRouter {
|
|
|
2606
2878
|
description: "Serves static files from " + normalizedPrefix,
|
|
2607
2879
|
tags: [groupName]
|
|
2608
2880
|
};
|
|
2609
|
-
const spec = config.openapi ? config.openapi : defaultSpec;
|
|
2610
|
-
if (!spec.tags) spec.tags = [groupName];
|
|
2611
|
-
else if (!spec.tags.includes(groupName)) spec.tags.push(groupName);
|
|
2612
|
-
const pattern = `^${normalizedPrefix}(/.*)?$`;
|
|
2613
|
-
const regex = new RegExp(pattern);
|
|
2614
|
-
const displayPath = normalizedPrefix === "/" ? "/*" : normalizedPrefix + "/*";
|
|
2615
|
-
this.add({ method: "GET", path: displayPath, handler: routeHandler, spec, regex });
|
|
2616
|
-
this.add({ method: "HEAD", path: displayPath, handler: routeHandler, spec, regex });
|
|
2617
|
-
return this;
|
|
2881
|
+
const spec = config.openapi ? config.openapi : defaultSpec;
|
|
2882
|
+
if (!spec.tags) spec.tags = [groupName];
|
|
2883
|
+
else if (!spec.tags.includes(groupName)) spec.tags.push(groupName);
|
|
2884
|
+
const pattern = `^${normalizedPrefix}(/.*)?$`;
|
|
2885
|
+
const regex = new RegExp(pattern);
|
|
2886
|
+
const displayPath = normalizedPrefix === "/" ? "/*" : normalizedPrefix + "/*";
|
|
2887
|
+
this.add({ method: "GET", path: displayPath, handler: routeHandler, spec, regex });
|
|
2888
|
+
this.add({ method: "HEAD", path: displayPath, handler: routeHandler, spec, regex });
|
|
2889
|
+
return this;
|
|
2890
|
+
}
|
|
2891
|
+
/**
|
|
2892
|
+
* Attach the verb routes with their overload signatures.
|
|
2893
|
+
* Use compose to handle multiple handlers (middleware).
|
|
2894
|
+
*/
|
|
2895
|
+
attachVerb(method, path, ...args) {
|
|
2896
|
+
let spec;
|
|
2897
|
+
let handlers = [];
|
|
2898
|
+
if (args.length > 0) {
|
|
2899
|
+
if (typeof args[0] === "object" && args[0] !== null) {
|
|
2900
|
+
spec = args[0];
|
|
2901
|
+
handlers = args.slice(1);
|
|
2902
|
+
} else {
|
|
2903
|
+
handlers = args;
|
|
2904
|
+
}
|
|
2905
|
+
}
|
|
2906
|
+
if (handlers.length === 0) {
|
|
2907
|
+
return;
|
|
2908
|
+
}
|
|
2909
|
+
let finalHandler = handlers[handlers.length - 1];
|
|
2910
|
+
if (handlers.length > 1) {
|
|
2911
|
+
const fn = compose(handlers);
|
|
2912
|
+
finalHandler = (ctx) => fn(ctx);
|
|
2913
|
+
}
|
|
2914
|
+
this.add({
|
|
2915
|
+
method,
|
|
2916
|
+
path,
|
|
2917
|
+
spec,
|
|
2918
|
+
handler: finalHandler,
|
|
2919
|
+
middleware: handlers.slice(0, handlers.length - 1)
|
|
2920
|
+
});
|
|
2921
|
+
}
|
|
2922
|
+
/**
|
|
2923
|
+
* Generates an OpenAPI 3.1 Document by recursing through the router and its descendants.
|
|
2924
|
+
* Now includes runtime analysis of handler functions to infer request/response types.
|
|
2925
|
+
*/
|
|
2926
|
+
generateApiSpec(options = {}) {
|
|
2927
|
+
return generateOpenApi(this, options);
|
|
2928
|
+
}
|
|
2929
|
+
hasHooks(name) {
|
|
2930
|
+
if (!this.hooksInitialized) {
|
|
2931
|
+
this.ensureHooksInitialized();
|
|
2932
|
+
}
|
|
2933
|
+
const hooks = this.hookCache.get(name);
|
|
2934
|
+
return hooks !== void 0 && hooks.length > 0;
|
|
2935
|
+
}
|
|
2936
|
+
ensureHooksInitialized() {
|
|
2937
|
+
const hooks = this.config?.hooks;
|
|
2938
|
+
if (hooks) {
|
|
2939
|
+
const hookList = Array.isArray(hooks) ? hooks : [hooks];
|
|
2940
|
+
const hookTypes = [
|
|
2941
|
+
"onRequestStart",
|
|
2942
|
+
"onRequestEnd",
|
|
2943
|
+
"onResponseStart",
|
|
2944
|
+
"onResponseEnd",
|
|
2945
|
+
"onError",
|
|
2946
|
+
"beforeValidate",
|
|
2947
|
+
"afterValidate",
|
|
2948
|
+
"onRequestTimeout",
|
|
2949
|
+
"onReadTimeout",
|
|
2950
|
+
"onWriteTimeout"
|
|
2951
|
+
];
|
|
2952
|
+
for (let i = 0; i < hookTypes.length; i++) {
|
|
2953
|
+
const type = hookTypes[i];
|
|
2954
|
+
const fns = [];
|
|
2955
|
+
for (let j = 0; j < hookList.length; j++) {
|
|
2956
|
+
const h = hookList[j];
|
|
2957
|
+
if (h[type]) fns.push(h[type]);
|
|
2958
|
+
}
|
|
2959
|
+
if (fns.length > 0) {
|
|
2960
|
+
this._hasOnErrorHook ||= type === "onError";
|
|
2961
|
+
this._hasOnRequestStartHook ||= type === "onRequestStart";
|
|
2962
|
+
this._hasOnRequestEndHook ||= type === "onRequestEnd";
|
|
2963
|
+
this._hasOnResponseStartHook ||= type === "onResponseStart";
|
|
2964
|
+
this._hasOnResponseEndHook ||= type === "onResponseEnd";
|
|
2965
|
+
this._hasOnRequestTimeoutHook ||= type === "onRequestTimeout";
|
|
2966
|
+
this._hasOnReadTimeoutHook ||= type === "onReadTimeout";
|
|
2967
|
+
this._hasOnWriteTimeoutHook ||= type === "onWriteTimeout";
|
|
2968
|
+
this._hasBeforeValidateHook ||= type === "beforeValidate";
|
|
2969
|
+
this._hasAfterValidateHook ||= type === "afterValidate";
|
|
2970
|
+
this.hookCache.set(type, fns);
|
|
2971
|
+
}
|
|
2972
|
+
}
|
|
2973
|
+
}
|
|
2974
|
+
this.hooksInitialized = true;
|
|
2975
|
+
}
|
|
2976
|
+
runHooks(name, ...args) {
|
|
2977
|
+
if (!this.hooksInitialized) {
|
|
2978
|
+
this.ensureHooksInitialized();
|
|
2979
|
+
}
|
|
2980
|
+
const fns = this.hookCache.get(name);
|
|
2981
|
+
if (!fns) return;
|
|
2982
|
+
const ctx = args?.[0] instanceof ShokupanContext ? args[0] : void 0;
|
|
2983
|
+
const debug = ctx?.[$debug];
|
|
2984
|
+
if (debug) {
|
|
2985
|
+
return Promise.all(fns.map(async (fn, index) => {
|
|
2986
|
+
const hookId = `hook_${name}_${fn.name || index}`;
|
|
2987
|
+
const previousNode = debug.getCurrentNode();
|
|
2988
|
+
debug.trackEdge(previousNode, hookId);
|
|
2989
|
+
debug.setNode(hookId);
|
|
2990
|
+
const start = performance.now();
|
|
2991
|
+
try {
|
|
2992
|
+
await fn(...args);
|
|
2993
|
+
const duration = performance.now() - start;
|
|
2994
|
+
debug.trackStep(hookId, "hook", duration, "success");
|
|
2995
|
+
} catch (error) {
|
|
2996
|
+
const duration = performance.now() - start;
|
|
2997
|
+
debug.trackStep(hookId, "hook", duration, "error", error);
|
|
2998
|
+
throw error;
|
|
2999
|
+
} finally {
|
|
3000
|
+
if (previousNode) debug.setNode(previousNode);
|
|
3001
|
+
}
|
|
3002
|
+
}));
|
|
3003
|
+
} else {
|
|
3004
|
+
return Promise.all(fns.map((fn) => fn(...args)));
|
|
3005
|
+
}
|
|
3006
|
+
}
|
|
3007
|
+
}
|
|
3008
|
+
function createHttpServer() {
|
|
3009
|
+
return async (options) => {
|
|
3010
|
+
const server = http$1.createServer(async (req, res) => {
|
|
3011
|
+
const url = new URL(req.url, `http://${req.headers.host}`);
|
|
3012
|
+
const request = new Request(url.toString(), {
|
|
3013
|
+
method: req.method,
|
|
3014
|
+
headers: req.headers,
|
|
3015
|
+
body: ["GET", "HEAD"].includes(req.method) ? void 0 : new ReadableStream({
|
|
3016
|
+
start(controller) {
|
|
3017
|
+
req.on("data", (chunk) => controller.enqueue(chunk));
|
|
3018
|
+
req.on("end", () => controller.close());
|
|
3019
|
+
req.on("error", (err) => controller.error(err));
|
|
3020
|
+
}
|
|
3021
|
+
}),
|
|
3022
|
+
// Required for Node.js undici when sending a body
|
|
3023
|
+
duplex: "half"
|
|
3024
|
+
});
|
|
3025
|
+
const response = await options.fetch(request, fauxServer);
|
|
3026
|
+
res.statusCode = response.status;
|
|
3027
|
+
response.headers.forEach((v, k) => res.setHeader(k, v));
|
|
3028
|
+
if (response.body) {
|
|
3029
|
+
const buffer = await response.arrayBuffer();
|
|
3030
|
+
res.end(Buffer.from(buffer));
|
|
3031
|
+
} else {
|
|
3032
|
+
res.end();
|
|
3033
|
+
}
|
|
3034
|
+
});
|
|
3035
|
+
const fauxServer = {
|
|
3036
|
+
stop: () => {
|
|
3037
|
+
server.close();
|
|
3038
|
+
return Promise.resolve();
|
|
3039
|
+
},
|
|
3040
|
+
upgrade(req, options2) {
|
|
3041
|
+
return false;
|
|
3042
|
+
},
|
|
3043
|
+
reload(options2) {
|
|
3044
|
+
return fauxServer;
|
|
3045
|
+
},
|
|
3046
|
+
get port() {
|
|
3047
|
+
const addr = server.address();
|
|
3048
|
+
if (typeof addr === "object" && addr !== null) {
|
|
3049
|
+
return addr.port;
|
|
3050
|
+
}
|
|
3051
|
+
return options.port;
|
|
3052
|
+
},
|
|
3053
|
+
hostname: options.hostname,
|
|
3054
|
+
development: options.development,
|
|
3055
|
+
pendingRequests: 0,
|
|
3056
|
+
requestIP: (req) => null,
|
|
3057
|
+
publish: () => 0,
|
|
3058
|
+
subscriberCount: () => 0,
|
|
3059
|
+
url: new URL(`http://${options.hostname}:${options.port}`),
|
|
3060
|
+
// Expose the raw Node.js server for generic socket/websocket support (e.g. Socket.IO)
|
|
3061
|
+
nodeServer: server
|
|
3062
|
+
};
|
|
3063
|
+
return new Promise((resolve2) => {
|
|
3064
|
+
server.listen(options.port, options.hostname, () => {
|
|
3065
|
+
resolve2(fauxServer);
|
|
3066
|
+
});
|
|
3067
|
+
});
|
|
3068
|
+
};
|
|
3069
|
+
}
|
|
3070
|
+
class BunAdapter {
|
|
3071
|
+
server;
|
|
3072
|
+
async listen(port, app) {
|
|
3073
|
+
if (typeof Bun === "undefined") {
|
|
3074
|
+
throw new Error("BunAdapter requires the Bun runtime.");
|
|
3075
|
+
}
|
|
3076
|
+
const serveOptions = {
|
|
3077
|
+
port,
|
|
3078
|
+
hostname: app.applicationConfig.hostname,
|
|
3079
|
+
development: app.applicationConfig.development,
|
|
3080
|
+
fetch: app.fetch.bind(app),
|
|
3081
|
+
reusePort: app.applicationConfig.reusePort,
|
|
3082
|
+
idleTimeout: app.applicationConfig.readTimeout ? app.applicationConfig.readTimeout / 1e3 : void 0,
|
|
3083
|
+
websocket: {
|
|
3084
|
+
// @ts-ignore
|
|
3085
|
+
open(ws) {
|
|
3086
|
+
ws.data?.handler?.open?.(ws);
|
|
3087
|
+
},
|
|
3088
|
+
// @ts-ignore
|
|
3089
|
+
async message(ws, message) {
|
|
3090
|
+
if (ws.data?.handler?.message) {
|
|
3091
|
+
return ws.data.handler.message(ws, message);
|
|
3092
|
+
}
|
|
3093
|
+
let msgString = "";
|
|
3094
|
+
if (typeof message === "string") {
|
|
3095
|
+
msgString = message;
|
|
3096
|
+
} else if (message instanceof Uint8Array || message instanceof ArrayBuffer) {
|
|
3097
|
+
msgString = new TextDecoder().decode(message);
|
|
3098
|
+
} else if (typeof Buffer !== "undefined" && message instanceof Buffer) {
|
|
3099
|
+
msgString = message.toString();
|
|
3100
|
+
} else {
|
|
3101
|
+
return;
|
|
3102
|
+
}
|
|
3103
|
+
if (typeof msgString !== "string") return;
|
|
3104
|
+
let payload;
|
|
3105
|
+
let isJSONPayload = false;
|
|
3106
|
+
if (msgString.startsWith("{")) {
|
|
3107
|
+
try {
|
|
3108
|
+
payload = JSON.parse(msgString);
|
|
3109
|
+
isJSONPayload = true;
|
|
3110
|
+
} catch {
|
|
3111
|
+
}
|
|
3112
|
+
}
|
|
3113
|
+
if (payload) {
|
|
3114
|
+
const self = app;
|
|
3115
|
+
if (isJSONPayload && self.applicationConfig["enableHttpBridge"] && payload.type === "HTTP") {
|
|
3116
|
+
const { id, method, path, headers, body } = payload;
|
|
3117
|
+
const url = new URL(path, `http://${self.applicationConfig.hostname || "localhost"}:${port}`);
|
|
3118
|
+
const req = new Request(url.toString(), {
|
|
3119
|
+
method,
|
|
3120
|
+
headers,
|
|
3121
|
+
body: typeof body === "object" ? JSON.stringify(body) : body
|
|
3122
|
+
});
|
|
3123
|
+
const res = await self.fetch(req);
|
|
3124
|
+
const resBody = await res.json().catch((err) => res.text());
|
|
3125
|
+
const resHeaders = {};
|
|
3126
|
+
res.headers.forEach((v, k) => resHeaders[k] = v);
|
|
3127
|
+
ws.send(JSON.stringify({
|
|
3128
|
+
type: "RESPONSE",
|
|
3129
|
+
id,
|
|
3130
|
+
status: res.status,
|
|
3131
|
+
headers: resHeaders,
|
|
3132
|
+
body: resBody
|
|
3133
|
+
}));
|
|
3134
|
+
return;
|
|
3135
|
+
}
|
|
3136
|
+
const eventName = payload.event || (payload.type === "EVENT" ? payload.name : void 0);
|
|
3137
|
+
if (eventName) {
|
|
3138
|
+
const handlers = self.findEvent(eventName);
|
|
3139
|
+
const handler = handlers?.length == 1 ? handlers[0] : compose(handlers || []);
|
|
3140
|
+
if (handler) {
|
|
3141
|
+
const data = payload.data || payload.body || payload.payload || payload;
|
|
3142
|
+
const req = new ShokupanRequest({
|
|
3143
|
+
url: `http://${self.applicationConfig.hostname || "localhost"}/event/${eventName}`,
|
|
3144
|
+
method: "POST",
|
|
3145
|
+
headers: new Headers({ "content-type": "application/json" }),
|
|
3146
|
+
body: JSON.stringify(data)
|
|
3147
|
+
});
|
|
3148
|
+
const ctx = new ShokupanContext(
|
|
3149
|
+
// @ts-ignore
|
|
3150
|
+
req,
|
|
3151
|
+
// @ts-ignore
|
|
3152
|
+
self.server,
|
|
3153
|
+
{},
|
|
3154
|
+
self,
|
|
3155
|
+
null,
|
|
3156
|
+
self.applicationConfig.enableMiddlewareTracking,
|
|
3157
|
+
payload.id
|
|
3158
|
+
);
|
|
3159
|
+
ctx[$ws] = ws;
|
|
3160
|
+
ws.data ??= {};
|
|
3161
|
+
ws.data["ctx"] = ctx;
|
|
3162
|
+
try {
|
|
3163
|
+
await handler(ctx);
|
|
3164
|
+
} catch (err) {
|
|
3165
|
+
if (self.applicationConfig["websocketErrorHandler"]) {
|
|
3166
|
+
await self.applicationConfig["websocketErrorHandler"](err, ctx);
|
|
3167
|
+
} else {
|
|
3168
|
+
console.error(`Error in event ${eventName}:`, err);
|
|
3169
|
+
}
|
|
3170
|
+
}
|
|
3171
|
+
}
|
|
3172
|
+
}
|
|
3173
|
+
}
|
|
3174
|
+
},
|
|
3175
|
+
// @ts-ignore
|
|
3176
|
+
drain(ws) {
|
|
3177
|
+
ws.data?.handler?.drain?.(ws);
|
|
3178
|
+
},
|
|
3179
|
+
// @ts-ignore
|
|
3180
|
+
close(ws, code, reason) {
|
|
3181
|
+
ws.data?.handler?.close?.(ws, code, reason);
|
|
3182
|
+
const ctx = ws.data?.["ctx"];
|
|
3183
|
+
if (ctx && typeof ctx.getDisconnectCallbacks === "function") {
|
|
3184
|
+
const callbacks = ctx.getDisconnectCallbacks();
|
|
3185
|
+
if (Array.isArray(callbacks) && callbacks.length > 0) {
|
|
3186
|
+
Promise.all(callbacks.map((cb) => cb())).catch((err) => {
|
|
3187
|
+
console.error("Error executing socket disconnect hook:", err);
|
|
3188
|
+
});
|
|
3189
|
+
}
|
|
3190
|
+
}
|
|
3191
|
+
}
|
|
3192
|
+
}
|
|
3193
|
+
};
|
|
3194
|
+
this.server = Bun.serve(serveOptions);
|
|
3195
|
+
return this.server;
|
|
2618
3196
|
}
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
*/
|
|
2623
|
-
attachVerb(method, path, ...args) {
|
|
2624
|
-
let spec;
|
|
2625
|
-
let handlers = [];
|
|
2626
|
-
if (args.length > 0) {
|
|
2627
|
-
if (typeof args[0] === "object" && args[0] !== null) {
|
|
2628
|
-
spec = args[0];
|
|
2629
|
-
handlers = args.slice(1);
|
|
2630
|
-
} else {
|
|
2631
|
-
handlers = args;
|
|
2632
|
-
}
|
|
2633
|
-
}
|
|
2634
|
-
if (handlers.length === 0) {
|
|
2635
|
-
return;
|
|
2636
|
-
}
|
|
2637
|
-
let finalHandler = handlers[handlers.length - 1];
|
|
2638
|
-
if (handlers.length > 1) {
|
|
2639
|
-
const fn = compose(handlers);
|
|
2640
|
-
finalHandler = (ctx) => fn(ctx);
|
|
3197
|
+
async stop() {
|
|
3198
|
+
if (this.server) {
|
|
3199
|
+
this.server.stop();
|
|
2641
3200
|
}
|
|
2642
|
-
this.add({
|
|
2643
|
-
method,
|
|
2644
|
-
path,
|
|
2645
|
-
spec,
|
|
2646
|
-
handler: finalHandler,
|
|
2647
|
-
middleware: handlers.slice(0, handlers.length - 1)
|
|
2648
|
-
});
|
|
2649
3201
|
}
|
|
2650
|
-
|
|
2651
|
-
|
|
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
|
};
|
|
@@ -3658,16 +4158,48 @@ function ApiExplorerApp({ spec, asyncSpec, config }) {
|
|
|
3658
4158
|
const commonPrefix = findCommonPrefix(routes);
|
|
3659
4159
|
const commonPrefixPath = "/" + commonPrefix.join("/");
|
|
3660
4160
|
const children = createSubgroups(routes, 0, commonPrefix.length);
|
|
4161
|
+
const groupMiddleware = [];
|
|
4162
|
+
if (spec["x-middleware-registry"]) {
|
|
4163
|
+
Object.entries(spec["x-middleware-registry"]).forEach(([id, mw]) => {
|
|
4164
|
+
const firstRoute = routes[0];
|
|
4165
|
+
const routeSource = firstRoute?.op?.["x-shokupan-source"];
|
|
4166
|
+
const mwFile = mw.file?.split("/").pop();
|
|
4167
|
+
const routeFile = routeSource?.file?.split("/").pop();
|
|
4168
|
+
if (mwFile && routeFile && mwFile === routeFile && mw.scope !== "global") {
|
|
4169
|
+
groupMiddleware.push({ ...mw, id, type: "middleware" });
|
|
4170
|
+
}
|
|
4171
|
+
});
|
|
4172
|
+
}
|
|
4173
|
+
const isBuiltin = routes.some((r) => r.op["x-shokupan-builtin"] === true);
|
|
3661
4174
|
return {
|
|
3662
4175
|
name,
|
|
3663
4176
|
type: "group",
|
|
3664
4177
|
children,
|
|
3665
|
-
|
|
4178
|
+
middleware: groupMiddleware,
|
|
4179
|
+
commonPrefixPath,
|
|
3666
4180
|
// Store for display stripping
|
|
4181
|
+
isBuiltin
|
|
3667
4182
|
};
|
|
3668
|
-
})
|
|
4183
|
+
});
|
|
4184
|
+
if (spec["x-middleware-registry"]) {
|
|
4185
|
+
const allGroupMiddleware = hierarchicalGroups.flatMap((g) => g.middleware || []).map((m) => m.id);
|
|
4186
|
+
const globalMiddleware = Object.entries(spec["x-middleware-registry"]).filter(([id]) => !allGroupMiddleware.includes(id)).map(([id, mw]) => ({ ...mw, id, type: "middleware" }));
|
|
4187
|
+
if (globalMiddleware.length > 0) {
|
|
4188
|
+
hierarchicalGroups.push({
|
|
4189
|
+
name: "Global Middleware",
|
|
4190
|
+
type: "group",
|
|
4191
|
+
children: [],
|
|
4192
|
+
middleware: globalMiddleware,
|
|
4193
|
+
commonPrefixPath: "",
|
|
4194
|
+
isBuiltin: false
|
|
4195
|
+
});
|
|
4196
|
+
}
|
|
4197
|
+
}
|
|
4198
|
+
hierarchicalGroups.sort((a, b) => {
|
|
3669
4199
|
if (a.name === "Ungrouped") return 1;
|
|
3670
4200
|
if (b.name === "Ungrouped") return -1;
|
|
4201
|
+
if (a.name === "Global Middleware") return 1;
|
|
4202
|
+
if (b.name === "Global Middleware") return -1;
|
|
3671
4203
|
return a.name.localeCompare(b.name);
|
|
3672
4204
|
});
|
|
3673
4205
|
const allRoutes = Array.from(hierarchy.values()).flat();
|
|
@@ -3676,6 +4208,9 @@ function ApiExplorerApp({ spec, asyncSpec, config }) {
|
|
|
3676
4208
|
/* @__PURE__ */ jsx("meta", { charSet: "UTF-8" }),
|
|
3677
4209
|
/* @__PURE__ */ jsx("meta", { name: "viewport", content: "width=device-width, initial-scale=1.0" }),
|
|
3678
4210
|
/* @__PURE__ */ jsx("title", { children: spec.info?.title || "API Explorer" }),
|
|
4211
|
+
/* @__PURE__ */ jsx("link", { rel: "preconnect", href: "https://fonts.googleapis.com" }),
|
|
4212
|
+
/* @__PURE__ */ jsx("link", { rel: "preconnect", href: "https://fonts.gstatic.com", crossorigin: "anonymous" }),
|
|
4213
|
+
/* @__PURE__ */ jsx("link", { href: "https://fonts.googleapis.com/css2?family=Google+Sans+Code:ital,wght@0,300..800;1,300..800&family=Vend+Sans:ital,wght@0,300..700;1,300..700&display=swap", rel: "stylesheet" }),
|
|
3679
4214
|
/* @__PURE__ */ jsx("link", { rel: "stylesheet", href: "style.css" }),
|
|
3680
4215
|
/* @__PURE__ */ jsx("link", { rel: "stylesheet", href: "theme.css" }),
|
|
3681
4216
|
/* @__PURE__ */ jsx("script", { src: "https://cdn.jsdelivr.net/npm/marked@4.3.0/marked.min.js" }),
|
|
@@ -3772,13 +4307,46 @@ function Sidebar$1({ spec, hierarchicalGroups }) {
|
|
|
3772
4307
|
/* @__PURE__ */ jsx("div", { class: "version", children: spec.info?.version })
|
|
3773
4308
|
] }),
|
|
3774
4309
|
/* @__PURE__ */ jsx("div", { class: "sidebar-collapse-trigger", children: "➔" }),
|
|
3775
|
-
/* @__PURE__ */ jsx("nav", { class: "nav-groups", children: hierarchicalGroups.map((group) => /* @__PURE__ */ jsxs("div", { class:
|
|
4310
|
+
/* @__PURE__ */ jsx("nav", { class: "nav-groups", children: hierarchicalGroups.map((group) => /* @__PURE__ */ jsxs("div", { class: `nav-group collapsed ${group.isBuiltin ? "builtin-group" : ""}`, children: [
|
|
3776
4311
|
/* @__PURE__ */ jsxs("div", { class: "nav-group-title", children: [
|
|
3777
4312
|
/* @__PURE__ */ jsx("span", { class: "chevron", children: /* @__PURE__ */ jsx("svg", { width: "12", height: "12", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round", children: /* @__PURE__ */ jsx("polyline", { points: "9 18 15 12 9 6" }) }) }),
|
|
3778
|
-
" ",
|
|
4313
|
+
group.isBuiltin && /* @__PURE__ */ jsx("span", { class: "builtin-icon", title: "Built-in Plugin", children: /* @__PURE__ */ jsxs("svg", { width: "12", height: "12", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round", children: [
|
|
4314
|
+
/* @__PURE__ */ jsx("rect", { x: "2", y: "2", width: "20", height: "20", rx: "5", ry: "5" }),
|
|
4315
|
+
/* @__PURE__ */ jsx("path", { d: "M16 11.37A4 4 0 1 1 12.63 8 4 4 0 0 1 16 11.37z" }),
|
|
4316
|
+
/* @__PURE__ */ jsx("line", { x1: "17.5", y1: "6.5", x2: "17.51", y2: "6.5" })
|
|
4317
|
+
] }) }),
|
|
3779
4318
|
group.name
|
|
3780
4319
|
] }),
|
|
3781
|
-
/* @__PURE__ */
|
|
4320
|
+
/* @__PURE__ */ jsxs("div", { class: "nav-items", children: [
|
|
4321
|
+
group.middleware && group.middleware.length > 0 && /* @__PURE__ */ jsx("div", { class: "group-middleware", children: group.middleware.map((mw) => /* @__PURE__ */ jsxs(
|
|
4322
|
+
"a",
|
|
4323
|
+
{
|
|
4324
|
+
href: `#middleware-${mw.id}`,
|
|
4325
|
+
class: "nav-item middleware-nav-item",
|
|
4326
|
+
"data-middleware-id": mw.id,
|
|
4327
|
+
title: mw.name,
|
|
4328
|
+
children: [
|
|
4329
|
+
/* @__PURE__ */ jsx("span", { class: "middleware-icon", children: "⚙" }),
|
|
4330
|
+
/* @__PURE__ */ jsx("span", { class: "nav-label", children: mw.name }),
|
|
4331
|
+
mw.usedBy && mw.usedBy.length > 0 && /* @__PURE__ */ jsx("span", { class: "middleware-badge", title: `Used by ${mw.usedBy.length} routes`, children: mw.usedBy.length }),
|
|
4332
|
+
mw.file && /* @__PURE__ */ jsx(
|
|
4333
|
+
"a",
|
|
4334
|
+
{
|
|
4335
|
+
href: `vscode://file/${mw.file}:${mw.startLine || 1}`,
|
|
4336
|
+
class: "nav-source-link",
|
|
4337
|
+
title: `${mw.file}:${mw.startLine || 1}`,
|
|
4338
|
+
children: /* @__PURE__ */ jsxs("svg", { width: "10", height: "10", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", children: [
|
|
4339
|
+
/* @__PURE__ */ jsx("polyline", { points: "16 18 22 12 16 6" }),
|
|
4340
|
+
/* @__PURE__ */ jsx("polyline", { points: "8 6 2 12 8 18" })
|
|
4341
|
+
] })
|
|
4342
|
+
}
|
|
4343
|
+
)
|
|
4344
|
+
]
|
|
4345
|
+
},
|
|
4346
|
+
mw.id
|
|
4347
|
+
)) }),
|
|
4348
|
+
group.children?.map((child) => renderNavNode(child, 0, group.commonPrefixPath || ""))
|
|
4349
|
+
] })
|
|
3782
4350
|
] }, group.name)) })
|
|
3783
4351
|
] });
|
|
3784
4352
|
}
|
|
@@ -3786,7 +4354,8 @@ function MainContent$1({ allRoutes, config, spec }) {
|
|
|
3786
4354
|
const explorerData = JSON.stringify({
|
|
3787
4355
|
routes: allRoutes,
|
|
3788
4356
|
config,
|
|
3789
|
-
info: spec.info
|
|
4357
|
+
info: spec.info,
|
|
4358
|
+
middlewareRegistry: spec["x-middleware-registry"] || {}
|
|
3790
4359
|
});
|
|
3791
4360
|
const safeJson = explorerData.replace(/<\/script>/g, "<\\/script>");
|
|
3792
4361
|
return /* @__PURE__ */ jsxs("main", { class: "content", id: "main-content", children: [
|
|
@@ -3796,9 +4365,16 @@ function MainContent$1({ allRoutes, config, spec }) {
|
|
|
3796
4365
|
}
|
|
3797
4366
|
class ApiExplorerPlugin extends ShokupanRouter {
|
|
3798
4367
|
constructor(pluginOptions = {}) {
|
|
4368
|
+
console.log("ApiExplorerPlugin: CONSTRUCTOR CALLED");
|
|
3799
4369
|
super({ renderer: renderToString });
|
|
3800
4370
|
this.pluginOptions = pluginOptions;
|
|
3801
4371
|
pluginOptions.path ??= "/explorer";
|
|
4372
|
+
this.metadata = {
|
|
4373
|
+
file: import.meta.file,
|
|
4374
|
+
line: 1,
|
|
4375
|
+
name: "ApiExplorerPlugin",
|
|
4376
|
+
pluginName: "ApiExplorer"
|
|
4377
|
+
};
|
|
3802
4378
|
this.init();
|
|
3803
4379
|
}
|
|
3804
4380
|
onInit(app, options) {
|
|
@@ -3829,6 +4405,7 @@ class ApiExplorerPlugin extends ShokupanRouter {
|
|
|
3829
4405
|
delete op["x-source-info"].snippet;
|
|
3830
4406
|
}
|
|
3831
4407
|
if (op["x-shokupan-source"]?.code) {
|
|
4408
|
+
console.log("Deleting x-shokupan-source.code");
|
|
3832
4409
|
delete op["x-shokupan-source"].code;
|
|
3833
4410
|
}
|
|
3834
4411
|
});
|
|
@@ -3855,7 +4432,10 @@ class ApiExplorerPlugin extends ShokupanRouter {
|
|
|
3855
4432
|
this.get("/", async (ctx) => {
|
|
3856
4433
|
const spec = this.root.openApiSpec ? structuredClone(this.root.openApiSpec) : await (this.root || this).generateApiSpec();
|
|
3857
4434
|
const asyncSpec = ctx.app.asyncApiSpec;
|
|
3858
|
-
|
|
4435
|
+
const element = ApiExplorerApp({ spec: stripSourceCode(spec), asyncSpec });
|
|
4436
|
+
const html = renderToString(element);
|
|
4437
|
+
if (html.length === 0) throw new Error("ApiExplorerPlugin: rendered page is blank.");
|
|
4438
|
+
return ctx.html(html);
|
|
3859
4439
|
});
|
|
3860
4440
|
}
|
|
3861
4441
|
}
|
|
@@ -3866,8 +4446,8 @@ function AsyncApiApp({ spec, serverUrl, base, disableSourceView, navTree }) {
|
|
|
3866
4446
|
/* @__PURE__ */ jsx("meta", { name: "viewport", content: "width=device-width, initial-scale=1.0" }),
|
|
3867
4447
|
/* @__PURE__ */ jsx("title", { children: "Shokupan AsyncAPI" }),
|
|
3868
4448
|
/* @__PURE__ */ jsx("link", { rel: "preconnect", href: "https://fonts.googleapis.com" }),
|
|
3869
|
-
/* @__PURE__ */ jsx("link", { rel: "preconnect", href: "https://fonts.gstatic.com",
|
|
3870
|
-
/* @__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" }),
|
|
3871
4451
|
/* @__PURE__ */ jsx("link", { rel: "stylesheet", href: `${base}/theme.css` }),
|
|
3872
4452
|
/* @__PURE__ */ jsx("link", { rel: "stylesheet", href: `${base}/style.css` }),
|
|
3873
4453
|
/* @__PURE__ */ jsx("script", { dangerouslySetInnerHTML: {
|
|
@@ -3875,6 +4455,7 @@ function AsyncApiApp({ spec, serverUrl, base, disableSourceView, navTree }) {
|
|
|
3875
4455
|
window.INITIAL_SPEC = ${JSON.stringify(spec)};
|
|
3876
4456
|
window.INITIAL_SERVER_URL = "${serverUrl}";
|
|
3877
4457
|
window.DISABLE_SOURCE_VIEW = ${JSON.stringify(disableSourceView)};
|
|
4458
|
+
window.BASE_PATH = "${base}";
|
|
3878
4459
|
`
|
|
3879
4460
|
} })
|
|
3880
4461
|
] }),
|
|
@@ -3945,8 +4526,14 @@ function LeafNode({ item, label, disableSourceView }) {
|
|
|
3945
4526
|
] });
|
|
3946
4527
|
} else {
|
|
3947
4528
|
const badgeText = item.data.type === "publish" ? "SEND" : "RECV";
|
|
4529
|
+
const isPlugin = item.data.op?.["x-shokupan-plugin-name"] || sourceInfo?.pluginName;
|
|
3948
4530
|
content = /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
3949
4531
|
/* @__PURE__ */ jsx("span", { class: `badge badge-${badgeText}`, children: badgeText }),
|
|
4532
|
+
isPlugin && /* @__PURE__ */ jsx("span", { class: "builtin-icon", title: "Built-in Plugin", children: /* @__PURE__ */ jsxs("svg", { width: "12", height: "12", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round", children: [
|
|
4533
|
+
/* @__PURE__ */ jsx("rect", { x: "2", y: "2", width: "20", height: "20", rx: "5", ry: "5" }),
|
|
4534
|
+
/* @__PURE__ */ jsx("path", { d: "M16 11.37A4 4 0 1 1 12.63 8 4 4 0 0 1 16 11.37z" }),
|
|
4535
|
+
/* @__PURE__ */ jsx("line", { x1: "17.5", y1: "6.5", x2: "17.51", y2: "6.5" })
|
|
4536
|
+
] }) }),
|
|
3950
4537
|
/* @__PURE__ */ jsx("span", { class: "tree-label", children: label })
|
|
3951
4538
|
] });
|
|
3952
4539
|
}
|
|
@@ -4043,45 +4630,56 @@ function buildNavTree(spec) {
|
|
|
4043
4630
|
});
|
|
4044
4631
|
return root;
|
|
4045
4632
|
}
|
|
4046
|
-
|
|
4047
|
-
|
|
4048
|
-
|
|
4049
|
-
|
|
4050
|
-
|
|
4051
|
-
|
|
4052
|
-
|
|
4053
|
-
|
|
4054
|
-
|
|
4055
|
-
|
|
4056
|
-
|
|
4057
|
-
|
|
4058
|
-
});
|
|
4059
|
-
}
|
|
4060
|
-
if (app.mounted) {
|
|
4061
|
-
for (const mount of app.mounted) {
|
|
4062
|
-
const targetApp = applications.find((a) => a.name === mount.target || a.className === mount.target);
|
|
4063
|
-
if (targetApp) {
|
|
4064
|
-
expanded.push(...getExpandedRoutes(targetApp, "", newSeen));
|
|
4065
|
-
}
|
|
4066
|
-
}
|
|
4067
|
-
}
|
|
4068
|
-
return expanded;
|
|
4069
|
-
};
|
|
4070
|
-
applications.forEach((app) => {
|
|
4071
|
-
astRoutes.push(...getExpandedRoutes(app));
|
|
4072
|
-
});
|
|
4073
|
-
return astRoutes;
|
|
4633
|
+
function hasUnknownFields(schema) {
|
|
4634
|
+
if (!schema) return false;
|
|
4635
|
+
if (schema["x-unknown"]) return true;
|
|
4636
|
+
if (schema.type === "object" && schema.properties) {
|
|
4637
|
+
return Object.values(schema.properties).some(
|
|
4638
|
+
(prop) => hasUnknownFields(prop)
|
|
4639
|
+
);
|
|
4640
|
+
}
|
|
4641
|
+
if (schema.type === "array" && schema.items) {
|
|
4642
|
+
return hasUnknownFields(schema.items);
|
|
4643
|
+
}
|
|
4644
|
+
return false;
|
|
4074
4645
|
}
|
|
4075
4646
|
async function generateAsyncApi(rootRouter, options = {}) {
|
|
4076
4647
|
const channels = {};
|
|
4077
4648
|
let astRoutes = [];
|
|
4649
|
+
let astMiddlewareRegistry = {};
|
|
4650
|
+
let applications = [];
|
|
4078
4651
|
try {
|
|
4079
|
-
const { OpenAPIAnalyzer: OpenAPIAnalyzer2 } = await import("./analyzer-
|
|
4652
|
+
const { OpenAPIAnalyzer: OpenAPIAnalyzer2 } = await import("./analyzer-CnKnQ5KV.js");
|
|
4080
4653
|
const entrypoint = globalThis.Bun?.main || require.main?.filename || process.argv[1];
|
|
4081
4654
|
const analyzer = new OpenAPIAnalyzer2(process.cwd(), entrypoint);
|
|
4082
|
-
const
|
|
4083
|
-
|
|
4655
|
+
const analysisResult = await analyzer.analyze();
|
|
4656
|
+
applications = analysisResult.applications;
|
|
4657
|
+
astRoutes = await getAstRoutes(applications, {
|
|
4658
|
+
includePrefix: false,
|
|
4659
|
+
pathTransform: (p) => p.startsWith("/") ? p.slice(1) : p
|
|
4660
|
+
});
|
|
4661
|
+
let middlewareId = 0;
|
|
4662
|
+
for (const app of applications) {
|
|
4663
|
+
if (app.middleware && app.middleware.length > 0) {
|
|
4664
|
+
for (const mw of app.middleware) {
|
|
4665
|
+
const id = `middleware-${middlewareId++}`;
|
|
4666
|
+
astMiddlewareRegistry[id] = {
|
|
4667
|
+
...mw,
|
|
4668
|
+
id,
|
|
4669
|
+
usedBy: []
|
|
4670
|
+
// Will be populated when processing events
|
|
4671
|
+
};
|
|
4672
|
+
}
|
|
4673
|
+
}
|
|
4674
|
+
}
|
|
4084
4675
|
} catch (e) {
|
|
4676
|
+
if (options.warnings) {
|
|
4677
|
+
options.warnings.push({
|
|
4678
|
+
type: "ast-analysis-failed",
|
|
4679
|
+
message: "AST Analysis failed or skipped",
|
|
4680
|
+
detail: e.message
|
|
4681
|
+
});
|
|
4682
|
+
}
|
|
4085
4683
|
}
|
|
4086
4684
|
const matchedAstRoutes = /* @__PURE__ */ new Set();
|
|
4087
4685
|
const collect = async (router, prefix = "") => {
|
|
@@ -4125,23 +4723,45 @@ async function generateAsyncApi(rootRouter, options = {}) {
|
|
|
4125
4723
|
endLine: astMatch?.sourceContext?.endLine,
|
|
4126
4724
|
highlightLines: astMatch?.sourceContext ? [astMatch.sourceContext.startLine, astMatch.sourceContext.endLine] : void 0
|
|
4127
4725
|
} : void 0;
|
|
4726
|
+
const message = {
|
|
4727
|
+
...userSpec?.message || {}
|
|
4728
|
+
};
|
|
4729
|
+
let inferenceFailed = false;
|
|
4730
|
+
if (!message.payload) {
|
|
4731
|
+
if (astMatch) {
|
|
4732
|
+
if (astMatch.requestTypes?.body) {
|
|
4733
|
+
message.payload = astMatch.requestTypes.body;
|
|
4734
|
+
if (message.payload.type === "object" && !message.payload.properties && !message.payload.additionalProperties && Object.keys(message.payload).length === 1) {
|
|
4735
|
+
inferenceFailed = true;
|
|
4736
|
+
}
|
|
4737
|
+
}
|
|
4738
|
+
} else {
|
|
4739
|
+
message.payload = { type: "object" };
|
|
4740
|
+
inferenceFailed = true;
|
|
4741
|
+
}
|
|
4742
|
+
}
|
|
4128
4743
|
if (!channels[eventName]) {
|
|
4129
|
-
|
|
4130
|
-
|
|
4131
|
-
|
|
4132
|
-
|
|
4133
|
-
|
|
4134
|
-
|
|
4135
|
-
|
|
4136
|
-
|
|
4137
|
-
|
|
4138
|
-
"x-source-info": sourceInfo ? [sourceInfo] : [],
|
|
4139
|
-
"x-shokupan-source": sourceInfo
|
|
4140
|
-
// Simplified
|
|
4744
|
+
const publishOp = {
|
|
4745
|
+
operationId: `on${eventName.charAt(0).toUpperCase() + eventName.slice(1)}`,
|
|
4746
|
+
tags,
|
|
4747
|
+
message,
|
|
4748
|
+
...userSpec?.type === "publish" ? userSpec : {},
|
|
4749
|
+
"x-source-info": sourceInfo ? [sourceInfo] : [],
|
|
4750
|
+
"x-shokupan-source": {
|
|
4751
|
+
...sourceInfo,
|
|
4752
|
+
pluginName: handler.pluginName
|
|
4141
4753
|
}
|
|
4142
4754
|
};
|
|
4143
|
-
if (
|
|
4144
|
-
|
|
4755
|
+
if (inferenceFailed) {
|
|
4756
|
+
publishOp["x-warning"] = true;
|
|
4757
|
+
if (!publishOp.summary) publishOp.summary = "Payload Inference Failed";
|
|
4758
|
+
if (!publishOp.description) publishOp.description = "The payload format could not be statically inferred from the source code. Please add a type assertion or @Spec decorator.";
|
|
4759
|
+
}
|
|
4760
|
+
if (userSpec?.summary && !publishOp.summary) publishOp.summary = userSpec.summary;
|
|
4761
|
+
if (userSpec?.description && !publishOp.description) publishOp.description = userSpec.description;
|
|
4762
|
+
channels[eventName] = {
|
|
4763
|
+
publish: publishOp
|
|
4764
|
+
};
|
|
4145
4765
|
} else {
|
|
4146
4766
|
if (sourceInfo) {
|
|
4147
4767
|
if (!channels[eventName].publish["x-source-info"]) {
|
|
@@ -4159,6 +4779,14 @@ async function generateAsyncApi(rootRouter, options = {}) {
|
|
|
4159
4779
|
for (const emit of emits) {
|
|
4160
4780
|
if (emit.event === "__DYNAMIC_EMIT__") {
|
|
4161
4781
|
const warningKey = `${eventName}/Dynamic Emit`;
|
|
4782
|
+
if (options.warnings) {
|
|
4783
|
+
options.warnings.push({
|
|
4784
|
+
type: "dynamic-emit",
|
|
4785
|
+
message: "Dynamic emit detected",
|
|
4786
|
+
detail: `Event: ${eventName}`,
|
|
4787
|
+
location: { file: astMatch?.sourceContext?.file, line: emit.location?.startLine }
|
|
4788
|
+
});
|
|
4789
|
+
}
|
|
4162
4790
|
channels[warningKey] = {
|
|
4163
4791
|
subscribe: {
|
|
4164
4792
|
operationId: `dynamicEmitWarning${eventName}`,
|
|
@@ -4193,17 +4821,24 @@ async function generateAsyncApi(rootRouter, options = {}) {
|
|
|
4193
4821
|
emitHighlightLines: [emitStart, emitEnd]
|
|
4194
4822
|
} : void 0;
|
|
4195
4823
|
if (!channels[emit.event]) {
|
|
4824
|
+
const payload = emit.payload || { type: "object" };
|
|
4825
|
+
const warning = hasUnknownFields(payload);
|
|
4196
4826
|
channels[emit.event] = {
|
|
4197
4827
|
subscribe: {
|
|
4198
4828
|
operationId: `emit${emit.event.charAt(0).toUpperCase() + emit.event.slice(1)}`,
|
|
4199
4829
|
tags,
|
|
4200
4830
|
message: {
|
|
4201
|
-
payload
|
|
4831
|
+
payload
|
|
4202
4832
|
},
|
|
4833
|
+
...warning ? {
|
|
4834
|
+
"x-warning": true,
|
|
4835
|
+
"x-warning-reason": "Payload contains fields with unknown types that could not be statically analyzed"
|
|
4836
|
+
} : {},
|
|
4203
4837
|
"x-source-info": newSourceInfo ? [newSourceInfo] : [],
|
|
4204
4838
|
"x-shokupan-source": sourceInfo && emitStart ? {
|
|
4205
4839
|
file: sourceInfo.file,
|
|
4206
|
-
line: emitStart
|
|
4840
|
+
line: emitStart,
|
|
4841
|
+
pluginName: handler.pluginName
|
|
4207
4842
|
} : void 0
|
|
4208
4843
|
}
|
|
4209
4844
|
};
|
|
@@ -4267,13 +4902,19 @@ async function generateAsyncApi(rootRouter, options = {}) {
|
|
|
4267
4902
|
emitHighlightLines: [emitStart, emitEnd]
|
|
4268
4903
|
} : void 0;
|
|
4269
4904
|
if (!channels[emit.event]) {
|
|
4905
|
+
const payload = emit.payload || { type: "object" };
|
|
4906
|
+
const warning = hasUnknownFields(payload);
|
|
4270
4907
|
channels[emit.event] = {
|
|
4271
4908
|
subscribe: {
|
|
4272
4909
|
operationId: `emit${emit.event.charAt(0).toUpperCase() + emit.event.slice(1)}`,
|
|
4273
4910
|
tags,
|
|
4274
4911
|
message: {
|
|
4275
|
-
payload
|
|
4912
|
+
payload
|
|
4276
4913
|
},
|
|
4914
|
+
...warning ? {
|
|
4915
|
+
"x-warning": true,
|
|
4916
|
+
"x-warning-reason": "Payload contains fields with unknown types that could not be statically analyzed"
|
|
4917
|
+
} : {},
|
|
4277
4918
|
"x-source-info": newSourceInfo ? [newSourceInfo] : [],
|
|
4278
4919
|
"x-shokupan-source": sourceInfo && emitStart ? {
|
|
4279
4920
|
file: sourceInfo.file,
|
|
@@ -4312,6 +4953,14 @@ async function generateAsyncApi(rootRouter, options = {}) {
|
|
|
4312
4953
|
if (parts.length > 0) prefix = parts[0];
|
|
4313
4954
|
}
|
|
4314
4955
|
const key = `${prefix}.Dynamic Event ${i + 1}`;
|
|
4956
|
+
if (options.warnings) {
|
|
4957
|
+
options.warnings.push({
|
|
4958
|
+
type: "dynamic-event",
|
|
4959
|
+
message: "Dynamic event listener detected",
|
|
4960
|
+
detail: `Event listener with dynamic name`,
|
|
4961
|
+
location: { file: r.sourceContext?.file, line: r.sourceContext?.startLine }
|
|
4962
|
+
});
|
|
4963
|
+
}
|
|
4315
4964
|
channels[key] = {
|
|
4316
4965
|
publish: {
|
|
4317
4966
|
operationId: `dynamicEventWarning${i}`,
|
|
@@ -4337,7 +4986,8 @@ async function generateAsyncApi(rootRouter, options = {}) {
|
|
|
4337
4986
|
return {
|
|
4338
4987
|
asyncapi: "3.0.0",
|
|
4339
4988
|
info: { title: "Shokupan AsyncAPI", version: "1.0.0", ...options.info },
|
|
4340
|
-
channels
|
|
4989
|
+
channels,
|
|
4990
|
+
"x-middleware-registry": astMiddlewareRegistry
|
|
4341
4991
|
};
|
|
4342
4992
|
}
|
|
4343
4993
|
const generator = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
|
|
@@ -4349,6 +4999,12 @@ class AsyncApiPlugin extends ShokupanRouter {
|
|
|
4349
4999
|
super({ renderer: renderToString });
|
|
4350
5000
|
this.pluginOptions = pluginOptions;
|
|
4351
5001
|
this.pluginOptions.path ??= "/asyncapi";
|
|
5002
|
+
this.metadata = {
|
|
5003
|
+
file: import.meta.file,
|
|
5004
|
+
line: 1,
|
|
5005
|
+
name: "AsyncApiPlugin",
|
|
5006
|
+
pluginName: "AsyncAPI"
|
|
5007
|
+
};
|
|
4352
5008
|
this.init();
|
|
4353
5009
|
}
|
|
4354
5010
|
static getBasePath() {
|
|
@@ -4761,12 +5417,15 @@ class ClusterPlugin {
|
|
|
4761
5417
|
}
|
|
4762
5418
|
}
|
|
4763
5419
|
}
|
|
4764
|
-
function DashboardApp({ metrics, uptime, integrations, base, getRequestHeadersSource }) {
|
|
5420
|
+
function DashboardApp({ metrics, uptime, integrations, base, getRequestHeadersSource, rootPath, linkPattern }) {
|
|
4765
5421
|
return /* @__PURE__ */ jsxs("html", { lang: "en", children: [
|
|
4766
5422
|
/* @__PURE__ */ jsxs("head", { children: [
|
|
4767
5423
|
/* @__PURE__ */ jsx("meta", { charSet: "UTF-8" }),
|
|
4768
5424
|
/* @__PURE__ */ jsx("meta", { name: "viewport", content: "width=device-width, initial-scale=1.0" }),
|
|
4769
5425
|
/* @__PURE__ */ jsx("title", { children: "Shokupan Debug Dashboard" }),
|
|
5426
|
+
/* @__PURE__ */ jsx("link", { rel: "preconnect", href: "https://fonts.googleapis.com" }),
|
|
5427
|
+
/* @__PURE__ */ jsx("link", { rel: "preconnect", href: "https://fonts.gstatic.com", crossorigin: "anonymous" }),
|
|
5428
|
+
/* @__PURE__ */ jsx("link", { href: "https://fonts.googleapis.com/css2?family=Google+Sans+Code:ital,wght@0,300..800;1,300..800&family=Vend+Sans:ital,wght@0,300..700;1,300..700&display=swap", rel: "stylesheet" }),
|
|
4770
5429
|
/* @__PURE__ */ jsx("link", { href: "https://unpkg.com/tabulator-tables@5.5.0/dist/css/tabulator_bootstrap5.min.css", rel: "stylesheet" }),
|
|
4771
5430
|
/* @__PURE__ */ jsx("link", { rel: "stylesheet", href: "https://esm.sh/@xyflow/react@12.3.6/dist/style.css" }),
|
|
4772
5431
|
/* @__PURE__ */ jsx("link", { rel: "stylesheet", href: `${base}/theme.css` }),
|
|
@@ -4775,104 +5434,134 @@ function DashboardApp({ metrics, uptime, integrations, base, getRequestHeadersSo
|
|
|
4775
5434
|
/* @__PURE__ */ jsx("link", { rel: "stylesheet", href: `${base}/registry.css` }),
|
|
4776
5435
|
/* @__PURE__ */ jsx("link", { rel: "stylesheet", href: `${base}/tabulator.css` }),
|
|
4777
5436
|
/* @__PURE__ */ jsx("script", { src: "https://cdn.jsdelivr.net/npm/chart.js" }),
|
|
4778
|
-
/* @__PURE__ */ jsx("script", { type: "text/javascript", src: "https://unpkg.com/tabulator-tables@5.5.0/dist/js/tabulator.min.js" })
|
|
5437
|
+
/* @__PURE__ */ jsx("script", { type: "text/javascript", src: "https://unpkg.com/tabulator-tables@5.5.0/dist/js/tabulator.min.js" }),
|
|
5438
|
+
/* @__PURE__ */ jsx("script", { src: "https://cdn.jsdelivr.net/npm/monaco-editor@0.44.0/min/vs/loader.js" })
|
|
4779
5439
|
] }),
|
|
4780
5440
|
/* @__PURE__ */ jsxs("body", { children: [
|
|
4781
5441
|
/* @__PURE__ */ jsxs("div", { class: "container", children: [
|
|
4782
5442
|
/* @__PURE__ */ jsxs("header", { children: [
|
|
4783
|
-
/* @__PURE__ */
|
|
4784
|
-
|
|
4785
|
-
/* @__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: [
|
|
4786
5446
|
"Uptime: ",
|
|
4787
5447
|
/* @__PURE__ */ jsx("span", { id: "uptime", children: uptime })
|
|
4788
|
-
] })
|
|
5448
|
+
] }),
|
|
5449
|
+
/* @__PURE__ */ jsx("span", { id: "ws-status", title: "WebSocket: Disconnected", style: "width: 10px; height: 10px; border-radius: 50%; background: #6b7280; display: inline-block; margin-left: 10px;" })
|
|
4789
5450
|
] }),
|
|
5451
|
+
/* @__PURE__ */ jsx("div", { style: "flex: 1;" }),
|
|
4790
5452
|
/* @__PURE__ */ jsxs("div", { class: "tabs", children: [
|
|
4791
5453
|
/* @__PURE__ */ jsx("button", { class: "tab-btn active", onclick: "switchTab('overview')", children: "Overview" }),
|
|
4792
|
-
/* @__PURE__ */ jsx("button", { class: "tab-btn", onclick: "switchTab('
|
|
4793
|
-
/* @__PURE__ */ jsx("button", { class: "tab-btn", onclick: "switchTab('
|
|
4794
|
-
/* @__PURE__ */ jsx("button", { class: "tab-btn", onclick: "switchTab('
|
|
4795
|
-
/* @__PURE__ */ jsx("button", { class: "tab-btn", onclick: "switchTab('
|
|
4796
|
-
integrations.
|
|
4797
|
-
integrations.asyncapi && /* @__PURE__ */ jsx("button", { class: "tab-btn", onclick: "switchTab('asyncapi')", children: "AsyncAPI" })
|
|
5454
|
+
/* @__PURE__ */ jsx("button", { class: "tab-btn", onclick: "switchTab('application')", children: "Application" }),
|
|
5455
|
+
/* @__PURE__ */ jsx("button", { class: "tab-btn", onclick: "switchTab('network')", children: "Network" }),
|
|
5456
|
+
integrations.scalar && /* @__PURE__ */ jsx("button", { class: "tab-btn", onclick: "switchTab('scalar')", children: "Scalar" }),
|
|
5457
|
+
integrations.apiExplorer && /* @__PURE__ */ jsx("button", { class: "tab-btn", onclick: "switchTab('api-explorer')", children: "REST API" }),
|
|
5458
|
+
integrations.asyncapi && /* @__PURE__ */ jsx("button", { class: "tab-btn", onclick: "switchTab('asyncapi')", children: "WS API" })
|
|
4798
5459
|
] })
|
|
4799
5460
|
] }),
|
|
4800
|
-
/* @__PURE__ */ jsxs("div", {
|
|
4801
|
-
/* @__PURE__ */
|
|
4802
|
-
|
|
4803
|
-
/* @__PURE__ */
|
|
4804
|
-
/* @__PURE__ */ jsx("
|
|
4805
|
-
|
|
4806
|
-
|
|
4807
|
-
|
|
4808
|
-
|
|
4809
|
-
|
|
4810
|
-
|
|
4811
|
-
|
|
4812
|
-
|
|
4813
|
-
|
|
4814
|
-
|
|
4815
|
-
|
|
4816
|
-
|
|
4817
|
-
/* @__PURE__ */
|
|
4818
|
-
|
|
4819
|
-
|
|
4820
|
-
|
|
4821
|
-
|
|
4822
|
-
|
|
4823
|
-
|
|
5461
|
+
/* @__PURE__ */ jsxs("div", { class: "contents", children: [
|
|
5462
|
+
/* @__PURE__ */ jsxs("div", { id: "tab-overview", class: "tab-content active", children: [
|
|
5463
|
+
/* @__PURE__ */ jsx(MetricsGrid, { metrics }),
|
|
5464
|
+
/* @__PURE__ */ jsxs("div", { id: "chart-container", style: "display: flex; flex-direction: column; gap: 1rem;", children: [
|
|
5465
|
+
/* @__PURE__ */ jsx("div", { style: "display: flex; justify-content: flex-end;", children: /* @__PURE__ */ jsxs("select", { id: "time-range-selector", onchange: "updateCharts(); updateDashboard(); fetchTopStats();", style: "background: var(--bg-primary); color: var(--text-primary); border: 1px solid var(--card-border); padding: 5px; border-radius: 4px;", children: [
|
|
5466
|
+
/* @__PURE__ */ jsx("option", { value: "1m", children: "1 Minute" }),
|
|
5467
|
+
/* @__PURE__ */ jsx("option", { value: "5m", children: "5 Minutes" }),
|
|
5468
|
+
/* @__PURE__ */ jsx("option", { value: "30m", children: "30 Minutes" }),
|
|
5469
|
+
/* @__PURE__ */ jsx("option", { value: "1h", children: "1 Hour" }),
|
|
5470
|
+
/* @__PURE__ */ jsx("option", { value: "2h", children: "2 Hours" }),
|
|
5471
|
+
/* @__PURE__ */ jsx("option", { value: "6h", children: "6 Hours" }),
|
|
5472
|
+
/* @__PURE__ */ jsx("option", { value: "12h", children: "12 Hours" }),
|
|
5473
|
+
/* @__PURE__ */ jsx("option", { value: "1d", children: "1 Day" }),
|
|
5474
|
+
/* @__PURE__ */ jsx("option", { value: "3d", children: "3 Days" }),
|
|
5475
|
+
/* @__PURE__ */ jsx("option", { value: "7d", children: "7 Days" }),
|
|
5476
|
+
/* @__PURE__ */ jsx("option", { value: "30d", children: "30 Days" })
|
|
5477
|
+
] }) }),
|
|
5478
|
+
/* @__PURE__ */ jsxs("div", { class: "card-container", children: [
|
|
5479
|
+
/* @__PURE__ */ jsx(ChartCard, { title: "Response Time", id: "latencyChart" }),
|
|
5480
|
+
/* @__PURE__ */ jsx(ChartCard, { title: "Requests / Second", id: "rpsChart" }),
|
|
5481
|
+
/* @__PURE__ */ jsx(ChartCard, { title: "CPU & Load", id: "cpuChart" }),
|
|
5482
|
+
/* @__PURE__ */ jsx(ChartCard, { title: "Memory", id: "memoryChart" }),
|
|
5483
|
+
/* @__PURE__ */ jsx(ChartCard, { title: "Heap Usage", id: "heapChart" }),
|
|
5484
|
+
/* @__PURE__ */ jsx(ChartCard, { title: "Event Loop Latency", id: "eventLoopChart" }),
|
|
5485
|
+
/* @__PURE__ */ jsx(ChartCard, { title: "Error Rate", id: "errorRateChart" })
|
|
5486
|
+
] }),
|
|
5487
|
+
/* @__PURE__ */ jsx("div", { class: "card-title", style: "margin-top: 1rem;", children: "Top Statistics" }),
|
|
5488
|
+
/* @__PURE__ */ jsxs("div", { class: "card-container", children: [
|
|
5489
|
+
/* @__PURE__ */ jsx(Card, { title: "Top Requests", contentId: "top-requests-table" }),
|
|
5490
|
+
/* @__PURE__ */ jsx(Card, { title: "Top Errors", contentId: "top-errors-table" }),
|
|
5491
|
+
/* @__PURE__ */ jsx(Card, { title: "Most Frequent Failures", contentId: "failing-requests-table" }),
|
|
5492
|
+
/* @__PURE__ */ jsx(Card, { title: "Slowest Requests", contentId: "slowest-requests-table" })
|
|
5493
|
+
] }),
|
|
5494
|
+
/* @__PURE__ */ jsx("div", { id: "table-container", style: "padding: 0; margin-top: 1rem;", children: /* @__PURE__ */ jsx("div", { id: "requests-table", class: "table-dark" }) })
|
|
4824
5495
|
] }),
|
|
4825
|
-
/* @__PURE__ */ jsx("div", {
|
|
4826
|
-
/* @__PURE__ */ jsxs("div", { class: "card-container", children: [
|
|
4827
|
-
/* @__PURE__ */ jsx(Card, { title: "Top Requests", contentId: "top-requests-table" }),
|
|
4828
|
-
/* @__PURE__ */ jsx(Card, { title: "Top Errors", contentId: "top-errors-table" }),
|
|
4829
|
-
/* @__PURE__ */ jsx(Card, { title: "Most Frequent Failures", contentId: "failing-requests-table" }),
|
|
4830
|
-
/* @__PURE__ */ jsx(Card, { title: "Slowest Requests", contentId: "slowest-requests-table" })
|
|
4831
|
-
] }),
|
|
4832
|
-
/* @__PURE__ */ jsx("div", { id: "table-container", style: "padding: 0; margin-top: 1rem;", children: /* @__PURE__ */ jsx("div", { id: "requests-table", class: "table-dark" }) })
|
|
4833
|
-
] })
|
|
4834
|
-
] }),
|
|
4835
|
-
/* @__PURE__ */ jsx("div", { id: "tab-registry", class: "tab-content", children: /* @__PURE__ */ jsxs("div", { id: "registry-container", class: "card", style: "margin-top: 2rem;", children: [
|
|
4836
|
-
/* @__PURE__ */ jsx("div", { class: "card-title", children: "Component Registry" }),
|
|
4837
|
-
/* @__PURE__ */ jsx("div", { id: "registry-tree", style: "padding: 0 1rem 1rem 1rem; font-family: monospace; font-size: 0.9rem;" })
|
|
4838
|
-
] }) }),
|
|
4839
|
-
/* @__PURE__ */ jsxs("div", { id: "tab-graph", class: "tab-content", children: [
|
|
4840
|
-
/* @__PURE__ */ jsx("div", { class: "card", style: "margin-bottom: 1rem;", children: /* @__PURE__ */ jsx("div", { style: "display: flex; gap: 1rem;", children: /* @__PURE__ */ jsx("input", { type: "text", id: "graph-search", placeholder: "Search routes or middleware...", "aria-label": "Search routes or middleware", style: "flex:1; padding: 0.5rem; border-radius: 0.5rem; background: var(--bg-primary); border: 1px solid var(--card-border); color: var(--text-primary);" }) }) }),
|
|
4841
|
-
/* @__PURE__ */ jsx("div", { id: "cy" })
|
|
4842
|
-
] }),
|
|
4843
|
-
/* @__PURE__ */ jsxs("div", { id: "tab-requests", class: "tab-content", children: [
|
|
4844
|
-
/* @__PURE__ */ jsxs("div", { class: "card", style: "margin-bottom: 1rem; display: flex; justify-content: space-between; align-items: center;", children: [
|
|
4845
|
-
/* @__PURE__ */ jsx("div", { class: "card-title", children: "Recent Requests (Last 100)" }),
|
|
4846
|
-
/* @__PURE__ */ jsx("div", { children: /* @__PURE__ */ jsx("button", { onclick: "fetchRequests()", style: "background: var(--bg-primary); color: var(--text-primary); border: 1px solid var(--card-border); padding: 5px 10px; border-radius: 4px; cursor: pointer;", children: "Refresh" }) })
|
|
5496
|
+
/* @__PURE__ */ jsx("div", { style: "height: 2rem" })
|
|
4847
5497
|
] }),
|
|
4848
|
-
/* @__PURE__ */
|
|
4849
|
-
|
|
4850
|
-
|
|
4851
|
-
|
|
4852
|
-
|
|
4853
|
-
/* @__PURE__ */
|
|
4854
|
-
|
|
4855
|
-
|
|
4856
|
-
|
|
4857
|
-
|
|
4858
|
-
|
|
4859
|
-
|
|
4860
|
-
|
|
4861
|
-
/* @__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);" })
|
|
4862
5513
|
] })
|
|
4863
5514
|
] }),
|
|
4864
|
-
/* @__PURE__ */
|
|
4865
|
-
|
|
4866
|
-
|
|
4867
|
-
|
|
5515
|
+
/* @__PURE__ */ jsxs("div", { id: "tab-network", class: "tab-content", children: [
|
|
5516
|
+
/* @__PURE__ */ jsx("div", { style: "margin: 1rem 2rem 0 2rem;", children: /* @__PURE__ */ jsxs("div", { id: "network-filter-bar", class: "card", style: "margin-bottom: 1rem; padding: 0.5rem; display: flex; gap: 0.5rem; flex-wrap: wrap; flex-direction: row", children: [
|
|
5517
|
+
/* @__PURE__ */ jsxs("div", { style: "display: flex; background: var(--bg-secondary); border: 1px solid var(--card-border); border-radius: 4px; overflow: hidden;", children: [
|
|
5518
|
+
/* @__PURE__ */ jsx("button", { class: "filter-direction active", "data-value": "all", style: "padding: 4px 12px; border: none; background: var(--bg-primary); color: var(--text-primary); cursor: pointer; border-right: 1px solid var(--card-border);", children: "All" }),
|
|
5519
|
+
/* @__PURE__ */ jsx("button", { class: "filter-direction", "data-value": "inbound", style: "padding: 4px 12px; border: none; background: transparent; color: var(--text-secondary); cursor: pointer; border-right: 1px solid var(--card-border);", children: "Inbound" }),
|
|
5520
|
+
/* @__PURE__ */ jsx("button", { class: "filter-direction", "data-value": "outbound", style: "padding: 4px 12px; border: none; background: transparent; color: var(--text-secondary); cursor: pointer;", children: "Outbound" })
|
|
5521
|
+
] }),
|
|
5522
|
+
/* @__PURE__ */ jsx("input", { type: "text", id: "network-filter-text", placeholder: "Filter...", style: "background: var(--bg-primary); color: var(--text-primary); border: 1px solid var(--card-border); padding: 4px 8px; border-radius: 4px; flex: 1;" }),
|
|
5523
|
+
/* @__PURE__ */ jsxs("select", { id: "network-filter-type", style: "background: var(--bg-primary); color: var(--text-primary); border: 1px solid var(--card-border); borderRadius: 4px;", children: [
|
|
5524
|
+
/* @__PURE__ */ jsx("option", { value: "all", children: "All Types" }),
|
|
5525
|
+
/* @__PURE__ */ jsx("option", { value: "xhr", children: "XHR/Fetch" }),
|
|
5526
|
+
/* @__PURE__ */ jsx("option", { value: "fetch", children: "Outbound" }),
|
|
5527
|
+
/* @__PURE__ */ jsx("option", { value: "ws", children: "WS" }),
|
|
5528
|
+
/* @__PURE__ */ jsx("option", { value: "other", children: "Other" })
|
|
5529
|
+
] }),
|
|
5530
|
+
/* @__PURE__ */ jsx("button", { onclick: "fetchRequests()", style: "background: var(--bg-primary); color: var(--text-primary); border: 1px solid var(--card-border); padding: 4px 8px; border-radius: 4px; cursor: pointer;", children: "Refresh" }),
|
|
5531
|
+
/* @__PURE__ */ jsx("button", { onclick: "purgeRequests()", style: "background: var(--bg-primary); color: var(--color-error, #ef4444); border: 1px solid var(--card-border); padding: 4px 8px; border-radius: 4px; cursor: pointer;", children: "Purge" })
|
|
5532
|
+
] }) }),
|
|
5533
|
+
/* @__PURE__ */ jsx("div", { id: "network-view", class: "active", style: "display: block; height: calc(100vh - 170px);", children: /* @__PURE__ */ jsxs("div", { style: "margin: 0 2rem; display: flex; gap: 1rem; height: 100%;", children: [
|
|
5534
|
+
/* @__PURE__ */ jsx("div", { id: "requests-list-container", style: "flex: 1; height: 100%; border-radius: 6px; overflow: hidden; border: 1px solid var(--card-border);" }),
|
|
5535
|
+
/* @__PURE__ */ jsxs("div", { id: "request-details-container", class: "card", style: "display: none; width: 500px; height: 100%; overflow-y: auto; flex-shrink: 0; background: var(--bg-secondary); border: 1px solid var(--card-border); position: relative;", children: [
|
|
5536
|
+
/* @__PURE__ */ jsx("div", { id: "details-drag-handle", style: "position: absolute; left: 0; top: 0; bottom: 0; width: 5px; cursor: col-resize; z-index: 11; background: transparent;" }),
|
|
5537
|
+
/* @__PURE__ */ jsxs("div", { style: "display: flex; justify-content: space-between; align-items: center; position: sticky; top: 0; background: var(--bg-secondary); padding: 0.5rem 1rem; border-bottom: 1px solid var(--border-color); z-index: 10;", children: [
|
|
5538
|
+
/* @__PURE__ */ jsx("div", { class: "card-title", style: "margin: 0;", children: "Request Details" }),
|
|
5539
|
+
/* @__PURE__ */ jsx("button", { onclick: "closeRequestDetails()", style: "background: transparent; border: none; color: var(--text-secondary); cursor: pointer; font-size: 1.2rem;", children: "×" })
|
|
5540
|
+
] }),
|
|
5541
|
+
/* @__PURE__ */ jsxs("div", { style: "padding: 1rem;", children: [
|
|
5542
|
+
/* @__PURE__ */ jsx("div", { id: "request-details-content" }),
|
|
5543
|
+
/* @__PURE__ */ jsx("div", { class: "card-title", style: "margin-top: 1rem;", children: "Middleware Trace" }),
|
|
5544
|
+
/* @__PURE__ */ jsx("div", { id: "middleware-trace-container" })
|
|
5545
|
+
] })
|
|
5546
|
+
] })
|
|
5547
|
+
] }) })
|
|
5548
|
+
] }),
|
|
5549
|
+
integrations.scalar && /* @__PURE__ */ jsx("div", { id: "tab-scalar", class: "tab-content", style: "overflow: hidden; height: 100%; width: 100%", children: /* @__PURE__ */ jsx("iframe", { src: integrations.scalar, style: "width: 100%; height: 100%; border: none;" }) }),
|
|
5550
|
+
integrations.apiExplorer && /* @__PURE__ */ jsx("div", { id: "tab-api-explorer", class: "tab-content", style: "overflow: hidden; height: 100%; width: 100%", children: /* @__PURE__ */ jsx("iframe", { src: integrations.apiExplorer, style: "width: 100%; height: 100%; border: none;" }) }),
|
|
5551
|
+
integrations.asyncapi && /* @__PURE__ */ jsx("div", { id: "tab-asyncapi", class: "tab-content", style: "overflow: hidden; height: 100%; width: 100%", children: /* @__PURE__ */ jsx("iframe", { src: integrations.asyncapi, style: "width: 100%; height: 100%; border: none;" }) })
|
|
5552
|
+
] })
|
|
4868
5553
|
] }),
|
|
4869
5554
|
/* @__PURE__ */ jsx("script", { dangerouslySetInnerHTML: {
|
|
4870
5555
|
__html: `
|
|
4871
5556
|
// Injected function from server config
|
|
4872
5557
|
const getRequestHeaders = ${getRequestHeadersSource};
|
|
5558
|
+
window.SHOKUPAN_CONFIG = {
|
|
5559
|
+
rootPath: "${rootPath || ""}",
|
|
5560
|
+
linkPattern: "${linkPattern || ""}"
|
|
5561
|
+
};
|
|
4873
5562
|
`
|
|
4874
5563
|
} }),
|
|
4875
|
-
/* @__PURE__ */ jsx("script", { src: `${base}/
|
|
5564
|
+
/* @__PURE__ */ jsx("script", { src: `${base}/client.js` }),
|
|
4876
5565
|
/* @__PURE__ */ jsx("script", { src: `${base}/graph.mjs`, type: "module" }),
|
|
4877
5566
|
/* @__PURE__ */ jsx("script", { src: `${base}/charts.js` }),
|
|
4878
5567
|
/* @__PURE__ */ jsx("script", { src: `${base}/tables.js` }),
|
|
@@ -4942,6 +5631,264 @@ function Card({ title, contentId }) {
|
|
|
4942
5631
|
/* @__PURE__ */ jsx("div", { id: contentId })
|
|
4943
5632
|
] });
|
|
4944
5633
|
}
|
|
5634
|
+
const require$1 = createRequire(import.meta.url);
|
|
5635
|
+
const http = require$1("node:http");
|
|
5636
|
+
const https = require$1("node:https");
|
|
5637
|
+
class FetchInterceptor {
|
|
5638
|
+
originalFetch;
|
|
5639
|
+
originalHttpRequest;
|
|
5640
|
+
originalHttpsRequest;
|
|
5641
|
+
callbacks = [];
|
|
5642
|
+
isPatched = false;
|
|
5643
|
+
constructor() {
|
|
5644
|
+
this.originalFetch = global.fetch;
|
|
5645
|
+
this.originalHttpRequest = http.request;
|
|
5646
|
+
this.originalHttpsRequest = https.request;
|
|
5647
|
+
}
|
|
5648
|
+
/**
|
|
5649
|
+
* Patches the global `fetch` function to intercept requests.
|
|
5650
|
+
* If already patched, this method does nothing.
|
|
5651
|
+
*/
|
|
5652
|
+
patch() {
|
|
5653
|
+
if (this.isPatched) return;
|
|
5654
|
+
this.patchGlobalFetch();
|
|
5655
|
+
this.patchNodeRequests();
|
|
5656
|
+
this.isPatched = true;
|
|
5657
|
+
console.log("[FetchInterceptor] Network layer patched.");
|
|
5658
|
+
}
|
|
5659
|
+
patchGlobalFetch() {
|
|
5660
|
+
const self = this;
|
|
5661
|
+
const newFetch = async function(input, init) {
|
|
5662
|
+
const startTime = performance.now();
|
|
5663
|
+
const timestamp = Date.now();
|
|
5664
|
+
let method = "GET";
|
|
5665
|
+
let url = "";
|
|
5666
|
+
let requestHeaders = {};
|
|
5667
|
+
let requestBody = void 0;
|
|
5668
|
+
try {
|
|
5669
|
+
if (input instanceof URL$1) {
|
|
5670
|
+
url = input.toString();
|
|
5671
|
+
} else if (typeof input === "string") {
|
|
5672
|
+
url = input;
|
|
5673
|
+
} else if (typeof input === "object" && "url" in input) {
|
|
5674
|
+
url = input.url;
|
|
5675
|
+
method = input.method;
|
|
5676
|
+
}
|
|
5677
|
+
if (init) {
|
|
5678
|
+
if (init.method) method = init.method;
|
|
5679
|
+
if (init.headers) {
|
|
5680
|
+
if (init.headers instanceof Headers) {
|
|
5681
|
+
init.headers.forEach((v, k) => requestHeaders[k] = v);
|
|
5682
|
+
} else if (Array.isArray(init.headers)) {
|
|
5683
|
+
init.headers.forEach(([k, v]) => requestHeaders[k] = v);
|
|
5684
|
+
} else {
|
|
5685
|
+
Object.assign(requestHeaders, init.headers);
|
|
5686
|
+
}
|
|
5687
|
+
}
|
|
5688
|
+
if (init.body) requestBody = init.body;
|
|
5689
|
+
}
|
|
5690
|
+
} catch (e) {
|
|
5691
|
+
console.warn("[FetchInterceptor] Failed to parse request arguments", e);
|
|
5692
|
+
}
|
|
5693
|
+
try {
|
|
5694
|
+
const response = await self.originalFetch.apply(global, [input, init]);
|
|
5695
|
+
const clone = response.clone();
|
|
5696
|
+
const duration = performance.now() - startTime;
|
|
5697
|
+
self.processResponse(clone, {
|
|
5698
|
+
method,
|
|
5699
|
+
url,
|
|
5700
|
+
requestHeaders,
|
|
5701
|
+
requestBody,
|
|
5702
|
+
status: response.status,
|
|
5703
|
+
startTime: timestamp,
|
|
5704
|
+
duration,
|
|
5705
|
+
...self.extractRequestMeta(url, requestHeaders),
|
|
5706
|
+
protocol: "1.1"
|
|
5707
|
+
// native fetch doesn't expose this easily, assume 1.1/2
|
|
5708
|
+
});
|
|
5709
|
+
return response;
|
|
5710
|
+
} catch (error) {
|
|
5711
|
+
const duration = performance.now() - startTime;
|
|
5712
|
+
self.notify({
|
|
5713
|
+
method,
|
|
5714
|
+
url,
|
|
5715
|
+
requestHeaders,
|
|
5716
|
+
requestBody,
|
|
5717
|
+
status: 0,
|
|
5718
|
+
responseHeaders: {},
|
|
5719
|
+
responseBody: `Network Error: ${String(error)}`,
|
|
5720
|
+
startTime: timestamp,
|
|
5721
|
+
duration
|
|
5722
|
+
});
|
|
5723
|
+
throw error;
|
|
5724
|
+
}
|
|
5725
|
+
};
|
|
5726
|
+
Object.assign(newFetch, this.originalFetch);
|
|
5727
|
+
global.fetch = newFetch;
|
|
5728
|
+
}
|
|
5729
|
+
patchNodeRequests() {
|
|
5730
|
+
const self = this;
|
|
5731
|
+
const intercept = (module, original, defaultScheme) => {
|
|
5732
|
+
module.request = function(...args) {
|
|
5733
|
+
const startTime = performance.now();
|
|
5734
|
+
const timestamp = Date.now();
|
|
5735
|
+
let options = {};
|
|
5736
|
+
let urlObj;
|
|
5737
|
+
if (typeof args[0] === "string" || args[0] instanceof URL$1) {
|
|
5738
|
+
try {
|
|
5739
|
+
urlObj = new URL$1(args[0]);
|
|
5740
|
+
options = typeof args[1] === "object" ? args[1] : {};
|
|
5741
|
+
} catch (e) {
|
|
5742
|
+
}
|
|
5743
|
+
} else {
|
|
5744
|
+
options = args[0] || {};
|
|
5745
|
+
try {
|
|
5746
|
+
const protocol = options.protocol || defaultScheme + ":";
|
|
5747
|
+
const host = options.hostname || options.host || "localhost";
|
|
5748
|
+
const port = options.port ? ":" + options.port : "";
|
|
5749
|
+
const path = options.path || "/";
|
|
5750
|
+
urlObj = new URL$1(`${protocol}//${host}${port}${path}`);
|
|
5751
|
+
} catch (e) {
|
|
5752
|
+
}
|
|
5753
|
+
}
|
|
5754
|
+
const method = (options.method || "GET").toUpperCase();
|
|
5755
|
+
const url = urlObj ? urlObj.toString() : "unknown";
|
|
5756
|
+
const req = original.apply(this, args);
|
|
5757
|
+
const getReqHeaders = () => {
|
|
5758
|
+
try {
|
|
5759
|
+
const h = req.getHeaders();
|
|
5760
|
+
const normalized = {};
|
|
5761
|
+
for (const k in h) {
|
|
5762
|
+
const v = h[k];
|
|
5763
|
+
normalized[k] = Array.isArray(v) ? v.join(", ") : String(v);
|
|
5764
|
+
}
|
|
5765
|
+
return normalized;
|
|
5766
|
+
} catch (e) {
|
|
5767
|
+
return {};
|
|
5768
|
+
}
|
|
5769
|
+
};
|
|
5770
|
+
req.on("response", (res) => {
|
|
5771
|
+
const duration = performance.now() - startTime;
|
|
5772
|
+
const resHeaders = {};
|
|
5773
|
+
if (res.headers) {
|
|
5774
|
+
for (const k in res.headers) {
|
|
5775
|
+
const v = res.headers[k];
|
|
5776
|
+
resHeaders[k] = Array.isArray(v) ? v.join(", ") : String(v || "");
|
|
5777
|
+
}
|
|
5778
|
+
}
|
|
5779
|
+
self.notify({
|
|
5780
|
+
method,
|
|
5781
|
+
url,
|
|
5782
|
+
requestHeaders: getReqHeaders(),
|
|
5783
|
+
status: res.statusCode || 0,
|
|
5784
|
+
responseHeaders: resHeaders,
|
|
5785
|
+
startTime: timestamp,
|
|
5786
|
+
duration,
|
|
5787
|
+
...self.extractRequestMeta(url, getReqHeaders()),
|
|
5788
|
+
protocol: req.httpVersion
|
|
5789
|
+
});
|
|
5790
|
+
});
|
|
5791
|
+
req.on("error", (err) => {
|
|
5792
|
+
const duration = performance.now() - startTime;
|
|
5793
|
+
self.notify({
|
|
5794
|
+
method,
|
|
5795
|
+
url,
|
|
5796
|
+
requestHeaders: getReqHeaders(),
|
|
5797
|
+
status: 0,
|
|
5798
|
+
responseHeaders: {},
|
|
5799
|
+
responseBody: `Error: ${err.message}`,
|
|
5800
|
+
// Capture error
|
|
5801
|
+
startTime: timestamp,
|
|
5802
|
+
duration
|
|
5803
|
+
});
|
|
5804
|
+
});
|
|
5805
|
+
return req;
|
|
5806
|
+
};
|
|
5807
|
+
};
|
|
5808
|
+
intercept(http, this.originalHttpRequest, "http");
|
|
5809
|
+
intercept(https, this.originalHttpsRequest, "https");
|
|
5810
|
+
}
|
|
5811
|
+
/**
|
|
5812
|
+
* Restores the original functions.
|
|
5813
|
+
*/
|
|
5814
|
+
unpatch() {
|
|
5815
|
+
if (!this.isPatched) return;
|
|
5816
|
+
global.fetch = this.originalFetch;
|
|
5817
|
+
http.request = this.originalHttpRequest;
|
|
5818
|
+
https.request = this.originalHttpsRequest;
|
|
5819
|
+
this.isPatched = false;
|
|
5820
|
+
console.log("[FetchInterceptor] Network layer restored.");
|
|
5821
|
+
}
|
|
5822
|
+
/**
|
|
5823
|
+
* Adds a callback to be notified of outbound requests.
|
|
5824
|
+
* @param callback The callback function.
|
|
5825
|
+
*/
|
|
5826
|
+
on(callback) {
|
|
5827
|
+
this.callbacks.push(callback);
|
|
5828
|
+
}
|
|
5829
|
+
extractRequestMeta(urlStr, headers) {
|
|
5830
|
+
try {
|
|
5831
|
+
const url = new URL$1(urlStr);
|
|
5832
|
+
const cookiesHeader = headers["cookie"] || headers["Cookie"];
|
|
5833
|
+
const cookies = cookiesHeader ? cookiesHeader.split(";").length : 0;
|
|
5834
|
+
return {
|
|
5835
|
+
domain: url.hostname,
|
|
5836
|
+
path: url.pathname,
|
|
5837
|
+
scheme: url.protocol.replace(":", ""),
|
|
5838
|
+
cookies,
|
|
5839
|
+
remoteIP: void 0
|
|
5840
|
+
// Not easily accessible via fetch
|
|
5841
|
+
};
|
|
5842
|
+
} catch (e) {
|
|
5843
|
+
return {};
|
|
5844
|
+
}
|
|
5845
|
+
}
|
|
5846
|
+
async processResponse(response, meta) {
|
|
5847
|
+
const responseHeaders = {};
|
|
5848
|
+
response.headers.forEach((v, k) => responseHeaders[k] = v);
|
|
5849
|
+
let responseBody;
|
|
5850
|
+
let transferred = 0;
|
|
5851
|
+
try {
|
|
5852
|
+
const contentType = response.headers.get("content-type") || "";
|
|
5853
|
+
let bodyText = "";
|
|
5854
|
+
if (contentType.includes("application/json") || contentType.includes("text/")) {
|
|
5855
|
+
bodyText = await response.text();
|
|
5856
|
+
if (bodyText.length > 524288) {
|
|
5857
|
+
responseBody = bodyText.substring(0, 524288) + "... (truncated)";
|
|
5858
|
+
} else {
|
|
5859
|
+
responseBody = bodyText;
|
|
5860
|
+
}
|
|
5861
|
+
} else {
|
|
5862
|
+
responseBody = "[Binary Content]";
|
|
5863
|
+
const cl = response.headers.get("content-length");
|
|
5864
|
+
if (cl) transferred = parseInt(cl, 10);
|
|
5865
|
+
}
|
|
5866
|
+
const headersSize = Object.entries(responseHeaders).reduce((acc, [k, v]) => acc + k.length + v.length + 2, 0);
|
|
5867
|
+
if (!transferred && bodyText) {
|
|
5868
|
+
transferred = headersSize + bodyText.length;
|
|
5869
|
+
} else if (!transferred) {
|
|
5870
|
+
transferred = headersSize;
|
|
5871
|
+
}
|
|
5872
|
+
} catch (e) {
|
|
5873
|
+
responseBody = "[Failed to read response body]";
|
|
5874
|
+
}
|
|
5875
|
+
this.notify({
|
|
5876
|
+
...meta,
|
|
5877
|
+
responseHeaders,
|
|
5878
|
+
responseBody,
|
|
5879
|
+
transferred
|
|
5880
|
+
});
|
|
5881
|
+
}
|
|
5882
|
+
notify(log) {
|
|
5883
|
+
this.callbacks.forEach((cb) => {
|
|
5884
|
+
try {
|
|
5885
|
+
cb(log);
|
|
5886
|
+
} catch (e) {
|
|
5887
|
+
console.error("[FetchInterceptor] Callback failed", e);
|
|
5888
|
+
}
|
|
5889
|
+
});
|
|
5890
|
+
}
|
|
5891
|
+
}
|
|
4945
5892
|
const INTERVALS = [
|
|
4946
5893
|
{ label: "10s", ms: 10 * 1e3 },
|
|
4947
5894
|
{ label: "1m", ms: 60 * 1e3 },
|
|
@@ -4956,7 +5903,8 @@ const INTERVALS = [
|
|
|
4956
5903
|
{ label: "30d", ms: 30 * 24 * 60 * 60 * 1e3 }
|
|
4957
5904
|
];
|
|
4958
5905
|
class MetricsCollector {
|
|
4959
|
-
constructor(db) {
|
|
5906
|
+
constructor(db, onCollect) {
|
|
5907
|
+
this.onCollect = onCollect;
|
|
4960
5908
|
this.db = db;
|
|
4961
5909
|
this.eventLoopHistogram.enable();
|
|
4962
5910
|
const now = Date.now();
|
|
@@ -4964,11 +5912,13 @@ class MetricsCollector {
|
|
|
4964
5912
|
this.currentIntervalStart[int.label] = this.alignTimestamp(now, int.ms);
|
|
4965
5913
|
this.pendingDetails[int.label] = [];
|
|
4966
5914
|
});
|
|
5915
|
+
this.timer = setInterval(() => this.collect(), 1e4);
|
|
4967
5916
|
}
|
|
4968
5917
|
currentIntervalStart = {};
|
|
4969
5918
|
pendingDetails = {};
|
|
4970
5919
|
eventLoopHistogram = monitorEventLoopDelay({ resolution: 10 });
|
|
4971
5920
|
timer = null;
|
|
5921
|
+
db;
|
|
4972
5922
|
recordRequest(duration, isError) {
|
|
4973
5923
|
INTERVALS.forEach((int) => {
|
|
4974
5924
|
this.pendingDetails[int.label].push({ duration, isError });
|
|
@@ -5044,14 +5994,17 @@ class MetricsCollector {
|
|
|
5044
5994
|
p99: getP(0.99)
|
|
5045
5995
|
}
|
|
5046
5996
|
};
|
|
5997
|
+
if (!this.db) {
|
|
5998
|
+
return;
|
|
5999
|
+
}
|
|
5047
6000
|
try {
|
|
5048
|
-
|
|
5049
|
-
await this.db.upsert(recordId, metric);
|
|
5050
|
-
const test = await this.db.select(recordId);
|
|
5051
|
-
const queryTest = await this.db.query("SELECT * FROM metrics WHERE id = $id", { id: recordId });
|
|
6001
|
+
await this.db.upsert(new RecordId("metric", timestamp), metric);
|
|
5052
6002
|
} catch (e) {
|
|
5053
6003
|
console.error(`[MetricsCollector] ✗ Failed to save metrics for ${label}:`, e);
|
|
5054
6004
|
}
|
|
6005
|
+
if (this.onCollect) {
|
|
6006
|
+
this.onCollect(metric);
|
|
6007
|
+
}
|
|
5055
6008
|
}
|
|
5056
6009
|
// Cleanup if needed
|
|
5057
6010
|
stop() {
|
|
@@ -5097,8 +6050,13 @@ class Dashboard {
|
|
|
5097
6050
|
nodeMetrics: {},
|
|
5098
6051
|
edgeMetrics: {}
|
|
5099
6052
|
};
|
|
6053
|
+
clients = /* @__PURE__ */ new Set();
|
|
6054
|
+
broadcastTimer;
|
|
6055
|
+
requestPushTimer;
|
|
6056
|
+
requestsBuffer = [];
|
|
5100
6057
|
startTime = Date.now();
|
|
5101
6058
|
instrumented = false;
|
|
6059
|
+
mountPath = "/dashboard";
|
|
5102
6060
|
metricsCollector;
|
|
5103
6061
|
get db() {
|
|
5104
6062
|
return this[$appRoot].db;
|
|
@@ -5106,8 +6064,69 @@ class Dashboard {
|
|
|
5106
6064
|
// ShokupanPlugin interface implementation
|
|
5107
6065
|
onInit(app, options) {
|
|
5108
6066
|
this[$appRoot] = app;
|
|
5109
|
-
|
|
5110
|
-
|
|
6067
|
+
const onCollect = (metric) => {
|
|
6068
|
+
this.broadcastMetricUpdate(metric);
|
|
6069
|
+
};
|
|
6070
|
+
this.metricsCollector = new MetricsCollector(this.db, onCollect);
|
|
6071
|
+
const fetchInterceptor = new FetchInterceptor();
|
|
6072
|
+
fetchInterceptor.patch();
|
|
6073
|
+
fetchInterceptor.on((log) => {
|
|
6074
|
+
if (log.url.includes("/rpc")) return;
|
|
6075
|
+
try {
|
|
6076
|
+
const u = new URL(log.url);
|
|
6077
|
+
if (u.pathname.startsWith(this.mountPath)) return;
|
|
6078
|
+
} catch (e) {
|
|
6079
|
+
}
|
|
6080
|
+
const requestData = {
|
|
6081
|
+
method: log.method,
|
|
6082
|
+
url: log.url,
|
|
6083
|
+
status: log.status,
|
|
6084
|
+
duration: log.duration,
|
|
6085
|
+
timestamp: log.startTime,
|
|
6086
|
+
// Use startTime as timestamp
|
|
6087
|
+
type: "fetch",
|
|
6088
|
+
direction: "outbound",
|
|
6089
|
+
size: log.responseBody ? String(log.responseBody).length : 0,
|
|
6090
|
+
contentType: log.responseHeaders["content-type"] || log.responseHeaders["Content-Type"],
|
|
6091
|
+
body: log.responseBody,
|
|
6092
|
+
requestBody: log.requestBody,
|
|
6093
|
+
domain: log.domain,
|
|
6094
|
+
path: log.path,
|
|
6095
|
+
scheme: log.scheme,
|
|
6096
|
+
protocol: log.protocol,
|
|
6097
|
+
remoteIP: log.remoteIP,
|
|
6098
|
+
cookies: log.cookies,
|
|
6099
|
+
transferred: log.transferred,
|
|
6100
|
+
requestHeaders: log.requestHeaders,
|
|
6101
|
+
responseHeaders: log.responseHeaders
|
|
6102
|
+
// No handler stack for outbound
|
|
6103
|
+
};
|
|
6104
|
+
this.metrics.logs.push(requestData);
|
|
6105
|
+
const recordId = new RecordId("request", nanoid());
|
|
6106
|
+
const idString = recordId.toString();
|
|
6107
|
+
this.db.query("UPSERT $id CONTENT $data", {
|
|
6108
|
+
id: recordId,
|
|
6109
|
+
data: requestData
|
|
6110
|
+
}).catch((e) => console.error("Failed to save outbound request", e));
|
|
6111
|
+
const strategy2 = this.dashboardConfig.updateStrategy || "immediate";
|
|
6112
|
+
if (strategy2 === "immediate") {
|
|
6113
|
+
this.broadcastRequestUpdates([{ ...requestData, id: idString }]);
|
|
6114
|
+
} else {
|
|
6115
|
+
this.requestsBuffer.push({ ...requestData, id: idString });
|
|
6116
|
+
}
|
|
6117
|
+
});
|
|
6118
|
+
if (app.onStart) {
|
|
6119
|
+
app.onStart(async () => {
|
|
6120
|
+
if (app.dbPromise) {
|
|
6121
|
+
await app.dbPromise;
|
|
6122
|
+
if (app.db) {
|
|
6123
|
+
this.metricsCollector.db = app.db;
|
|
6124
|
+
console.log("[Dashboard] Attached datastore to MetricsCollector");
|
|
6125
|
+
}
|
|
6126
|
+
}
|
|
6127
|
+
});
|
|
6128
|
+
}
|
|
6129
|
+
this.mountPath = options?.path || this.dashboardConfig.path || "/dashboard";
|
|
5111
6130
|
const hooks = this.getHooks();
|
|
5112
6131
|
if (!app.middleware) {
|
|
5113
6132
|
app.middleware = [];
|
|
@@ -5116,15 +6135,25 @@ class Dashboard {
|
|
|
5116
6135
|
if (hooks.onRequestStart) {
|
|
5117
6136
|
await hooks.onRequestStart(ctx);
|
|
5118
6137
|
}
|
|
6138
|
+
ctx._startTime = performance.now();
|
|
5119
6139
|
await next();
|
|
5120
|
-
if (hooks.onResponseEnd) {
|
|
5121
|
-
const effectiveResponse = ctx._finalResponse || ctx.response || {};
|
|
5122
|
-
await hooks.onResponseEnd(ctx, effectiveResponse);
|
|
5123
|
-
}
|
|
5124
6140
|
};
|
|
5125
6141
|
app.use(hooksMiddleware);
|
|
5126
|
-
|
|
6142
|
+
if (hooks.onResponseEnd) {
|
|
6143
|
+
app.hook("onResponseEnd", hooks.onResponseEnd);
|
|
6144
|
+
}
|
|
6145
|
+
app.mount(this.mountPath, this.router);
|
|
6146
|
+
this.router.metadata = {
|
|
6147
|
+
file: import.meta.file,
|
|
6148
|
+
line: 1,
|
|
6149
|
+
name: "DashboardPlugin",
|
|
6150
|
+
pluginName: "Dashboard"
|
|
6151
|
+
};
|
|
5127
6152
|
this.setupRoutes();
|
|
6153
|
+
const strategy = this.dashboardConfig.updateStrategy || "immediate";
|
|
6154
|
+
if (strategy === "batched") {
|
|
6155
|
+
this.startRequestPushTimer();
|
|
6156
|
+
}
|
|
5128
6157
|
}
|
|
5129
6158
|
detectIntegrations() {
|
|
5130
6159
|
const integrations = {};
|
|
@@ -5157,6 +6186,17 @@ class Dashboard {
|
|
|
5157
6186
|
}
|
|
5158
6187
|
}
|
|
5159
6188
|
}
|
|
6189
|
+
const apiExplorerConf = checkConfig("apiExplorer");
|
|
6190
|
+
if (apiExplorerConf.enabled) {
|
|
6191
|
+
if (apiExplorerConf.path) {
|
|
6192
|
+
integrations["apiExplorer"] = apiExplorerConf.path;
|
|
6193
|
+
} else {
|
|
6194
|
+
const plugin = routers.find((r) => r.constructor.name === "ApiExplorerPlugin");
|
|
6195
|
+
if (plugin) {
|
|
6196
|
+
integrations["apiExplorer"] = plugin[$mountPath];
|
|
6197
|
+
}
|
|
6198
|
+
}
|
|
6199
|
+
}
|
|
5160
6200
|
return integrations;
|
|
5161
6201
|
}
|
|
5162
6202
|
// Get base path for dashboard files - works in both dev (src/) and production (dist/)
|
|
@@ -5168,9 +6208,36 @@ class Dashboard {
|
|
|
5168
6208
|
return dir;
|
|
5169
6209
|
}
|
|
5170
6210
|
setupRoutes() {
|
|
6211
|
+
this.router.get("/ws", (ctx) => {
|
|
6212
|
+
const success = ctx.upgrade({
|
|
6213
|
+
data: {
|
|
6214
|
+
handler: {
|
|
6215
|
+
open: (ws) => {
|
|
6216
|
+
this.clients.add(ws);
|
|
6217
|
+
console.log(`[Dashboard] Client connected. Total clients: ${this.clients.size}`);
|
|
6218
|
+
this.sendHistory(ws, "1m");
|
|
6219
|
+
},
|
|
6220
|
+
close: (ws) => {
|
|
6221
|
+
this.clients.delete(ws);
|
|
6222
|
+
console.log(`[Dashboard] Client disconnected. Total clients: ${this.clients.size}`);
|
|
6223
|
+
},
|
|
6224
|
+
message: (ws, message) => {
|
|
6225
|
+
try {
|
|
6226
|
+
const msg = JSON.parse(message);
|
|
6227
|
+
if (msg.type === "get-history") {
|
|
6228
|
+
this.sendHistory(ws, msg.interval || "1m");
|
|
6229
|
+
}
|
|
6230
|
+
} catch (e) {
|
|
6231
|
+
}
|
|
6232
|
+
}
|
|
6233
|
+
}
|
|
6234
|
+
}
|
|
6235
|
+
});
|
|
6236
|
+
if (success) return void 0;
|
|
6237
|
+
return ctx.text("WebSocket upgrade failed", 400);
|
|
6238
|
+
});
|
|
5171
6239
|
this.router.get("/metrics", async (ctx) => {
|
|
5172
|
-
const
|
|
5173
|
-
const uptime = `${Math.floor(uptimeSeconds / 3600)}h ${Math.floor(uptimeSeconds % 3600 / 60)}m ${uptimeSeconds % 60}s`;
|
|
6240
|
+
const uptime = this.getUptime();
|
|
5174
6241
|
const interval = ctx.query["interval"];
|
|
5175
6242
|
if (interval) {
|
|
5176
6243
|
const intervalMap = {
|
|
@@ -5197,13 +6264,15 @@ class Dashboard {
|
|
|
5197
6264
|
count(IF status < 400 THEN 1 END) as success,
|
|
5198
6265
|
count(IF status >= 400 THEN 1 END) as failed,
|
|
5199
6266
|
math::mean(duration) as avg_latency
|
|
5200
|
-
FROM
|
|
6267
|
+
FROM request
|
|
5201
6268
|
WHERE timestamp >= $start
|
|
5202
6269
|
GROUP ALL
|
|
5203
6270
|
`, { start: startTime });
|
|
5204
6271
|
} catch (error) {
|
|
5205
6272
|
console.error("[Dashboard] Query failed at plugin.ts:180-191", {
|
|
5206
6273
|
error,
|
|
6274
|
+
errorMessage: error.message,
|
|
6275
|
+
errorStack: error.stack,
|
|
5207
6276
|
interval,
|
|
5208
6277
|
startTime,
|
|
5209
6278
|
query: "metrics interval stats",
|
|
@@ -5211,12 +6280,12 @@ class Dashboard {
|
|
|
5211
6280
|
});
|
|
5212
6281
|
throw error;
|
|
5213
6282
|
}
|
|
5214
|
-
const s = stats[0] || {
|
|
6283
|
+
const s = stats[0] || { avg_latency: 0 };
|
|
5215
6284
|
return ctx.json({
|
|
5216
6285
|
metrics: {
|
|
5217
|
-
totalRequests:
|
|
5218
|
-
successfulRequests:
|
|
5219
|
-
failedRequests:
|
|
6286
|
+
totalRequests: this.metrics.totalRequests,
|
|
6287
|
+
successfulRequests: this.metrics.successfulRequests,
|
|
6288
|
+
failedRequests: this.metrics.failedRequests,
|
|
5220
6289
|
activeRequests: this.metrics.activeRequests,
|
|
5221
6290
|
averageTotalTime_ms: s.avg_latency || 0,
|
|
5222
6291
|
recentTimings: this.metrics.recentTimings,
|
|
@@ -5282,7 +6351,7 @@ class Dashboard {
|
|
|
5282
6351
|
this.router.get("/requests/top", async (ctx) => {
|
|
5283
6352
|
const startTime = getIntervalStartTime(ctx.query["interval"]);
|
|
5284
6353
|
const result = await this.db.query(
|
|
5285
|
-
"SELECT method, url, count() as count FROM
|
|
6354
|
+
"SELECT method, url, count() as count FROM request WHERE timestamp >= $start GROUP BY method, url ORDER BY count DESC LIMIT 10",
|
|
5286
6355
|
{ start: startTime }
|
|
5287
6356
|
);
|
|
5288
6357
|
return ctx.json({ top: result[0] || [] });
|
|
@@ -5290,7 +6359,7 @@ class Dashboard {
|
|
|
5290
6359
|
this.router.get("/errors/top", async (ctx) => {
|
|
5291
6360
|
const startTime = getIntervalStartTime(ctx.query["interval"]);
|
|
5292
6361
|
const result = await this.db.query(
|
|
5293
|
-
"SELECT status, count() as count FROM
|
|
6362
|
+
"SELECT status, count() as count FROM failed_request WHERE timestamp >= $start GROUP BY status ORDER BY count DESC LIMIT 10",
|
|
5294
6363
|
{ start: startTime }
|
|
5295
6364
|
);
|
|
5296
6365
|
return ctx.json({ top: result[0] || [] });
|
|
@@ -5298,7 +6367,7 @@ class Dashboard {
|
|
|
5298
6367
|
this.router.get("/requests/failing", async (ctx) => {
|
|
5299
6368
|
const startTime = getIntervalStartTime(ctx.query["interval"]);
|
|
5300
6369
|
const result = await this.db.query(
|
|
5301
|
-
"SELECT method, url, count() as count FROM
|
|
6370
|
+
"SELECT method, url, count() as count FROM failed_request WHERE timestamp >= $start GROUP BY method, url ORDER BY count DESC LIMIT 10",
|
|
5302
6371
|
{ start: startTime }
|
|
5303
6372
|
);
|
|
5304
6373
|
return ctx.json({ top: result[0] || [] });
|
|
@@ -5306,7 +6375,7 @@ class Dashboard {
|
|
|
5306
6375
|
this.router.get("/requests/slowest", async (ctx) => {
|
|
5307
6376
|
const startTime = getIntervalStartTime(ctx.query["interval"]);
|
|
5308
6377
|
const result = await this.db.query(
|
|
5309
|
-
"SELECT method, url, duration, status, timestamp FROM
|
|
6378
|
+
"SELECT method, url, duration, status, timestamp FROM request WHERE timestamp >= $start ORDER BY duration DESC LIMIT 10",
|
|
5310
6379
|
{ start: startTime }
|
|
5311
6380
|
);
|
|
5312
6381
|
return ctx.json({ slowest: result[0] || [] });
|
|
@@ -5323,15 +6392,32 @@ class Dashboard {
|
|
|
5323
6392
|
return ctx.json({ registry: registry || {} });
|
|
5324
6393
|
});
|
|
5325
6394
|
this.router.get("/requests", async (ctx) => {
|
|
5326
|
-
|
|
5327
|
-
|
|
6395
|
+
console.log(`[Dashboard] Handling /requests from ${ctx.ip} ${ctx.get("User-Agent")}`);
|
|
6396
|
+
const result = await this.db.query("SELECT * FROM request ORDER BY timestamp DESC LIMIT 100");
|
|
6397
|
+
const items = result[0] || [];
|
|
6398
|
+
console.log(`[Dashboard] /requests returning ${items.length} items`);
|
|
6399
|
+
return ctx.json({ requests: items });
|
|
6400
|
+
});
|
|
6401
|
+
this.router.delete("/requests", async (ctx) => {
|
|
6402
|
+
console.log(`[Dashboard] Purging all requests`);
|
|
6403
|
+
await this.db.query("DELETE request; DELETE failed_request;");
|
|
6404
|
+
this.metrics.logs = [];
|
|
6405
|
+
this.metrics.totalRequests = 0;
|
|
6406
|
+
this.metrics.activeRequests = 0;
|
|
6407
|
+
this.metrics.successfulRequests = 0;
|
|
6408
|
+
this.metrics.failedRequests = 0;
|
|
6409
|
+
this.metrics.recentTimings = [];
|
|
6410
|
+
this.metrics.rateLimitedCounts = {};
|
|
6411
|
+
this.metrics.nodeMetrics = {};
|
|
6412
|
+
this.metrics.edgeMetrics = {};
|
|
6413
|
+
return ctx.json({ success: true });
|
|
5328
6414
|
});
|
|
5329
6415
|
this.router.get("/requests/:id", async (ctx) => {
|
|
5330
|
-
const result = await this.db.query("SELECT * FROM
|
|
6416
|
+
const result = await this.db.query("SELECT * FROM request WHERE id = $id", { id: ctx.params["id"] });
|
|
5331
6417
|
return ctx.json({ request: result[0]?.[0] });
|
|
5332
6418
|
});
|
|
5333
6419
|
this.router.get("/failures", async (ctx) => {
|
|
5334
|
-
const result = await this.db.query("SELECT * FROM
|
|
6420
|
+
const result = await this.db.query("SELECT * FROM failed_request ORDER BY timestamp DESC LIMIT 50");
|
|
5335
6421
|
return ctx.json({ failures: result[0] });
|
|
5336
6422
|
});
|
|
5337
6423
|
this.router.post("/replay", async (ctx) => {
|
|
@@ -5369,7 +6455,7 @@ class Dashboard {
|
|
|
5369
6455
|
"charts.js",
|
|
5370
6456
|
"failures.js",
|
|
5371
6457
|
"graph.mjs",
|
|
5372
|
-
"
|
|
6458
|
+
"client.js",
|
|
5373
6459
|
"reactflow.css",
|
|
5374
6460
|
"registry.css",
|
|
5375
6461
|
"registry.js",
|
|
@@ -5386,15 +6472,15 @@ class Dashboard {
|
|
|
5386
6472
|
else if (path.endsWith(".js") || path.endsWith(".mjs")) ctx.set("Content-Type", "application/javascript");
|
|
5387
6473
|
return ctx.send(content);
|
|
5388
6474
|
}
|
|
5389
|
-
const
|
|
5390
|
-
const
|
|
5391
|
-
this.getLinkPattern();
|
|
6475
|
+
const uptime = this.getUptime();
|
|
6476
|
+
const linkPattern = this.getLinkPattern();
|
|
5392
6477
|
const integrations = this.detectIntegrations();
|
|
5393
6478
|
const getRequestHeadersSource = this.dashboardConfig.getRequestHeaders ? this.dashboardConfig.getRequestHeaders.toString() : "undefined";
|
|
5394
6479
|
const html = renderToString(DashboardApp({
|
|
5395
6480
|
metrics: this.metrics,
|
|
5396
6481
|
uptime,
|
|
5397
6482
|
rootPath: process.cwd(),
|
|
6483
|
+
linkPattern,
|
|
5398
6484
|
integrations,
|
|
5399
6485
|
base: mountPath,
|
|
5400
6486
|
getRequestHeadersSource
|
|
@@ -5402,6 +6488,82 @@ class Dashboard {
|
|
|
5402
6488
|
return ctx.html(`<!DOCTYPE html>${html}`);
|
|
5403
6489
|
});
|
|
5404
6490
|
}
|
|
6491
|
+
getUptime() {
|
|
6492
|
+
const uptimeSeconds = Math.floor((Date.now() - this.startTime) / 1e3);
|
|
6493
|
+
return `${Math.floor(uptimeSeconds / 3600)}h ${Math.floor(uptimeSeconds % 3600 / 60)}m ${uptimeSeconds % 60}s`;
|
|
6494
|
+
}
|
|
6495
|
+
getPublicMetrics() {
|
|
6496
|
+
return {
|
|
6497
|
+
totalRequests: this.metrics.totalRequests,
|
|
6498
|
+
successfulRequests: this.metrics.successfulRequests,
|
|
6499
|
+
failedRequests: this.metrics.failedRequests,
|
|
6500
|
+
activeRequests: this.metrics.activeRequests,
|
|
6501
|
+
averageTotalTime_ms: this.metrics.averageTotalTime_ms,
|
|
6502
|
+
recentTimings: this.metrics.recentTimings,
|
|
6503
|
+
logs: [],
|
|
6504
|
+
// Don't broadcast logs for now to save bandwidth
|
|
6505
|
+
rateLimitedCounts: this.metrics.rateLimitedCounts,
|
|
6506
|
+
nodeMetrics: this.metrics.nodeMetrics,
|
|
6507
|
+
edgeMetrics: this.metrics.edgeMetrics
|
|
6508
|
+
};
|
|
6509
|
+
}
|
|
6510
|
+
broadcastMetricUpdate(metric) {
|
|
6511
|
+
if (this.clients.size === 0) return;
|
|
6512
|
+
const data = JSON.stringify({
|
|
6513
|
+
type: "metric-update",
|
|
6514
|
+
metric
|
|
6515
|
+
});
|
|
6516
|
+
for (const client of this.clients) {
|
|
6517
|
+
client.send(data);
|
|
6518
|
+
}
|
|
6519
|
+
}
|
|
6520
|
+
async sendHistory(ws, interval) {
|
|
6521
|
+
const intervalMap = {
|
|
6522
|
+
"10s": 10 * 1e3,
|
|
6523
|
+
"1m": 60 * 1e3,
|
|
6524
|
+
"5m": 5 * 60 * 1e3,
|
|
6525
|
+
"30m": 30 * 60 * 1e3,
|
|
6526
|
+
"1h": 60 * 60 * 1e3,
|
|
6527
|
+
"2h": 2 * 60 * 60 * 1e3,
|
|
6528
|
+
"6h": 6 * 60 * 60 * 1e3,
|
|
6529
|
+
"12h": 12 * 60 * 60 * 1e3,
|
|
6530
|
+
"1d": 24 * 60 * 60 * 1e3,
|
|
6531
|
+
"3d": 3 * 24 * 60 * 60 * 1e3,
|
|
6532
|
+
"7d": 7 * 24 * 60 * 60 * 1e3,
|
|
6533
|
+
"30d": 30 * 24 * 60 * 60 * 1e3
|
|
6534
|
+
};
|
|
6535
|
+
const periodMs = intervalMap[interval] || 60 * 1e3;
|
|
6536
|
+
const startTime = Date.now() - periodMs * 30;
|
|
6537
|
+
const endTime = Date.now();
|
|
6538
|
+
let history = [];
|
|
6539
|
+
try {
|
|
6540
|
+
const result = await this.db.query(
|
|
6541
|
+
"SELECT * FROM metric WHERE timestamp >= $start AND timestamp <= $end AND interval = $interval ORDER BY timestamp ASC",
|
|
6542
|
+
{ start: startTime, end: endTime, interval }
|
|
6543
|
+
);
|
|
6544
|
+
history = result[0] || [];
|
|
6545
|
+
} catch (e) {
|
|
6546
|
+
console.error("[Dashboard] Failed to fetch history for WS", e);
|
|
6547
|
+
}
|
|
6548
|
+
ws.send(JSON.stringify({
|
|
6549
|
+
type: "init",
|
|
6550
|
+
metrics: { ...this.metrics, logs: [] },
|
|
6551
|
+
uptime: this.getUptime(),
|
|
6552
|
+
history
|
|
6553
|
+
}));
|
|
6554
|
+
}
|
|
6555
|
+
broadcastMetrics() {
|
|
6556
|
+
if (this.clients.size === 0) return;
|
|
6557
|
+
console.log(`[Dashboard] Broadcasting metrics to ${this.clients.size} clients`);
|
|
6558
|
+
const data = JSON.stringify({
|
|
6559
|
+
type: "metrics",
|
|
6560
|
+
metrics: this.getPublicMetrics(),
|
|
6561
|
+
uptime: this.getUptime()
|
|
6562
|
+
});
|
|
6563
|
+
for (const client of this.clients) {
|
|
6564
|
+
client.send(data);
|
|
6565
|
+
}
|
|
6566
|
+
}
|
|
5405
6567
|
instrumentApp(app) {
|
|
5406
6568
|
if (!app.getComponentRegistry) return;
|
|
5407
6569
|
const registry = app.getComponentRegistry();
|
|
@@ -5431,6 +6593,11 @@ class Dashboard {
|
|
|
5431
6593
|
r.id = id;
|
|
5432
6594
|
this.assignIdsToRegistry(r.children, id);
|
|
5433
6595
|
});
|
|
6596
|
+
node.events?.forEach((e, idx) => {
|
|
6597
|
+
const id = makeId("event", parentId, idx, e.name);
|
|
6598
|
+
e.id = id;
|
|
6599
|
+
if (e._fn) e._fn._debugId = id;
|
|
6600
|
+
});
|
|
5434
6601
|
}
|
|
5435
6602
|
recordNodeMetric(id, type, duration, isError) {
|
|
5436
6603
|
if (!this.metrics.nodeMetrics[id]) {
|
|
@@ -5458,7 +6625,7 @@ class Dashboard {
|
|
|
5458
6625
|
if (["vscode", "cursor", "antigravity"].some((t) => term.includes(t))) {
|
|
5459
6626
|
return "vscode://file/{{absolute}}:{{line}}";
|
|
5460
6627
|
}
|
|
5461
|
-
return "file
|
|
6628
|
+
return "vscode://file/{{absolute}}:{{line}}";
|
|
5462
6629
|
}
|
|
5463
6630
|
getHooks() {
|
|
5464
6631
|
return {
|
|
@@ -5469,19 +6636,36 @@ class Dashboard {
|
|
|
5469
6636
|
}
|
|
5470
6637
|
this.metrics.totalRequests++;
|
|
5471
6638
|
this.metrics.activeRequests++;
|
|
5472
|
-
ctx._debugStartTime = performance.now();
|
|
5473
6639
|
ctx[$debug] = new Collector(this);
|
|
6640
|
+
if (!this.broadcastTimer) {
|
|
6641
|
+
this.broadcastTimer = setTimeout(() => {
|
|
6642
|
+
this.broadcastMetrics();
|
|
6643
|
+
this.broadcastTimer = void 0;
|
|
6644
|
+
}, 100);
|
|
6645
|
+
}
|
|
5474
6646
|
},
|
|
5475
6647
|
onResponseEnd: async (ctx, response) => {
|
|
6648
|
+
if (ctx.path.startsWith(this.mountPath)) return;
|
|
5476
6649
|
this.metrics.activeRequests = Math.max(0, this.metrics.activeRequests - 1);
|
|
5477
|
-
const
|
|
5478
|
-
|
|
5479
|
-
|
|
5480
|
-
|
|
5481
|
-
|
|
6650
|
+
const duration = performance.now() - ctx._startTime || 0;
|
|
6651
|
+
if (!response) {
|
|
6652
|
+
if (ctx.isUpgraded) {
|
|
6653
|
+
response = {
|
|
6654
|
+
status: 101,
|
|
6655
|
+
headers: {}
|
|
6656
|
+
};
|
|
6657
|
+
} else {
|
|
6658
|
+
return;
|
|
6659
|
+
}
|
|
5482
6660
|
}
|
|
5483
6661
|
const isError = response.status >= 400;
|
|
5484
6662
|
this.metricsCollector.recordRequest(duration, isError);
|
|
6663
|
+
if (!this.broadcastTimer) {
|
|
6664
|
+
this.broadcastTimer = setTimeout(() => {
|
|
6665
|
+
this.broadcastMetrics();
|
|
6666
|
+
this.broadcastTimer = void 0;
|
|
6667
|
+
}, 100);
|
|
6668
|
+
}
|
|
5485
6669
|
if (response.status >= 400) {
|
|
5486
6670
|
this.metrics.failedRequests++;
|
|
5487
6671
|
if (response.status === 429) {
|
|
@@ -5489,20 +6673,28 @@ class Dashboard {
|
|
|
5489
6673
|
this.metrics.rateLimitedCounts[path] = (this.metrics.rateLimitedCounts[path] || 0) + 1;
|
|
5490
6674
|
}
|
|
5491
6675
|
try {
|
|
5492
|
-
const
|
|
6676
|
+
const headers2 = {};
|
|
5493
6677
|
if (ctx.request.headers && typeof ctx.request.headers.forEach === "function") {
|
|
5494
6678
|
ctx.request.headers.forEach((v, k) => {
|
|
5495
|
-
|
|
6679
|
+
headers2[k] = v;
|
|
6680
|
+
});
|
|
6681
|
+
}
|
|
6682
|
+
const resHeaders2 = {};
|
|
6683
|
+
if (response.headers && typeof response.headers.forEach === "function") {
|
|
6684
|
+
response.headers.forEach((v, k) => {
|
|
6685
|
+
resHeaders2[k] = v;
|
|
5496
6686
|
});
|
|
5497
6687
|
}
|
|
5498
|
-
await this.db.upsert(new RecordId(
|
|
6688
|
+
await this.db.upsert(new RecordId(`failed_request`, ctx.requestId), {
|
|
5499
6689
|
method: ctx.method,
|
|
5500
6690
|
url: ctx.url.toString(),
|
|
5501
|
-
headers,
|
|
6691
|
+
headers: headers2,
|
|
5502
6692
|
status: response.status,
|
|
5503
6693
|
timestamp: Date.now(),
|
|
5504
|
-
state: ctx.state
|
|
5505
|
-
|
|
6694
|
+
state: ctx.state,
|
|
6695
|
+
body: this.serializeBody(ctx.bodyData || ctx.requestBody),
|
|
6696
|
+
responseHeaders: resHeaders2,
|
|
6697
|
+
responseBody: this.serializeBody(ctx.responseBody)
|
|
5506
6698
|
});
|
|
5507
6699
|
} catch (e) {
|
|
5508
6700
|
console.error("Failed to record failed request", e);
|
|
@@ -5510,17 +6702,58 @@ class Dashboard {
|
|
|
5510
6702
|
} else {
|
|
5511
6703
|
this.metrics.successfulRequests++;
|
|
5512
6704
|
}
|
|
6705
|
+
const urlObj = new URL(ctx.url.toString());
|
|
6706
|
+
const cookieHeader = ctx.request.headers.get("cookie") || "";
|
|
6707
|
+
const cookiesCount = cookieHeader ? cookieHeader.split(";").length : 0;
|
|
6708
|
+
const headers = {};
|
|
6709
|
+
if (ctx.request.headers && typeof ctx.request.headers.forEach === "function") {
|
|
6710
|
+
ctx.request.headers.forEach((v, k) => {
|
|
6711
|
+
headers[k] = v;
|
|
6712
|
+
});
|
|
6713
|
+
}
|
|
6714
|
+
const resHeaders = {};
|
|
6715
|
+
if (response.headers && typeof response.headers.forEach === "function") {
|
|
6716
|
+
response.headers.forEach((v, k) => {
|
|
6717
|
+
resHeaders[k] = v;
|
|
6718
|
+
});
|
|
6719
|
+
}
|
|
6720
|
+
const responseHeadersSize = Object.entries(response.headers || {}).reduce((acc, [k, v]) => acc + k.length + String(v).length + 2, 0);
|
|
6721
|
+
const responseSize = ctx.responseBody ? String(ctx.responseBody).length : 0;
|
|
6722
|
+
const remoteIP = ctx.request.headers.get("x-forwarded-for") || ctx.req?.socket?.remoteAddress;
|
|
5513
6723
|
const logEntry = {
|
|
5514
6724
|
method: ctx.method,
|
|
5515
6725
|
url: ctx.url.toString(),
|
|
5516
6726
|
status: response.status,
|
|
5517
6727
|
duration,
|
|
5518
6728
|
timestamp: Date.now(),
|
|
5519
|
-
handlerStack: ctx.handlerStack
|
|
6729
|
+
handlerStack: this.serializeHandlerStack(ctx.handlerStack),
|
|
6730
|
+
body: this.serializeBody(ctx.responseBody),
|
|
6731
|
+
requestBody: ctx.bodyData || ctx.requestBody,
|
|
6732
|
+
// ShokupanContext usually stores parsed body here if parsed
|
|
6733
|
+
contentType: response.headers["content-type"] || response.headers["Content-Type"],
|
|
6734
|
+
type: "xhr",
|
|
6735
|
+
direction: "inbound",
|
|
6736
|
+
size: responseSize,
|
|
6737
|
+
protocol: ctx.req?.httpVersion,
|
|
6738
|
+
// Try to get protocol from raw request if available, Bun might expose it
|
|
6739
|
+
domain: urlObj.hostname,
|
|
6740
|
+
path: urlObj.pathname,
|
|
6741
|
+
scheme: urlObj.protocol.replace(":", ""),
|
|
6742
|
+
cookies: cookiesCount,
|
|
6743
|
+
transferred: responseSize + responseHeadersSize,
|
|
6744
|
+
remoteIP,
|
|
6745
|
+
requestHeaders: headers,
|
|
6746
|
+
responseHeaders: resHeaders
|
|
5520
6747
|
};
|
|
5521
6748
|
this.metrics.logs.push(logEntry);
|
|
5522
6749
|
try {
|
|
5523
|
-
await this.db.
|
|
6750
|
+
await this.db.query("UPSERT $id CONTENT $data", {
|
|
6751
|
+
id: new RecordId("request", ctx.requestId),
|
|
6752
|
+
data: {
|
|
6753
|
+
...logEntry,
|
|
6754
|
+
direction: "inbound"
|
|
6755
|
+
}
|
|
6756
|
+
});
|
|
5524
6757
|
} catch (e) {
|
|
5525
6758
|
console.error("Failed to record request log", e);
|
|
5526
6759
|
}
|
|
@@ -5529,9 +6762,49 @@ class Dashboard {
|
|
|
5529
6762
|
if (this.metrics.logs.length > 0 && this.metrics.logs[0].timestamp < cutoff) {
|
|
5530
6763
|
this.metrics.logs = this.metrics.logs.filter((log) => log.timestamp >= cutoff);
|
|
5531
6764
|
}
|
|
6765
|
+
const requestData = {
|
|
6766
|
+
id: ctx.requestId,
|
|
6767
|
+
...logEntry
|
|
6768
|
+
};
|
|
6769
|
+
const strategy = this.dashboardConfig.updateStrategy || "immediate";
|
|
6770
|
+
if (strategy === "immediate") {
|
|
6771
|
+
this.broadcastRequestUpdates([requestData]);
|
|
6772
|
+
} else {
|
|
6773
|
+
this.requestsBuffer.push(requestData);
|
|
6774
|
+
}
|
|
5532
6775
|
}
|
|
5533
6776
|
};
|
|
5534
6777
|
}
|
|
6778
|
+
startRequestPushTimer() {
|
|
6779
|
+
const interval = this.dashboardConfig.updateInterval || 1e4;
|
|
6780
|
+
this.requestPushTimer = setInterval(() => {
|
|
6781
|
+
if (this.requestsBuffer.length > 0) {
|
|
6782
|
+
this.broadcastRequestUpdates();
|
|
6783
|
+
}
|
|
6784
|
+
}, interval);
|
|
6785
|
+
}
|
|
6786
|
+
broadcastRequestUpdates(requestsOverride) {
|
|
6787
|
+
if (this.clients.size === 0) {
|
|
6788
|
+
if (!requestsOverride) this.requestsBuffer = [];
|
|
6789
|
+
return;
|
|
6790
|
+
}
|
|
6791
|
+
let requests;
|
|
6792
|
+
if (requestsOverride) {
|
|
6793
|
+
requests = requestsOverride;
|
|
6794
|
+
} else {
|
|
6795
|
+
requests = [...this.requestsBuffer];
|
|
6796
|
+
this.requestsBuffer = [];
|
|
6797
|
+
}
|
|
6798
|
+
if (requests.length === 0) return;
|
|
6799
|
+
console.log(`[Dashboard] Broadcasting ${requests.length} requests. Sample ID: ${requests[0].id}`);
|
|
6800
|
+
const data = JSON.stringify({
|
|
6801
|
+
type: "requests-update",
|
|
6802
|
+
requests
|
|
6803
|
+
});
|
|
6804
|
+
for (const client of this.clients) {
|
|
6805
|
+
client.send(data);
|
|
6806
|
+
}
|
|
6807
|
+
}
|
|
5535
6808
|
updateTiming(duration) {
|
|
5536
6809
|
const alpha = 0.1;
|
|
5537
6810
|
if (this.metrics.averageTotalTime_ms === 0) {
|
|
@@ -5544,6 +6817,39 @@ class Dashboard {
|
|
|
5544
6817
|
this.metrics.recentTimings.shift();
|
|
5545
6818
|
}
|
|
5546
6819
|
}
|
|
6820
|
+
serializeHandlerStack(stack) {
|
|
6821
|
+
if (!stack || !Array.isArray(stack)) return [];
|
|
6822
|
+
return stack.map((item) => ({
|
|
6823
|
+
name: item.name,
|
|
6824
|
+
file: item.file,
|
|
6825
|
+
line: item.line,
|
|
6826
|
+
duration: item.duration,
|
|
6827
|
+
startTime: item.startTime,
|
|
6828
|
+
isBuiltin: item.isBuiltin
|
|
6829
|
+
// stateChanges: item.stateChanges // Exclude complex objects for now
|
|
6830
|
+
}));
|
|
6831
|
+
}
|
|
6832
|
+
serializeBody(body) {
|
|
6833
|
+
if (!body) return void 0;
|
|
6834
|
+
if (typeof body === "string") {
|
|
6835
|
+
if (body.length > 524288) {
|
|
6836
|
+
return body.substring(0, 524288) + "... (truncated)";
|
|
6837
|
+
}
|
|
6838
|
+
return body;
|
|
6839
|
+
}
|
|
6840
|
+
if (typeof body === "object") {
|
|
6841
|
+
try {
|
|
6842
|
+
const str = JSON.stringify(body);
|
|
6843
|
+
if (str.length > 524288) {
|
|
6844
|
+
return str.substring(0, 524288) + "... (truncated)";
|
|
6845
|
+
}
|
|
6846
|
+
return body;
|
|
6847
|
+
} catch (e) {
|
|
6848
|
+
return "[Circular or Non-Serializable Body]";
|
|
6849
|
+
}
|
|
6850
|
+
}
|
|
6851
|
+
return "[Binary or Unreadable Body]";
|
|
6852
|
+
}
|
|
5547
6853
|
}
|
|
5548
6854
|
function unknownError(ctx) {
|
|
5549
6855
|
return ctx.json({ error: "Unknown Error" }, 500);
|
|
@@ -5627,6 +6933,12 @@ class ScalarPlugin extends ShokupanRouter {
|
|
|
5627
6933
|
pluginOptions.config ??= {};
|
|
5628
6934
|
super();
|
|
5629
6935
|
this.pluginOptions = pluginOptions;
|
|
6936
|
+
this.metadata = {
|
|
6937
|
+
file: import.meta.file,
|
|
6938
|
+
line: 1,
|
|
6939
|
+
name: "ScalarPlugin",
|
|
6940
|
+
pluginName: "Scalar"
|
|
6941
|
+
};
|
|
5630
6942
|
this.initRoutes();
|
|
5631
6943
|
}
|
|
5632
6944
|
eta;
|
|
@@ -5645,34 +6957,73 @@ class ScalarPlugin extends ShokupanRouter {
|
|
|
5645
6957
|
}
|
|
5646
6958
|
initRoutes() {
|
|
5647
6959
|
const bootId = Date.now().toString();
|
|
5648
|
-
this.get("/_lifecycle", (ctx) =>
|
|
6960
|
+
this.get("/_lifecycle", (ctx) => {
|
|
6961
|
+
const success = ctx.upgrade({
|
|
6962
|
+
data: {
|
|
6963
|
+
bootId,
|
|
6964
|
+
handler: {
|
|
6965
|
+
open: (ws) => {
|
|
6966
|
+
ws.send(JSON.stringify({ type: "hello", bootId }));
|
|
6967
|
+
}
|
|
6968
|
+
}
|
|
6969
|
+
}
|
|
6970
|
+
});
|
|
6971
|
+
if (success) return void 0;
|
|
6972
|
+
return ctx.json({ boot: bootId });
|
|
6973
|
+
});
|
|
5649
6974
|
this.get("/", async (ctx) => {
|
|
5650
6975
|
await this.ensureEta();
|
|
5651
|
-
let path = ctx.
|
|
6976
|
+
let path = ctx.path;
|
|
5652
6977
|
if (!path.endsWith("/")) path += "/";
|
|
5653
6978
|
const devScript = ctx.app?.applicationConfig.development ? `
|
|
5654
6979
|
<script>
|
|
5655
6980
|
(function() {
|
|
5656
6981
|
const bootId = "${bootId}";
|
|
5657
|
-
let
|
|
6982
|
+
let ws;
|
|
6983
|
+
let reconnectTimer;
|
|
5658
6984
|
|
|
5659
|
-
|
|
5660
|
-
|
|
5661
|
-
|
|
5662
|
-
|
|
5663
|
-
|
|
5664
|
-
|
|
5665
|
-
|
|
5666
|
-
|
|
5667
|
-
|
|
5668
|
-
|
|
5669
|
-
|
|
6985
|
+
function connect() {
|
|
6986
|
+
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
6987
|
+
const wsUrl = protocol + '//' + window.location.host + '${path}_lifecycle';
|
|
6988
|
+
|
|
6989
|
+
ws = new WebSocket(wsUrl);
|
|
6990
|
+
|
|
6991
|
+
ws.onopen = () => {
|
|
6992
|
+
console.log('[Scalar] Connected to lifecycle monitor');
|
|
6993
|
+
if (reconnectTimer) {
|
|
6994
|
+
clearTimeout(reconnectTimer);
|
|
6995
|
+
reconnectTimer = undefined;
|
|
5670
6996
|
}
|
|
5671
|
-
}
|
|
5672
|
-
|
|
5673
|
-
|
|
5674
|
-
|
|
5675
|
-
|
|
6997
|
+
};
|
|
6998
|
+
|
|
6999
|
+
ws.onmessage = (event) => {
|
|
7000
|
+
try {
|
|
7001
|
+
const data = JSON.parse(event.data);
|
|
7002
|
+
if (data.type === 'hello') {
|
|
7003
|
+
if (data.bootId !== bootId) {
|
|
7004
|
+
console.log('[Scalar] Server restarted (timestamp change), reloading...');
|
|
7005
|
+
window.location.reload();
|
|
7006
|
+
}
|
|
7007
|
+
}
|
|
7008
|
+
} catch (e) {}
|
|
7009
|
+
};
|
|
7010
|
+
|
|
7011
|
+
ws.onclose = () => {
|
|
7012
|
+
console.log('[Scalar] Lifecycle connection lost');
|
|
7013
|
+
ws = undefined;
|
|
7014
|
+
scheduleReconnect();
|
|
7015
|
+
};
|
|
7016
|
+
}
|
|
7017
|
+
|
|
7018
|
+
function scheduleReconnect() {
|
|
7019
|
+
if (reconnectTimer) return;
|
|
7020
|
+
reconnectTimer = setTimeout(() => {
|
|
7021
|
+
reconnectTimer = undefined;
|
|
7022
|
+
connect();
|
|
7023
|
+
}, 2000);
|
|
7024
|
+
}
|
|
7025
|
+
|
|
7026
|
+
connect();
|
|
5676
7027
|
})();
|
|
5677
7028
|
<\/script>
|
|
5678
7029
|
` : "";
|
|
@@ -5866,10 +7217,13 @@ function Cors(options = {}) {
|
|
|
5866
7217
|
};
|
|
5867
7218
|
const opts = { ...defaults2, ...options };
|
|
5868
7219
|
const corsMiddleware = async function CorsMiddleware(ctx, next) {
|
|
5869
|
-
const headers =
|
|
7220
|
+
const headers = {};
|
|
5870
7221
|
const origin = ctx.headers.get("origin");
|
|
5871
|
-
const set = (k, v) => headers
|
|
5872
|
-
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
|
+
};
|
|
5873
7227
|
if (origin === "null" && opts.origin !== "null") {
|
|
5874
7228
|
return next();
|
|
5875
7229
|
}
|
|
@@ -5928,10 +7282,10 @@ function Cors(options = {}) {
|
|
|
5928
7282
|
}
|
|
5929
7283
|
const response = await next();
|
|
5930
7284
|
if (response instanceof Response) {
|
|
5931
|
-
const
|
|
5932
|
-
for (let i = 0; i <
|
|
5933
|
-
const
|
|
5934
|
-
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]);
|
|
5935
7289
|
}
|
|
5936
7290
|
}
|
|
5937
7291
|
return response;
|