shokupan 0.9.0 → 0.10.1
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-BqIe1p0R.js +35 -0
- package/dist/analyzer-BqIe1p0R.js.map +1 -0
- package/dist/analyzer-CKLGLFtx.cjs +35 -0
- package/dist/analyzer-CKLGLFtx.cjs.map +1 -0
- package/dist/{analyzer-Ce_7JxZh.js → analyzer.impl-CV6W1Eq7.js} +238 -21
- package/dist/analyzer.impl-CV6W1Eq7.js.map +1 -0
- package/dist/{analyzer-Bei1sVWp.cjs → analyzer.impl-D9Yi1Hax.cjs} +237 -20
- package/dist/analyzer.impl-D9Yi1Hax.cjs.map +1 -0
- package/dist/cli.cjs +1 -1
- package/dist/cli.js +1 -1
- package/dist/context.d.ts +19 -7
- package/dist/http-server-BEMPIs33.cjs.map +1 -1
- package/dist/http-server-CCeagTyU.js.map +1 -1
- package/dist/index.cjs +1500 -275
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.js +1482 -256
- package/dist/index.js.map +1 -1
- package/dist/plugins/application/api-explorer/plugin.d.ts +9 -0
- package/dist/plugins/application/api-explorer/static/explorer-client.mjs +880 -0
- package/dist/plugins/application/api-explorer/static/style.css +767 -0
- package/dist/plugins/application/api-explorer/static/theme.css +128 -0
- package/dist/plugins/application/asyncapi/generator.d.ts +3 -0
- package/dist/plugins/application/asyncapi/plugin.d.ts +15 -0
- package/dist/plugins/application/asyncapi/static/asyncapi-client.mjs +748 -0
- package/dist/plugins/application/asyncapi/static/style.css +565 -0
- package/dist/plugins/application/asyncapi/static/theme.css +128 -0
- package/dist/plugins/application/auth.d.ts +3 -1
- package/dist/plugins/application/dashboard/metrics-collector.d.ts +3 -1
- package/dist/plugins/application/dashboard/plugin.d.ts +13 -3
- package/dist/plugins/application/dashboard/static/registry.css +0 -53
- package/dist/plugins/application/dashboard/static/styles.css +29 -20
- package/dist/plugins/application/dashboard/static/tabulator.css +83 -31
- package/dist/plugins/application/dashboard/static/theme.css +128 -0
- package/dist/plugins/application/graphql-apollo.d.ts +33 -0
- package/dist/plugins/application/graphql-yoga.d.ts +25 -0
- package/dist/plugins/application/openapi/analyzer.d.ts +12 -119
- package/dist/plugins/application/openapi/analyzer.impl.d.ts +167 -0
- package/dist/plugins/application/scalar.d.ts +9 -2
- package/dist/router.d.ts +80 -51
- package/dist/shokupan.d.ts +14 -8
- package/dist/util/datastore.d.ts +71 -7
- package/dist/util/decorators.d.ts +2 -2
- package/dist/util/types.d.ts +96 -3
- package/package.json +32 -12
- package/dist/analyzer-Bei1sVWp.cjs.map +0 -1
- package/dist/analyzer-Ce_7JxZh.js.map +0 -1
- package/dist/plugins/application/dashboard/static/scrollbar.css +0 -24
- package/dist/plugins/application/dashboard/template.eta +0 -246
package/dist/index.cjs
CHANGED
|
@@ -24,21 +24,24 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
24
24
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
25
25
|
const nanoid = require("nanoid");
|
|
26
26
|
const promises = require("node:fs/promises");
|
|
27
|
-
const
|
|
28
|
-
const node_async_hooks = require("node:async_hooks");
|
|
27
|
+
const node_util = require("node:util");
|
|
29
28
|
const surrealdb = require("surrealdb");
|
|
30
|
-
const eta$
|
|
29
|
+
const eta$1 = require("eta");
|
|
31
30
|
const promises$1 = require("fs/promises");
|
|
32
31
|
const path = require("path");
|
|
32
|
+
const api = require("@opentelemetry/api");
|
|
33
|
+
const jsYaml = require("js-yaml");
|
|
34
|
+
const node_async_hooks = require("node:async_hooks");
|
|
33
35
|
const os = require("node:os");
|
|
34
|
-
const arctic = require("arctic");
|
|
35
|
-
const jose = require("jose");
|
|
36
|
-
const cluster = require("node:cluster");
|
|
37
|
-
const net = require("node:net");
|
|
38
36
|
const path$1 = require("node:path");
|
|
39
37
|
const node_url = require("node:url");
|
|
38
|
+
const renderToString = require("preact-render-to-string");
|
|
39
|
+
const jsxRuntime = require("preact/jsx-runtime");
|
|
40
|
+
const cluster = require("node:cluster");
|
|
41
|
+
const net = require("node:net");
|
|
40
42
|
const node_perf_hooks = require("node:perf_hooks");
|
|
41
|
-
const
|
|
43
|
+
const fs = require("node:fs");
|
|
44
|
+
const analyzer = require("./analyzer-CKLGLFtx.cjs");
|
|
42
45
|
const zlib = require("node:zlib");
|
|
43
46
|
const Ajv = require("ajv");
|
|
44
47
|
const addFormats = require("ajv-formats");
|
|
@@ -62,7 +65,6 @@ function _interopNamespaceDefault(e) {
|
|
|
62
65
|
return Object.freeze(n);
|
|
63
66
|
}
|
|
64
67
|
const os__namespace = /* @__PURE__ */ _interopNamespaceDefault(os);
|
|
65
|
-
const jose__namespace = /* @__PURE__ */ _interopNamespaceDefault(jose);
|
|
66
68
|
const zlib__namespace = /* @__PURE__ */ _interopNamespaceDefault(zlib);
|
|
67
69
|
const HTTP_STATUS = {
|
|
68
70
|
// 2xx Success
|
|
@@ -311,6 +313,21 @@ class ShokupanContext {
|
|
|
311
313
|
[$cachedHost];
|
|
312
314
|
[$cachedOrigin];
|
|
313
315
|
[$cachedQuery];
|
|
316
|
+
disconnectCallbacks = [];
|
|
317
|
+
/**
|
|
318
|
+
* Registers a callback to be executed when the associated WebSocket disconnects.
|
|
319
|
+
* This is only applicable for requests that are part of a WebSocket interaction or upgrade.
|
|
320
|
+
*/
|
|
321
|
+
onSocketDisconnect(callback) {
|
|
322
|
+
this.disconnectCallbacks.push(callback);
|
|
323
|
+
}
|
|
324
|
+
/**
|
|
325
|
+
* @internal
|
|
326
|
+
* Retrieves registered disconnect callbacks for execution.
|
|
327
|
+
*/
|
|
328
|
+
getDisconnectCallbacks() {
|
|
329
|
+
return this.disconnectCallbacks;
|
|
330
|
+
}
|
|
314
331
|
[$ws];
|
|
315
332
|
[$socket];
|
|
316
333
|
[$io];
|
|
@@ -325,6 +342,20 @@ class ShokupanContext {
|
|
|
325
342
|
get requestId() {
|
|
326
343
|
return this[$requestId] ??= this.app?.applicationConfig?.idGenerator?.() ?? nanoid.nanoid();
|
|
327
344
|
}
|
|
345
|
+
[/* @__PURE__ */ Symbol.for("nodejs.util.inspect.custom")]() {
|
|
346
|
+
const innerString = node_util.inspect({
|
|
347
|
+
method: this.request.method,
|
|
348
|
+
url: this.request.url,
|
|
349
|
+
requestHeaders: new Map(this.request.headers),
|
|
350
|
+
sessionId: this.sessionID,
|
|
351
|
+
state: this.state,
|
|
352
|
+
params: this.params,
|
|
353
|
+
response: this[$finalResponse]?.body,
|
|
354
|
+
responseHeaders: new Map(this[$finalResponse]?.headers),
|
|
355
|
+
handlerStack: this.handlerStack.map((h) => h.name === "anonymous" ? h.file + ":" + h.line : h.name)
|
|
356
|
+
}, { depth: null, colors: true, numericSeparator: true, customInspect: true });
|
|
357
|
+
return "Context(" + this.requestId + ") {" + innerString.slice(1, -2) + ",\n ...others\n}";
|
|
358
|
+
}
|
|
328
359
|
get url() {
|
|
329
360
|
if (!this[$url]) {
|
|
330
361
|
const urlString = this.request.url || "http://localhost/";
|
|
@@ -376,10 +407,8 @@ class ShokupanContext {
|
|
|
376
407
|
if (this[$cachedQuery]) return this[$cachedQuery];
|
|
377
408
|
const q = /* @__PURE__ */ Object.create(null);
|
|
378
409
|
const blocklist = ["__proto__", "constructor", "prototype"];
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
const [key, value] = entries[i];
|
|
382
|
-
if (blocklist.includes(key)) continue;
|
|
410
|
+
this.url.searchParams.forEach((value, key) => {
|
|
411
|
+
if (blocklist.includes(key)) return;
|
|
383
412
|
if (Object.prototype.hasOwnProperty.call(q, key)) {
|
|
384
413
|
if (Array.isArray(q[key])) {
|
|
385
414
|
q[key].push(value);
|
|
@@ -389,7 +418,7 @@ class ShokupanContext {
|
|
|
389
418
|
} else {
|
|
390
419
|
q[key] = value;
|
|
391
420
|
}
|
|
392
|
-
}
|
|
421
|
+
});
|
|
393
422
|
this[$cachedQuery] = q;
|
|
394
423
|
return q;
|
|
395
424
|
}
|
|
@@ -682,12 +711,12 @@ class ShokupanContext {
|
|
|
682
711
|
/**
|
|
683
712
|
* Respond with a JSON object
|
|
684
713
|
*/
|
|
685
|
-
json(data, status, headers) {
|
|
714
|
+
async json(data, status, headers) {
|
|
686
715
|
const finalStatus = status ?? this.response.status ?? 200;
|
|
687
716
|
if (!VALID_HTTP_STATUSES.has(finalStatus)) {
|
|
688
717
|
throw new Error(`Invalid HTTP status code: ${finalStatus}`);
|
|
689
718
|
}
|
|
690
|
-
const jsonString = JSON.stringify(data);
|
|
719
|
+
const jsonString = JSON.stringify(data instanceof Promise ? await data : data);
|
|
691
720
|
this[$rawBody] = jsonString;
|
|
692
721
|
if (!headers && !this.response.hasPopulatedHeaders) {
|
|
693
722
|
this[$finalResponse] = new Response(jsonString, {
|
|
@@ -704,14 +733,14 @@ class ShokupanContext {
|
|
|
704
733
|
/**
|
|
705
734
|
* Respond with a text string
|
|
706
735
|
*/
|
|
707
|
-
text(data, status, headers) {
|
|
736
|
+
async text(data, status, headers) {
|
|
708
737
|
const finalStatus = status ?? this.response.status ?? 200;
|
|
709
738
|
if (this.app.applicationConfig.validateStatusCodes && !VALID_HTTP_STATUSES.has(finalStatus)) {
|
|
710
739
|
throw new Error(`Invalid HTTP status code: ${finalStatus}`);
|
|
711
740
|
}
|
|
712
|
-
this[$rawBody] = data;
|
|
741
|
+
this[$rawBody] = data instanceof Promise ? await data : data;
|
|
713
742
|
if (!headers && !this.response.hasPopulatedHeaders) {
|
|
714
|
-
this[$finalResponse] = new Response(
|
|
743
|
+
this[$finalResponse] = new Response(this[$rawBody], {
|
|
715
744
|
status: finalStatus,
|
|
716
745
|
headers: { "content-type": "text/plain; charset=utf-8" }
|
|
717
746
|
});
|
|
@@ -719,71 +748,73 @@ class ShokupanContext {
|
|
|
719
748
|
}
|
|
720
749
|
const finalHeaders = this.mergeHeaders(headers);
|
|
721
750
|
finalHeaders.set("content-type", "text/plain; charset=utf-8");
|
|
722
|
-
this[$finalResponse] = new Response(
|
|
751
|
+
this[$finalResponse] = new Response(this[$rawBody], { status: finalStatus, headers: finalHeaders });
|
|
723
752
|
return this[$finalResponse];
|
|
724
753
|
}
|
|
725
754
|
/**
|
|
726
755
|
* Respond with HTML content
|
|
727
756
|
*/
|
|
728
|
-
html(html, status, headers) {
|
|
757
|
+
async html(html, status, headers) {
|
|
729
758
|
const finalStatus = status ?? this.response.status ?? 200;
|
|
730
759
|
if (this.app.applicationConfig.validateStatusCodes && !VALID_HTTP_STATUSES.has(finalStatus)) {
|
|
731
760
|
throw new Error(`Invalid HTTP status code: ${finalStatus}`);
|
|
732
761
|
}
|
|
733
762
|
const finalHeaders = this.mergeHeaders(headers);
|
|
734
763
|
finalHeaders.set("content-type", "text/html; charset=utf-8");
|
|
735
|
-
this[$rawBody] = html;
|
|
736
|
-
this[$finalResponse] = new Response(
|
|
764
|
+
this[$rawBody] = html instanceof Promise ? await html : html;
|
|
765
|
+
this[$finalResponse] = new Response(this[$rawBody], { status: finalStatus, headers: finalHeaders });
|
|
737
766
|
return this[$finalResponse];
|
|
738
767
|
}
|
|
739
768
|
/**
|
|
740
769
|
* Respond with a redirect
|
|
741
770
|
*/
|
|
742
|
-
redirect(url, status = 302) {
|
|
771
|
+
async redirect(url, status = 302) {
|
|
743
772
|
if (this.app.applicationConfig.validateStatusCodes && !VALID_REDIRECT_STATUSES.has(status)) {
|
|
744
773
|
throw new Error(`Invalid redirect status code: ${status}`);
|
|
745
774
|
}
|
|
746
|
-
const
|
|
747
|
-
|
|
748
|
-
this[$finalResponse] = new Response(null, { status, headers });
|
|
775
|
+
const finalHeaders = this.mergeHeaders();
|
|
776
|
+
finalHeaders.set("Location", url instanceof Promise ? await url : url);
|
|
777
|
+
this[$finalResponse] = new Response(null, { status, headers: finalHeaders });
|
|
749
778
|
return this[$finalResponse];
|
|
750
779
|
}
|
|
751
780
|
/**
|
|
752
781
|
* Respond with a status code
|
|
753
782
|
* DOES NOT CHAIN!
|
|
754
783
|
*/
|
|
755
|
-
status(
|
|
784
|
+
async status(statusCode) {
|
|
785
|
+
const status = statusCode instanceof Promise ? await statusCode : statusCode;
|
|
756
786
|
if (this.app.applicationConfig.validateStatusCodes && !VALID_HTTP_STATUSES.has(status)) {
|
|
757
787
|
throw new Error(`Invalid HTTP status code: ${status}`);
|
|
758
788
|
}
|
|
759
|
-
const
|
|
760
|
-
this[$finalResponse] = new Response(null, { status, headers });
|
|
789
|
+
const finalHeaders = this.mergeHeaders();
|
|
790
|
+
this[$finalResponse] = new Response(null, { status, headers: finalHeaders });
|
|
761
791
|
return this[$finalResponse];
|
|
762
792
|
}
|
|
763
793
|
/**
|
|
764
794
|
* Respond with a file
|
|
765
795
|
*/
|
|
766
796
|
async file(path2, fileOptions, responseOptions) {
|
|
767
|
-
const
|
|
797
|
+
const finalHeaders = this.mergeHeaders(responseOptions?.headers);
|
|
768
798
|
const status = responseOptions?.status ?? this.response.status;
|
|
769
799
|
if (this.app.applicationConfig.validateStatusCodes && !VALID_HTTP_STATUSES.has(status)) {
|
|
770
800
|
throw new Error(`Invalid HTTP status code: ${status}`);
|
|
771
801
|
}
|
|
772
802
|
if (typeof Bun !== "undefined") {
|
|
773
|
-
this[$finalResponse] = new Response(Bun.file(path2, fileOptions), { status, headers });
|
|
803
|
+
this[$finalResponse] = new Response(Bun.file(path2, fileOptions), { status, headers: finalHeaders });
|
|
774
804
|
return this[$finalResponse];
|
|
775
805
|
} else {
|
|
776
806
|
const fileBuffer = await promises.readFile(path2);
|
|
777
807
|
if (fileOptions?.type) {
|
|
778
|
-
|
|
808
|
+
finalHeaders.set("content-type", fileOptions.type);
|
|
779
809
|
}
|
|
780
|
-
this[$finalResponse] = new Response(fileBuffer, { status, headers });
|
|
810
|
+
this[$finalResponse] = new Response(fileBuffer, { status, headers: finalHeaders });
|
|
781
811
|
return this[$finalResponse];
|
|
782
812
|
}
|
|
783
813
|
}
|
|
784
814
|
/**
|
|
785
815
|
* Render a JSX element
|
|
786
816
|
* @param element JSX Element
|
|
817
|
+
* @param args JSX Element Args/Props
|
|
787
818
|
* @param status HTTP Status
|
|
788
819
|
* @param headers HTTP Headers
|
|
789
820
|
*/
|
|
@@ -837,29 +868,6 @@ const compose = (middleware) => {
|
|
|
837
868
|
return runner(0);
|
|
838
869
|
};
|
|
839
870
|
};
|
|
840
|
-
const tracer = api.trace.getTracer("shokupan.middleware");
|
|
841
|
-
function traceHandler(fn, name) {
|
|
842
|
-
return async function(...args) {
|
|
843
|
-
return tracer.startActiveSpan(`route handler - ${name}`, {
|
|
844
|
-
kind: api.SpanKind.INTERNAL,
|
|
845
|
-
attributes: {
|
|
846
|
-
"http.route": name,
|
|
847
|
-
"component": "shokupan.route"
|
|
848
|
-
}
|
|
849
|
-
}, async (span) => {
|
|
850
|
-
try {
|
|
851
|
-
const result = await fn.apply(this, args);
|
|
852
|
-
return result;
|
|
853
|
-
} catch (err) {
|
|
854
|
-
span.recordException(err);
|
|
855
|
-
span.setStatus({ code: api.SpanStatusCode.ERROR, message: err.message });
|
|
856
|
-
throw err;
|
|
857
|
-
} finally {
|
|
858
|
-
span.end();
|
|
859
|
-
}
|
|
860
|
-
});
|
|
861
|
-
};
|
|
862
|
-
}
|
|
863
871
|
function isObject(item) {
|
|
864
872
|
return item && typeof item === "object" && !Array.isArray(item);
|
|
865
873
|
}
|
|
@@ -1032,24 +1040,34 @@ function analyzeHandler(handler) {
|
|
|
1032
1040
|
}
|
|
1033
1041
|
return { inferredSpec };
|
|
1034
1042
|
}
|
|
1035
|
-
async function getAstRoutes(applications) {
|
|
1043
|
+
async function getAstRoutes$1(applications) {
|
|
1036
1044
|
const astRoutes = [];
|
|
1037
|
-
const getExpandedRoutes = (app, prefix = "", seen = /* @__PURE__ */ new Set()) => {
|
|
1045
|
+
const getExpandedRoutes = (app, prefix = "", seen = /* @__PURE__ */ new Set(), sourceOverride) => {
|
|
1038
1046
|
if (seen.has(app.name)) return [];
|
|
1039
1047
|
const newSeen = new Set(seen);
|
|
1040
1048
|
newSeen.add(app.name);
|
|
1041
1049
|
const expanded = [];
|
|
1050
|
+
let currentPrefix = prefix;
|
|
1051
|
+
if (app.controllerPrefix) {
|
|
1052
|
+
const cleanPrefix = currentPrefix.endsWith("/") ? currentPrefix.slice(0, -1) : currentPrefix;
|
|
1053
|
+
const cleanCont = app.controllerPrefix.startsWith("/") ? app.controllerPrefix : "/" + app.controllerPrefix;
|
|
1054
|
+
currentPrefix = cleanPrefix + cleanCont;
|
|
1055
|
+
}
|
|
1042
1056
|
for (const route of app.routes) {
|
|
1043
|
-
const cleanPrefix =
|
|
1057
|
+
const cleanPrefix = currentPrefix.endsWith("/") ? currentPrefix.slice(0, -1) : currentPrefix;
|
|
1044
1058
|
const cleanPath = route.path.startsWith("/") ? route.path : "/" + route.path;
|
|
1045
1059
|
let joined = cleanPrefix + cleanPath;
|
|
1046
1060
|
if (joined.length > 1 && joined.endsWith("/")) {
|
|
1047
1061
|
joined = joined.slice(0, -1);
|
|
1048
1062
|
}
|
|
1049
|
-
|
|
1063
|
+
const expandedRoute = {
|
|
1050
1064
|
...route,
|
|
1051
1065
|
path: joined || "/"
|
|
1052
|
-
}
|
|
1066
|
+
};
|
|
1067
|
+
if (sourceOverride) {
|
|
1068
|
+
expandedRoute.sourceContext = sourceOverride;
|
|
1069
|
+
}
|
|
1070
|
+
expanded.push(expandedRoute);
|
|
1053
1071
|
}
|
|
1054
1072
|
if (app.mounted) {
|
|
1055
1073
|
for (const mount of app.mounted) {
|
|
@@ -1057,7 +1075,23 @@ async function getAstRoutes(applications) {
|
|
|
1057
1075
|
if (targetApp) {
|
|
1058
1076
|
const cleanPrefix = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
|
|
1059
1077
|
const mountPrefix = mount.prefix.startsWith("/") ? mount.prefix : "/" + mount.prefix;
|
|
1060
|
-
|
|
1078
|
+
let nextSourceOverride = sourceOverride;
|
|
1079
|
+
if (mount.dependency || mount.targetFilePath && mount.targetFilePath.includes("node_modules")) {
|
|
1080
|
+
if (mount.sourceContext) {
|
|
1081
|
+
nextSourceOverride = {
|
|
1082
|
+
...mount.sourceContext,
|
|
1083
|
+
// Add highlight for the mount line to make it clear
|
|
1084
|
+
highlightLines: [mount.sourceContext.startLine, mount.sourceContext.endLine],
|
|
1085
|
+
highlights: [{
|
|
1086
|
+
startLine: mount.sourceContext.startLine,
|
|
1087
|
+
endLine: mount.sourceContext.endLine,
|
|
1088
|
+
type: "return-success"
|
|
1089
|
+
// Use the success color (cyan) for the mount point
|
|
1090
|
+
}]
|
|
1091
|
+
};
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
expanded.push(...getExpandedRoutes(targetApp, cleanPrefix + mountPrefix, newSeen, nextSourceOverride));
|
|
1061
1095
|
}
|
|
1062
1096
|
}
|
|
1063
1097
|
}
|
|
@@ -1085,13 +1119,13 @@ async function generateOpenApi(rootRouter, options = {}) {
|
|
|
1085
1119
|
const defaultTagName = options.defaultTag || "Application";
|
|
1086
1120
|
let astRoutes = [];
|
|
1087
1121
|
try {
|
|
1088
|
-
const { OpenAPIAnalyzer } = await Promise.resolve().then(() => require("./analyzer-
|
|
1122
|
+
const { OpenAPIAnalyzer } = await Promise.resolve().then(() => require("./analyzer-CKLGLFtx.cjs"));
|
|
1089
1123
|
const analyzer2 = new OpenAPIAnalyzer(process.cwd());
|
|
1090
1124
|
const { applications } = await analyzer2.analyze();
|
|
1091
|
-
astRoutes = await getAstRoutes(applications);
|
|
1125
|
+
astRoutes = await getAstRoutes$1(applications);
|
|
1092
1126
|
} catch (e) {
|
|
1093
1127
|
}
|
|
1094
|
-
const collect = (router, prefix = "", currentGroup = defaultTagGroup, defaultTag = defaultTagName) => {
|
|
1128
|
+
const collect = (router, prefix = "", currentGroup = defaultTagGroup, defaultTag = defaultTagName, inheritedMiddleware = []) => {
|
|
1095
1129
|
let group = currentGroup;
|
|
1096
1130
|
let tag = defaultTag;
|
|
1097
1131
|
if (router.config?.group) group = router.config.group;
|
|
@@ -1108,21 +1142,33 @@ async function generateOpenApi(rootRouter, options = {}) {
|
|
|
1108
1142
|
}
|
|
1109
1143
|
}
|
|
1110
1144
|
if (!tagGroups.has(group)) tagGroups.set(group, /* @__PURE__ */ new Set());
|
|
1145
|
+
const routerMiddleware = router.middleware || [];
|
|
1111
1146
|
const routes = router[$routes] || [];
|
|
1112
1147
|
for (const route of routes) {
|
|
1148
|
+
if (!["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS", "HEAD"].includes(route.method.toUpperCase())) {
|
|
1149
|
+
continue;
|
|
1150
|
+
}
|
|
1113
1151
|
const routeGroup = route.group || group;
|
|
1114
1152
|
const cleanPrefix = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
|
|
1115
1153
|
const cleanSubPath = route.path.startsWith("/") ? route.path : "/" + route.path;
|
|
1116
1154
|
let fullPath = cleanPrefix + cleanSubPath || "/";
|
|
1117
|
-
fullPath = fullPath.replace(/:([a-zA-Z0-9_]+)/g, "{$1}");
|
|
1118
1155
|
if (fullPath.length > 1 && fullPath.endsWith("/")) {
|
|
1119
1156
|
fullPath = fullPath.slice(0, -1);
|
|
1120
1157
|
}
|
|
1158
|
+
fullPath = fullPath.replace(/:([a-zA-Z0-9_]+)/g, "{$1}");
|
|
1121
1159
|
if (!paths[fullPath]) paths[fullPath] = {};
|
|
1122
1160
|
const operation = {
|
|
1123
1161
|
responses: { "200": { description: "Successful response" } },
|
|
1124
1162
|
tags: [tag]
|
|
1125
1163
|
};
|
|
1164
|
+
const routeMiddleware = route.middleware || [];
|
|
1165
|
+
const allMiddleware = [...inheritedMiddleware, ...routerMiddleware, ...routeMiddleware];
|
|
1166
|
+
if (allMiddleware.length > 0) {
|
|
1167
|
+
operation["x-shokupan-middleware"] = allMiddleware.map((mw) => ({
|
|
1168
|
+
name: mw.name || "middleware",
|
|
1169
|
+
metadata: mw.metadata
|
|
1170
|
+
}));
|
|
1171
|
+
}
|
|
1126
1172
|
if (route.guards) {
|
|
1127
1173
|
for (const guard of route.guards) {
|
|
1128
1174
|
if (guard.spec) {
|
|
@@ -1160,6 +1206,23 @@ async function generateOpenApi(rootRouter, options = {}) {
|
|
|
1160
1206
|
if (astMatch.description) operation.description = astMatch.description;
|
|
1161
1207
|
if (astMatch.tags) operation.tags = astMatch.tags;
|
|
1162
1208
|
if (astMatch.operationId) operation.operationId = astMatch.operationId;
|
|
1209
|
+
if (astMatch.sourceContext) {
|
|
1210
|
+
const sc = astMatch.sourceContext;
|
|
1211
|
+
operation["x-source-info"] = {
|
|
1212
|
+
file: sc.file,
|
|
1213
|
+
line: sc.startLine,
|
|
1214
|
+
snippet: sc.snippet || astMatch.handlerSource,
|
|
1215
|
+
// Fallback
|
|
1216
|
+
offset: sc.snippetStartLine || sc.startLine,
|
|
1217
|
+
highlightLines: [sc.startLine, sc.endLine],
|
|
1218
|
+
highlights: sc.highlights
|
|
1219
|
+
};
|
|
1220
|
+
operation["x-shokupan-source"] = {
|
|
1221
|
+
file: sc.file,
|
|
1222
|
+
line: sc.startLine,
|
|
1223
|
+
code: sc.snippet || astMatch.handlerSource || ""
|
|
1224
|
+
};
|
|
1225
|
+
}
|
|
1163
1226
|
if (astMatch.requestTypes?.body) {
|
|
1164
1227
|
operation.requestBody = {
|
|
1165
1228
|
content: { "application/json": { schema: astMatch.requestTypes.body } }
|
|
@@ -1171,10 +1234,12 @@ async function generateOpenApi(rootRouter, options = {}) {
|
|
|
1171
1234
|
content: { "application/json": { schema: astMatch.responseSchema } }
|
|
1172
1235
|
};
|
|
1173
1236
|
} else if (astMatch.responseType) {
|
|
1174
|
-
|
|
1237
|
+
let contentType = "application/json";
|
|
1238
|
+
if (astMatch.responseType === "string") contentType = "text/plain";
|
|
1239
|
+
else if (astMatch.responseType === "html") contentType = "text/html";
|
|
1175
1240
|
operation.responses["200"] = {
|
|
1176
1241
|
description: "Successful response",
|
|
1177
|
-
content: { [contentType]: { schema: { type:
|
|
1242
|
+
content: { [contentType]: { schema: { type: "string" } } }
|
|
1178
1243
|
};
|
|
1179
1244
|
}
|
|
1180
1245
|
const params = [];
|
|
@@ -1186,6 +1251,26 @@ async function generateOpenApi(rootRouter, options = {}) {
|
|
|
1186
1251
|
if (params.length > 0) {
|
|
1187
1252
|
operation.parameters = params;
|
|
1188
1253
|
}
|
|
1254
|
+
} else {
|
|
1255
|
+
const runtimeSource = (route.handler.originalHandler || route.handler).toString();
|
|
1256
|
+
let file;
|
|
1257
|
+
let line;
|
|
1258
|
+
if (route.metadata?.file) {
|
|
1259
|
+
file = route.metadata.file;
|
|
1260
|
+
line = route.metadata.line || 1;
|
|
1261
|
+
}
|
|
1262
|
+
operation["x-source-info"] = {
|
|
1263
|
+
snippet: runtimeSource,
|
|
1264
|
+
isRuntime: true,
|
|
1265
|
+
...file ? { file, line: line || 1 } : {}
|
|
1266
|
+
};
|
|
1267
|
+
if (file) {
|
|
1268
|
+
operation["x-shokupan-source"] = {
|
|
1269
|
+
file,
|
|
1270
|
+
line: line || 1,
|
|
1271
|
+
code: runtimeSource
|
|
1272
|
+
};
|
|
1273
|
+
}
|
|
1189
1274
|
}
|
|
1190
1275
|
if (route.keys.length > 0) {
|
|
1191
1276
|
const pathParams = route.keys.map((key) => ({
|
|
@@ -1255,7 +1340,7 @@ async function generateOpenApi(rootRouter, options = {}) {
|
|
|
1255
1340
|
const cleanPrefix = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
|
|
1256
1341
|
const cleanMount = mountPath.startsWith("/") ? mountPath : "/" + mountPath;
|
|
1257
1342
|
const nextPrefix = cleanPrefix + cleanMount || "/";
|
|
1258
|
-
collect(child, nextPrefix, group, tag);
|
|
1343
|
+
collect(child, nextPrefix, group, tag, [...inheritedMiddleware, ...routerMiddleware]);
|
|
1259
1344
|
}
|
|
1260
1345
|
};
|
|
1261
1346
|
collect(rootRouter);
|
|
@@ -1274,41 +1359,7 @@ async function generateOpenApi(rootRouter, options = {}) {
|
|
|
1274
1359
|
"x-tagGroups": xTagGroups
|
|
1275
1360
|
};
|
|
1276
1361
|
}
|
|
1277
|
-
|
|
1278
|
-
request;
|
|
1279
|
-
span;
|
|
1280
|
-
}
|
|
1281
|
-
const asyncContext = new node_async_hooks.AsyncLocalStorage();
|
|
1282
|
-
class HttpError extends Error {
|
|
1283
|
-
status;
|
|
1284
|
-
constructor(message, status) {
|
|
1285
|
-
super(message);
|
|
1286
|
-
this.name = "HttpError";
|
|
1287
|
-
this.status = status;
|
|
1288
|
-
if (Error.captureStackTrace) {
|
|
1289
|
-
Error.captureStackTrace(this, HttpError);
|
|
1290
|
-
}
|
|
1291
|
-
}
|
|
1292
|
-
}
|
|
1293
|
-
function getErrorStatus(err) {
|
|
1294
|
-
if (!err || typeof err !== "object") {
|
|
1295
|
-
return 500;
|
|
1296
|
-
}
|
|
1297
|
-
if (typeof err.status === "number") {
|
|
1298
|
-
return err.status;
|
|
1299
|
-
}
|
|
1300
|
-
if (typeof err.statusCode === "number") {
|
|
1301
|
-
return err.statusCode;
|
|
1302
|
-
}
|
|
1303
|
-
return 500;
|
|
1304
|
-
}
|
|
1305
|
-
class EventError extends HttpError {
|
|
1306
|
-
constructor(message = "Event Error") {
|
|
1307
|
-
super(message, 500);
|
|
1308
|
-
this.name = "EventError";
|
|
1309
|
-
}
|
|
1310
|
-
}
|
|
1311
|
-
const eta$1 = new eta$2.Eta();
|
|
1362
|
+
const eta = new eta$1.Eta();
|
|
1312
1363
|
function serveStatic(config, prefix) {
|
|
1313
1364
|
const rootPath = path.resolve(config.root || ".");
|
|
1314
1365
|
const normalizedPrefix = prefix.endsWith("/") && prefix !== "/" ? prefix.slice(0, -1) : prefix;
|
|
@@ -1407,7 +1458,7 @@ function serveStatic(config, prefix) {
|
|
|
1407
1458
|
if (config.listDirectory) {
|
|
1408
1459
|
try {
|
|
1409
1460
|
const files = await promises$1.readdir(requestPath);
|
|
1410
|
-
const listing = eta
|
|
1461
|
+
const listing = eta.renderString(`
|
|
1411
1462
|
<!DOCTYPE html>
|
|
1412
1463
|
<html>
|
|
1413
1464
|
<head>
|
|
@@ -1447,7 +1498,7 @@ function serveStatic(config, prefix) {
|
|
|
1447
1498
|
if (typeof Bun !== "undefined") {
|
|
1448
1499
|
response = new Response(Bun.file(finalPath));
|
|
1449
1500
|
} else {
|
|
1450
|
-
const fileBuffer = await promises$1.readFile(finalPath);
|
|
1501
|
+
const fileBuffer = await promises$1.readFile(finalPath, { encoding: "binary" });
|
|
1451
1502
|
response = new Response(fileBuffer);
|
|
1452
1503
|
}
|
|
1453
1504
|
if (config.hooks?.onResponse) {
|
|
@@ -1460,67 +1511,6 @@ function serveStatic(config, prefix) {
|
|
|
1460
1511
|
serveStaticMiddleware.pluginName = "ServeStatic";
|
|
1461
1512
|
return serveStaticMiddleware;
|
|
1462
1513
|
}
|
|
1463
|
-
const G = globalThis;
|
|
1464
|
-
G.__shokupan_db = G.__shokupan_db || null;
|
|
1465
|
-
G.__shokupan_db_promise = G.__shokupan_db_promise || null;
|
|
1466
|
-
async function ensureDb() {
|
|
1467
|
-
if (G.__shokupan_db) return G.__shokupan_db;
|
|
1468
|
-
if (G.__shokupan_db_promise) return G.__shokupan_db_promise;
|
|
1469
|
-
G.__shokupan_db_promise = (async () => {
|
|
1470
|
-
try {
|
|
1471
|
-
const { createNodeEngines } = await import("@surrealdb/node");
|
|
1472
|
-
const surreal = await import("surrealdb");
|
|
1473
|
-
const engine = process.env["SHOKUPAN_DB_ENGINE"] === "memory" ? "mem://" : "rocksdb://database";
|
|
1474
|
-
const _db = new surrealdb.Surreal({
|
|
1475
|
-
engines: createNodeEngines()
|
|
1476
|
-
});
|
|
1477
|
-
await _db.connect(engine, { namespace: "vendor", database: "shokupan" });
|
|
1478
|
-
await _db.query(`
|
|
1479
|
-
DEFINE TABLE OVERWRITE failed_requests SCHEMALESS COMMENT "Created by Shokupan";
|
|
1480
|
-
DEFINE TABLE OVERWRITE sessions SCHEMALESS COMMENT "Created by Shokupan";
|
|
1481
|
-
DEFINE TABLE OVERWRITE users SCHEMALESS COMMENT "Created by Shokupan";
|
|
1482
|
-
DEFINE TABLE OVERWRITE idempotency_keys SCHEMALESS COMMENT "Created by Shokupan";
|
|
1483
|
-
DEFINE TABLE OVERWRITE middleware_tracking SCHEMALESS COMMENT "Created by Shokupan";
|
|
1484
|
-
DEFINE TABLE OVERWRITE requests SCHEMALESS COMMENT "Created by Shokupan";
|
|
1485
|
-
DEFINE TABLE OVERWRITE metrics SCHEMALESS COMMENT "Created by Shokupan";
|
|
1486
|
-
`);
|
|
1487
|
-
G.__shokupan_db = _db;
|
|
1488
|
-
return _db;
|
|
1489
|
-
} catch (e) {
|
|
1490
|
-
G.__shokupan_db_promise = null;
|
|
1491
|
-
if (e.code === "ERR_MODULE_NOT_FOUND" || e.message.includes("Cannot find module")) {
|
|
1492
|
-
throw new Error("SurrealDB dependencies not found. To use the datastore, please install 'surrealdb' and '@surrealdb/node'.");
|
|
1493
|
-
}
|
|
1494
|
-
throw e;
|
|
1495
|
-
}
|
|
1496
|
-
})();
|
|
1497
|
-
return G.__shokupan_db_promise;
|
|
1498
|
-
}
|
|
1499
|
-
const datastore = {
|
|
1500
|
-
async get(recordId) {
|
|
1501
|
-
await ensureDb();
|
|
1502
|
-
return G.__shokupan_db.select(recordId);
|
|
1503
|
-
},
|
|
1504
|
-
async set(recordId, value) {
|
|
1505
|
-
await ensureDb();
|
|
1506
|
-
return G.__shokupan_db.upsert(recordId).content(value);
|
|
1507
|
-
},
|
|
1508
|
-
async query(query, vars) {
|
|
1509
|
-
await ensureDb();
|
|
1510
|
-
try {
|
|
1511
|
-
return G.__shokupan_db.query(query, vars).collect();
|
|
1512
|
-
} catch (e) {
|
|
1513
|
-
console.error("DS ERROR:", e);
|
|
1514
|
-
throw e;
|
|
1515
|
-
}
|
|
1516
|
-
},
|
|
1517
|
-
get ready() {
|
|
1518
|
-
return ensureDb().then(() => void 0);
|
|
1519
|
-
}
|
|
1520
|
-
};
|
|
1521
|
-
process.on("exit", async () => {
|
|
1522
|
-
if (G.__shokupan_db) await G.__shokupan_db.close();
|
|
1523
|
-
});
|
|
1524
1514
|
class Container {
|
|
1525
1515
|
static services = /* @__PURE__ */ new Map();
|
|
1526
1516
|
static register(target, instance) {
|
|
@@ -1554,6 +1544,58 @@ function Inject(token) {
|
|
|
1554
1544
|
});
|
|
1555
1545
|
};
|
|
1556
1546
|
}
|
|
1547
|
+
class HttpError extends Error {
|
|
1548
|
+
status;
|
|
1549
|
+
constructor(message, status) {
|
|
1550
|
+
super(message);
|
|
1551
|
+
this.name = "HttpError";
|
|
1552
|
+
this.status = status;
|
|
1553
|
+
if (Error.captureStackTrace) {
|
|
1554
|
+
Error.captureStackTrace(this, HttpError);
|
|
1555
|
+
}
|
|
1556
|
+
}
|
|
1557
|
+
}
|
|
1558
|
+
function getErrorStatus(err) {
|
|
1559
|
+
if (!err || typeof err !== "object") {
|
|
1560
|
+
return 500;
|
|
1561
|
+
}
|
|
1562
|
+
if (typeof err.status === "number") {
|
|
1563
|
+
return err.status;
|
|
1564
|
+
}
|
|
1565
|
+
if (typeof err.statusCode === "number") {
|
|
1566
|
+
return err.statusCode;
|
|
1567
|
+
}
|
|
1568
|
+
return 500;
|
|
1569
|
+
}
|
|
1570
|
+
class EventError extends HttpError {
|
|
1571
|
+
constructor(message = "Event Error") {
|
|
1572
|
+
super(message, 500);
|
|
1573
|
+
this.name = "EventError";
|
|
1574
|
+
}
|
|
1575
|
+
}
|
|
1576
|
+
const tracer = api.trace.getTracer("shokupan.middleware");
|
|
1577
|
+
function traceHandler(fn, name) {
|
|
1578
|
+
return async function(...args) {
|
|
1579
|
+
return tracer.startActiveSpan(`route handler - ${name}`, {
|
|
1580
|
+
kind: api.SpanKind.INTERNAL,
|
|
1581
|
+
attributes: {
|
|
1582
|
+
"http.route": name,
|
|
1583
|
+
"component": "shokupan.route"
|
|
1584
|
+
}
|
|
1585
|
+
}, async (span) => {
|
|
1586
|
+
try {
|
|
1587
|
+
const result = await fn.apply(this, args);
|
|
1588
|
+
return result;
|
|
1589
|
+
} catch (err) {
|
|
1590
|
+
span.recordException(err);
|
|
1591
|
+
span.setStatus({ code: api.SpanStatusCode.ERROR, message: err.message });
|
|
1592
|
+
throw err;
|
|
1593
|
+
} finally {
|
|
1594
|
+
span.end();
|
|
1595
|
+
}
|
|
1596
|
+
});
|
|
1597
|
+
};
|
|
1598
|
+
}
|
|
1557
1599
|
class ShokupanRequestBase {
|
|
1558
1600
|
method;
|
|
1559
1601
|
url;
|
|
@@ -1591,8 +1633,10 @@ function getCallerInfo(skipFrames = 1) {
|
|
|
1591
1633
|
if (!l.includes(":")) continue;
|
|
1592
1634
|
if (l.includes("node_modules")) continue;
|
|
1593
1635
|
if (l.includes("bun:main")) continue;
|
|
1636
|
+
if (l.includes("bun:wrap")) continue;
|
|
1594
1637
|
if (l.includes("src/util/stack.ts")) continue;
|
|
1595
1638
|
if (l.includes("src/router.ts")) continue;
|
|
1639
|
+
if (l.includes("src/util/decorators.ts")) continue;
|
|
1596
1640
|
if (l.includes("src/shokupan.ts")) continue;
|
|
1597
1641
|
found++;
|
|
1598
1642
|
if (found >= skipFrames) {
|
|
@@ -1718,6 +1762,8 @@ var RouteParamType = /* @__PURE__ */ ((RouteParamType2) => {
|
|
|
1718
1762
|
RouteParamType2["CONTEXT"] = "CONTEXT";
|
|
1719
1763
|
return RouteParamType2;
|
|
1720
1764
|
})(RouteParamType || {});
|
|
1765
|
+
const RouterRegistry = /* @__PURE__ */ new Map();
|
|
1766
|
+
const ShokupanApplicationTree = {};
|
|
1721
1767
|
class ShokupanRouter {
|
|
1722
1768
|
constructor(config) {
|
|
1723
1769
|
this.config = config;
|
|
@@ -1735,6 +1781,9 @@ class ShokupanRouter {
|
|
|
1735
1781
|
[$parent] = null;
|
|
1736
1782
|
[$childRouters] = [];
|
|
1737
1783
|
[$childControllers] = [];
|
|
1784
|
+
get db() {
|
|
1785
|
+
return this.root?.db;
|
|
1786
|
+
}
|
|
1738
1787
|
hookCache = /* @__PURE__ */ new Map();
|
|
1739
1788
|
hooksInitialized = false;
|
|
1740
1789
|
middleware = [];
|
|
@@ -1751,6 +1800,14 @@ class ShokupanRouter {
|
|
|
1751
1800
|
// Metadata for the router itself
|
|
1752
1801
|
currentGuards = [];
|
|
1753
1802
|
eventHandlers = /* @__PURE__ */ new Map();
|
|
1803
|
+
/**
|
|
1804
|
+
* Registers middleware for this router.
|
|
1805
|
+
* Middleware will run for all routes matched by this router.
|
|
1806
|
+
*/
|
|
1807
|
+
use(middleware) {
|
|
1808
|
+
this.middleware.push(middleware);
|
|
1809
|
+
return this;
|
|
1810
|
+
}
|
|
1754
1811
|
// Registry Accessor
|
|
1755
1812
|
getComponentRegistry() {
|
|
1756
1813
|
const controllerRoutesMap = /* @__PURE__ */ new Map();
|
|
@@ -1815,6 +1872,8 @@ class ShokupanRouter {
|
|
|
1815
1872
|
* Registers an event handler for WebSocket.
|
|
1816
1873
|
*/
|
|
1817
1874
|
event(name, handler) {
|
|
1875
|
+
const info = getCallerInfo();
|
|
1876
|
+
handler.source = { file: info.file, line: info.line };
|
|
1818
1877
|
if (this.eventHandlers.has(name)) {
|
|
1819
1878
|
const err = new EventError(`Event handler \`${name}\` already exists.`);
|
|
1820
1879
|
console.warn(err);
|
|
@@ -1839,6 +1898,12 @@ class ShokupanRouter {
|
|
|
1839
1898
|
}
|
|
1840
1899
|
return null;
|
|
1841
1900
|
}
|
|
1901
|
+
/**
|
|
1902
|
+
* Returns all registered event handlers.
|
|
1903
|
+
*/
|
|
1904
|
+
getEventHandlers() {
|
|
1905
|
+
return this.eventHandlers;
|
|
1906
|
+
}
|
|
1842
1907
|
/**
|
|
1843
1908
|
* Mounts a controller instance to a path prefix.
|
|
1844
1909
|
*
|
|
@@ -2082,10 +2147,12 @@ class ShokupanRouter {
|
|
|
2082
2147
|
if (typeof originalHandler !== "function") continue;
|
|
2083
2148
|
let method;
|
|
2084
2149
|
let subPath = "";
|
|
2150
|
+
let methodSource;
|
|
2085
2151
|
if (decoratedRoutes && decoratedRoutes.has(name)) {
|
|
2086
2152
|
const config = decoratedRoutes.get(name);
|
|
2087
2153
|
method = config.method;
|
|
2088
2154
|
subPath = config.path;
|
|
2155
|
+
methodSource = config.source;
|
|
2089
2156
|
} else {
|
|
2090
2157
|
for (let j = 0; j < HTTPMethods.length; j++) {
|
|
2091
2158
|
const m = HTTPMethods[j];
|
|
@@ -2215,7 +2282,16 @@ class ShokupanRouter {
|
|
|
2215
2282
|
const decoratedSpecs = instance[$routeSpec] || proto && proto[$routeSpec];
|
|
2216
2283
|
const userSpec = decoratedSpecs && decoratedSpecs.get(name);
|
|
2217
2284
|
const spec = { tags: [tagName], ...userSpec };
|
|
2218
|
-
this.add({
|
|
2285
|
+
this.add({
|
|
2286
|
+
method,
|
|
2287
|
+
path: normalizedPath,
|
|
2288
|
+
handler: finalHandler,
|
|
2289
|
+
spec,
|
|
2290
|
+
controller: instance,
|
|
2291
|
+
metadata: methodSource || instance.metadata,
|
|
2292
|
+
middleware: allMiddleware
|
|
2293
|
+
// Capture all resolved middleware
|
|
2294
|
+
});
|
|
2219
2295
|
}
|
|
2220
2296
|
if (decoratedEvents?.has(name)) {
|
|
2221
2297
|
routesAttached++;
|
|
@@ -2248,6 +2324,11 @@ class ShokupanRouter {
|
|
|
2248
2324
|
}
|
|
2249
2325
|
return originalHandler.apply(instance, args);
|
|
2250
2326
|
};
|
|
2327
|
+
const decoratedSpecs = instance[$routeSpec] || proto && proto[$routeSpec];
|
|
2328
|
+
const userSpec = decoratedSpecs && decoratedSpecs.get(name);
|
|
2329
|
+
const spec = { tags: [{ name: instance.constructor.name }], ...userSpec };
|
|
2330
|
+
wrappedHandler.spec = spec;
|
|
2331
|
+
wrappedHandler.originalHandler = originalHandler;
|
|
2251
2332
|
this.event(config.eventName, wrappedHandler);
|
|
2252
2333
|
}
|
|
2253
2334
|
}
|
|
@@ -2317,7 +2398,7 @@ class ShokupanRouter {
|
|
|
2317
2398
|
* @param arg.renderer - JSX renderer for the route
|
|
2318
2399
|
* @param arg.controller - Controller for the route
|
|
2319
2400
|
*/
|
|
2320
|
-
add({ method, path: path2, spec, handler, regex: customRegex, group, requestTimeout, renderer, controller }) {
|
|
2401
|
+
add({ method, path: path2, spec, handler, regex: customRegex, group, requestTimeout, renderer, controller, metadata, middleware }) {
|
|
2321
2402
|
const { regex, keys } = customRegex ? { regex: customRegex, keys: [] } : this.parsePath(path2);
|
|
2322
2403
|
if (this.currentGuards.length > 0) {
|
|
2323
2404
|
spec = spec || {};
|
|
@@ -2335,7 +2416,13 @@ class ShokupanRouter {
|
|
|
2335
2416
|
}
|
|
2336
2417
|
}
|
|
2337
2418
|
}
|
|
2338
|
-
let wrappedHandler =
|
|
2419
|
+
let wrappedHandler = async (ctx) => {
|
|
2420
|
+
if (ctx.upgrade()) {
|
|
2421
|
+
return void 0;
|
|
2422
|
+
}
|
|
2423
|
+
return handler(ctx);
|
|
2424
|
+
};
|
|
2425
|
+
wrappedHandler.originalHandler = handler.originalHandler || handler;
|
|
2339
2426
|
const routeGuards = [...this.currentGuards];
|
|
2340
2427
|
const effectiveTimeout = requestTimeout ?? this.requestTimeout ?? this.rootConfig?.requestTimeout;
|
|
2341
2428
|
if (effectiveTimeout !== void 0 && effectiveTimeout > 0) {
|
|
@@ -2386,7 +2473,7 @@ class ShokupanRouter {
|
|
|
2386
2473
|
return innerHandler(ctx);
|
|
2387
2474
|
};
|
|
2388
2475
|
}
|
|
2389
|
-
const { file, line } = getCallerInfo();
|
|
2476
|
+
const { file, line } = metadata || getCallerInfo();
|
|
2390
2477
|
const trackingHandler = wrappedHandler;
|
|
2391
2478
|
wrappedHandler = async (ctx) => {
|
|
2392
2479
|
if (!ctx.app?.applicationConfig.enableMiddlewareTracking) {
|
|
@@ -2412,8 +2499,10 @@ class ShokupanRouter {
|
|
|
2412
2499
|
const config = ctx.app.applicationConfig;
|
|
2413
2500
|
Promise.resolve().then(async () => {
|
|
2414
2501
|
try {
|
|
2502
|
+
const db = ctx.app?.db;
|
|
2503
|
+
if (!db) return;
|
|
2415
2504
|
const timestamp = Date.now();
|
|
2416
|
-
await
|
|
2505
|
+
await db.upsert(new surrealdb.RecordId("middleware_tracking", {
|
|
2417
2506
|
timestamp,
|
|
2418
2507
|
name: handler.name || "anonymous"
|
|
2419
2508
|
}), {
|
|
@@ -2432,11 +2521,11 @@ class ShokupanRouter {
|
|
|
2432
2521
|
const ttl = config.middlewareTrackingTTL ?? 864e5;
|
|
2433
2522
|
const maxCapacity = config.middlewareTrackingMaxCapacity ?? 1e4;
|
|
2434
2523
|
const cutoff = Date.now() - ttl;
|
|
2435
|
-
await
|
|
2436
|
-
const results = await
|
|
2524
|
+
await db.query(`DELETE middleware_tracking WHERE timestamp < ${cutoff}`);
|
|
2525
|
+
const results = await db.query("SELECT count() FROM middleware_tracking GROUP ALL");
|
|
2437
2526
|
if (results?.[0]?.count > maxCapacity) {
|
|
2438
2527
|
const toDelete = results[0].count - maxCapacity;
|
|
2439
|
-
await
|
|
2528
|
+
await db.query(`DELETE middleware_tracking ORDER BY timestamp ASC LIMIT ${toDelete}`);
|
|
2440
2529
|
}
|
|
2441
2530
|
} catch (datastoreError) {
|
|
2442
2531
|
console.error("Failed to store middleware tracking:", datastoreError);
|
|
@@ -2466,7 +2555,8 @@ class ShokupanRouter {
|
|
|
2466
2555
|
file,
|
|
2467
2556
|
line
|
|
2468
2557
|
},
|
|
2469
|
-
controller
|
|
2558
|
+
controller,
|
|
2559
|
+
middleware: middleware || []
|
|
2470
2560
|
});
|
|
2471
2561
|
this.trie.insert(method, path2, bakedHandler);
|
|
2472
2562
|
return this;
|
|
@@ -2595,7 +2685,8 @@ class ShokupanRouter {
|
|
|
2595
2685
|
method,
|
|
2596
2686
|
path: path2,
|
|
2597
2687
|
spec,
|
|
2598
|
-
handler: finalHandler
|
|
2688
|
+
handler: finalHandler,
|
|
2689
|
+
middleware: handlers.slice(0, handlers.length - 1)
|
|
2599
2690
|
});
|
|
2600
2691
|
}
|
|
2601
2692
|
/**
|
|
@@ -2635,7 +2726,7 @@ class ShokupanRouter {
|
|
|
2635
2726
|
}
|
|
2636
2727
|
this.hooksInitialized = true;
|
|
2637
2728
|
}
|
|
2638
|
-
|
|
2729
|
+
runHooks(name, ...args) {
|
|
2639
2730
|
if (!this.hooksInitialized) {
|
|
2640
2731
|
this.ensureHooksInitialized();
|
|
2641
2732
|
}
|
|
@@ -2644,7 +2735,7 @@ class ShokupanRouter {
|
|
|
2644
2735
|
const ctx = args?.[0] instanceof ShokupanContext ? args[0] : void 0;
|
|
2645
2736
|
const debug = ctx?.[$debug];
|
|
2646
2737
|
if (debug) {
|
|
2647
|
-
|
|
2738
|
+
return Promise.all(fns.map(async (fn, index) => {
|
|
2648
2739
|
const hookId = `hook_${name}_${fn.name || index}`;
|
|
2649
2740
|
const previousNode = debug.getCurrentNode();
|
|
2650
2741
|
debug.trackEdge(previousNode, hookId);
|
|
@@ -2663,10 +2754,15 @@ class ShokupanRouter {
|
|
|
2663
2754
|
}
|
|
2664
2755
|
}));
|
|
2665
2756
|
} else {
|
|
2666
|
-
|
|
2757
|
+
return Promise.all(fns.map((fn) => fn(...args)));
|
|
2667
2758
|
}
|
|
2668
2759
|
}
|
|
2669
2760
|
}
|
|
2761
|
+
class RequestContextStore {
|
|
2762
|
+
request;
|
|
2763
|
+
span;
|
|
2764
|
+
}
|
|
2765
|
+
const asyncContext = new node_async_hooks.AsyncLocalStorage();
|
|
2670
2766
|
class SystemCpuMonitor {
|
|
2671
2767
|
constructor(intervalMs = 1e3) {
|
|
2672
2768
|
this.intervalMs = intervalMs;
|
|
@@ -2710,6 +2806,100 @@ class SystemCpuMonitor {
|
|
|
2710
2806
|
this.currentUsage = total === 0 ? 0 : (1 - idle / total) * 100;
|
|
2711
2807
|
}
|
|
2712
2808
|
}
|
|
2809
|
+
class SurrealDatastore {
|
|
2810
|
+
constructor(db) {
|
|
2811
|
+
this.db = db;
|
|
2812
|
+
process.on("exit", async () => {
|
|
2813
|
+
await this.disconnect();
|
|
2814
|
+
});
|
|
2815
|
+
}
|
|
2816
|
+
createSchema() {
|
|
2817
|
+
this.db.query(`
|
|
2818
|
+
DEFINE TABLE OVERWRITE failed_requests SCHEMALESS COMMENT "Created by Shokupan";
|
|
2819
|
+
DEFINE TABLE OVERWRITE sessions SCHEMALESS COMMENT "Created by Shokupan";
|
|
2820
|
+
DEFINE TABLE OVERWRITE users SCHEMALESS COMMENT "Created by Shokupan";
|
|
2821
|
+
DEFINE TABLE OVERWRITE idempotency_keys SCHEMALESS COMMENT "Created by Shokupan";
|
|
2822
|
+
DEFINE TABLE OVERWRITE middleware_tracking SCHEMALESS COMMENT "Created by Shokupan";
|
|
2823
|
+
DEFINE TABLE OVERWRITE requests SCHEMALESS COMMENT "Created by Shokupan";
|
|
2824
|
+
DEFINE TABLE OVERWRITE metrics SCHEMALESS COMMENT "Created by Shokupan";
|
|
2825
|
+
`).collect();
|
|
2826
|
+
}
|
|
2827
|
+
/**
|
|
2828
|
+
* Select a record or contents of a table by its ID.
|
|
2829
|
+
*/
|
|
2830
|
+
async select(id) {
|
|
2831
|
+
return this.db.select(id);
|
|
2832
|
+
}
|
|
2833
|
+
/**
|
|
2834
|
+
* Merge update data into a record by its ID.
|
|
2835
|
+
*/
|
|
2836
|
+
async merge(id, data) {
|
|
2837
|
+
return this.db.update(id).merge(data).catch((err) => {
|
|
2838
|
+
if (err.message.includes("This transaction can be retried")) {
|
|
2839
|
+
return this.db.update(id).merge(data);
|
|
2840
|
+
}
|
|
2841
|
+
throw err;
|
|
2842
|
+
});
|
|
2843
|
+
}
|
|
2844
|
+
/**
|
|
2845
|
+
* Create a record by its ID.
|
|
2846
|
+
*/
|
|
2847
|
+
async create(id, data) {
|
|
2848
|
+
return this.db.create(id).content(data).catch((err) => {
|
|
2849
|
+
if (err.message.includes("This transaction can be retried")) {
|
|
2850
|
+
return this.db.create(id).content(data);
|
|
2851
|
+
}
|
|
2852
|
+
throw err;
|
|
2853
|
+
});
|
|
2854
|
+
}
|
|
2855
|
+
/**
|
|
2856
|
+
* Upsert a record by its ID.
|
|
2857
|
+
*/
|
|
2858
|
+
async upsert(id, data) {
|
|
2859
|
+
return this.db.upsert(id).content(data).catch((err) => {
|
|
2860
|
+
if (err.message.includes("This transaction can be retried")) {
|
|
2861
|
+
return this.db.upsert(id).content(data);
|
|
2862
|
+
}
|
|
2863
|
+
throw err;
|
|
2864
|
+
});
|
|
2865
|
+
}
|
|
2866
|
+
/**
|
|
2867
|
+
* Delete a record by its ID.
|
|
2868
|
+
*/
|
|
2869
|
+
async delete(id) {
|
|
2870
|
+
return this.db.delete(id).catch((err) => {
|
|
2871
|
+
if (err.message.includes("This transaction can be retried")) {
|
|
2872
|
+
return this.db.delete(id);
|
|
2873
|
+
}
|
|
2874
|
+
throw err;
|
|
2875
|
+
});
|
|
2876
|
+
}
|
|
2877
|
+
/**
|
|
2878
|
+
* Run a SurrealDB query.
|
|
2879
|
+
*/
|
|
2880
|
+
async query(query, vars) {
|
|
2881
|
+
return this.db.query(query, vars).collect().catch((err) => {
|
|
2882
|
+
if (err.message.includes("This transaction can be retried")) {
|
|
2883
|
+
return this.db.query(query, vars).collect();
|
|
2884
|
+
}
|
|
2885
|
+
throw err;
|
|
2886
|
+
});
|
|
2887
|
+
}
|
|
2888
|
+
/**
|
|
2889
|
+
* Create a relationship between two records.
|
|
2890
|
+
*/
|
|
2891
|
+
async relate(fromId, edgeId, toId, data) {
|
|
2892
|
+
return this.db.relate(fromId, edgeId, toId, data).catch((err) => {
|
|
2893
|
+
if (err.message.includes("This transaction can be retried")) {
|
|
2894
|
+
return this.db.relate(fromId, edgeId, toId, data);
|
|
2895
|
+
}
|
|
2896
|
+
throw err;
|
|
2897
|
+
});
|
|
2898
|
+
}
|
|
2899
|
+
disconnect() {
|
|
2900
|
+
return this.db.close();
|
|
2901
|
+
}
|
|
2902
|
+
}
|
|
2713
2903
|
const defaults = {
|
|
2714
2904
|
port: 3e3,
|
|
2715
2905
|
hostname: "localhost",
|
|
@@ -2722,9 +2912,15 @@ api.trace.getTracer("shokupan.application");
|
|
|
2722
2912
|
class Shokupan extends ShokupanRouter {
|
|
2723
2913
|
applicationConfig = {};
|
|
2724
2914
|
openApiSpec;
|
|
2915
|
+
asyncApiSpec;
|
|
2725
2916
|
composedMiddleware;
|
|
2726
2917
|
cpuMonitor;
|
|
2727
2918
|
server;
|
|
2919
|
+
datastore;
|
|
2920
|
+
dbPromise;
|
|
2921
|
+
get db() {
|
|
2922
|
+
return this.datastore;
|
|
2923
|
+
}
|
|
2728
2924
|
get logger() {
|
|
2729
2925
|
return this.applicationConfig.logger;
|
|
2730
2926
|
}
|
|
@@ -2741,6 +2937,19 @@ class Shokupan extends ShokupanRouter {
|
|
|
2741
2937
|
line,
|
|
2742
2938
|
name: "ShokupanApplication"
|
|
2743
2939
|
};
|
|
2940
|
+
this.dbPromise = this.initDatastore();
|
|
2941
|
+
}
|
|
2942
|
+
async initDatastore() {
|
|
2943
|
+
const db = new surrealdb.Surreal({ engines: this.applicationConfig.surreal?.engines ?? (await import("@surrealdb/node")).createNodeEngines() });
|
|
2944
|
+
this.datastore = new SurrealDatastore(db);
|
|
2945
|
+
await db.connect(
|
|
2946
|
+
this.applicationConfig.surreal?.url ?? (process.env.NODE_ENV === "test" ? "mem://" : "surrealkv://database"),
|
|
2947
|
+
this.applicationConfig.surreal?.connectOptions
|
|
2948
|
+
);
|
|
2949
|
+
await db.use({
|
|
2950
|
+
namespace: this.applicationConfig.surreal?.namespace ?? "vendor",
|
|
2951
|
+
database: this.applicationConfig.surreal?.database ?? "shokupan"
|
|
2952
|
+
});
|
|
2744
2953
|
}
|
|
2745
2954
|
/**
|
|
2746
2955
|
* Adds middleware to the application.
|
|
@@ -2820,14 +3029,70 @@ class Shokupan extends ShokupanRouter {
|
|
|
2820
3029
|
*/
|
|
2821
3030
|
async listen(port) {
|
|
2822
3031
|
const finalPort = port ?? this.applicationConfig.port ?? 3e3;
|
|
2823
|
-
if (finalPort < 0 || finalPort > 65535) {
|
|
3032
|
+
if (finalPort < 0 || finalPort > 65535 || finalPort % 1 !== 0) {
|
|
2824
3033
|
throw new Error("Invalid port number");
|
|
2825
3034
|
}
|
|
2826
3035
|
await Promise.all(this.startupHooks.map((hook) => hook()));
|
|
2827
3036
|
if (this.applicationConfig.enableOpenApiGen) {
|
|
2828
3037
|
this.openApiSpec = await generateOpenApi(this);
|
|
3038
|
+
this.get("/.well-known/openapi.yaml", (ctx) => {
|
|
3039
|
+
try {
|
|
3040
|
+
const yaml = jsYaml.dump(this.openApiSpec);
|
|
3041
|
+
return ctx.send(yaml, { status: 200, headers: { "content-type": "application/yaml" } });
|
|
3042
|
+
} catch (e) {
|
|
3043
|
+
this.logger.error("Failed to generate OpenAPI YAML", { error: e });
|
|
3044
|
+
return ctx.text("Internal Server Error", 500);
|
|
3045
|
+
}
|
|
3046
|
+
});
|
|
3047
|
+
if (this.applicationConfig.aiPlugin?.enabled !== false) {
|
|
3048
|
+
this.get("/.well-known/ai-plugin.json", async (ctx) => {
|
|
3049
|
+
const config = this.applicationConfig.aiPlugin || {};
|
|
3050
|
+
let pkg = {};
|
|
3051
|
+
try {
|
|
3052
|
+
pkg = await Bun.file("package.json").json();
|
|
3053
|
+
} catch (e) {
|
|
3054
|
+
}
|
|
3055
|
+
const manifest = {
|
|
3056
|
+
schema_version: "v1",
|
|
3057
|
+
name_for_human: config.name_for_human || this.openApiSpec.info.title || pkg.name || "Shokupan App",
|
|
3058
|
+
name_for_model: config.name_for_model || this.openApiSpec.info.title || pkg.name || "Shokupan App",
|
|
3059
|
+
description_for_human: config.description_for_human || this.openApiSpec.info.description || pkg.description || "Shokupan Application",
|
|
3060
|
+
description_for_model: config.description_for_model || this.openApiSpec.info.description || pkg.description || "Shokupan Application",
|
|
3061
|
+
auth: config.auth || { type: "none" },
|
|
3062
|
+
api: config.api || {
|
|
3063
|
+
type: "openapi",
|
|
3064
|
+
url: `${this.applicationConfig.hostname === "localhost" ? "http" : "https"}://${this.applicationConfig.hostname}:${finalPort}/.well-known/openapi.yaml`,
|
|
3065
|
+
is_user_authenticated: false
|
|
3066
|
+
},
|
|
3067
|
+
logo_url: config.logo_url || `${this.applicationConfig.hostname === "localhost" ? "http" : "https"}://${this.applicationConfig.hostname}:${finalPort}/logo.png`,
|
|
3068
|
+
// Placeholder default
|
|
3069
|
+
contact_email: config.contact_email || pkg.author?.email || "support@example.com",
|
|
3070
|
+
legal_info_url: config.legal_info_url || `${this.applicationConfig.hostname === "localhost" ? "http" : "https"}://${this.applicationConfig.hostname}:${finalPort}/legal`
|
|
3071
|
+
};
|
|
3072
|
+
return ctx.json(manifest);
|
|
3073
|
+
});
|
|
3074
|
+
}
|
|
3075
|
+
if (this.applicationConfig.apiCatalog?.enabled !== false) {
|
|
3076
|
+
this.get("/.well-known/api-catalog", (ctx) => {
|
|
3077
|
+
const config = this.applicationConfig.apiCatalog || {};
|
|
3078
|
+
const catalog = {
|
|
3079
|
+
versions: config.versions || [
|
|
3080
|
+
{
|
|
3081
|
+
name: this.openApiSpec.info.version || "v1",
|
|
3082
|
+
url: `${this.applicationConfig.hostname === "localhost" ? "http" : "https"}://${this.applicationConfig.hostname}:${finalPort}/`,
|
|
3083
|
+
spec_url: `${this.applicationConfig.hostname === "localhost" ? "http" : "https"}://${this.applicationConfig.hostname}:${finalPort}/.well-known/openapi.yaml`
|
|
3084
|
+
}
|
|
3085
|
+
]
|
|
3086
|
+
};
|
|
3087
|
+
return ctx.json(catalog);
|
|
3088
|
+
});
|
|
3089
|
+
}
|
|
2829
3090
|
await Promise.all(this.specAvailableHooks.map((hook) => hook(this.openApiSpec)));
|
|
2830
3091
|
}
|
|
3092
|
+
if (this.applicationConfig.enableAsyncApiGen) {
|
|
3093
|
+
const { generateAsyncApi: generateAsyncApi2 } = await Promise.resolve().then(() => generator);
|
|
3094
|
+
this.asyncApiSpec = await generateAsyncApi2(this);
|
|
3095
|
+
}
|
|
2831
3096
|
if (port === 0 && process.platform === "linux") ;
|
|
2832
3097
|
if (this.applicationConfig.autoBackpressureFeedback === true) {
|
|
2833
3098
|
this.cpuMonitor = new SystemCpuMonitor();
|
|
@@ -2887,6 +3152,8 @@ class Shokupan extends ShokupanRouter {
|
|
|
2887
3152
|
});
|
|
2888
3153
|
const ctx = new ShokupanContext(req, self.server);
|
|
2889
3154
|
ctx[$ws] = ws;
|
|
3155
|
+
ws.data ??= {};
|
|
3156
|
+
ws.data["ctx"] = ctx;
|
|
2890
3157
|
try {
|
|
2891
3158
|
await handler(ctx);
|
|
2892
3159
|
} catch (err) {
|
|
@@ -2906,6 +3173,15 @@ class Shokupan extends ShokupanRouter {
|
|
|
2906
3173
|
},
|
|
2907
3174
|
close(ws, code, reason) {
|
|
2908
3175
|
ws.data?.handler?.close?.(ws, code, reason);
|
|
3176
|
+
const ctx = ws.data?.["ctx"];
|
|
3177
|
+
if (ctx && typeof ctx.getDisconnectCallbacks === "function") {
|
|
3178
|
+
const callbacks = ctx.getDisconnectCallbacks();
|
|
3179
|
+
if (Array.isArray(callbacks) && callbacks.length > 0) {
|
|
3180
|
+
Promise.all(callbacks.map((cb) => cb())).catch((err) => {
|
|
3181
|
+
console.error("Error executing socket disconnect hook:", err);
|
|
3182
|
+
});
|
|
3183
|
+
}
|
|
3184
|
+
}
|
|
2909
3185
|
}
|
|
2910
3186
|
}
|
|
2911
3187
|
};
|
|
@@ -2915,7 +3191,6 @@ class Shokupan extends ShokupanRouter {
|
|
|
2915
3191
|
factory = createHttpServer();
|
|
2916
3192
|
}
|
|
2917
3193
|
this.server = factory ? await factory(serveOptions) : Bun.serve(serveOptions);
|
|
2918
|
-
console.log(`Shokupan server listening on http://${serveOptions.hostname}:${serveOptions.port}`);
|
|
2919
3194
|
return this.server;
|
|
2920
3195
|
}
|
|
2921
3196
|
/**
|
|
@@ -3047,9 +3322,6 @@ class Shokupan extends ShokupanRouter {
|
|
|
3047
3322
|
await bodyParsing;
|
|
3048
3323
|
return match.handler(ctx);
|
|
3049
3324
|
}
|
|
3050
|
-
if (ctx.upgrade()) {
|
|
3051
|
-
return void 0;
|
|
3052
|
-
}
|
|
3053
3325
|
return null;
|
|
3054
3326
|
});
|
|
3055
3327
|
let response;
|
|
@@ -3065,6 +3337,9 @@ class Shokupan extends ShokupanRouter {
|
|
|
3065
3337
|
} else if (ctx[$routeMatched]) {
|
|
3066
3338
|
response = ctx.send(null, { status: ctx.response.status, headers: ctx.response.headers });
|
|
3067
3339
|
} else {
|
|
3340
|
+
if (ctx.upgrade()) {
|
|
3341
|
+
return void 0;
|
|
3342
|
+
}
|
|
3068
3343
|
if (ctx.response.status !== HTTP_STATUS.OK) {
|
|
3069
3344
|
response = ctx.send(null, { status: ctx.response.status, headers: ctx.response.headers });
|
|
3070
3345
|
} else {
|
|
@@ -3077,6 +3352,9 @@ class Shokupan extends ShokupanRouter {
|
|
|
3077
3352
|
response = ctx.text(String(result));
|
|
3078
3353
|
}
|
|
3079
3354
|
await this.runHooks("onRequestEnd", ctx);
|
|
3355
|
+
if (response instanceof Promise) {
|
|
3356
|
+
response = await response;
|
|
3357
|
+
}
|
|
3080
3358
|
await this.runHooks("onResponseStart", ctx, response);
|
|
3081
3359
|
return response;
|
|
3082
3360
|
} catch (err) {
|
|
@@ -3186,8 +3464,7 @@ function RateLimitMiddleware(options = {}) {
|
|
|
3186
3464
|
}
|
|
3187
3465
|
}
|
|
3188
3466
|
const msg = typeof message === "function" ? message(ctx, key) : message;
|
|
3189
|
-
typeof msg === "object" ?
|
|
3190
|
-
const res = typeof msg === "object" ? ctx.json(msg, statusCode) : ctx.text(String(msg), statusCode);
|
|
3467
|
+
const res = await (typeof msg === "object" ? ctx.json(msg, statusCode) : ctx.text(String(msg), statusCode));
|
|
3191
3468
|
if (headers) {
|
|
3192
3469
|
setHeaders(res);
|
|
3193
3470
|
res.headers.set("Retry-After", String(retryAfter));
|
|
@@ -3262,8 +3539,12 @@ function createMethodDecorator(method) {
|
|
|
3262
3539
|
}
|
|
3263
3540
|
target[$routeMethods].set(propertyKey, {
|
|
3264
3541
|
method,
|
|
3265
|
-
path: path2
|
|
3542
|
+
path: path2,
|
|
3543
|
+
source: getCallerInfo(2)
|
|
3266
3544
|
});
|
|
3545
|
+
if (path2.includes("/user")) {
|
|
3546
|
+
console.log(`[Decorator] Captured source for ${propertyKey}:`, getCallerInfo());
|
|
3547
|
+
}
|
|
3267
3548
|
};
|
|
3268
3549
|
};
|
|
3269
3550
|
}
|
|
@@ -3286,15 +3567,572 @@ function Event(eventName) {
|
|
|
3286
3567
|
function RateLimit(options) {
|
|
3287
3568
|
return Use(RateLimitMiddleware(options));
|
|
3288
3569
|
}
|
|
3570
|
+
function AsyncApiApp({ spec, serverUrl, base, disableSourceView, navTree }) {
|
|
3571
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("html", { lang: "en", children: [
|
|
3572
|
+
/* @__PURE__ */ jsxRuntime.jsxs("head", { children: [
|
|
3573
|
+
/* @__PURE__ */ jsxRuntime.jsx("meta", { charSet: "UTF-8" }),
|
|
3574
|
+
/* @__PURE__ */ jsxRuntime.jsx("meta", { name: "viewport", content: "width=device-width, initial-scale=1.0" }),
|
|
3575
|
+
/* @__PURE__ */ jsxRuntime.jsx("title", { children: "Shokupan AsyncAPI" }),
|
|
3576
|
+
/* @__PURE__ */ jsxRuntime.jsx("link", { rel: "preconnect", href: "https://fonts.googleapis.com" }),
|
|
3577
|
+
/* @__PURE__ */ jsxRuntime.jsx("link", { rel: "preconnect", href: "https://fonts.gstatic.com", crossOrigin: "anonymous" }),
|
|
3578
|
+
/* @__PURE__ */ jsxRuntime.jsx("link", { href: "https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap", rel: "stylesheet" }),
|
|
3579
|
+
/* @__PURE__ */ jsxRuntime.jsx("link", { rel: "stylesheet", href: `${base}/theme.css` }),
|
|
3580
|
+
/* @__PURE__ */ jsxRuntime.jsx("link", { rel: "stylesheet", href: `${base}/style.css` }),
|
|
3581
|
+
/* @__PURE__ */ jsxRuntime.jsx("script", { dangerouslySetInnerHTML: {
|
|
3582
|
+
__html: `
|
|
3583
|
+
window.INITIAL_SPEC = ${JSON.stringify(spec)};
|
|
3584
|
+
window.INITIAL_SERVER_URL = "${serverUrl}";
|
|
3585
|
+
window.DISABLE_SOURCE_VIEW = ${JSON.stringify(disableSourceView)};
|
|
3586
|
+
`
|
|
3587
|
+
} })
|
|
3588
|
+
] }),
|
|
3589
|
+
/* @__PURE__ */ jsxRuntime.jsxs("body", { children: [
|
|
3590
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { class: "app-container", children: [
|
|
3591
|
+
/* @__PURE__ */ jsxRuntime.jsx(Sidebar, { navTree, disableSourceView }),
|
|
3592
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { class: "resizer", id: "resizer-left" }),
|
|
3593
|
+
/* @__PURE__ */ jsxRuntime.jsx(MainContent, {}),
|
|
3594
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { class: "resizer", id: "resizer-right" }),
|
|
3595
|
+
/* @__PURE__ */ jsxRuntime.jsx(ConsolePanel, { serverUrl })
|
|
3596
|
+
] }),
|
|
3597
|
+
/* @__PURE__ */ jsxRuntime.jsx("script", { src: "https://cdn.socket.io/4.7.4/socket.io.min.js" }),
|
|
3598
|
+
/* @__PURE__ */ jsxRuntime.jsx("script", { src: "https://cdn.jsdelivr.net/npm/monaco-editor@0.45.0/min/vs/loader.js" }),
|
|
3599
|
+
/* @__PURE__ */ jsxRuntime.jsx("script", { src: `${base}/asyncapi-client.mjs`, type: "module" })
|
|
3600
|
+
] })
|
|
3601
|
+
] });
|
|
3602
|
+
}
|
|
3603
|
+
function Sidebar({ navTree, disableSourceView }) {
|
|
3604
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { class: "sidebar scroller", id: "sidebar", children: [
|
|
3605
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { class: "sidebar-header", style: "display:flex; justify-content:space-between; align-items:center;", children: [
|
|
3606
|
+
/* @__PURE__ */ jsxRuntime.jsx("h2", { children: "AsyncAPI" }),
|
|
3607
|
+
/* @__PURE__ */ jsxRuntime.jsx("button", { id: "btn-collapse-nav", class: "btn-icon", title: "Collapse Sidebar", children: /* @__PURE__ */ jsxRuntime.jsx("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", children: /* @__PURE__ */ jsxRuntime.jsx("polyline", { points: "15 18 9 12 15 6" }) }) })
|
|
3608
|
+
] }),
|
|
3609
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { class: "nav-list", id: "nav-list", children: /* @__PURE__ */ jsxRuntime.jsx(NavNode, { node: navTree, level: 0, disableSourceView }) })
|
|
3610
|
+
] });
|
|
3611
|
+
}
|
|
3612
|
+
function NavNode({ node, level, disableSourceView }) {
|
|
3613
|
+
const sortedEntries = Object.entries(node.children || {}).sort((a, b) => {
|
|
3614
|
+
const [aKey, aItem] = a;
|
|
3615
|
+
const [bKey, bItem] = b;
|
|
3616
|
+
const isWarningA = aItem.data?.op?.["x-warning"];
|
|
3617
|
+
const isWarningB = bItem.data?.op?.["x-warning"];
|
|
3618
|
+
if (isWarningA && !isWarningB) return -1;
|
|
3619
|
+
if (!isWarningA && isWarningB) return 1;
|
|
3620
|
+
if (aKey === bKey) return 0;
|
|
3621
|
+
if (aKey === "Warning" || aKey === "Warnings") return -1;
|
|
3622
|
+
if (bKey === "Warning" || bKey === "Warnings") return 1;
|
|
3623
|
+
if (aKey === "Application") return -1;
|
|
3624
|
+
if (bKey === "Application") return 1;
|
|
3625
|
+
if (aKey[0] === "/") return 1;
|
|
3626
|
+
if (bKey[0] === "/") return -1;
|
|
3627
|
+
return aKey.localeCompare(bKey);
|
|
3628
|
+
});
|
|
3629
|
+
return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: sortedEntries.map(([key, item]) => {
|
|
3630
|
+
const hasChildren = Object.keys(item.children || {}).length > 0;
|
|
3631
|
+
if (level === 0) {
|
|
3632
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
|
|
3633
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { class: "group-label", children: key }),
|
|
3634
|
+
hasChildren && /* @__PURE__ */ jsxRuntime.jsx("div", { class: "tree-node", style: "margin-left: 0", children: /* @__PURE__ */ jsxRuntime.jsx(NavNode, { node: item, level: level + 1, disableSourceView }) })
|
|
3635
|
+
] }, key);
|
|
3636
|
+
}
|
|
3637
|
+
const isLeaf = item.isLeaf;
|
|
3638
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
|
|
3639
|
+
isLeaf ? /* @__PURE__ */ jsxRuntime.jsx(LeafNode, { item, label: key, disableSourceView }) : /* @__PURE__ */ jsxRuntime.jsx("div", { class: "tree-item", style: "color: var(--text-muted)", children: /* @__PURE__ */ jsxRuntime.jsx("span", { class: "tree-label", children: key }) }),
|
|
3640
|
+
hasChildren && /* @__PURE__ */ jsxRuntime.jsx("div", { class: "tree-node", children: /* @__PURE__ */ jsxRuntime.jsx(NavNode, { node: item, level: level + 1, disableSourceView }) })
|
|
3641
|
+
] }, key);
|
|
3642
|
+
}) });
|
|
3643
|
+
}
|
|
3644
|
+
function LeafNode({ item, label, disableSourceView }) {
|
|
3645
|
+
const isWarning = item.data?.op?.["x-warning"];
|
|
3646
|
+
const opId = item.data?.name;
|
|
3647
|
+
const sourceInfo = item.data?.op?.["x-source-info"];
|
|
3648
|
+
let content;
|
|
3649
|
+
if (isWarning) {
|
|
3650
|
+
content = /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
3651
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { style: "margin-right: 6px;", children: "⚠️" }),
|
|
3652
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { class: "tree-label", children: label })
|
|
3653
|
+
] });
|
|
3654
|
+
} else {
|
|
3655
|
+
const badgeText = item.data.type === "publish" ? "SEND" : "RECV";
|
|
3656
|
+
content = /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
3657
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { class: `badge badge-${badgeText}`, children: badgeText }),
|
|
3658
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { class: "tree-label", children: label })
|
|
3659
|
+
] });
|
|
3660
|
+
}
|
|
3661
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { class: "tree-item", "data-event": opId, style: isWarning ? "color: #fbbf24" : "", children: [
|
|
3662
|
+
content,
|
|
3663
|
+
sourceInfo && !disableSourceView && /* @__PURE__ */ jsxRuntime.jsx(
|
|
3664
|
+
"a",
|
|
3665
|
+
{
|
|
3666
|
+
href: `vscode://file/${sourceInfo.file}:${sourceInfo.line}`,
|
|
3667
|
+
class: "source-link",
|
|
3668
|
+
onClick: (e) => {
|
|
3669
|
+
e.stopPropagation();
|
|
3670
|
+
},
|
|
3671
|
+
title: `${sourceInfo.file}:${sourceInfo.line}`,
|
|
3672
|
+
children: /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", style: "display:block", children: [
|
|
3673
|
+
/* @__PURE__ */ jsxRuntime.jsx("polyline", { points: "16 18 22 12 16 6" }),
|
|
3674
|
+
/* @__PURE__ */ jsxRuntime.jsx("polyline", { points: "8 6 2 12 8 18" })
|
|
3675
|
+
] })
|
|
3676
|
+
}
|
|
3677
|
+
)
|
|
3678
|
+
] });
|
|
3679
|
+
}
|
|
3680
|
+
function MainContent() {
|
|
3681
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { id: "main-wrapper", style: "flex: 1; min-width: 0; position: relative; overflow: hidden;", children: [
|
|
3682
|
+
/* @__PURE__ */ jsxRuntime.jsx("button", { id: "btn-expand-nav", class: "btn-icon floating-toggle left", title: "Expand Sidebar", style: "display:none;", children: /* @__PURE__ */ jsxRuntime.jsx("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", children: /* @__PURE__ */ jsxRuntime.jsx("polyline", { points: "9 18 15 12 9 6" }) }) }),
|
|
3683
|
+
/* @__PURE__ */ jsxRuntime.jsx("button", { id: "btn-expand-console", class: "btn-icon floating-toggle right", title: "Expand Console", style: "display:none;", children: /* @__PURE__ */ jsxRuntime.jsx("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", children: /* @__PURE__ */ jsxRuntime.jsx("polyline", { points: "15 18 9 12 15 6" }) }) }),
|
|
3684
|
+
/* @__PURE__ */ jsxRuntime.jsx("main", { class: "main-content scroller", id: "doc-panel", style: "height: 100%;", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { class: "empty-state", children: [
|
|
3685
|
+
/* @__PURE__ */ jsxRuntime.jsxs("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "1.5", children: [
|
|
3686
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M4 19.5A2.5 2.5 0 0 1 6.5 17H20" }),
|
|
3687
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z" })
|
|
3688
|
+
] }),
|
|
3689
|
+
/* @__PURE__ */ jsxRuntime.jsx("h3", { children: "Select an event to view details" })
|
|
3690
|
+
] }) })
|
|
3691
|
+
] });
|
|
3692
|
+
}
|
|
3693
|
+
function ConsolePanel({ serverUrl }) {
|
|
3694
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { class: "console-panel", id: "console-panel", children: [
|
|
3695
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { class: "console-header", children: [
|
|
3696
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { style: "display:flex; justify-content:space-between; align-items:center; margin-bottom: 8px;", children: [
|
|
3697
|
+
/* @__PURE__ */ jsxRuntime.jsx("h3", { style: "margin:0; font-size:1rem;", children: "Console" }),
|
|
3698
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { style: "display:flex; gap: 4px;", children: [
|
|
3699
|
+
/* @__PURE__ */ jsxRuntime.jsx("button", { id: "btn-maximize-console", class: "btn-icon", title: "Maximize Console", children: /* @__PURE__ */ jsxRuntime.jsx("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", children: /* @__PURE__ */ jsxRuntime.jsx("rect", { x: "3", y: "3", width: "18", height: "18", rx: "2", ry: "2" }) }) }),
|
|
3700
|
+
/* @__PURE__ */ jsxRuntime.jsx("button", { id: "btn-collapse-console", class: "btn-icon", title: "Collapse Console", children: /* @__PURE__ */ jsxRuntime.jsx("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", children: /* @__PURE__ */ jsxRuntime.jsx("polyline", { points: "9 18 15 12 9 6" }) }) })
|
|
3701
|
+
] })
|
|
3702
|
+
] }),
|
|
3703
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { class: "connection-bar", children: [
|
|
3704
|
+
/* @__PURE__ */ jsxRuntime.jsxs("select", { id: "protocol", children: [
|
|
3705
|
+
/* @__PURE__ */ jsxRuntime.jsx("option", { value: "ws", children: "WS" }),
|
|
3706
|
+
/* @__PURE__ */ jsxRuntime.jsx("option", { value: "wss", children: "WSS" }),
|
|
3707
|
+
/* @__PURE__ */ jsxRuntime.jsx("option", { value: "socket.io", children: "Socket.IO" })
|
|
3708
|
+
] }),
|
|
3709
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { style: "width: 1px; background: rgba(255,255,255,0.1); margin: 2px 0;" }),
|
|
3710
|
+
/* @__PURE__ */ jsxRuntime.jsx("input", { type: "text", id: "url", value: serverUrl })
|
|
3711
|
+
] }),
|
|
3712
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { style: "display: grid; grid-template-columns: 1fr auto; gap: 8px;", children: [
|
|
3713
|
+
/* @__PURE__ */ jsxRuntime.jsx("button", { id: "connect-btn", class: "btn", children: "Connect" }),
|
|
3714
|
+
/* @__PURE__ */ jsxRuntime.jsx("button", { id: "clear-logs-btn", class: "btn secondary", title: "Clear Logs", children: /* @__PURE__ */ jsxRuntime.jsx("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", children: /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M3 6h18M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" }) }) })
|
|
3715
|
+
] }),
|
|
3716
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { class: "status-indicator", children: [
|
|
3717
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { id: "status-dot", class: "dot" }),
|
|
3718
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { id: "connection-status", children: "Disconnected" })
|
|
3719
|
+
] })
|
|
3720
|
+
] }),
|
|
3721
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { class: "logs-container scroller", id: "logs", children: /* @__PURE__ */ jsxRuntime.jsx("div", { class: "log-shim", id: "log-shim" }) }),
|
|
3722
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { class: "compose-area", children: [
|
|
3723
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { class: "compose-header", children: [
|
|
3724
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { children: "Payload" }),
|
|
3725
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { id: "target-event", style: "color: var(--primary);", children: "--" })
|
|
3726
|
+
] }),
|
|
3727
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { id: "editor-container" }),
|
|
3728
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { class: "send-bar", children: /* @__PURE__ */ jsxRuntime.jsx("button", { id: "send-btn", class: "btn", children: "Send Message" }) })
|
|
3729
|
+
] })
|
|
3730
|
+
] });
|
|
3731
|
+
}
|
|
3732
|
+
function buildNavTree(spec) {
|
|
3733
|
+
if (!spec || !spec.channels) return { children: {} };
|
|
3734
|
+
const root = { children: {} };
|
|
3735
|
+
Object.keys(spec.channels).forEach((name) => {
|
|
3736
|
+
const ch = spec.channels[name];
|
|
3737
|
+
const op = ch.publish || ch.subscribe;
|
|
3738
|
+
const type = ch.publish ? "publish" : "subscribe";
|
|
3739
|
+
const tag = op.tags && op.tags.length > 0 ? op.tags[0].name : "General";
|
|
3740
|
+
if (!root.children[tag]) root.children[tag] = { children: {} };
|
|
3741
|
+
const parts = name.split(/[\.\/]/);
|
|
3742
|
+
let current = root.children[tag];
|
|
3743
|
+
parts.forEach((part, i) => {
|
|
3744
|
+
if (!current.children[part]) current.children[part] = { children: {} };
|
|
3745
|
+
current = current.children[part];
|
|
3746
|
+
if (i === parts.length - 1) {
|
|
3747
|
+
current.isLeaf = true;
|
|
3748
|
+
current.data = { name, op, type };
|
|
3749
|
+
}
|
|
3750
|
+
});
|
|
3751
|
+
});
|
|
3752
|
+
return root;
|
|
3753
|
+
}
|
|
3754
|
+
async function getAstRoutes(applications) {
|
|
3755
|
+
const astRoutes = [];
|
|
3756
|
+
const getExpandedRoutes = (app, prefix = "", seen = /* @__PURE__ */ new Set()) => {
|
|
3757
|
+
if (seen.has(app.name)) return [];
|
|
3758
|
+
const newSeen = new Set(seen);
|
|
3759
|
+
newSeen.add(app.name);
|
|
3760
|
+
const expanded = [];
|
|
3761
|
+
for (const route of app.routes) {
|
|
3762
|
+
expanded.push({
|
|
3763
|
+
...route,
|
|
3764
|
+
// For events, path is the event name
|
|
3765
|
+
path: route.path.startsWith("/") ? route.path.slice(1) : route.path
|
|
3766
|
+
});
|
|
3767
|
+
}
|
|
3768
|
+
if (app.mounted) {
|
|
3769
|
+
for (const mount of app.mounted) {
|
|
3770
|
+
const targetApp = applications.find((a) => a.name === mount.target || a.className === mount.target);
|
|
3771
|
+
if (targetApp) {
|
|
3772
|
+
expanded.push(...getExpandedRoutes(targetApp, "", newSeen));
|
|
3773
|
+
}
|
|
3774
|
+
}
|
|
3775
|
+
}
|
|
3776
|
+
return expanded;
|
|
3777
|
+
};
|
|
3778
|
+
applications.forEach((app) => {
|
|
3779
|
+
astRoutes.push(...getExpandedRoutes(app));
|
|
3780
|
+
});
|
|
3781
|
+
return astRoutes;
|
|
3782
|
+
}
|
|
3783
|
+
async function generateAsyncApi(rootRouter, options = {}) {
|
|
3784
|
+
const channels = {};
|
|
3785
|
+
let astRoutes = [];
|
|
3786
|
+
try {
|
|
3787
|
+
const { OpenAPIAnalyzer } = await Promise.resolve().then(() => require("./analyzer-CKLGLFtx.cjs"));
|
|
3788
|
+
const entrypoint = globalThis.Bun?.main || require.main?.filename || process.argv[1];
|
|
3789
|
+
const analyzer2 = new OpenAPIAnalyzer(process.cwd(), entrypoint);
|
|
3790
|
+
const { applications } = await analyzer2.analyze();
|
|
3791
|
+
astRoutes = await getAstRoutes(applications);
|
|
3792
|
+
} catch (e) {
|
|
3793
|
+
}
|
|
3794
|
+
const matchedAstRoutes = /* @__PURE__ */ new Set();
|
|
3795
|
+
const collect = async (router, prefix = "") => {
|
|
3796
|
+
const eventHandlers = router.getEventHandlers();
|
|
3797
|
+
let routerTag = "Other";
|
|
3798
|
+
if (router[$isApplication]) {
|
|
3799
|
+
routerTag = "Application";
|
|
3800
|
+
} else if (router.constructor.name && router.constructor.name !== "ShokupanRouter") {
|
|
3801
|
+
routerTag = router.constructor.name;
|
|
3802
|
+
} else {
|
|
3803
|
+
routerTag = router[$mountPath] || "Router";
|
|
3804
|
+
}
|
|
3805
|
+
if (eventHandlers) {
|
|
3806
|
+
for (const [eventName, handlers] of eventHandlers.entries()) {
|
|
3807
|
+
for (const handler of handlers) {
|
|
3808
|
+
const userSpec = handler.spec;
|
|
3809
|
+
let tags = userSpec?.tags;
|
|
3810
|
+
if (!tags && routerTag) {
|
|
3811
|
+
tags = [{ name: routerTag }];
|
|
3812
|
+
}
|
|
3813
|
+
let astMatch = astRoutes.find(
|
|
3814
|
+
(r) => (r.method === "EVENT" || r.method === "ON") && r.path === eventName
|
|
3815
|
+
);
|
|
3816
|
+
if (!astMatch) {
|
|
3817
|
+
const runtimeSource = (handler.originalHandler || handler).toString();
|
|
3818
|
+
const stripComments = (s) => s.replace(/\/\*[\s\S]*?\*\/|([^\\:]|^)\/\/.*$/gm, "$1");
|
|
3819
|
+
const normalize = (s) => stripComments(s).replace(/\s+/g, "");
|
|
3820
|
+
const runtimeHandlerSrc = normalize(runtimeSource);
|
|
3821
|
+
const eventRoutes = astRoutes.filter((r) => r.method === "EVENT" || r.method === "ON");
|
|
3822
|
+
astMatch = eventRoutes.find((r) => {
|
|
3823
|
+
const astHandlerSrc = normalize(r.handlerSource || r.handlerName || "");
|
|
3824
|
+
if (!astHandlerSrc || astHandlerSrc.length < 5) return false;
|
|
3825
|
+
return runtimeHandlerSrc.includes(astHandlerSrc) || astHandlerSrc.includes(runtimeHandlerSrc) || r.handlerSource && runtimeHandlerSrc.includes(normalize(r.handlerSource).substring(0, 50));
|
|
3826
|
+
});
|
|
3827
|
+
}
|
|
3828
|
+
if (astMatch) matchedAstRoutes.add(astMatch);
|
|
3829
|
+
const sourceInfo = handler.source || astMatch?.sourceContext ? {
|
|
3830
|
+
file: handler.source?.file || astMatch?.sourceContext?.file,
|
|
3831
|
+
line: handler.source?.line || astMatch?.sourceContext?.startLine,
|
|
3832
|
+
startLine: handler.source?.line || astMatch?.sourceContext?.startLine,
|
|
3833
|
+
endLine: astMatch?.sourceContext?.endLine,
|
|
3834
|
+
highlightLines: astMatch?.sourceContext ? [astMatch.sourceContext.startLine, astMatch.sourceContext.endLine] : void 0
|
|
3835
|
+
} : void 0;
|
|
3836
|
+
if (!channels[eventName]) {
|
|
3837
|
+
channels[eventName] = {
|
|
3838
|
+
publish: {
|
|
3839
|
+
operationId: `on${eventName.charAt(0).toUpperCase() + eventName.slice(1)}`,
|
|
3840
|
+
tags,
|
|
3841
|
+
message: {
|
|
3842
|
+
payload: { type: "object" },
|
|
3843
|
+
...userSpec?.message ? userSpec.message : {}
|
|
3844
|
+
},
|
|
3845
|
+
...userSpec?.type === "publish" ? userSpec : {},
|
|
3846
|
+
"x-source-info": sourceInfo ? [sourceInfo] : [],
|
|
3847
|
+
"x-shokupan-source": sourceInfo
|
|
3848
|
+
// Simplified
|
|
3849
|
+
}
|
|
3850
|
+
};
|
|
3851
|
+
if (userSpec?.summary) channels[eventName].publish.summary = userSpec.summary;
|
|
3852
|
+
if (userSpec?.description) channels[eventName].publish.description = userSpec.description;
|
|
3853
|
+
} else {
|
|
3854
|
+
if (sourceInfo) {
|
|
3855
|
+
if (!channels[eventName].publish["x-source-info"]) {
|
|
3856
|
+
channels[eventName].publish["x-source-info"] = [];
|
|
3857
|
+
}
|
|
3858
|
+
const exists = channels[eventName].publish["x-source-info"].some(
|
|
3859
|
+
(s) => s.file === sourceInfo.file && s.line === sourceInfo.line
|
|
3860
|
+
);
|
|
3861
|
+
if (!exists) {
|
|
3862
|
+
channels[eventName].publish["x-source-info"].push(sourceInfo);
|
|
3863
|
+
}
|
|
3864
|
+
}
|
|
3865
|
+
}
|
|
3866
|
+
let emits = astMatch?.emits || [];
|
|
3867
|
+
for (const emit of emits) {
|
|
3868
|
+
if (emit.event === "__DYNAMIC_EMIT__") {
|
|
3869
|
+
const warningKey = `${eventName}/Dynamic Emit`;
|
|
3870
|
+
channels[warningKey] = {
|
|
3871
|
+
subscribe: {
|
|
3872
|
+
operationId: `dynamicEmitWarning${eventName}`,
|
|
3873
|
+
summary: "Dynamic Emit Detected",
|
|
3874
|
+
description: "This handler emits an event with a dynamic name that could not be determined statically.",
|
|
3875
|
+
tags,
|
|
3876
|
+
"x-warning": true,
|
|
3877
|
+
"x-source-info": {
|
|
3878
|
+
file: astMatch?.sourceContext?.file,
|
|
3879
|
+
line: emit.location?.startLine,
|
|
3880
|
+
startLine: emit.location?.startLine,
|
|
3881
|
+
endLine: emit.location?.endLine,
|
|
3882
|
+
highlightLines: emit.location ? [emit.location.startLine, emit.location.endLine] : void 0
|
|
3883
|
+
},
|
|
3884
|
+
"x-shokupan-source": {
|
|
3885
|
+
file: astMatch?.sourceContext?.file,
|
|
3886
|
+
line: emit.location?.startLine
|
|
3887
|
+
},
|
|
3888
|
+
message: { payload: { type: "object" } }
|
|
3889
|
+
}
|
|
3890
|
+
};
|
|
3891
|
+
continue;
|
|
3892
|
+
}
|
|
3893
|
+
const emitStart = emit.location?.startLine;
|
|
3894
|
+
const emitEnd = emit.location?.endLine;
|
|
3895
|
+
const newSourceInfo = sourceInfo && emitStart ? {
|
|
3896
|
+
file: sourceInfo.file,
|
|
3897
|
+
line: emitStart,
|
|
3898
|
+
startLine: emitStart,
|
|
3899
|
+
endLine: emitEnd,
|
|
3900
|
+
highlightLines: sourceInfo.highlightLines,
|
|
3901
|
+
emitHighlightLines: [emitStart, emitEnd]
|
|
3902
|
+
} : void 0;
|
|
3903
|
+
if (!channels[emit.event]) {
|
|
3904
|
+
channels[emit.event] = {
|
|
3905
|
+
subscribe: {
|
|
3906
|
+
operationId: `emit${emit.event.charAt(0).toUpperCase() + emit.event.slice(1)}`,
|
|
3907
|
+
tags,
|
|
3908
|
+
message: {
|
|
3909
|
+
payload: emit.payload || { type: "object" }
|
|
3910
|
+
},
|
|
3911
|
+
"x-source-info": newSourceInfo ? [newSourceInfo] : [],
|
|
3912
|
+
"x-shokupan-source": sourceInfo && emitStart ? {
|
|
3913
|
+
file: sourceInfo.file,
|
|
3914
|
+
line: emitStart
|
|
3915
|
+
} : void 0
|
|
3916
|
+
}
|
|
3917
|
+
};
|
|
3918
|
+
} else {
|
|
3919
|
+
if (newSourceInfo) {
|
|
3920
|
+
if (!channels[emit.event].subscribe["x-source-info"]) {
|
|
3921
|
+
channels[emit.event].subscribe["x-source-info"] = [];
|
|
3922
|
+
}
|
|
3923
|
+
const existing = channels[emit.event].subscribe["x-source-info"];
|
|
3924
|
+
const exists = existing.some(
|
|
3925
|
+
(s) => s.file === newSourceInfo.file && s.line === newSourceInfo.line
|
|
3926
|
+
);
|
|
3927
|
+
if (!exists) {
|
|
3928
|
+
existing.push(newSourceInfo);
|
|
3929
|
+
}
|
|
3930
|
+
}
|
|
3931
|
+
}
|
|
3932
|
+
}
|
|
3933
|
+
}
|
|
3934
|
+
}
|
|
3935
|
+
}
|
|
3936
|
+
const httpRoutes = router[$routes];
|
|
3937
|
+
if (httpRoutes) {
|
|
3938
|
+
for (const route of httpRoutes) {
|
|
3939
|
+
const handler = route.handler;
|
|
3940
|
+
let tags = route.handlerSpec?.tags;
|
|
3941
|
+
if (!tags && routerTag) {
|
|
3942
|
+
tags = [{ name: routerTag }];
|
|
3943
|
+
}
|
|
3944
|
+
const methodUpper = route.method.toUpperCase();
|
|
3945
|
+
let astMatch = astRoutes.find(
|
|
3946
|
+
(r) => r.method === methodUpper && (r.path === route.path || r.path === "/" + route.path)
|
|
3947
|
+
);
|
|
3948
|
+
if (!astMatch) {
|
|
3949
|
+
const runtimeSource = (handler.originalHandler || handler).toString();
|
|
3950
|
+
const runtimeHandlerSrc = runtimeSource.replace(/\s+/g, " ");
|
|
3951
|
+
const sameMethodRoutes = astRoutes.filter((r) => r.method === methodUpper);
|
|
3952
|
+
astMatch = sameMethodRoutes.find((r) => {
|
|
3953
|
+
const astHandlerSrc = (r.handlerSource || r.handlerName || "").replace(/\s+/g, " ");
|
|
3954
|
+
if (!astHandlerSrc || astHandlerSrc.length < 20) return false;
|
|
3955
|
+
return runtimeHandlerSrc.includes(astHandlerSrc) || astHandlerSrc.includes(runtimeHandlerSrc) || r.handlerSource && runtimeHandlerSrc.includes(r.handlerSource.substring(0, 50));
|
|
3956
|
+
});
|
|
3957
|
+
}
|
|
3958
|
+
const sourceInfo = handler.source || astMatch?.sourceContext ? {
|
|
3959
|
+
file: handler.source?.file || astMatch?.sourceContext?.file,
|
|
3960
|
+
line: handler.source?.line || astMatch?.sourceContext?.startLine,
|
|
3961
|
+
startLine: handler.source?.line || astMatch?.sourceContext?.startLine,
|
|
3962
|
+
endLine: astMatch?.sourceContext?.endLine,
|
|
3963
|
+
highlightLines: astMatch?.sourceContext ? [astMatch.sourceContext.startLine, astMatch.sourceContext.endLine] : void 0
|
|
3964
|
+
} : void 0;
|
|
3965
|
+
let emits = astMatch?.emits || [];
|
|
3966
|
+
for (const emit of emits) {
|
|
3967
|
+
const emitStart = emit.location?.startLine;
|
|
3968
|
+
const emitEnd = emit.location?.endLine;
|
|
3969
|
+
const newSourceInfo = sourceInfo && emitStart ? {
|
|
3970
|
+
file: sourceInfo.file,
|
|
3971
|
+
line: emitStart,
|
|
3972
|
+
startLine: emitStart,
|
|
3973
|
+
endLine: emitEnd,
|
|
3974
|
+
highlightLines: sourceInfo.highlightLines,
|
|
3975
|
+
emitHighlightLines: [emitStart, emitEnd]
|
|
3976
|
+
} : void 0;
|
|
3977
|
+
if (!channels[emit.event]) {
|
|
3978
|
+
channels[emit.event] = {
|
|
3979
|
+
subscribe: {
|
|
3980
|
+
operationId: `emit${emit.event.charAt(0).toUpperCase() + emit.event.slice(1)}`,
|
|
3981
|
+
tags,
|
|
3982
|
+
message: {
|
|
3983
|
+
payload: emit.payload || { type: "object" }
|
|
3984
|
+
},
|
|
3985
|
+
"x-source-info": newSourceInfo ? [newSourceInfo] : [],
|
|
3986
|
+
"x-shokupan-source": sourceInfo && emitStart ? {
|
|
3987
|
+
file: sourceInfo.file,
|
|
3988
|
+
line: emitStart
|
|
3989
|
+
} : void 0
|
|
3990
|
+
}
|
|
3991
|
+
};
|
|
3992
|
+
} else {
|
|
3993
|
+
if (newSourceInfo) {
|
|
3994
|
+
if (!channels[emit.event].subscribe["x-source-info"]) {
|
|
3995
|
+
channels[emit.event].subscribe["x-source-info"] = [];
|
|
3996
|
+
}
|
|
3997
|
+
const existing = channels[emit.event].subscribe["x-source-info"];
|
|
3998
|
+
const exists = existing.some(
|
|
3999
|
+
(s) => s.file === newSourceInfo.file && s.line === newSourceInfo.line
|
|
4000
|
+
);
|
|
4001
|
+
if (!exists) {
|
|
4002
|
+
existing.push(newSourceInfo);
|
|
4003
|
+
}
|
|
4004
|
+
}
|
|
4005
|
+
}
|
|
4006
|
+
}
|
|
4007
|
+
}
|
|
4008
|
+
}
|
|
4009
|
+
const childRouters = router[$childRouters];
|
|
4010
|
+
for (const child of childRouters) {
|
|
4011
|
+
await collect(child);
|
|
4012
|
+
}
|
|
4013
|
+
};
|
|
4014
|
+
await collect(rootRouter);
|
|
4015
|
+
const dynamicEvents = astRoutes.filter((r) => r.path === "__DYNAMIC_EVENT__" && !matchedAstRoutes.has(r));
|
|
4016
|
+
dynamicEvents.forEach((r, i) => {
|
|
4017
|
+
let prefix = "Anonymous";
|
|
4018
|
+
if (r.handlerName && !r.handlerName.includes("=>") && !r.handlerName.includes("{")) {
|
|
4019
|
+
const parts = r.handlerName.split(".");
|
|
4020
|
+
if (parts.length > 0) prefix = parts[0];
|
|
4021
|
+
}
|
|
4022
|
+
const key = `${prefix}.Dynamic Event ${i + 1}`;
|
|
4023
|
+
channels[key] = {
|
|
4024
|
+
publish: {
|
|
4025
|
+
operationId: `dynamicEventWarning${i}`,
|
|
4026
|
+
summary: "Dynamic Event Detected",
|
|
4027
|
+
description: `A dynamic event listener was detected in your source code but the event name could not be determined statically.`,
|
|
4028
|
+
tags: [{ name: "Warnings" }],
|
|
4029
|
+
"x-warning": true,
|
|
4030
|
+
"x-source-info": {
|
|
4031
|
+
file: r.sourceContext?.file,
|
|
4032
|
+
line: r.sourceContext?.startLine,
|
|
4033
|
+
startLine: r.sourceContext?.startLine,
|
|
4034
|
+
endLine: r.sourceContext?.endLine,
|
|
4035
|
+
highlightLines: r.sourceContext ? [r.sourceContext.startLine, r.sourceContext.endLine] : void 0
|
|
4036
|
+
},
|
|
4037
|
+
"x-shokupan-source": {
|
|
4038
|
+
file: r.sourceContext?.file,
|
|
4039
|
+
line: r.sourceContext?.startLine
|
|
4040
|
+
},
|
|
4041
|
+
message: { payload: { type: "object" } }
|
|
4042
|
+
}
|
|
4043
|
+
};
|
|
4044
|
+
});
|
|
4045
|
+
return {
|
|
4046
|
+
asyncapi: "3.0.0",
|
|
4047
|
+
info: { title: "Shokupan AsyncAPI", version: "1.0.0", ...options.info },
|
|
4048
|
+
channels
|
|
4049
|
+
};
|
|
4050
|
+
}
|
|
4051
|
+
const generator = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
|
|
4052
|
+
__proto__: null,
|
|
4053
|
+
generateAsyncApi
|
|
4054
|
+
}, Symbol.toStringTag, { value: "Module" }));
|
|
4055
|
+
class AsyncApiPlugin extends ShokupanRouter {
|
|
4056
|
+
constructor(pluginOptions = {}) {
|
|
4057
|
+
super({ renderer: renderToString });
|
|
4058
|
+
this.pluginOptions = pluginOptions;
|
|
4059
|
+
this.pluginOptions.path ??= "/asyncapi";
|
|
4060
|
+
this.init();
|
|
4061
|
+
}
|
|
4062
|
+
static getBasePath() {
|
|
4063
|
+
const dir = path$1.dirname(node_url.fileURLToPath(typeof document === "undefined" ? require("url").pathToFileURL(__filename).href : _documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === "SCRIPT" && _documentCurrentScript.src || new URL("index.cjs", document.baseURI).href));
|
|
4064
|
+
if (dir.endsWith("dist")) {
|
|
4065
|
+
return dir + "/plugins/application/asyncapi";
|
|
4066
|
+
}
|
|
4067
|
+
return dir;
|
|
4068
|
+
}
|
|
4069
|
+
onInit(app, options) {
|
|
4070
|
+
const path2 = this.pluginOptions.path || options?.path || "/asyncapi";
|
|
4071
|
+
app.mount(path2, this);
|
|
4072
|
+
if (app.applicationConfig.enableAsyncApiGen !== true) {
|
|
4073
|
+
console.warn("AsyncApiPlugin: enableAsyncApiGen is disabled. AsyncApiPlugin will not generate spec.");
|
|
4074
|
+
}
|
|
4075
|
+
}
|
|
4076
|
+
init() {
|
|
4077
|
+
const serveFile = async (ctx, file, type) => {
|
|
4078
|
+
const content = await promises.readFile(path$1.join(AsyncApiPlugin.getBasePath(), "static", file), "utf-8");
|
|
4079
|
+
ctx.set("Content-Type", type);
|
|
4080
|
+
return ctx.send(content);
|
|
4081
|
+
};
|
|
4082
|
+
this.get("/style.css", (ctx) => serveFile(ctx, "style.css", "text/css"));
|
|
4083
|
+
this.get("/theme.css", (ctx) => serveFile(ctx, "theme.css", "text/css"));
|
|
4084
|
+
this.get("/asyncapi-client.mjs", (ctx) => serveFile(ctx, "asyncapi-client.mjs", "application/javascript"));
|
|
4085
|
+
this.get("/", async (ctx) => {
|
|
4086
|
+
let spec = ctx.app?.asyncApiSpec;
|
|
4087
|
+
if (!spec) {
|
|
4088
|
+
spec = await generateAsyncApi(ctx.app);
|
|
4089
|
+
}
|
|
4090
|
+
if (this.pluginOptions.spec) {
|
|
4091
|
+
deepMerge(spec, this.pluginOptions.spec);
|
|
4092
|
+
}
|
|
4093
|
+
const serverUrl = `${ctx.hostname}:${ctx.app?.applicationConfig.port}`;
|
|
4094
|
+
const base = this.pluginOptions.path;
|
|
4095
|
+
const disableSourceView = this.pluginOptions.disableSourceView;
|
|
4096
|
+
const navTree = buildNavTree(spec);
|
|
4097
|
+
return ctx.jsx(AsyncApiApp({ spec, serverUrl, base, disableSourceView, navTree }));
|
|
4098
|
+
});
|
|
4099
|
+
this.get("/json", async (ctx) => {
|
|
4100
|
+
let spec = ctx.app?.asyncApiSpec;
|
|
4101
|
+
if (!spec) {
|
|
4102
|
+
spec = await generateAsyncApi(ctx.app);
|
|
4103
|
+
}
|
|
4104
|
+
if (this.pluginOptions.spec) {
|
|
4105
|
+
deepMerge(spec, this.pluginOptions.spec);
|
|
4106
|
+
}
|
|
4107
|
+
return ctx.json(spec);
|
|
4108
|
+
});
|
|
4109
|
+
this.get("/_code", async (ctx) => {
|
|
4110
|
+
const file = ctx.query["file"];
|
|
4111
|
+
if (!file || typeof file !== "string") {
|
|
4112
|
+
return ctx.text("Missing file parameter", 400);
|
|
4113
|
+
}
|
|
4114
|
+
try {
|
|
4115
|
+
const content = await promises.readFile(file, "utf8");
|
|
4116
|
+
return ctx.text(content);
|
|
4117
|
+
} catch (e) {
|
|
4118
|
+
return ctx.text("File not found: " + e.message, 404);
|
|
4119
|
+
}
|
|
4120
|
+
});
|
|
4121
|
+
}
|
|
4122
|
+
}
|
|
3289
4123
|
class AuthPlugin extends ShokupanRouter {
|
|
3290
4124
|
constructor(authConfig) {
|
|
3291
4125
|
super();
|
|
3292
4126
|
this.authConfig = authConfig;
|
|
3293
4127
|
this.secret = typeof authConfig.jwtSecret === "string" ? new TextEncoder().encode(authConfig.jwtSecret) : authConfig.jwtSecret;
|
|
3294
|
-
this.init();
|
|
3295
4128
|
}
|
|
3296
4129
|
secret;
|
|
3297
|
-
|
|
4130
|
+
arctic;
|
|
4131
|
+
jose;
|
|
4132
|
+
async onInit(app, options) {
|
|
4133
|
+
this.arctic = await import("arctic");
|
|
4134
|
+
this.jose = await import("jose");
|
|
4135
|
+
this.init();
|
|
3298
4136
|
if (options?.path) {
|
|
3299
4137
|
app.mount(options.path, this);
|
|
3300
4138
|
} else {
|
|
@@ -3302,15 +4140,16 @@ class AuthPlugin extends ShokupanRouter {
|
|
|
3302
4140
|
}
|
|
3303
4141
|
}
|
|
3304
4142
|
getProviderInstance(name, p) {
|
|
4143
|
+
const { GitHub, Google, MicrosoftEntraId, Apple, Auth0, Okta, OAuth2Client } = this.arctic;
|
|
3305
4144
|
switch (name) {
|
|
3306
4145
|
case "github":
|
|
3307
|
-
return new
|
|
4146
|
+
return new GitHub(p.clientId, p.clientSecret, p.redirectUri);
|
|
3308
4147
|
case "google":
|
|
3309
|
-
return new
|
|
4148
|
+
return new Google(p.clientId, p.clientSecret, p.redirectUri);
|
|
3310
4149
|
case "microsoft":
|
|
3311
|
-
return new
|
|
4150
|
+
return new MicrosoftEntraId(p.tenantId, p.clientId, p.clientSecret, p.redirectUri);
|
|
3312
4151
|
case "apple":
|
|
3313
|
-
return new
|
|
4152
|
+
return new Apple(
|
|
3314
4153
|
p.clientId,
|
|
3315
4154
|
p.teamId,
|
|
3316
4155
|
p.keyId,
|
|
@@ -3318,18 +4157,18 @@ class AuthPlugin extends ShokupanRouter {
|
|
|
3318
4157
|
p.redirectUri
|
|
3319
4158
|
);
|
|
3320
4159
|
case "auth0":
|
|
3321
|
-
return new
|
|
4160
|
+
return new Auth0(p.domain, p.clientId, p.clientSecret, p.redirectUri);
|
|
3322
4161
|
case "okta":
|
|
3323
|
-
return new
|
|
4162
|
+
return new Okta(p.domain, p.authUrl, p.clientId, p.clientSecret, p.redirectUri);
|
|
3324
4163
|
case "oauth2":
|
|
3325
|
-
return new
|
|
4164
|
+
return new OAuth2Client(p.clientId, p.clientSecret, p.redirectUri);
|
|
3326
4165
|
default:
|
|
3327
4166
|
return null;
|
|
3328
4167
|
}
|
|
3329
4168
|
}
|
|
3330
4169
|
async createSession(user, ctx) {
|
|
3331
4170
|
const alg = "HS256";
|
|
3332
|
-
const jwt = await new
|
|
4171
|
+
const jwt = await new this.jose.SignJWT({ ...user }).setProtectedHeader({ alg }).setIssuedAt().setExpirationTime(this.authConfig.jwtExpiration || "24h").sign(this.secret);
|
|
3333
4172
|
const opts = this.authConfig.cookieOptions || {};
|
|
3334
4173
|
let cookie = `auth_token=${jwt}; Path=${opts.path || "/"}; HttpOnly`;
|
|
3335
4174
|
if (opts.secure) cookie += "; Secure";
|
|
@@ -3339,6 +4178,7 @@ class AuthPlugin extends ShokupanRouter {
|
|
|
3339
4178
|
return jwt;
|
|
3340
4179
|
}
|
|
3341
4180
|
init() {
|
|
4181
|
+
const { generateState, generateCodeVerifier, GitHub, Google, MicrosoftEntraId, Apple, Auth0, Okta, OAuth2Client } = this.arctic;
|
|
3342
4182
|
const providerEntries = Object.entries(this.authConfig.providers);
|
|
3343
4183
|
for (let i = 0; i < providerEntries.length; i++) {
|
|
3344
4184
|
const [providerName, providerConfig] = providerEntries[i];
|
|
@@ -3348,17 +4188,17 @@ class AuthPlugin extends ShokupanRouter {
|
|
|
3348
4188
|
continue;
|
|
3349
4189
|
}
|
|
3350
4190
|
this.get(`/auth/${providerName}/login`, async (ctx) => {
|
|
3351
|
-
const state =
|
|
3352
|
-
const codeVerifier = providerName === "google" || providerName === "microsoft" || providerName === "auth0" || providerName === "okta" ?
|
|
4191
|
+
const state = generateState();
|
|
4192
|
+
const codeVerifier = providerName === "google" || providerName === "microsoft" || providerName === "auth0" || providerName === "okta" ? generateCodeVerifier() : void 0;
|
|
3353
4193
|
const scopes = providerConfig.scopes || [];
|
|
3354
4194
|
let url;
|
|
3355
|
-
if (provider instanceof
|
|
4195
|
+
if (provider instanceof GitHub) {
|
|
3356
4196
|
url = await provider.createAuthorizationURL(state, scopes);
|
|
3357
|
-
} else if (provider instanceof
|
|
4197
|
+
} else if (provider instanceof Google || provider instanceof MicrosoftEntraId || provider instanceof Auth0 || provider instanceof Okta) {
|
|
3358
4198
|
url = await provider.createAuthorizationURL(state, codeVerifier, scopes);
|
|
3359
|
-
} else if (provider instanceof
|
|
4199
|
+
} else if (provider instanceof Apple) {
|
|
3360
4200
|
url = await provider.createAuthorizationURL(state, scopes);
|
|
3361
|
-
} else if (provider instanceof
|
|
4201
|
+
} else if (provider instanceof OAuth2Client) {
|
|
3362
4202
|
if (!providerConfig.authUrl) return ctx.text("Config error: authUrl required for oauth2", 500);
|
|
3363
4203
|
url = await provider.createAuthorizationURL(providerConfig.authUrl, state, scopes);
|
|
3364
4204
|
} else {
|
|
@@ -3384,17 +4224,17 @@ class AuthPlugin extends ShokupanRouter {
|
|
|
3384
4224
|
try {
|
|
3385
4225
|
let tokens;
|
|
3386
4226
|
let idToken;
|
|
3387
|
-
if (provider instanceof
|
|
4227
|
+
if (provider instanceof GitHub) {
|
|
3388
4228
|
tokens = await provider.validateAuthorizationCode(code);
|
|
3389
|
-
} else if (provider instanceof
|
|
4229
|
+
} else if (provider instanceof Google || provider instanceof MicrosoftEntraId) {
|
|
3390
4230
|
if (!storedVerifier) return ctx.text("Missing verifier", 400);
|
|
3391
4231
|
tokens = await provider.validateAuthorizationCode(code, storedVerifier);
|
|
3392
|
-
} else if (provider instanceof
|
|
4232
|
+
} else if (provider instanceof Auth0 || provider instanceof Okta) {
|
|
3393
4233
|
tokens = await provider.validateAuthorizationCode(code, storedVerifier || "");
|
|
3394
|
-
} else if (provider instanceof
|
|
4234
|
+
} else if (provider instanceof Apple) {
|
|
3395
4235
|
tokens = await provider.validateAuthorizationCode(code);
|
|
3396
4236
|
idToken = tokens.idToken;
|
|
3397
|
-
} else if (provider instanceof
|
|
4237
|
+
} else if (provider instanceof OAuth2Client) {
|
|
3398
4238
|
if (!providerConfig.tokenUrl) return ctx.text("Config error: tokenUrl required for oauth2", 500);
|
|
3399
4239
|
tokens = await provider.validateAuthorizationCode(providerConfig.tokenUrl, code, null);
|
|
3400
4240
|
}
|
|
@@ -3470,7 +4310,7 @@ class AuthPlugin extends ShokupanRouter {
|
|
|
3470
4310
|
};
|
|
3471
4311
|
} else if (provider === "apple") {
|
|
3472
4312
|
if (idToken) {
|
|
3473
|
-
const payload =
|
|
4313
|
+
const payload = this.jose.decodeJwt(idToken);
|
|
3474
4314
|
user = {
|
|
3475
4315
|
id: payload.sub,
|
|
3476
4316
|
email: payload["email"],
|
|
@@ -3501,6 +4341,9 @@ class AuthPlugin extends ShokupanRouter {
|
|
|
3501
4341
|
*/
|
|
3502
4342
|
getMiddleware() {
|
|
3503
4343
|
return async (ctx, next) => {
|
|
4344
|
+
if (!this.jose) {
|
|
4345
|
+
this.jose = await import("jose");
|
|
4346
|
+
}
|
|
3504
4347
|
const authHeader = ctx.req.headers.get("Authorization");
|
|
3505
4348
|
let token = authHeader?.startsWith("Bearer ") ? authHeader.substring(7) : null;
|
|
3506
4349
|
if (!token) {
|
|
@@ -3509,7 +4352,7 @@ class AuthPlugin extends ShokupanRouter {
|
|
|
3509
4352
|
}
|
|
3510
4353
|
if (token) {
|
|
3511
4354
|
try {
|
|
3512
|
-
const { payload } = await
|
|
4355
|
+
const { payload } = await this.jose.jwtVerify(token, this.secret);
|
|
3513
4356
|
ctx.user = payload;
|
|
3514
4357
|
} catch {
|
|
3515
4358
|
}
|
|
@@ -3626,6 +4469,187 @@ class ClusterPlugin {
|
|
|
3626
4469
|
}
|
|
3627
4470
|
}
|
|
3628
4471
|
}
|
|
4472
|
+
function DashboardApp({ metrics, uptime, integrations, base, getRequestHeadersSource }) {
|
|
4473
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("html", { lang: "en", children: [
|
|
4474
|
+
/* @__PURE__ */ jsxRuntime.jsxs("head", { children: [
|
|
4475
|
+
/* @__PURE__ */ jsxRuntime.jsx("meta", { charSet: "UTF-8" }),
|
|
4476
|
+
/* @__PURE__ */ jsxRuntime.jsx("meta", { name: "viewport", content: "width=device-width, initial-scale=1.0" }),
|
|
4477
|
+
/* @__PURE__ */ jsxRuntime.jsx("title", { children: "Shokupan Debug Dashboard" }),
|
|
4478
|
+
/* @__PURE__ */ jsxRuntime.jsx("link", { href: "https://unpkg.com/tabulator-tables@5.5.0/dist/css/tabulator_bootstrap5.min.css", rel: "stylesheet" }),
|
|
4479
|
+
/* @__PURE__ */ jsxRuntime.jsx("link", { rel: "stylesheet", href: "https://esm.sh/@xyflow/react@12.3.6/dist/style.css" }),
|
|
4480
|
+
/* @__PURE__ */ jsxRuntime.jsx("link", { rel: "stylesheet", href: `${base}/theme.css` }),
|
|
4481
|
+
/* @__PURE__ */ jsxRuntime.jsx("link", { rel: "stylesheet", href: `${base}/styles.css` }),
|
|
4482
|
+
/* @__PURE__ */ jsxRuntime.jsx("link", { rel: "stylesheet", href: `${base}/reactflow.css` }),
|
|
4483
|
+
/* @__PURE__ */ jsxRuntime.jsx("link", { rel: "stylesheet", href: `${base}/registry.css` }),
|
|
4484
|
+
/* @__PURE__ */ jsxRuntime.jsx("link", { rel: "stylesheet", href: `${base}/tabulator.css` }),
|
|
4485
|
+
/* @__PURE__ */ jsxRuntime.jsx("script", { src: "https://cdn.jsdelivr.net/npm/chart.js" }),
|
|
4486
|
+
/* @__PURE__ */ jsxRuntime.jsx("script", { type: "text/javascript", src: "https://unpkg.com/tabulator-tables@5.5.0/dist/js/tabulator.min.js" })
|
|
4487
|
+
] }),
|
|
4488
|
+
/* @__PURE__ */ jsxRuntime.jsxs("body", { children: [
|
|
4489
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { class: "container", children: [
|
|
4490
|
+
/* @__PURE__ */ jsxRuntime.jsxs("header", { children: [
|
|
4491
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
|
|
4492
|
+
/* @__PURE__ */ jsxRuntime.jsx("h1", { children: "Dashboard" }),
|
|
4493
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { style: "color: var(--text-secondary)", children: [
|
|
4494
|
+
"Uptime: ",
|
|
4495
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { id: "uptime", children: uptime })
|
|
4496
|
+
] })
|
|
4497
|
+
] }),
|
|
4498
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { class: "tabs", children: [
|
|
4499
|
+
/* @__PURE__ */ jsxRuntime.jsx("button", { class: "tab-btn active", onclick: "switchTab('overview')", children: "Overview" }),
|
|
4500
|
+
/* @__PURE__ */ jsxRuntime.jsx("button", { class: "tab-btn", onclick: "switchTab('registry')", children: "Registry" }),
|
|
4501
|
+
/* @__PURE__ */ jsxRuntime.jsx("button", { class: "tab-btn", onclick: "switchTab('graph')", children: "Graph" }),
|
|
4502
|
+
/* @__PURE__ */ jsxRuntime.jsx("button", { class: "tab-btn", onclick: "switchTab('requests')", children: "Requests" }),
|
|
4503
|
+
/* @__PURE__ */ jsxRuntime.jsx("button", { class: "tab-btn", onclick: "switchTab('failures')", children: "Failures" }),
|
|
4504
|
+
integrations.scalar && /* @__PURE__ */ jsxRuntime.jsx("button", { class: "tab-btn", onclick: "switchTab('scalar')", children: "API Reference" }),
|
|
4505
|
+
integrations.asyncapi && /* @__PURE__ */ jsxRuntime.jsx("button", { class: "tab-btn", onclick: "switchTab('asyncapi')", children: "AsyncAPI" })
|
|
4506
|
+
] })
|
|
4507
|
+
] }),
|
|
4508
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { id: "tab-overview", class: "tab-content active", children: [
|
|
4509
|
+
/* @__PURE__ */ jsxRuntime.jsx(MetricsGrid, { metrics }),
|
|
4510
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { id: "chart-container", style: "display: flex; flex-direction: column; gap: 1rem;", children: [
|
|
4511
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { style: "display: flex; justify-content: flex-end;", children: /* @__PURE__ */ jsxRuntime.jsxs("select", { id: "time-range-selector", onchange: "updateCharts(); updateDashboard(); fetchTopStats();", style: "background: var(--bg-primary); color: var(--text-primary); border: 1px solid var(--card-border); padding: 5px; border-radius: 4px;", children: [
|
|
4512
|
+
/* @__PURE__ */ jsxRuntime.jsx("option", { value: "1m", children: "1 Minute" }),
|
|
4513
|
+
/* @__PURE__ */ jsxRuntime.jsx("option", { value: "5m", children: "5 Minutes" }),
|
|
4514
|
+
/* @__PURE__ */ jsxRuntime.jsx("option", { value: "30m", children: "30 Minutes" }),
|
|
4515
|
+
/* @__PURE__ */ jsxRuntime.jsx("option", { value: "1h", children: "1 Hour" }),
|
|
4516
|
+
/* @__PURE__ */ jsxRuntime.jsx("option", { value: "2h", children: "2 Hours" }),
|
|
4517
|
+
/* @__PURE__ */ jsxRuntime.jsx("option", { value: "6h", children: "6 Hours" }),
|
|
4518
|
+
/* @__PURE__ */ jsxRuntime.jsx("option", { value: "12h", children: "12 Hours" }),
|
|
4519
|
+
/* @__PURE__ */ jsxRuntime.jsx("option", { value: "1d", children: "1 Day" }),
|
|
4520
|
+
/* @__PURE__ */ jsxRuntime.jsx("option", { value: "3d", children: "3 Days" }),
|
|
4521
|
+
/* @__PURE__ */ jsxRuntime.jsx("option", { value: "7d", children: "7 Days" }),
|
|
4522
|
+
/* @__PURE__ */ jsxRuntime.jsx("option", { value: "30d", children: "30 Days" })
|
|
4523
|
+
] }) }),
|
|
4524
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { class: "card-container", children: [
|
|
4525
|
+
/* @__PURE__ */ jsxRuntime.jsx(ChartCard, { title: "Response Time", id: "latencyChart" }),
|
|
4526
|
+
/* @__PURE__ */ jsxRuntime.jsx(ChartCard, { title: "Requests / Second", id: "rpsChart" }),
|
|
4527
|
+
/* @__PURE__ */ jsxRuntime.jsx(ChartCard, { title: "CPU & Load", id: "cpuChart" }),
|
|
4528
|
+
/* @__PURE__ */ jsxRuntime.jsx(ChartCard, { title: "Memory", id: "memoryChart" }),
|
|
4529
|
+
/* @__PURE__ */ jsxRuntime.jsx(ChartCard, { title: "Heap Usage", id: "heapChart" }),
|
|
4530
|
+
/* @__PURE__ */ jsxRuntime.jsx(ChartCard, { title: "Event Loop Latency", id: "eventLoopChart" }),
|
|
4531
|
+
/* @__PURE__ */ jsxRuntime.jsx(ChartCard, { title: "Error Rate", id: "errorRateChart" })
|
|
4532
|
+
] }),
|
|
4533
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { class: "card-title", style: "margin-top: 1rem;", children: "Top Statistics" }),
|
|
4534
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { class: "card-container", children: [
|
|
4535
|
+
/* @__PURE__ */ jsxRuntime.jsx(Card, { title: "Top Requests", contentId: "top-requests-table" }),
|
|
4536
|
+
/* @__PURE__ */ jsxRuntime.jsx(Card, { title: "Top Errors", contentId: "top-errors-table" }),
|
|
4537
|
+
/* @__PURE__ */ jsxRuntime.jsx(Card, { title: "Most Frequent Failures", contentId: "failing-requests-table" }),
|
|
4538
|
+
/* @__PURE__ */ jsxRuntime.jsx(Card, { title: "Slowest Requests", contentId: "slowest-requests-table" })
|
|
4539
|
+
] }),
|
|
4540
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { id: "table-container", style: "padding: 0; margin-top: 1rem;", children: /* @__PURE__ */ jsxRuntime.jsx("div", { id: "requests-table", class: "table-dark" }) })
|
|
4541
|
+
] })
|
|
4542
|
+
] }),
|
|
4543
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { id: "tab-registry", class: "tab-content", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { id: "registry-container", class: "card", style: "margin-top: 2rem;", children: [
|
|
4544
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { class: "card-title", children: "Component Registry" }),
|
|
4545
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { id: "registry-tree", style: "padding: 0 1rem 1rem 1rem; font-family: monospace; font-size: 0.9rem;" })
|
|
4546
|
+
] }) }),
|
|
4547
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { id: "tab-graph", class: "tab-content", children: [
|
|
4548
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { class: "card", style: "margin-bottom: 1rem;", children: /* @__PURE__ */ jsxRuntime.jsx("div", { style: "display: flex; gap: 1rem;", children: /* @__PURE__ */ jsxRuntime.jsx("input", { type: "text", id: "graph-search", placeholder: "Search routes or middleware...", "aria-label": "Search routes or middleware", style: "flex:1; padding: 0.5rem; border-radius: 0.5rem; background: var(--bg-primary); border: 1px solid var(--card-border); color: var(--text-primary);" }) }) }),
|
|
4549
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { id: "cy" })
|
|
4550
|
+
] }),
|
|
4551
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { id: "tab-requests", class: "tab-content", children: [
|
|
4552
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { class: "card", style: "margin-bottom: 1rem; display: flex; justify-content: space-between; align-items: center;", children: [
|
|
4553
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { class: "card-title", children: "Recent Requests (Last 100)" }),
|
|
4554
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { children: /* @__PURE__ */ jsxRuntime.jsx("button", { onclick: "fetchRequests()", style: "background: var(--bg-primary); color: var(--text-primary); border: 1px solid var(--card-border); padding: 5px 10px; border-radius: 4px; cursor: pointer;", children: "Refresh" }) })
|
|
4555
|
+
] }),
|
|
4556
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { id: "requests-list-container", style: "height: calc(100vh - 300px); margin-bottom: 2rem;" }),
|
|
4557
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { id: "request-details-container", class: "card", style: "display: none;", children: [
|
|
4558
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { class: "card-title", children: "Request Details" }),
|
|
4559
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { id: "request-details-content" }),
|
|
4560
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { class: "card-title", style: "margin-top: 1rem;", children: "Middleware Trace" }),
|
|
4561
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { id: "middleware-trace-container" })
|
|
4562
|
+
] })
|
|
4563
|
+
] }),
|
|
4564
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { id: "tab-failures", class: "tab-content", children: [
|
|
4565
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { class: "card", style: "margin-bottom: 1rem; display: flex; justify-content: space-between; align-items: center;", children: [
|
|
4566
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { class: "card-title", children: "Failed Requests (Last 50)" }),
|
|
4567
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
|
|
4568
|
+
/* @__PURE__ */ jsxRuntime.jsx("button", { onclick: "importFailure()", style: "background: var(--bg-primary); color: var(--text-primary); border: 1px solid var(--card-border); padding: 5px 10px; border-radius: 4px; cursor: pointer; margin-right: 8px;", children: "Import" }),
|
|
4569
|
+
/* @__PURE__ */ jsxRuntime.jsx("button", { onclick: "fetchFailures()", style: "background: var(--bg-primary); color: var(--text-primary); border: 1px solid var(--card-border); padding: 5px 10px; border-radius: 4px; cursor: pointer;", children: "Refresh" })
|
|
4570
|
+
] })
|
|
4571
|
+
] }),
|
|
4572
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { id: "failures-table-container" })
|
|
4573
|
+
] }),
|
|
4574
|
+
integrations.scalar && /* @__PURE__ */ jsxRuntime.jsx("div", { id: "tab-scalar", class: "tab-content", style: "margin: 0; overflow: hidden; height: 100%; max-width: unset", children: /* @__PURE__ */ jsxRuntime.jsx("iframe", { src: integrations.scalar, style: "width: 100%; height: 100%; border: none;" }) }),
|
|
4575
|
+
integrations.asyncapi && /* @__PURE__ */ jsxRuntime.jsx("div", { id: "tab-asyncapi", class: "tab-content", style: "margin: 0; overflow: hidden; height: 100%; max-width: unset", children: /* @__PURE__ */ jsxRuntime.jsx("iframe", { src: integrations.asyncapi, style: "width: 100%; height: 100%; border: none;" }) })
|
|
4576
|
+
] }),
|
|
4577
|
+
/* @__PURE__ */ jsxRuntime.jsx("script", { dangerouslySetInnerHTML: {
|
|
4578
|
+
__html: `
|
|
4579
|
+
// Injected function from server config
|
|
4580
|
+
const getRequestHeaders = ${getRequestHeadersSource};
|
|
4581
|
+
`
|
|
4582
|
+
} }),
|
|
4583
|
+
/* @__PURE__ */ jsxRuntime.jsx("script", { src: `${base}/poll.js` }),
|
|
4584
|
+
/* @__PURE__ */ jsxRuntime.jsx("script", { src: `${base}/graph.mjs`, type: "module" }),
|
|
4585
|
+
/* @__PURE__ */ jsxRuntime.jsx("script", { src: `${base}/charts.js` }),
|
|
4586
|
+
/* @__PURE__ */ jsxRuntime.jsx("script", { src: `${base}/tables.js` }),
|
|
4587
|
+
/* @__PURE__ */ jsxRuntime.jsx("script", { src: `${base}/registry.js` }),
|
|
4588
|
+
/* @__PURE__ */ jsxRuntime.jsx("script", { src: `${base}/failures.js` }),
|
|
4589
|
+
/* @__PURE__ */ jsxRuntime.jsx("script", { src: `${base}/requests.js` }),
|
|
4590
|
+
/* @__PURE__ */ jsxRuntime.jsx("script", { src: `${base}/tabs.js` })
|
|
4591
|
+
] })
|
|
4592
|
+
] });
|
|
4593
|
+
}
|
|
4594
|
+
function MetricsGrid({ metrics }) {
|
|
4595
|
+
const total = metrics.totalRequests;
|
|
4596
|
+
const active = metrics.activeRequests;
|
|
4597
|
+
const finished = total - active;
|
|
4598
|
+
const successRate = finished ? Math.round(metrics.successfulRequests / finished * 100) : 100;
|
|
4599
|
+
const failRate = finished ? Math.round(metrics.failedRequests / finished * 100) : 0;
|
|
4600
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { class: "metrics-grid", children: [
|
|
4601
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { class: "card", children: [
|
|
4602
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { class: "card-title", children: "Total Requests" }),
|
|
4603
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { class: "card-value", id: "total-requests", children: metrics.totalRequests })
|
|
4604
|
+
] }),
|
|
4605
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { class: "card", children: [
|
|
4606
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { class: "card-title", children: "Active Requests" }),
|
|
4607
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { class: "card-value", style: "color: var(--accent)", id: "active-requests", children: metrics.activeRequests })
|
|
4608
|
+
] }),
|
|
4609
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { class: "card", children: [
|
|
4610
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { class: "card-title", children: "Success Rate" }),
|
|
4611
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { class: "card-value text-success", children: /* @__PURE__ */ jsxRuntime.jsxs("span", { id: "success-rate", children: [
|
|
4612
|
+
successRate,
|
|
4613
|
+
"%"
|
|
4614
|
+
] }) }),
|
|
4615
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { style: "color: var(--text-secondary); margin-top: 0.5rem", children: [
|
|
4616
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { id: "successful-requests", children: metrics.successfulRequests }),
|
|
4617
|
+
" successful"
|
|
4618
|
+
] })
|
|
4619
|
+
] }),
|
|
4620
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { class: "card", children: [
|
|
4621
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { class: "card-title", children: "Fail Rate" }),
|
|
4622
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { class: "card-value text-error", children: /* @__PURE__ */ jsxRuntime.jsxs("span", { id: "fail-rate", children: [
|
|
4623
|
+
failRate,
|
|
4624
|
+
"%"
|
|
4625
|
+
] }) }),
|
|
4626
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { style: "color: var(--text-secondary); margin-top: 0.5rem", children: [
|
|
4627
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { id: "failed-requests", children: metrics.failedRequests }),
|
|
4628
|
+
" failed"
|
|
4629
|
+
] })
|
|
4630
|
+
] }),
|
|
4631
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { class: "card", children: [
|
|
4632
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { class: "card-title", children: "Avg Latency" }),
|
|
4633
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { class: "card-value", children: [
|
|
4634
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { id: "avg-latency", children: metrics.averageTotalTime_ms.toFixed(2) }),
|
|
4635
|
+
" ",
|
|
4636
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { style: "font-size: 1rem; color: var(--text-secondary)", children: "ms" })
|
|
4637
|
+
] })
|
|
4638
|
+
] })
|
|
4639
|
+
] });
|
|
4640
|
+
}
|
|
4641
|
+
function ChartCard({ title, id }) {
|
|
4642
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { class: "card", style: "height: 300px;", children: [
|
|
4643
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { class: "card-title", children: title }),
|
|
4644
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { class: "card-chart", children: /* @__PURE__ */ jsxRuntime.jsx("canvas", { id }) })
|
|
4645
|
+
] });
|
|
4646
|
+
}
|
|
4647
|
+
function Card({ title, contentId }) {
|
|
4648
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { class: "card", children: [
|
|
4649
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { class: "card-title", children: title }),
|
|
4650
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { id: contentId })
|
|
4651
|
+
] });
|
|
4652
|
+
}
|
|
3629
4653
|
const INTERVALS = [
|
|
3630
4654
|
{ label: "10s", ms: 10 * 1e3 },
|
|
3631
4655
|
{ label: "1m", ms: 60 * 1e3 },
|
|
@@ -3640,19 +4664,19 @@ const INTERVALS = [
|
|
|
3640
4664
|
{ label: "30d", ms: 30 * 24 * 60 * 60 * 1e3 }
|
|
3641
4665
|
];
|
|
3642
4666
|
class MetricsCollector {
|
|
3643
|
-
|
|
3644
|
-
|
|
3645
|
-
eventLoopHistogram = node_perf_hooks.monitorEventLoopDelay({ resolution: 10 });
|
|
3646
|
-
timer = null;
|
|
3647
|
-
constructor() {
|
|
4667
|
+
constructor(db) {
|
|
4668
|
+
this.db = db;
|
|
3648
4669
|
this.eventLoopHistogram.enable();
|
|
3649
4670
|
const now = Date.now();
|
|
3650
4671
|
INTERVALS.forEach((int) => {
|
|
3651
4672
|
this.currentIntervalStart[int.label] = this.alignTimestamp(now, int.ms);
|
|
3652
4673
|
this.pendingDetails[int.label] = [];
|
|
3653
4674
|
});
|
|
3654
|
-
this.timer = setInterval(() => this.collect(), 1e4);
|
|
3655
4675
|
}
|
|
4676
|
+
currentIntervalStart = {};
|
|
4677
|
+
pendingDetails = {};
|
|
4678
|
+
eventLoopHistogram = node_perf_hooks.monitorEventLoopDelay({ resolution: 10 });
|
|
4679
|
+
timer = null;
|
|
3656
4680
|
recordRequest(duration, isError) {
|
|
3657
4681
|
INTERVALS.forEach((int) => {
|
|
3658
4682
|
this.pendingDetails[int.label].push({ duration, isError });
|
|
@@ -3664,11 +4688,9 @@ class MetricsCollector {
|
|
|
3664
4688
|
async collect() {
|
|
3665
4689
|
try {
|
|
3666
4690
|
const now = Date.now();
|
|
3667
|
-
console.log("[MetricsCollector] collect() called at", new Date(now).toISOString());
|
|
3668
4691
|
for (const int of INTERVALS) {
|
|
3669
4692
|
const start = this.currentIntervalStart[int.label];
|
|
3670
4693
|
if (now >= start + int.ms) {
|
|
3671
|
-
console.log(`[MetricsCollector] Flushing ${int.label} interval (boundary crossed)`);
|
|
3672
4694
|
await this.flushInterval(int.label, start, int.ms);
|
|
3673
4695
|
this.currentIntervalStart[int.label] = this.alignTimestamp(now, int.ms);
|
|
3674
4696
|
}
|
|
@@ -3679,10 +4701,8 @@ class MetricsCollector {
|
|
|
3679
4701
|
}
|
|
3680
4702
|
async flushInterval(label, timestamp, durationMs) {
|
|
3681
4703
|
const reqs = this.pendingDetails[label];
|
|
3682
|
-
console.log(`[MetricsCollector] flushInterval(${label}) - ${reqs.length} requests pending`);
|
|
3683
4704
|
this.pendingDetails[label] = [];
|
|
3684
4705
|
if (reqs.length === 0) {
|
|
3685
|
-
console.log(`[MetricsCollector] No requests for ${label}, skipping persist`);
|
|
3686
4706
|
return;
|
|
3687
4707
|
}
|
|
3688
4708
|
const totalReqs = reqs.length;
|
|
@@ -3732,15 +4752,11 @@ class MetricsCollector {
|
|
|
3732
4752
|
p99: getP(0.99)
|
|
3733
4753
|
}
|
|
3734
4754
|
};
|
|
3735
|
-
console.log(`[MetricsCollector] Persisting ${label} metric at timestamp ${timestamp}`);
|
|
3736
4755
|
try {
|
|
3737
4756
|
const recordId = new surrealdb.RecordId("metrics", timestamp);
|
|
3738
|
-
await
|
|
3739
|
-
|
|
3740
|
-
const
|
|
3741
|
-
console.log(`[MetricsCollector] DEBUG: Immediate .get() returned:`, test ? "DATA" : "NULL");
|
|
3742
|
-
const queryTest = await datastore.query("SELECT * FROM metrics WHERE id = $id", { id: recordId });
|
|
3743
|
-
console.log(`[MetricsCollector] DEBUG: Query by id returned ${queryTest[0]?.length || 0} records`);
|
|
4757
|
+
await this.db.upsert(recordId, metric);
|
|
4758
|
+
const test = await this.db.select(recordId);
|
|
4759
|
+
const queryTest = await this.db.query("SELECT * FROM metrics WHERE id = $id", { id: recordId });
|
|
3744
4760
|
} catch (e) {
|
|
3745
4761
|
console.error(`[MetricsCollector] ✗ Failed to save metrics for ${label}:`, e);
|
|
3746
4762
|
}
|
|
@@ -3775,16 +4791,8 @@ class Dashboard {
|
|
|
3775
4791
|
constructor(dashboardConfig = {}) {
|
|
3776
4792
|
this.dashboardConfig = dashboardConfig;
|
|
3777
4793
|
}
|
|
3778
|
-
|
|
3779
|
-
|
|
3780
|
-
static getBasePath() {
|
|
3781
|
-
const dir = path$1.dirname(node_url.fileURLToPath(typeof document === "undefined" ? require("url").pathToFileURL(__filename).href : _documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === "SCRIPT" && _documentCurrentScript.src || new URL("index.cjs", document.baseURI).href));
|
|
3782
|
-
if (dir.endsWith("dist")) {
|
|
3783
|
-
return dir + "/plugins/application/dashboard";
|
|
3784
|
-
}
|
|
3785
|
-
return dir;
|
|
3786
|
-
}
|
|
3787
|
-
router = new ShokupanRouter();
|
|
4794
|
+
[$appRoot];
|
|
4795
|
+
router = new ShokupanRouter({ renderer: renderToString });
|
|
3788
4796
|
metrics = {
|
|
3789
4797
|
totalRequests: 0,
|
|
3790
4798
|
successfulRequests: 0,
|
|
@@ -3797,16 +4805,16 @@ class Dashboard {
|
|
|
3797
4805
|
nodeMetrics: {},
|
|
3798
4806
|
edgeMetrics: {}
|
|
3799
4807
|
};
|
|
3800
|
-
eta = new eta$2.Eta({
|
|
3801
|
-
views: Dashboard.getBasePath() + "/static",
|
|
3802
|
-
cache: false
|
|
3803
|
-
});
|
|
3804
4808
|
startTime = Date.now();
|
|
3805
4809
|
instrumented = false;
|
|
3806
|
-
metricsCollector
|
|
4810
|
+
metricsCollector;
|
|
4811
|
+
get db() {
|
|
4812
|
+
return this[$appRoot].db;
|
|
4813
|
+
}
|
|
3807
4814
|
// ShokupanPlugin interface implementation
|
|
3808
4815
|
onInit(app, options) {
|
|
3809
4816
|
this[$appRoot] = app;
|
|
4817
|
+
this.metricsCollector = new MetricsCollector(this.db);
|
|
3810
4818
|
const mountPath = options?.path || this.dashboardConfig.path || "/dashboard";
|
|
3811
4819
|
const hooks = this.getHooks();
|
|
3812
4820
|
if (!app.middleware) {
|
|
@@ -3826,6 +4834,47 @@ class Dashboard {
|
|
|
3826
4834
|
app.mount(mountPath, this.router);
|
|
3827
4835
|
this.setupRoutes();
|
|
3828
4836
|
}
|
|
4837
|
+
detectIntegrations() {
|
|
4838
|
+
const integrations = {};
|
|
4839
|
+
const routers = this[$appRoot]?.[$childRouters] || [];
|
|
4840
|
+
const checkConfig = (key) => {
|
|
4841
|
+
const conf = this.dashboardConfig.integrations?.[key];
|
|
4842
|
+
if (conf === false) return { enabled: false };
|
|
4843
|
+
if (typeof conf === "object" && conf.path) return { enabled: true, path: conf.path };
|
|
4844
|
+
return { enabled: true };
|
|
4845
|
+
};
|
|
4846
|
+
const scalarConf = checkConfig("scalar");
|
|
4847
|
+
if (scalarConf.enabled) {
|
|
4848
|
+
if (scalarConf.path) {
|
|
4849
|
+
integrations["scalar"] = scalarConf.path;
|
|
4850
|
+
} else {
|
|
4851
|
+
const plugin = routers.find((r) => r.constructor.name === "ScalarPlugin");
|
|
4852
|
+
if (plugin) {
|
|
4853
|
+
integrations["scalar"] = plugin[$mountPath];
|
|
4854
|
+
}
|
|
4855
|
+
}
|
|
4856
|
+
}
|
|
4857
|
+
const asyncApiConf = checkConfig("asyncapi");
|
|
4858
|
+
if (asyncApiConf.enabled) {
|
|
4859
|
+
if (asyncApiConf.path) {
|
|
4860
|
+
integrations["asyncapi"] = asyncApiConf.path;
|
|
4861
|
+
} else {
|
|
4862
|
+
const plugin = routers.find((r) => r.constructor.name === "AsyncApiPlugin");
|
|
4863
|
+
if (plugin) {
|
|
4864
|
+
integrations["asyncapi"] = plugin[$mountPath];
|
|
4865
|
+
}
|
|
4866
|
+
}
|
|
4867
|
+
}
|
|
4868
|
+
return integrations;
|
|
4869
|
+
}
|
|
4870
|
+
// Get base path for dashboard files - works in both dev (src/) and production (dist/)
|
|
4871
|
+
static getBasePath() {
|
|
4872
|
+
const dir = path$1.dirname(node_url.fileURLToPath(typeof document === "undefined" ? require("url").pathToFileURL(__filename).href : _documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === "SCRIPT" && _documentCurrentScript.src || new URL("index.cjs", document.baseURI).href));
|
|
4873
|
+
if (dir.endsWith("dist")) {
|
|
4874
|
+
return dir + "/plugins/application/dashboard";
|
|
4875
|
+
}
|
|
4876
|
+
return dir;
|
|
4877
|
+
}
|
|
3829
4878
|
setupRoutes() {
|
|
3830
4879
|
this.router.get("/metrics", async (ctx) => {
|
|
3831
4880
|
const uptimeSeconds = Math.floor((Date.now() - this.startTime) / 1e3);
|
|
@@ -3850,7 +4899,7 @@ class Dashboard {
|
|
|
3850
4899
|
const startTime = Date.now() - ms;
|
|
3851
4900
|
let stats;
|
|
3852
4901
|
try {
|
|
3853
|
-
stats = await
|
|
4902
|
+
stats = await this.db.query(`
|
|
3854
4903
|
SELECT
|
|
3855
4904
|
count() as total,
|
|
3856
4905
|
count(IF status < 400 THEN 1 END) as success,
|
|
@@ -3911,7 +4960,7 @@ class Dashboard {
|
|
|
3911
4960
|
const periodMs = intervalMap[interval] || 60 * 1e3;
|
|
3912
4961
|
const startTime = Date.now() - periodMs * 3;
|
|
3913
4962
|
const endTime = Date.now();
|
|
3914
|
-
const result = await
|
|
4963
|
+
const result = await this.db.query(
|
|
3915
4964
|
"SELECT * FROM metrics WHERE timestamp >= $start AND timestamp <= $end AND interval = $interval ORDER BY timestamp ASC",
|
|
3916
4965
|
{ start: startTime, end: endTime, interval }
|
|
3917
4966
|
);
|
|
@@ -3940,7 +4989,7 @@ class Dashboard {
|
|
|
3940
4989
|
};
|
|
3941
4990
|
this.router.get("/requests/top", async (ctx) => {
|
|
3942
4991
|
const startTime = getIntervalStartTime(ctx.query["interval"]);
|
|
3943
|
-
const result = await
|
|
4992
|
+
const result = await this.db.query(
|
|
3944
4993
|
"SELECT method, url, count() as count FROM requests WHERE timestamp >= $start GROUP BY method, url ORDER BY count DESC LIMIT 10",
|
|
3945
4994
|
{ start: startTime }
|
|
3946
4995
|
);
|
|
@@ -3948,7 +4997,7 @@ class Dashboard {
|
|
|
3948
4997
|
});
|
|
3949
4998
|
this.router.get("/errors/top", async (ctx) => {
|
|
3950
4999
|
const startTime = getIntervalStartTime(ctx.query["interval"]);
|
|
3951
|
-
const result = await
|
|
5000
|
+
const result = await this.db.query(
|
|
3952
5001
|
"SELECT status, count() as count FROM failed_requests WHERE timestamp >= $start GROUP BY status ORDER BY count DESC LIMIT 10",
|
|
3953
5002
|
{ start: startTime }
|
|
3954
5003
|
);
|
|
@@ -3956,7 +5005,7 @@ class Dashboard {
|
|
|
3956
5005
|
});
|
|
3957
5006
|
this.router.get("/requests/failing", async (ctx) => {
|
|
3958
5007
|
const startTime = getIntervalStartTime(ctx.query["interval"]);
|
|
3959
|
-
const result = await
|
|
5008
|
+
const result = await this.db.query(
|
|
3960
5009
|
"SELECT method, url, count() as count FROM failed_requests WHERE timestamp >= $start GROUP BY method, url ORDER BY count DESC LIMIT 10",
|
|
3961
5010
|
{ start: startTime }
|
|
3962
5011
|
);
|
|
@@ -3964,7 +5013,7 @@ class Dashboard {
|
|
|
3964
5013
|
});
|
|
3965
5014
|
this.router.get("/requests/slowest", async (ctx) => {
|
|
3966
5015
|
const startTime = getIntervalStartTime(ctx.query["interval"]);
|
|
3967
|
-
const result = await
|
|
5016
|
+
const result = await this.db.query(
|
|
3968
5017
|
"SELECT method, url, duration, status, timestamp FROM requests WHERE timestamp >= $start ORDER BY duration DESC LIMIT 10",
|
|
3969
5018
|
{ start: startTime }
|
|
3970
5019
|
);
|
|
@@ -3982,15 +5031,15 @@ class Dashboard {
|
|
|
3982
5031
|
return ctx.json({ registry: registry || {} });
|
|
3983
5032
|
});
|
|
3984
5033
|
this.router.get("/requests", async (ctx) => {
|
|
3985
|
-
const result = await
|
|
5034
|
+
const result = await this.db.query("SELECT * FROM requests ORDER BY timestamp DESC LIMIT 100");
|
|
3986
5035
|
return ctx.json({ requests: result[0] || [] });
|
|
3987
5036
|
});
|
|
3988
5037
|
this.router.get("/requests/:id", async (ctx) => {
|
|
3989
|
-
const result = await
|
|
5038
|
+
const result = await this.db.query("SELECT * FROM requests WHERE id = $id", { id: ctx.params["id"] });
|
|
3990
5039
|
return ctx.json({ request: result[0]?.[0] });
|
|
3991
5040
|
});
|
|
3992
5041
|
this.router.get("/failures", async (ctx) => {
|
|
3993
|
-
const result = await
|
|
5042
|
+
const result = await this.db.query("SELECT * FROM failed_requests ORDER BY timestamp DESC LIMIT 50");
|
|
3994
5043
|
return ctx.json({ failures: result[0] });
|
|
3995
5044
|
});
|
|
3996
5045
|
this.router.post("/replay", async (ctx) => {
|
|
@@ -4014,18 +5063,51 @@ class Dashboard {
|
|
|
4014
5063
|
return ctx.json({ error: String(e) }, 500);
|
|
4015
5064
|
}
|
|
4016
5065
|
});
|
|
4017
|
-
this.router.get("
|
|
5066
|
+
this.router.get("/**", async (ctx) => {
|
|
5067
|
+
const mountPath = this.router[$mountPath] || this.dashboardConfig.path || "/dashboard";
|
|
5068
|
+
let relativePath = ctx.path;
|
|
5069
|
+
if (relativePath.startsWith(mountPath)) {
|
|
5070
|
+
relativePath = relativePath.slice(mountPath.length);
|
|
5071
|
+
}
|
|
5072
|
+
if (relativePath.startsWith("/")) {
|
|
5073
|
+
relativePath = relativePath.slice(1);
|
|
5074
|
+
}
|
|
5075
|
+
const path2 = relativePath;
|
|
5076
|
+
const staticFiles = [
|
|
5077
|
+
"charts.js",
|
|
5078
|
+
"failures.js",
|
|
5079
|
+
"graph.mjs",
|
|
5080
|
+
"poll.js",
|
|
5081
|
+
"reactflow.css",
|
|
5082
|
+
"registry.css",
|
|
5083
|
+
"registry.js",
|
|
5084
|
+
"requests.js",
|
|
5085
|
+
"styles.css",
|
|
5086
|
+
"tables.js",
|
|
5087
|
+
"tabs.js",
|
|
5088
|
+
"tabulator.css",
|
|
5089
|
+
"theme.css"
|
|
5090
|
+
];
|
|
5091
|
+
if (staticFiles.includes(path2)) {
|
|
5092
|
+
const content = await promises.readFile(path$1.join(Dashboard.getBasePath(), "static", path2), "utf-8");
|
|
5093
|
+
if (path2.endsWith(".css")) ctx.set("Content-Type", "text/css");
|
|
5094
|
+
else if (path2.endsWith(".js") || path2.endsWith(".mjs")) ctx.set("Content-Type", "application/javascript");
|
|
5095
|
+
return ctx.send(content);
|
|
5096
|
+
}
|
|
4018
5097
|
const uptimeSeconds = Math.floor((Date.now() - this.startTime) / 1e3);
|
|
4019
5098
|
const uptime = `${Math.floor(uptimeSeconds / 3600)}h ${Math.floor(uptimeSeconds % 3600 / 60)}m ${uptimeSeconds % 60}s`;
|
|
4020
|
-
|
|
4021
|
-
const
|
|
4022
|
-
|
|
5099
|
+
this.getLinkPattern();
|
|
5100
|
+
const integrations = this.detectIntegrations();
|
|
5101
|
+
const getRequestHeadersSource = this.dashboardConfig.getRequestHeaders ? this.dashboardConfig.getRequestHeaders.toString() : "undefined";
|
|
5102
|
+
const html = renderToString(DashboardApp({
|
|
4023
5103
|
metrics: this.metrics,
|
|
4024
5104
|
uptime,
|
|
4025
5105
|
rootPath: process.cwd(),
|
|
4026
|
-
|
|
4027
|
-
|
|
5106
|
+
integrations,
|
|
5107
|
+
base: mountPath,
|
|
5108
|
+
getRequestHeadersSource
|
|
4028
5109
|
}));
|
|
5110
|
+
return ctx.html(`<!DOCTYPE html>${html}`);
|
|
4029
5111
|
});
|
|
4030
5112
|
}
|
|
4031
5113
|
instrumentApp(app) {
|
|
@@ -4121,7 +5203,7 @@ class Dashboard {
|
|
|
4121
5203
|
headers[k] = v;
|
|
4122
5204
|
});
|
|
4123
5205
|
}
|
|
4124
|
-
await
|
|
5206
|
+
await this.db.upsert(new surrealdb.RecordId("failed_requests", ctx.requestId), {
|
|
4125
5207
|
method: ctx.method,
|
|
4126
5208
|
url: ctx.url.toString(),
|
|
4127
5209
|
headers,
|
|
@@ -4146,7 +5228,7 @@ class Dashboard {
|
|
|
4146
5228
|
};
|
|
4147
5229
|
this.metrics.logs.push(logEntry);
|
|
4148
5230
|
try {
|
|
4149
|
-
await
|
|
5231
|
+
await this.db.upsert(new surrealdb.RecordId("requests", ctx.requestId), logEntry);
|
|
4150
5232
|
} catch (e) {
|
|
4151
5233
|
console.error("Failed to record request log", e);
|
|
4152
5234
|
}
|
|
@@ -4174,32 +5256,169 @@ class Dashboard {
|
|
|
4174
5256
|
function unknownError(ctx) {
|
|
4175
5257
|
return ctx.json({ error: "Unknown Error" }, 500);
|
|
4176
5258
|
}
|
|
4177
|
-
|
|
5259
|
+
class GraphQLApolloPlugin extends ShokupanRouter {
|
|
5260
|
+
// Use generic any or verify type
|
|
5261
|
+
constructor(pluginOptions) {
|
|
5262
|
+
super();
|
|
5263
|
+
this.pluginOptions = pluginOptions;
|
|
5264
|
+
this.pluginOptions.path ??= "/graphql";
|
|
5265
|
+
}
|
|
5266
|
+
apolloServer;
|
|
5267
|
+
async onInit(app, options) {
|
|
5268
|
+
const { ApolloServer, HeaderMap } = await import("@apollo/server");
|
|
5269
|
+
this.apolloServer = new ApolloServer({
|
|
5270
|
+
typeDefs: this.pluginOptions.typeDefs,
|
|
5271
|
+
resolvers: this.pluginOptions.resolvers,
|
|
5272
|
+
...this.pluginOptions.apolloConfig || {}
|
|
5273
|
+
});
|
|
5274
|
+
const path2 = options?.path || this.pluginOptions.path || "/graphql";
|
|
5275
|
+
app.mount(path2, this);
|
|
5276
|
+
app.onStart(async () => {
|
|
5277
|
+
await this.apolloServer.start();
|
|
5278
|
+
});
|
|
5279
|
+
this.post("/", async (ctx) => {
|
|
5280
|
+
const body = await ctx.body();
|
|
5281
|
+
const httpGraphQLResponse = await this.apolloServer.executeHTTPGraphQLRequest({
|
|
5282
|
+
httpGraphQLRequest: {
|
|
5283
|
+
body,
|
|
5284
|
+
method: ctx.req.method,
|
|
5285
|
+
search: ctx.url.search,
|
|
5286
|
+
headers: new HeaderMap(ctx.req.headers)
|
|
5287
|
+
},
|
|
5288
|
+
// Pass the Shokupan Context as the GraphQL Context
|
|
5289
|
+
context: async () => ({ ...ctx, shokupan: ctx })
|
|
5290
|
+
});
|
|
5291
|
+
for (const [key, value] of httpGraphQLResponse.headers) {
|
|
5292
|
+
ctx.set(key, value);
|
|
5293
|
+
}
|
|
5294
|
+
if (httpGraphQLResponse.body.kind === "complete") {
|
|
5295
|
+
return ctx.send(httpGraphQLResponse.body.string, {
|
|
5296
|
+
status: httpGraphQLResponse.status ?? 200
|
|
5297
|
+
});
|
|
5298
|
+
} else {
|
|
5299
|
+
let string = "";
|
|
5300
|
+
for await (const chunk of httpGraphQLResponse.body.asyncIterator) {
|
|
5301
|
+
string += chunk;
|
|
5302
|
+
}
|
|
5303
|
+
return ctx.send(string, {
|
|
5304
|
+
status: httpGraphQLResponse.status ?? 200
|
|
5305
|
+
});
|
|
5306
|
+
}
|
|
5307
|
+
});
|
|
5308
|
+
this.get("/", async (ctx) => {
|
|
5309
|
+
const httpGraphQLResponse = await this.apolloServer.executeHTTPGraphQLRequest({
|
|
5310
|
+
httpGraphQLRequest: {
|
|
5311
|
+
body: Object.keys(ctx.query).length > 0 ? ctx.query : void 0,
|
|
5312
|
+
method: ctx.req.method,
|
|
5313
|
+
search: ctx.url.search,
|
|
5314
|
+
headers: new HeaderMap(ctx.req.headers)
|
|
5315
|
+
},
|
|
5316
|
+
context: async () => ({ ...ctx, shokupan: ctx })
|
|
5317
|
+
});
|
|
5318
|
+
for (const [key, value] of httpGraphQLResponse.headers) {
|
|
5319
|
+
ctx.set(key, value);
|
|
5320
|
+
}
|
|
5321
|
+
if (httpGraphQLResponse.body.kind === "complete") {
|
|
5322
|
+
return ctx.html(httpGraphQLResponse.body.string, httpGraphQLResponse.status ?? 200);
|
|
5323
|
+
} else {
|
|
5324
|
+
let string = "";
|
|
5325
|
+
for await (const chunk of httpGraphQLResponse.body.asyncIterator) {
|
|
5326
|
+
string += chunk;
|
|
5327
|
+
}
|
|
5328
|
+
return ctx.html(string, httpGraphQLResponse.status ?? 200);
|
|
5329
|
+
}
|
|
5330
|
+
});
|
|
5331
|
+
}
|
|
5332
|
+
}
|
|
4178
5333
|
class ScalarPlugin extends ShokupanRouter {
|
|
4179
5334
|
constructor(pluginOptions = {}) {
|
|
4180
5335
|
pluginOptions.config ??= {};
|
|
4181
5336
|
super();
|
|
4182
5337
|
this.pluginOptions = pluginOptions;
|
|
4183
|
-
this.
|
|
5338
|
+
this.initRoutes();
|
|
5339
|
+
}
|
|
5340
|
+
eta;
|
|
5341
|
+
async onInit(app, options) {
|
|
5342
|
+
const { Eta } = await import("eta");
|
|
5343
|
+
this.eta = new Eta();
|
|
5344
|
+
const path2 = options?.path || this.pluginOptions.path || "/reference";
|
|
5345
|
+
app.mount(path2, this);
|
|
5346
|
+
this.onMount(app);
|
|
4184
5347
|
}
|
|
4185
|
-
|
|
4186
|
-
if (
|
|
4187
|
-
|
|
4188
|
-
|
|
4189
|
-
app.mount(options.path ?? "/", this);
|
|
5348
|
+
async ensureEta() {
|
|
5349
|
+
if (!this.eta) {
|
|
5350
|
+
const { Eta } = await import("eta");
|
|
5351
|
+
this.eta = new Eta();
|
|
4190
5352
|
}
|
|
4191
|
-
this.onMount(app);
|
|
4192
5353
|
}
|
|
4193
|
-
|
|
4194
|
-
|
|
5354
|
+
initRoutes() {
|
|
5355
|
+
const bootId = Date.now().toString();
|
|
5356
|
+
this.get("/_lifecycle", (ctx) => ctx.json({ boot: bootId }));
|
|
5357
|
+
this.get("/", async (ctx) => {
|
|
5358
|
+
await this.ensureEta();
|
|
4195
5359
|
let path2 = ctx.url.toString();
|
|
4196
5360
|
if (!path2.endsWith("/")) path2 += "/";
|
|
4197
|
-
|
|
4198
|
-
<
|
|
5361
|
+
const devScript = ctx.app?.applicationConfig.development ? `
|
|
5362
|
+
<script>
|
|
5363
|
+
(function() {
|
|
5364
|
+
const bootId = "${bootId}";
|
|
5365
|
+
let isDown = false;
|
|
5366
|
+
|
|
5367
|
+
setInterval(async () => {
|
|
5368
|
+
try {
|
|
5369
|
+
const res = await fetch('${path2}_lifecycle');
|
|
5370
|
+
if (!res.ok) throw new Error('Down');
|
|
5371
|
+
const data = await res.json();
|
|
5372
|
+
if (data.boot !== bootId) {
|
|
5373
|
+
console.log('Server restarted, reloading...');
|
|
5374
|
+
window.location.reload();
|
|
5375
|
+
}
|
|
5376
|
+
else if (isDown) {
|
|
5377
|
+
isDown = false;
|
|
5378
|
+
}
|
|
5379
|
+
} catch (e) {
|
|
5380
|
+
isDown = true;
|
|
5381
|
+
console.log('Connection lost...');
|
|
5382
|
+
}
|
|
5383
|
+
}, 2000);
|
|
5384
|
+
})();
|
|
5385
|
+
<\/script>
|
|
5386
|
+
` : "";
|
|
5387
|
+
let themeCss = "";
|
|
5388
|
+
try {
|
|
5389
|
+
try {
|
|
5390
|
+
themeCss = fs.readFileSync(path$1.join(process.cwd(), "src/theme.css"), "utf-8");
|
|
5391
|
+
} catch {
|
|
5392
|
+
}
|
|
5393
|
+
} catch (e) {
|
|
5394
|
+
}
|
|
5395
|
+
if (!this.eta) throw new Error("Eta not initialized");
|
|
5396
|
+
return ctx.html(this.eta.renderString(`<!doctype html>
|
|
5397
|
+
<html lang="en">
|
|
4199
5398
|
<head>
|
|
4200
5399
|
<title>API Reference</title>
|
|
4201
5400
|
<meta charset = "utf-8" />
|
|
4202
5401
|
<meta name="viewport" content = "width=device-width, initial-scale=1" />
|
|
5402
|
+
<style>
|
|
5403
|
+
${themeCss}
|
|
5404
|
+
|
|
5405
|
+
:root {
|
|
5406
|
+
--scalar-color-1: var(--primary);
|
|
5407
|
+
--scalar-color-2: var(--secondary);
|
|
5408
|
+
--scalar-color-3: var(--accent);
|
|
5409
|
+
--scalar-color-accent: var(--accent);
|
|
5410
|
+
|
|
5411
|
+
--scalar-background-1: var(--bg-primary);
|
|
5412
|
+
--scalar-background-2: var(--bg-secondary);
|
|
5413
|
+
--scalar-background-3: var(--bg-card);
|
|
5414
|
+
|
|
5415
|
+
--scalar-text-1: var(--text-primary);
|
|
5416
|
+
--scalar-text-2: var(--text-secondary);
|
|
5417
|
+
--scalar-text-3: var(--text-muted);
|
|
5418
|
+
|
|
5419
|
+
--scalar-border-color: var(--border-color);
|
|
5420
|
+
}
|
|
5421
|
+
</style>
|
|
4203
5422
|
</head>
|
|
4204
5423
|
|
|
4205
5424
|
<body>
|
|
@@ -4211,9 +5430,10 @@ class ScalarPlugin extends ShokupanRouter {
|
|
|
4211
5430
|
}
|
|
4212
5431
|
])
|
|
4213
5432
|
<\/script>
|
|
5433
|
+
<%~ it.devScript %>
|
|
4214
5434
|
</body>
|
|
4215
5435
|
|
|
4216
|
-
</html>`, { path: path2, config: this.pluginOptions }));
|
|
5436
|
+
</html>`, { path: path2, config: this.pluginOptions, devScript }));
|
|
4217
5437
|
});
|
|
4218
5438
|
this.get("/openapi.json", async (ctx) => {
|
|
4219
5439
|
let spec;
|
|
@@ -4416,7 +5636,7 @@ function Cors(options = {}) {
|
|
|
4416
5636
|
}
|
|
4417
5637
|
const response = await next();
|
|
4418
5638
|
if (response instanceof Response) {
|
|
4419
|
-
const headerEntries =
|
|
5639
|
+
const headerEntries = Object.entries(headers);
|
|
4420
5640
|
for (let i = 0; i < headerEntries.length; i++) {
|
|
4421
5641
|
const [key, value] = headerEntries[i];
|
|
4422
5642
|
response.headers.set(key, value);
|
|
@@ -5187,6 +6407,7 @@ exports.$socket = $socket;
|
|
|
5187
6407
|
exports.$url = $url;
|
|
5188
6408
|
exports.$ws = $ws;
|
|
5189
6409
|
exports.All = All;
|
|
6410
|
+
exports.AsyncApiPlugin = AsyncApiPlugin;
|
|
5190
6411
|
exports.AuthPlugin = AuthPlugin;
|
|
5191
6412
|
exports.Body = Body;
|
|
5192
6413
|
exports.ClusterPlugin = ClusterPlugin;
|
|
@@ -5199,6 +6420,7 @@ exports.Dashboard = Dashboard;
|
|
|
5199
6420
|
exports.Delete = Delete;
|
|
5200
6421
|
exports.Event = Event;
|
|
5201
6422
|
exports.Get = Get;
|
|
6423
|
+
exports.GraphQLApolloPlugin = GraphQLApolloPlugin;
|
|
5202
6424
|
exports.HTTPMethods = HTTPMethods;
|
|
5203
6425
|
exports.Head = Head;
|
|
5204
6426
|
exports.Headers = Headers$1;
|
|
@@ -5215,13 +6437,16 @@ exports.RateLimit = RateLimit;
|
|
|
5215
6437
|
exports.RateLimitMiddleware = RateLimitMiddleware;
|
|
5216
6438
|
exports.Req = Req;
|
|
5217
6439
|
exports.RouteParamType = RouteParamType;
|
|
6440
|
+
exports.RouterRegistry = RouterRegistry;
|
|
5218
6441
|
exports.ScalarPlugin = ScalarPlugin;
|
|
5219
6442
|
exports.SecurityHeaders = SecurityHeaders;
|
|
5220
6443
|
exports.Session = Session;
|
|
5221
6444
|
exports.Shokupan = Shokupan;
|
|
6445
|
+
exports.ShokupanApplicationTree = ShokupanApplicationTree;
|
|
5222
6446
|
exports.ShokupanContext = ShokupanContext;
|
|
5223
6447
|
exports.ShokupanRequest = ShokupanRequest;
|
|
5224
6448
|
exports.ShokupanResponse = ShokupanResponse;
|
|
6449
|
+
exports.ShokupanRouter = ShokupanRouter;
|
|
5225
6450
|
exports.Spec = Spec;
|
|
5226
6451
|
exports.Use = Use;
|
|
5227
6452
|
exports.ValidationError = ValidationError;
|