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.js
CHANGED
|
@@ -1,21 +1,24 @@
|
|
|
1
1
|
import { nanoid } from "nanoid";
|
|
2
2
|
import { readFile } from "node:fs/promises";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import { Surreal, RecordId } from "surrealdb";
|
|
3
|
+
import { inspect } from "node:util";
|
|
4
|
+
import { RecordId, Surreal } from "surrealdb";
|
|
6
5
|
import { Eta } from "eta";
|
|
7
6
|
import { stat, readdir, readFile as readFile$1 } from "fs/promises";
|
|
8
7
|
import { resolve, join, sep, basename } from "path";
|
|
8
|
+
import { trace, SpanKind, SpanStatusCode, context } from "@opentelemetry/api";
|
|
9
|
+
import { dump } from "js-yaml";
|
|
10
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
9
11
|
import * as os from "node:os";
|
|
10
12
|
import os__default from "node:os";
|
|
11
|
-
import {
|
|
12
|
-
import
|
|
13
|
+
import { dirname, join as join$1 } from "node:path";
|
|
14
|
+
import { fileURLToPath } from "node:url";
|
|
15
|
+
import renderToString from "preact-render-to-string";
|
|
16
|
+
import { jsxs, jsx, Fragment } from "preact/jsx-runtime";
|
|
13
17
|
import cluster from "node:cluster";
|
|
14
18
|
import net from "node:net";
|
|
15
|
-
import { dirname } from "node:path";
|
|
16
|
-
import { fileURLToPath } from "node:url";
|
|
17
19
|
import { monitorEventLoopDelay } from "node:perf_hooks";
|
|
18
|
-
import {
|
|
20
|
+
import { readFileSync } from "node:fs";
|
|
21
|
+
import { OpenAPIAnalyzer } from "./analyzer-BqIe1p0R.js";
|
|
19
22
|
import * as zlib from "node:zlib";
|
|
20
23
|
import Ajv from "ajv";
|
|
21
24
|
import addFormats from "ajv-formats";
|
|
@@ -268,6 +271,21 @@ class ShokupanContext {
|
|
|
268
271
|
[$cachedHost];
|
|
269
272
|
[$cachedOrigin];
|
|
270
273
|
[$cachedQuery];
|
|
274
|
+
disconnectCallbacks = [];
|
|
275
|
+
/**
|
|
276
|
+
* Registers a callback to be executed when the associated WebSocket disconnects.
|
|
277
|
+
* This is only applicable for requests that are part of a WebSocket interaction or upgrade.
|
|
278
|
+
*/
|
|
279
|
+
onSocketDisconnect(callback) {
|
|
280
|
+
this.disconnectCallbacks.push(callback);
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* @internal
|
|
284
|
+
* Retrieves registered disconnect callbacks for execution.
|
|
285
|
+
*/
|
|
286
|
+
getDisconnectCallbacks() {
|
|
287
|
+
return this.disconnectCallbacks;
|
|
288
|
+
}
|
|
271
289
|
[$ws];
|
|
272
290
|
[$socket];
|
|
273
291
|
[$io];
|
|
@@ -282,6 +300,20 @@ class ShokupanContext {
|
|
|
282
300
|
get requestId() {
|
|
283
301
|
return this[$requestId] ??= this.app?.applicationConfig?.idGenerator?.() ?? nanoid();
|
|
284
302
|
}
|
|
303
|
+
[/* @__PURE__ */ Symbol.for("nodejs.util.inspect.custom")]() {
|
|
304
|
+
const innerString = inspect({
|
|
305
|
+
method: this.request.method,
|
|
306
|
+
url: this.request.url,
|
|
307
|
+
requestHeaders: new Map(this.request.headers),
|
|
308
|
+
sessionId: this.sessionID,
|
|
309
|
+
state: this.state,
|
|
310
|
+
params: this.params,
|
|
311
|
+
response: this[$finalResponse]?.body,
|
|
312
|
+
responseHeaders: new Map(this[$finalResponse]?.headers),
|
|
313
|
+
handlerStack: this.handlerStack.map((h) => h.name === "anonymous" ? h.file + ":" + h.line : h.name)
|
|
314
|
+
}, { depth: null, colors: true, numericSeparator: true, customInspect: true });
|
|
315
|
+
return "Context(" + this.requestId + ") {" + innerString.slice(1, -2) + ",\n ...others\n}";
|
|
316
|
+
}
|
|
285
317
|
get url() {
|
|
286
318
|
if (!this[$url]) {
|
|
287
319
|
const urlString = this.request.url || "http://localhost/";
|
|
@@ -333,10 +365,8 @@ class ShokupanContext {
|
|
|
333
365
|
if (this[$cachedQuery]) return this[$cachedQuery];
|
|
334
366
|
const q = /* @__PURE__ */ Object.create(null);
|
|
335
367
|
const blocklist = ["__proto__", "constructor", "prototype"];
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
const [key, value] = entries[i];
|
|
339
|
-
if (blocklist.includes(key)) continue;
|
|
368
|
+
this.url.searchParams.forEach((value, key) => {
|
|
369
|
+
if (blocklist.includes(key)) return;
|
|
340
370
|
if (Object.prototype.hasOwnProperty.call(q, key)) {
|
|
341
371
|
if (Array.isArray(q[key])) {
|
|
342
372
|
q[key].push(value);
|
|
@@ -346,7 +376,7 @@ class ShokupanContext {
|
|
|
346
376
|
} else {
|
|
347
377
|
q[key] = value;
|
|
348
378
|
}
|
|
349
|
-
}
|
|
379
|
+
});
|
|
350
380
|
this[$cachedQuery] = q;
|
|
351
381
|
return q;
|
|
352
382
|
}
|
|
@@ -639,12 +669,12 @@ class ShokupanContext {
|
|
|
639
669
|
/**
|
|
640
670
|
* Respond with a JSON object
|
|
641
671
|
*/
|
|
642
|
-
json(data, status, headers) {
|
|
672
|
+
async json(data, status, headers) {
|
|
643
673
|
const finalStatus = status ?? this.response.status ?? 200;
|
|
644
674
|
if (!VALID_HTTP_STATUSES.has(finalStatus)) {
|
|
645
675
|
throw new Error(`Invalid HTTP status code: ${finalStatus}`);
|
|
646
676
|
}
|
|
647
|
-
const jsonString = JSON.stringify(data);
|
|
677
|
+
const jsonString = JSON.stringify(data instanceof Promise ? await data : data);
|
|
648
678
|
this[$rawBody] = jsonString;
|
|
649
679
|
if (!headers && !this.response.hasPopulatedHeaders) {
|
|
650
680
|
this[$finalResponse] = new Response(jsonString, {
|
|
@@ -661,14 +691,14 @@ class ShokupanContext {
|
|
|
661
691
|
/**
|
|
662
692
|
* Respond with a text string
|
|
663
693
|
*/
|
|
664
|
-
text(data, status, headers) {
|
|
694
|
+
async text(data, status, headers) {
|
|
665
695
|
const finalStatus = status ?? this.response.status ?? 200;
|
|
666
696
|
if (this.app.applicationConfig.validateStatusCodes && !VALID_HTTP_STATUSES.has(finalStatus)) {
|
|
667
697
|
throw new Error(`Invalid HTTP status code: ${finalStatus}`);
|
|
668
698
|
}
|
|
669
|
-
this[$rawBody] = data;
|
|
699
|
+
this[$rawBody] = data instanceof Promise ? await data : data;
|
|
670
700
|
if (!headers && !this.response.hasPopulatedHeaders) {
|
|
671
|
-
this[$finalResponse] = new Response(
|
|
701
|
+
this[$finalResponse] = new Response(this[$rawBody], {
|
|
672
702
|
status: finalStatus,
|
|
673
703
|
headers: { "content-type": "text/plain; charset=utf-8" }
|
|
674
704
|
});
|
|
@@ -676,71 +706,73 @@ class ShokupanContext {
|
|
|
676
706
|
}
|
|
677
707
|
const finalHeaders = this.mergeHeaders(headers);
|
|
678
708
|
finalHeaders.set("content-type", "text/plain; charset=utf-8");
|
|
679
|
-
this[$finalResponse] = new Response(
|
|
709
|
+
this[$finalResponse] = new Response(this[$rawBody], { status: finalStatus, headers: finalHeaders });
|
|
680
710
|
return this[$finalResponse];
|
|
681
711
|
}
|
|
682
712
|
/**
|
|
683
713
|
* Respond with HTML content
|
|
684
714
|
*/
|
|
685
|
-
html(html, status, headers) {
|
|
715
|
+
async html(html, status, headers) {
|
|
686
716
|
const finalStatus = status ?? this.response.status ?? 200;
|
|
687
717
|
if (this.app.applicationConfig.validateStatusCodes && !VALID_HTTP_STATUSES.has(finalStatus)) {
|
|
688
718
|
throw new Error(`Invalid HTTP status code: ${finalStatus}`);
|
|
689
719
|
}
|
|
690
720
|
const finalHeaders = this.mergeHeaders(headers);
|
|
691
721
|
finalHeaders.set("content-type", "text/html; charset=utf-8");
|
|
692
|
-
this[$rawBody] = html;
|
|
693
|
-
this[$finalResponse] = new Response(
|
|
722
|
+
this[$rawBody] = html instanceof Promise ? await html : html;
|
|
723
|
+
this[$finalResponse] = new Response(this[$rawBody], { status: finalStatus, headers: finalHeaders });
|
|
694
724
|
return this[$finalResponse];
|
|
695
725
|
}
|
|
696
726
|
/**
|
|
697
727
|
* Respond with a redirect
|
|
698
728
|
*/
|
|
699
|
-
redirect(url, status = 302) {
|
|
729
|
+
async redirect(url, status = 302) {
|
|
700
730
|
if (this.app.applicationConfig.validateStatusCodes && !VALID_REDIRECT_STATUSES.has(status)) {
|
|
701
731
|
throw new Error(`Invalid redirect status code: ${status}`);
|
|
702
732
|
}
|
|
703
|
-
const
|
|
704
|
-
|
|
705
|
-
this[$finalResponse] = new Response(null, { status, headers });
|
|
733
|
+
const finalHeaders = this.mergeHeaders();
|
|
734
|
+
finalHeaders.set("Location", url instanceof Promise ? await url : url);
|
|
735
|
+
this[$finalResponse] = new Response(null, { status, headers: finalHeaders });
|
|
706
736
|
return this[$finalResponse];
|
|
707
737
|
}
|
|
708
738
|
/**
|
|
709
739
|
* Respond with a status code
|
|
710
740
|
* DOES NOT CHAIN!
|
|
711
741
|
*/
|
|
712
|
-
status(
|
|
742
|
+
async status(statusCode) {
|
|
743
|
+
const status = statusCode instanceof Promise ? await statusCode : statusCode;
|
|
713
744
|
if (this.app.applicationConfig.validateStatusCodes && !VALID_HTTP_STATUSES.has(status)) {
|
|
714
745
|
throw new Error(`Invalid HTTP status code: ${status}`);
|
|
715
746
|
}
|
|
716
|
-
const
|
|
717
|
-
this[$finalResponse] = new Response(null, { status, headers });
|
|
747
|
+
const finalHeaders = this.mergeHeaders();
|
|
748
|
+
this[$finalResponse] = new Response(null, { status, headers: finalHeaders });
|
|
718
749
|
return this[$finalResponse];
|
|
719
750
|
}
|
|
720
751
|
/**
|
|
721
752
|
* Respond with a file
|
|
722
753
|
*/
|
|
723
754
|
async file(path, fileOptions, responseOptions) {
|
|
724
|
-
const
|
|
755
|
+
const finalHeaders = this.mergeHeaders(responseOptions?.headers);
|
|
725
756
|
const status = responseOptions?.status ?? this.response.status;
|
|
726
757
|
if (this.app.applicationConfig.validateStatusCodes && !VALID_HTTP_STATUSES.has(status)) {
|
|
727
758
|
throw new Error(`Invalid HTTP status code: ${status}`);
|
|
728
759
|
}
|
|
729
760
|
if (typeof Bun !== "undefined") {
|
|
730
|
-
this[$finalResponse] = new Response(Bun.file(path, fileOptions), { status, headers });
|
|
761
|
+
this[$finalResponse] = new Response(Bun.file(path, fileOptions), { status, headers: finalHeaders });
|
|
731
762
|
return this[$finalResponse];
|
|
732
763
|
} else {
|
|
733
764
|
const fileBuffer = await readFile(path);
|
|
734
765
|
if (fileOptions?.type) {
|
|
735
|
-
|
|
766
|
+
finalHeaders.set("content-type", fileOptions.type);
|
|
736
767
|
}
|
|
737
|
-
this[$finalResponse] = new Response(fileBuffer, { status, headers });
|
|
768
|
+
this[$finalResponse] = new Response(fileBuffer, { status, headers: finalHeaders });
|
|
738
769
|
return this[$finalResponse];
|
|
739
770
|
}
|
|
740
771
|
}
|
|
741
772
|
/**
|
|
742
773
|
* Render a JSX element
|
|
743
774
|
* @param element JSX Element
|
|
775
|
+
* @param args JSX Element Args/Props
|
|
744
776
|
* @param status HTTP Status
|
|
745
777
|
* @param headers HTTP Headers
|
|
746
778
|
*/
|
|
@@ -794,29 +826,6 @@ const compose = (middleware) => {
|
|
|
794
826
|
return runner(0);
|
|
795
827
|
};
|
|
796
828
|
};
|
|
797
|
-
const tracer = trace.getTracer("shokupan.middleware");
|
|
798
|
-
function traceHandler(fn, name) {
|
|
799
|
-
return async function(...args) {
|
|
800
|
-
return tracer.startActiveSpan(`route handler - ${name}`, {
|
|
801
|
-
kind: SpanKind.INTERNAL,
|
|
802
|
-
attributes: {
|
|
803
|
-
"http.route": name,
|
|
804
|
-
"component": "shokupan.route"
|
|
805
|
-
}
|
|
806
|
-
}, async (span) => {
|
|
807
|
-
try {
|
|
808
|
-
const result = await fn.apply(this, args);
|
|
809
|
-
return result;
|
|
810
|
-
} catch (err) {
|
|
811
|
-
span.recordException(err);
|
|
812
|
-
span.setStatus({ code: SpanStatusCode.ERROR, message: err.message });
|
|
813
|
-
throw err;
|
|
814
|
-
} finally {
|
|
815
|
-
span.end();
|
|
816
|
-
}
|
|
817
|
-
});
|
|
818
|
-
};
|
|
819
|
-
}
|
|
820
829
|
function isObject(item) {
|
|
821
830
|
return item && typeof item === "object" && !Array.isArray(item);
|
|
822
831
|
}
|
|
@@ -989,24 +998,34 @@ function analyzeHandler(handler) {
|
|
|
989
998
|
}
|
|
990
999
|
return { inferredSpec };
|
|
991
1000
|
}
|
|
992
|
-
async function getAstRoutes(applications) {
|
|
1001
|
+
async function getAstRoutes$1(applications) {
|
|
993
1002
|
const astRoutes = [];
|
|
994
|
-
const getExpandedRoutes = (app, prefix = "", seen = /* @__PURE__ */ new Set()) => {
|
|
1003
|
+
const getExpandedRoutes = (app, prefix = "", seen = /* @__PURE__ */ new Set(), sourceOverride) => {
|
|
995
1004
|
if (seen.has(app.name)) return [];
|
|
996
1005
|
const newSeen = new Set(seen);
|
|
997
1006
|
newSeen.add(app.name);
|
|
998
1007
|
const expanded = [];
|
|
1008
|
+
let currentPrefix = prefix;
|
|
1009
|
+
if (app.controllerPrefix) {
|
|
1010
|
+
const cleanPrefix = currentPrefix.endsWith("/") ? currentPrefix.slice(0, -1) : currentPrefix;
|
|
1011
|
+
const cleanCont = app.controllerPrefix.startsWith("/") ? app.controllerPrefix : "/" + app.controllerPrefix;
|
|
1012
|
+
currentPrefix = cleanPrefix + cleanCont;
|
|
1013
|
+
}
|
|
999
1014
|
for (const route of app.routes) {
|
|
1000
|
-
const cleanPrefix =
|
|
1015
|
+
const cleanPrefix = currentPrefix.endsWith("/") ? currentPrefix.slice(0, -1) : currentPrefix;
|
|
1001
1016
|
const cleanPath = route.path.startsWith("/") ? route.path : "/" + route.path;
|
|
1002
1017
|
let joined = cleanPrefix + cleanPath;
|
|
1003
1018
|
if (joined.length > 1 && joined.endsWith("/")) {
|
|
1004
1019
|
joined = joined.slice(0, -1);
|
|
1005
1020
|
}
|
|
1006
|
-
|
|
1021
|
+
const expandedRoute = {
|
|
1007
1022
|
...route,
|
|
1008
1023
|
path: joined || "/"
|
|
1009
|
-
}
|
|
1024
|
+
};
|
|
1025
|
+
if (sourceOverride) {
|
|
1026
|
+
expandedRoute.sourceContext = sourceOverride;
|
|
1027
|
+
}
|
|
1028
|
+
expanded.push(expandedRoute);
|
|
1010
1029
|
}
|
|
1011
1030
|
if (app.mounted) {
|
|
1012
1031
|
for (const mount of app.mounted) {
|
|
@@ -1014,7 +1033,23 @@ async function getAstRoutes(applications) {
|
|
|
1014
1033
|
if (targetApp) {
|
|
1015
1034
|
const cleanPrefix = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
|
|
1016
1035
|
const mountPrefix = mount.prefix.startsWith("/") ? mount.prefix : "/" + mount.prefix;
|
|
1017
|
-
|
|
1036
|
+
let nextSourceOverride = sourceOverride;
|
|
1037
|
+
if (mount.dependency || mount.targetFilePath && mount.targetFilePath.includes("node_modules")) {
|
|
1038
|
+
if (mount.sourceContext) {
|
|
1039
|
+
nextSourceOverride = {
|
|
1040
|
+
...mount.sourceContext,
|
|
1041
|
+
// Add highlight for the mount line to make it clear
|
|
1042
|
+
highlightLines: [mount.sourceContext.startLine, mount.sourceContext.endLine],
|
|
1043
|
+
highlights: [{
|
|
1044
|
+
startLine: mount.sourceContext.startLine,
|
|
1045
|
+
endLine: mount.sourceContext.endLine,
|
|
1046
|
+
type: "return-success"
|
|
1047
|
+
// Use the success color (cyan) for the mount point
|
|
1048
|
+
}]
|
|
1049
|
+
};
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
expanded.push(...getExpandedRoutes(targetApp, cleanPrefix + mountPrefix, newSeen, nextSourceOverride));
|
|
1018
1053
|
}
|
|
1019
1054
|
}
|
|
1020
1055
|
}
|
|
@@ -1042,13 +1077,13 @@ async function generateOpenApi(rootRouter, options = {}) {
|
|
|
1042
1077
|
const defaultTagName = options.defaultTag || "Application";
|
|
1043
1078
|
let astRoutes = [];
|
|
1044
1079
|
try {
|
|
1045
|
-
const { OpenAPIAnalyzer: OpenAPIAnalyzer2 } = await import("./analyzer-
|
|
1080
|
+
const { OpenAPIAnalyzer: OpenAPIAnalyzer2 } = await import("./analyzer-BqIe1p0R.js");
|
|
1046
1081
|
const analyzer = new OpenAPIAnalyzer2(process.cwd());
|
|
1047
1082
|
const { applications } = await analyzer.analyze();
|
|
1048
|
-
astRoutes = await getAstRoutes(applications);
|
|
1083
|
+
astRoutes = await getAstRoutes$1(applications);
|
|
1049
1084
|
} catch (e) {
|
|
1050
1085
|
}
|
|
1051
|
-
const collect = (router, prefix = "", currentGroup = defaultTagGroup, defaultTag = defaultTagName) => {
|
|
1086
|
+
const collect = (router, prefix = "", currentGroup = defaultTagGroup, defaultTag = defaultTagName, inheritedMiddleware = []) => {
|
|
1052
1087
|
let group = currentGroup;
|
|
1053
1088
|
let tag = defaultTag;
|
|
1054
1089
|
if (router.config?.group) group = router.config.group;
|
|
@@ -1065,21 +1100,33 @@ async function generateOpenApi(rootRouter, options = {}) {
|
|
|
1065
1100
|
}
|
|
1066
1101
|
}
|
|
1067
1102
|
if (!tagGroups.has(group)) tagGroups.set(group, /* @__PURE__ */ new Set());
|
|
1103
|
+
const routerMiddleware = router.middleware || [];
|
|
1068
1104
|
const routes = router[$routes] || [];
|
|
1069
1105
|
for (const route of routes) {
|
|
1106
|
+
if (!["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS", "HEAD"].includes(route.method.toUpperCase())) {
|
|
1107
|
+
continue;
|
|
1108
|
+
}
|
|
1070
1109
|
const routeGroup = route.group || group;
|
|
1071
1110
|
const cleanPrefix = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
|
|
1072
1111
|
const cleanSubPath = route.path.startsWith("/") ? route.path : "/" + route.path;
|
|
1073
1112
|
let fullPath = cleanPrefix + cleanSubPath || "/";
|
|
1074
|
-
fullPath = fullPath.replace(/:([a-zA-Z0-9_]+)/g, "{$1}");
|
|
1075
1113
|
if (fullPath.length > 1 && fullPath.endsWith("/")) {
|
|
1076
1114
|
fullPath = fullPath.slice(0, -1);
|
|
1077
1115
|
}
|
|
1116
|
+
fullPath = fullPath.replace(/:([a-zA-Z0-9_]+)/g, "{$1}");
|
|
1078
1117
|
if (!paths[fullPath]) paths[fullPath] = {};
|
|
1079
1118
|
const operation = {
|
|
1080
1119
|
responses: { "200": { description: "Successful response" } },
|
|
1081
1120
|
tags: [tag]
|
|
1082
1121
|
};
|
|
1122
|
+
const routeMiddleware = route.middleware || [];
|
|
1123
|
+
const allMiddleware = [...inheritedMiddleware, ...routerMiddleware, ...routeMiddleware];
|
|
1124
|
+
if (allMiddleware.length > 0) {
|
|
1125
|
+
operation["x-shokupan-middleware"] = allMiddleware.map((mw) => ({
|
|
1126
|
+
name: mw.name || "middleware",
|
|
1127
|
+
metadata: mw.metadata
|
|
1128
|
+
}));
|
|
1129
|
+
}
|
|
1083
1130
|
if (route.guards) {
|
|
1084
1131
|
for (const guard of route.guards) {
|
|
1085
1132
|
if (guard.spec) {
|
|
@@ -1117,6 +1164,23 @@ async function generateOpenApi(rootRouter, options = {}) {
|
|
|
1117
1164
|
if (astMatch.description) operation.description = astMatch.description;
|
|
1118
1165
|
if (astMatch.tags) operation.tags = astMatch.tags;
|
|
1119
1166
|
if (astMatch.operationId) operation.operationId = astMatch.operationId;
|
|
1167
|
+
if (astMatch.sourceContext) {
|
|
1168
|
+
const sc = astMatch.sourceContext;
|
|
1169
|
+
operation["x-source-info"] = {
|
|
1170
|
+
file: sc.file,
|
|
1171
|
+
line: sc.startLine,
|
|
1172
|
+
snippet: sc.snippet || astMatch.handlerSource,
|
|
1173
|
+
// Fallback
|
|
1174
|
+
offset: sc.snippetStartLine || sc.startLine,
|
|
1175
|
+
highlightLines: [sc.startLine, sc.endLine],
|
|
1176
|
+
highlights: sc.highlights
|
|
1177
|
+
};
|
|
1178
|
+
operation["x-shokupan-source"] = {
|
|
1179
|
+
file: sc.file,
|
|
1180
|
+
line: sc.startLine,
|
|
1181
|
+
code: sc.snippet || astMatch.handlerSource || ""
|
|
1182
|
+
};
|
|
1183
|
+
}
|
|
1120
1184
|
if (astMatch.requestTypes?.body) {
|
|
1121
1185
|
operation.requestBody = {
|
|
1122
1186
|
content: { "application/json": { schema: astMatch.requestTypes.body } }
|
|
@@ -1128,10 +1192,12 @@ async function generateOpenApi(rootRouter, options = {}) {
|
|
|
1128
1192
|
content: { "application/json": { schema: astMatch.responseSchema } }
|
|
1129
1193
|
};
|
|
1130
1194
|
} else if (astMatch.responseType) {
|
|
1131
|
-
|
|
1195
|
+
let contentType = "application/json";
|
|
1196
|
+
if (astMatch.responseType === "string") contentType = "text/plain";
|
|
1197
|
+
else if (astMatch.responseType === "html") contentType = "text/html";
|
|
1132
1198
|
operation.responses["200"] = {
|
|
1133
1199
|
description: "Successful response",
|
|
1134
|
-
content: { [contentType]: { schema: { type:
|
|
1200
|
+
content: { [contentType]: { schema: { type: "string" } } }
|
|
1135
1201
|
};
|
|
1136
1202
|
}
|
|
1137
1203
|
const params = [];
|
|
@@ -1143,6 +1209,26 @@ async function generateOpenApi(rootRouter, options = {}) {
|
|
|
1143
1209
|
if (params.length > 0) {
|
|
1144
1210
|
operation.parameters = params;
|
|
1145
1211
|
}
|
|
1212
|
+
} else {
|
|
1213
|
+
const runtimeSource = (route.handler.originalHandler || route.handler).toString();
|
|
1214
|
+
let file;
|
|
1215
|
+
let line;
|
|
1216
|
+
if (route.metadata?.file) {
|
|
1217
|
+
file = route.metadata.file;
|
|
1218
|
+
line = route.metadata.line || 1;
|
|
1219
|
+
}
|
|
1220
|
+
operation["x-source-info"] = {
|
|
1221
|
+
snippet: runtimeSource,
|
|
1222
|
+
isRuntime: true,
|
|
1223
|
+
...file ? { file, line: line || 1 } : {}
|
|
1224
|
+
};
|
|
1225
|
+
if (file) {
|
|
1226
|
+
operation["x-shokupan-source"] = {
|
|
1227
|
+
file,
|
|
1228
|
+
line: line || 1,
|
|
1229
|
+
code: runtimeSource
|
|
1230
|
+
};
|
|
1231
|
+
}
|
|
1146
1232
|
}
|
|
1147
1233
|
if (route.keys.length > 0) {
|
|
1148
1234
|
const pathParams = route.keys.map((key) => ({
|
|
@@ -1212,7 +1298,7 @@ async function generateOpenApi(rootRouter, options = {}) {
|
|
|
1212
1298
|
const cleanPrefix = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
|
|
1213
1299
|
const cleanMount = mountPath.startsWith("/") ? mountPath : "/" + mountPath;
|
|
1214
1300
|
const nextPrefix = cleanPrefix + cleanMount || "/";
|
|
1215
|
-
collect(child, nextPrefix, group, tag);
|
|
1301
|
+
collect(child, nextPrefix, group, tag, [...inheritedMiddleware, ...routerMiddleware]);
|
|
1216
1302
|
}
|
|
1217
1303
|
};
|
|
1218
1304
|
collect(rootRouter);
|
|
@@ -1231,41 +1317,7 @@ async function generateOpenApi(rootRouter, options = {}) {
|
|
|
1231
1317
|
"x-tagGroups": xTagGroups
|
|
1232
1318
|
};
|
|
1233
1319
|
}
|
|
1234
|
-
|
|
1235
|
-
request;
|
|
1236
|
-
span;
|
|
1237
|
-
}
|
|
1238
|
-
const asyncContext = new AsyncLocalStorage();
|
|
1239
|
-
class HttpError extends Error {
|
|
1240
|
-
status;
|
|
1241
|
-
constructor(message, status) {
|
|
1242
|
-
super(message);
|
|
1243
|
-
this.name = "HttpError";
|
|
1244
|
-
this.status = status;
|
|
1245
|
-
if (Error.captureStackTrace) {
|
|
1246
|
-
Error.captureStackTrace(this, HttpError);
|
|
1247
|
-
}
|
|
1248
|
-
}
|
|
1249
|
-
}
|
|
1250
|
-
function getErrorStatus(err) {
|
|
1251
|
-
if (!err || typeof err !== "object") {
|
|
1252
|
-
return 500;
|
|
1253
|
-
}
|
|
1254
|
-
if (typeof err.status === "number") {
|
|
1255
|
-
return err.status;
|
|
1256
|
-
}
|
|
1257
|
-
if (typeof err.statusCode === "number") {
|
|
1258
|
-
return err.statusCode;
|
|
1259
|
-
}
|
|
1260
|
-
return 500;
|
|
1261
|
-
}
|
|
1262
|
-
class EventError extends HttpError {
|
|
1263
|
-
constructor(message = "Event Error") {
|
|
1264
|
-
super(message, 500);
|
|
1265
|
-
this.name = "EventError";
|
|
1266
|
-
}
|
|
1267
|
-
}
|
|
1268
|
-
const eta$1 = new Eta();
|
|
1320
|
+
const eta = new Eta();
|
|
1269
1321
|
function serveStatic(config, prefix) {
|
|
1270
1322
|
const rootPath = resolve(config.root || ".");
|
|
1271
1323
|
const normalizedPrefix = prefix.endsWith("/") && prefix !== "/" ? prefix.slice(0, -1) : prefix;
|
|
@@ -1364,7 +1416,7 @@ function serveStatic(config, prefix) {
|
|
|
1364
1416
|
if (config.listDirectory) {
|
|
1365
1417
|
try {
|
|
1366
1418
|
const files = await readdir(requestPath);
|
|
1367
|
-
const listing = eta
|
|
1419
|
+
const listing = eta.renderString(`
|
|
1368
1420
|
<!DOCTYPE html>
|
|
1369
1421
|
<html>
|
|
1370
1422
|
<head>
|
|
@@ -1404,7 +1456,7 @@ function serveStatic(config, prefix) {
|
|
|
1404
1456
|
if (typeof Bun !== "undefined") {
|
|
1405
1457
|
response = new Response(Bun.file(finalPath));
|
|
1406
1458
|
} else {
|
|
1407
|
-
const fileBuffer = await readFile$1(finalPath);
|
|
1459
|
+
const fileBuffer = await readFile$1(finalPath, { encoding: "binary" });
|
|
1408
1460
|
response = new Response(fileBuffer);
|
|
1409
1461
|
}
|
|
1410
1462
|
if (config.hooks?.onResponse) {
|
|
@@ -1417,67 +1469,6 @@ function serveStatic(config, prefix) {
|
|
|
1417
1469
|
serveStaticMiddleware.pluginName = "ServeStatic";
|
|
1418
1470
|
return serveStaticMiddleware;
|
|
1419
1471
|
}
|
|
1420
|
-
const G = globalThis;
|
|
1421
|
-
G.__shokupan_db = G.__shokupan_db || null;
|
|
1422
|
-
G.__shokupan_db_promise = G.__shokupan_db_promise || null;
|
|
1423
|
-
async function ensureDb() {
|
|
1424
|
-
if (G.__shokupan_db) return G.__shokupan_db;
|
|
1425
|
-
if (G.__shokupan_db_promise) return G.__shokupan_db_promise;
|
|
1426
|
-
G.__shokupan_db_promise = (async () => {
|
|
1427
|
-
try {
|
|
1428
|
-
const { createNodeEngines } = await import("@surrealdb/node");
|
|
1429
|
-
const surreal = await import("surrealdb");
|
|
1430
|
-
const engine = process.env["SHOKUPAN_DB_ENGINE"] === "memory" ? "mem://" : "rocksdb://database";
|
|
1431
|
-
const _db = new Surreal({
|
|
1432
|
-
engines: createNodeEngines()
|
|
1433
|
-
});
|
|
1434
|
-
await _db.connect(engine, { namespace: "vendor", database: "shokupan" });
|
|
1435
|
-
await _db.query(`
|
|
1436
|
-
DEFINE TABLE OVERWRITE failed_requests SCHEMALESS COMMENT "Created by Shokupan";
|
|
1437
|
-
DEFINE TABLE OVERWRITE sessions SCHEMALESS COMMENT "Created by Shokupan";
|
|
1438
|
-
DEFINE TABLE OVERWRITE users SCHEMALESS COMMENT "Created by Shokupan";
|
|
1439
|
-
DEFINE TABLE OVERWRITE idempotency_keys SCHEMALESS COMMENT "Created by Shokupan";
|
|
1440
|
-
DEFINE TABLE OVERWRITE middleware_tracking SCHEMALESS COMMENT "Created by Shokupan";
|
|
1441
|
-
DEFINE TABLE OVERWRITE requests SCHEMALESS COMMENT "Created by Shokupan";
|
|
1442
|
-
DEFINE TABLE OVERWRITE metrics SCHEMALESS COMMENT "Created by Shokupan";
|
|
1443
|
-
`);
|
|
1444
|
-
G.__shokupan_db = _db;
|
|
1445
|
-
return _db;
|
|
1446
|
-
} catch (e) {
|
|
1447
|
-
G.__shokupan_db_promise = null;
|
|
1448
|
-
if (e.code === "ERR_MODULE_NOT_FOUND" || e.message.includes("Cannot find module")) {
|
|
1449
|
-
throw new Error("SurrealDB dependencies not found. To use the datastore, please install 'surrealdb' and '@surrealdb/node'.");
|
|
1450
|
-
}
|
|
1451
|
-
throw e;
|
|
1452
|
-
}
|
|
1453
|
-
})();
|
|
1454
|
-
return G.__shokupan_db_promise;
|
|
1455
|
-
}
|
|
1456
|
-
const datastore = {
|
|
1457
|
-
async get(recordId) {
|
|
1458
|
-
await ensureDb();
|
|
1459
|
-
return G.__shokupan_db.select(recordId);
|
|
1460
|
-
},
|
|
1461
|
-
async set(recordId, value) {
|
|
1462
|
-
await ensureDb();
|
|
1463
|
-
return G.__shokupan_db.upsert(recordId).content(value);
|
|
1464
|
-
},
|
|
1465
|
-
async query(query, vars) {
|
|
1466
|
-
await ensureDb();
|
|
1467
|
-
try {
|
|
1468
|
-
return G.__shokupan_db.query(query, vars).collect();
|
|
1469
|
-
} catch (e) {
|
|
1470
|
-
console.error("DS ERROR:", e);
|
|
1471
|
-
throw e;
|
|
1472
|
-
}
|
|
1473
|
-
},
|
|
1474
|
-
get ready() {
|
|
1475
|
-
return ensureDb().then(() => void 0);
|
|
1476
|
-
}
|
|
1477
|
-
};
|
|
1478
|
-
process.on("exit", async () => {
|
|
1479
|
-
if (G.__shokupan_db) await G.__shokupan_db.close();
|
|
1480
|
-
});
|
|
1481
1472
|
class Container {
|
|
1482
1473
|
static services = /* @__PURE__ */ new Map();
|
|
1483
1474
|
static register(target, instance) {
|
|
@@ -1511,6 +1502,58 @@ function Inject(token) {
|
|
|
1511
1502
|
});
|
|
1512
1503
|
};
|
|
1513
1504
|
}
|
|
1505
|
+
class HttpError extends Error {
|
|
1506
|
+
status;
|
|
1507
|
+
constructor(message, status) {
|
|
1508
|
+
super(message);
|
|
1509
|
+
this.name = "HttpError";
|
|
1510
|
+
this.status = status;
|
|
1511
|
+
if (Error.captureStackTrace) {
|
|
1512
|
+
Error.captureStackTrace(this, HttpError);
|
|
1513
|
+
}
|
|
1514
|
+
}
|
|
1515
|
+
}
|
|
1516
|
+
function getErrorStatus(err) {
|
|
1517
|
+
if (!err || typeof err !== "object") {
|
|
1518
|
+
return 500;
|
|
1519
|
+
}
|
|
1520
|
+
if (typeof err.status === "number") {
|
|
1521
|
+
return err.status;
|
|
1522
|
+
}
|
|
1523
|
+
if (typeof err.statusCode === "number") {
|
|
1524
|
+
return err.statusCode;
|
|
1525
|
+
}
|
|
1526
|
+
return 500;
|
|
1527
|
+
}
|
|
1528
|
+
class EventError extends HttpError {
|
|
1529
|
+
constructor(message = "Event Error") {
|
|
1530
|
+
super(message, 500);
|
|
1531
|
+
this.name = "EventError";
|
|
1532
|
+
}
|
|
1533
|
+
}
|
|
1534
|
+
const tracer = trace.getTracer("shokupan.middleware");
|
|
1535
|
+
function traceHandler(fn, name) {
|
|
1536
|
+
return async function(...args) {
|
|
1537
|
+
return tracer.startActiveSpan(`route handler - ${name}`, {
|
|
1538
|
+
kind: SpanKind.INTERNAL,
|
|
1539
|
+
attributes: {
|
|
1540
|
+
"http.route": name,
|
|
1541
|
+
"component": "shokupan.route"
|
|
1542
|
+
}
|
|
1543
|
+
}, async (span) => {
|
|
1544
|
+
try {
|
|
1545
|
+
const result = await fn.apply(this, args);
|
|
1546
|
+
return result;
|
|
1547
|
+
} catch (err) {
|
|
1548
|
+
span.recordException(err);
|
|
1549
|
+
span.setStatus({ code: SpanStatusCode.ERROR, message: err.message });
|
|
1550
|
+
throw err;
|
|
1551
|
+
} finally {
|
|
1552
|
+
span.end();
|
|
1553
|
+
}
|
|
1554
|
+
});
|
|
1555
|
+
};
|
|
1556
|
+
}
|
|
1514
1557
|
class ShokupanRequestBase {
|
|
1515
1558
|
method;
|
|
1516
1559
|
url;
|
|
@@ -1548,8 +1591,10 @@ function getCallerInfo(skipFrames = 1) {
|
|
|
1548
1591
|
if (!l.includes(":")) continue;
|
|
1549
1592
|
if (l.includes("node_modules")) continue;
|
|
1550
1593
|
if (l.includes("bun:main")) continue;
|
|
1594
|
+
if (l.includes("bun:wrap")) continue;
|
|
1551
1595
|
if (l.includes("src/util/stack.ts")) continue;
|
|
1552
1596
|
if (l.includes("src/router.ts")) continue;
|
|
1597
|
+
if (l.includes("src/util/decorators.ts")) continue;
|
|
1553
1598
|
if (l.includes("src/shokupan.ts")) continue;
|
|
1554
1599
|
found++;
|
|
1555
1600
|
if (found >= skipFrames) {
|
|
@@ -1675,6 +1720,8 @@ var RouteParamType = /* @__PURE__ */ ((RouteParamType2) => {
|
|
|
1675
1720
|
RouteParamType2["CONTEXT"] = "CONTEXT";
|
|
1676
1721
|
return RouteParamType2;
|
|
1677
1722
|
})(RouteParamType || {});
|
|
1723
|
+
const RouterRegistry = /* @__PURE__ */ new Map();
|
|
1724
|
+
const ShokupanApplicationTree = {};
|
|
1678
1725
|
class ShokupanRouter {
|
|
1679
1726
|
constructor(config) {
|
|
1680
1727
|
this.config = config;
|
|
@@ -1692,6 +1739,9 @@ class ShokupanRouter {
|
|
|
1692
1739
|
[$parent] = null;
|
|
1693
1740
|
[$childRouters] = [];
|
|
1694
1741
|
[$childControllers] = [];
|
|
1742
|
+
get db() {
|
|
1743
|
+
return this.root?.db;
|
|
1744
|
+
}
|
|
1695
1745
|
hookCache = /* @__PURE__ */ new Map();
|
|
1696
1746
|
hooksInitialized = false;
|
|
1697
1747
|
middleware = [];
|
|
@@ -1708,6 +1758,14 @@ class ShokupanRouter {
|
|
|
1708
1758
|
// Metadata for the router itself
|
|
1709
1759
|
currentGuards = [];
|
|
1710
1760
|
eventHandlers = /* @__PURE__ */ new Map();
|
|
1761
|
+
/**
|
|
1762
|
+
* Registers middleware for this router.
|
|
1763
|
+
* Middleware will run for all routes matched by this router.
|
|
1764
|
+
*/
|
|
1765
|
+
use(middleware) {
|
|
1766
|
+
this.middleware.push(middleware);
|
|
1767
|
+
return this;
|
|
1768
|
+
}
|
|
1711
1769
|
// Registry Accessor
|
|
1712
1770
|
getComponentRegistry() {
|
|
1713
1771
|
const controllerRoutesMap = /* @__PURE__ */ new Map();
|
|
@@ -1772,6 +1830,8 @@ class ShokupanRouter {
|
|
|
1772
1830
|
* Registers an event handler for WebSocket.
|
|
1773
1831
|
*/
|
|
1774
1832
|
event(name, handler) {
|
|
1833
|
+
const info = getCallerInfo();
|
|
1834
|
+
handler.source = { file: info.file, line: info.line };
|
|
1775
1835
|
if (this.eventHandlers.has(name)) {
|
|
1776
1836
|
const err = new EventError(`Event handler \`${name}\` already exists.`);
|
|
1777
1837
|
console.warn(err);
|
|
@@ -1796,6 +1856,12 @@ class ShokupanRouter {
|
|
|
1796
1856
|
}
|
|
1797
1857
|
return null;
|
|
1798
1858
|
}
|
|
1859
|
+
/**
|
|
1860
|
+
* Returns all registered event handlers.
|
|
1861
|
+
*/
|
|
1862
|
+
getEventHandlers() {
|
|
1863
|
+
return this.eventHandlers;
|
|
1864
|
+
}
|
|
1799
1865
|
/**
|
|
1800
1866
|
* Mounts a controller instance to a path prefix.
|
|
1801
1867
|
*
|
|
@@ -2039,10 +2105,12 @@ class ShokupanRouter {
|
|
|
2039
2105
|
if (typeof originalHandler !== "function") continue;
|
|
2040
2106
|
let method;
|
|
2041
2107
|
let subPath = "";
|
|
2108
|
+
let methodSource;
|
|
2042
2109
|
if (decoratedRoutes && decoratedRoutes.has(name)) {
|
|
2043
2110
|
const config = decoratedRoutes.get(name);
|
|
2044
2111
|
method = config.method;
|
|
2045
2112
|
subPath = config.path;
|
|
2113
|
+
methodSource = config.source;
|
|
2046
2114
|
} else {
|
|
2047
2115
|
for (let j = 0; j < HTTPMethods.length; j++) {
|
|
2048
2116
|
const m = HTTPMethods[j];
|
|
@@ -2172,7 +2240,16 @@ class ShokupanRouter {
|
|
|
2172
2240
|
const decoratedSpecs = instance[$routeSpec] || proto && proto[$routeSpec];
|
|
2173
2241
|
const userSpec = decoratedSpecs && decoratedSpecs.get(name);
|
|
2174
2242
|
const spec = { tags: [tagName], ...userSpec };
|
|
2175
|
-
this.add({
|
|
2243
|
+
this.add({
|
|
2244
|
+
method,
|
|
2245
|
+
path: normalizedPath,
|
|
2246
|
+
handler: finalHandler,
|
|
2247
|
+
spec,
|
|
2248
|
+
controller: instance,
|
|
2249
|
+
metadata: methodSource || instance.metadata,
|
|
2250
|
+
middleware: allMiddleware
|
|
2251
|
+
// Capture all resolved middleware
|
|
2252
|
+
});
|
|
2176
2253
|
}
|
|
2177
2254
|
if (decoratedEvents?.has(name)) {
|
|
2178
2255
|
routesAttached++;
|
|
@@ -2205,6 +2282,11 @@ class ShokupanRouter {
|
|
|
2205
2282
|
}
|
|
2206
2283
|
return originalHandler.apply(instance, args);
|
|
2207
2284
|
};
|
|
2285
|
+
const decoratedSpecs = instance[$routeSpec] || proto && proto[$routeSpec];
|
|
2286
|
+
const userSpec = decoratedSpecs && decoratedSpecs.get(name);
|
|
2287
|
+
const spec = { tags: [{ name: instance.constructor.name }], ...userSpec };
|
|
2288
|
+
wrappedHandler.spec = spec;
|
|
2289
|
+
wrappedHandler.originalHandler = originalHandler;
|
|
2208
2290
|
this.event(config.eventName, wrappedHandler);
|
|
2209
2291
|
}
|
|
2210
2292
|
}
|
|
@@ -2274,7 +2356,7 @@ class ShokupanRouter {
|
|
|
2274
2356
|
* @param arg.renderer - JSX renderer for the route
|
|
2275
2357
|
* @param arg.controller - Controller for the route
|
|
2276
2358
|
*/
|
|
2277
|
-
add({ method, path, spec, handler, regex: customRegex, group, requestTimeout, renderer, controller }) {
|
|
2359
|
+
add({ method, path, spec, handler, regex: customRegex, group, requestTimeout, renderer, controller, metadata, middleware }) {
|
|
2278
2360
|
const { regex, keys } = customRegex ? { regex: customRegex, keys: [] } : this.parsePath(path);
|
|
2279
2361
|
if (this.currentGuards.length > 0) {
|
|
2280
2362
|
spec = spec || {};
|
|
@@ -2292,7 +2374,13 @@ class ShokupanRouter {
|
|
|
2292
2374
|
}
|
|
2293
2375
|
}
|
|
2294
2376
|
}
|
|
2295
|
-
let wrappedHandler =
|
|
2377
|
+
let wrappedHandler = async (ctx) => {
|
|
2378
|
+
if (ctx.upgrade()) {
|
|
2379
|
+
return void 0;
|
|
2380
|
+
}
|
|
2381
|
+
return handler(ctx);
|
|
2382
|
+
};
|
|
2383
|
+
wrappedHandler.originalHandler = handler.originalHandler || handler;
|
|
2296
2384
|
const routeGuards = [...this.currentGuards];
|
|
2297
2385
|
const effectiveTimeout = requestTimeout ?? this.requestTimeout ?? this.rootConfig?.requestTimeout;
|
|
2298
2386
|
if (effectiveTimeout !== void 0 && effectiveTimeout > 0) {
|
|
@@ -2343,7 +2431,7 @@ class ShokupanRouter {
|
|
|
2343
2431
|
return innerHandler(ctx);
|
|
2344
2432
|
};
|
|
2345
2433
|
}
|
|
2346
|
-
const { file, line } = getCallerInfo();
|
|
2434
|
+
const { file, line } = metadata || getCallerInfo();
|
|
2347
2435
|
const trackingHandler = wrappedHandler;
|
|
2348
2436
|
wrappedHandler = async (ctx) => {
|
|
2349
2437
|
if (!ctx.app?.applicationConfig.enableMiddlewareTracking) {
|
|
@@ -2369,8 +2457,10 @@ class ShokupanRouter {
|
|
|
2369
2457
|
const config = ctx.app.applicationConfig;
|
|
2370
2458
|
Promise.resolve().then(async () => {
|
|
2371
2459
|
try {
|
|
2460
|
+
const db = ctx.app?.db;
|
|
2461
|
+
if (!db) return;
|
|
2372
2462
|
const timestamp = Date.now();
|
|
2373
|
-
await
|
|
2463
|
+
await db.upsert(new RecordId("middleware_tracking", {
|
|
2374
2464
|
timestamp,
|
|
2375
2465
|
name: handler.name || "anonymous"
|
|
2376
2466
|
}), {
|
|
@@ -2389,11 +2479,11 @@ class ShokupanRouter {
|
|
|
2389
2479
|
const ttl = config.middlewareTrackingTTL ?? 864e5;
|
|
2390
2480
|
const maxCapacity = config.middlewareTrackingMaxCapacity ?? 1e4;
|
|
2391
2481
|
const cutoff = Date.now() - ttl;
|
|
2392
|
-
await
|
|
2393
|
-
const results = await
|
|
2482
|
+
await db.query(`DELETE middleware_tracking WHERE timestamp < ${cutoff}`);
|
|
2483
|
+
const results = await db.query("SELECT count() FROM middleware_tracking GROUP ALL");
|
|
2394
2484
|
if (results?.[0]?.count > maxCapacity) {
|
|
2395
2485
|
const toDelete = results[0].count - maxCapacity;
|
|
2396
|
-
await
|
|
2486
|
+
await db.query(`DELETE middleware_tracking ORDER BY timestamp ASC LIMIT ${toDelete}`);
|
|
2397
2487
|
}
|
|
2398
2488
|
} catch (datastoreError) {
|
|
2399
2489
|
console.error("Failed to store middleware tracking:", datastoreError);
|
|
@@ -2423,7 +2513,8 @@ class ShokupanRouter {
|
|
|
2423
2513
|
file,
|
|
2424
2514
|
line
|
|
2425
2515
|
},
|
|
2426
|
-
controller
|
|
2516
|
+
controller,
|
|
2517
|
+
middleware: middleware || []
|
|
2427
2518
|
});
|
|
2428
2519
|
this.trie.insert(method, path, bakedHandler);
|
|
2429
2520
|
return this;
|
|
@@ -2552,7 +2643,8 @@ class ShokupanRouter {
|
|
|
2552
2643
|
method,
|
|
2553
2644
|
path,
|
|
2554
2645
|
spec,
|
|
2555
|
-
handler: finalHandler
|
|
2646
|
+
handler: finalHandler,
|
|
2647
|
+
middleware: handlers.slice(0, handlers.length - 1)
|
|
2556
2648
|
});
|
|
2557
2649
|
}
|
|
2558
2650
|
/**
|
|
@@ -2592,7 +2684,7 @@ class ShokupanRouter {
|
|
|
2592
2684
|
}
|
|
2593
2685
|
this.hooksInitialized = true;
|
|
2594
2686
|
}
|
|
2595
|
-
|
|
2687
|
+
runHooks(name, ...args) {
|
|
2596
2688
|
if (!this.hooksInitialized) {
|
|
2597
2689
|
this.ensureHooksInitialized();
|
|
2598
2690
|
}
|
|
@@ -2601,7 +2693,7 @@ class ShokupanRouter {
|
|
|
2601
2693
|
const ctx = args?.[0] instanceof ShokupanContext ? args[0] : void 0;
|
|
2602
2694
|
const debug = ctx?.[$debug];
|
|
2603
2695
|
if (debug) {
|
|
2604
|
-
|
|
2696
|
+
return Promise.all(fns.map(async (fn, index) => {
|
|
2605
2697
|
const hookId = `hook_${name}_${fn.name || index}`;
|
|
2606
2698
|
const previousNode = debug.getCurrentNode();
|
|
2607
2699
|
debug.trackEdge(previousNode, hookId);
|
|
@@ -2620,10 +2712,15 @@ class ShokupanRouter {
|
|
|
2620
2712
|
}
|
|
2621
2713
|
}));
|
|
2622
2714
|
} else {
|
|
2623
|
-
|
|
2715
|
+
return Promise.all(fns.map((fn) => fn(...args)));
|
|
2624
2716
|
}
|
|
2625
2717
|
}
|
|
2626
2718
|
}
|
|
2719
|
+
class RequestContextStore {
|
|
2720
|
+
request;
|
|
2721
|
+
span;
|
|
2722
|
+
}
|
|
2723
|
+
const asyncContext = new AsyncLocalStorage();
|
|
2627
2724
|
class SystemCpuMonitor {
|
|
2628
2725
|
constructor(intervalMs = 1e3) {
|
|
2629
2726
|
this.intervalMs = intervalMs;
|
|
@@ -2667,6 +2764,100 @@ class SystemCpuMonitor {
|
|
|
2667
2764
|
this.currentUsage = total === 0 ? 0 : (1 - idle / total) * 100;
|
|
2668
2765
|
}
|
|
2669
2766
|
}
|
|
2767
|
+
class SurrealDatastore {
|
|
2768
|
+
constructor(db) {
|
|
2769
|
+
this.db = db;
|
|
2770
|
+
process.on("exit", async () => {
|
|
2771
|
+
await this.disconnect();
|
|
2772
|
+
});
|
|
2773
|
+
}
|
|
2774
|
+
createSchema() {
|
|
2775
|
+
this.db.query(`
|
|
2776
|
+
DEFINE TABLE OVERWRITE failed_requests SCHEMALESS COMMENT "Created by Shokupan";
|
|
2777
|
+
DEFINE TABLE OVERWRITE sessions SCHEMALESS COMMENT "Created by Shokupan";
|
|
2778
|
+
DEFINE TABLE OVERWRITE users SCHEMALESS COMMENT "Created by Shokupan";
|
|
2779
|
+
DEFINE TABLE OVERWRITE idempotency_keys SCHEMALESS COMMENT "Created by Shokupan";
|
|
2780
|
+
DEFINE TABLE OVERWRITE middleware_tracking SCHEMALESS COMMENT "Created by Shokupan";
|
|
2781
|
+
DEFINE TABLE OVERWRITE requests SCHEMALESS COMMENT "Created by Shokupan";
|
|
2782
|
+
DEFINE TABLE OVERWRITE metrics SCHEMALESS COMMENT "Created by Shokupan";
|
|
2783
|
+
`).collect();
|
|
2784
|
+
}
|
|
2785
|
+
/**
|
|
2786
|
+
* Select a record or contents of a table by its ID.
|
|
2787
|
+
*/
|
|
2788
|
+
async select(id) {
|
|
2789
|
+
return this.db.select(id);
|
|
2790
|
+
}
|
|
2791
|
+
/**
|
|
2792
|
+
* Merge update data into a record by its ID.
|
|
2793
|
+
*/
|
|
2794
|
+
async merge(id, data) {
|
|
2795
|
+
return this.db.update(id).merge(data).catch((err) => {
|
|
2796
|
+
if (err.message.includes("This transaction can be retried")) {
|
|
2797
|
+
return this.db.update(id).merge(data);
|
|
2798
|
+
}
|
|
2799
|
+
throw err;
|
|
2800
|
+
});
|
|
2801
|
+
}
|
|
2802
|
+
/**
|
|
2803
|
+
* Create a record by its ID.
|
|
2804
|
+
*/
|
|
2805
|
+
async create(id, data) {
|
|
2806
|
+
return this.db.create(id).content(data).catch((err) => {
|
|
2807
|
+
if (err.message.includes("This transaction can be retried")) {
|
|
2808
|
+
return this.db.create(id).content(data);
|
|
2809
|
+
}
|
|
2810
|
+
throw err;
|
|
2811
|
+
});
|
|
2812
|
+
}
|
|
2813
|
+
/**
|
|
2814
|
+
* Upsert a record by its ID.
|
|
2815
|
+
*/
|
|
2816
|
+
async upsert(id, data) {
|
|
2817
|
+
return this.db.upsert(id).content(data).catch((err) => {
|
|
2818
|
+
if (err.message.includes("This transaction can be retried")) {
|
|
2819
|
+
return this.db.upsert(id).content(data);
|
|
2820
|
+
}
|
|
2821
|
+
throw err;
|
|
2822
|
+
});
|
|
2823
|
+
}
|
|
2824
|
+
/**
|
|
2825
|
+
* Delete a record by its ID.
|
|
2826
|
+
*/
|
|
2827
|
+
async delete(id) {
|
|
2828
|
+
return this.db.delete(id).catch((err) => {
|
|
2829
|
+
if (err.message.includes("This transaction can be retried")) {
|
|
2830
|
+
return this.db.delete(id);
|
|
2831
|
+
}
|
|
2832
|
+
throw err;
|
|
2833
|
+
});
|
|
2834
|
+
}
|
|
2835
|
+
/**
|
|
2836
|
+
* Run a SurrealDB query.
|
|
2837
|
+
*/
|
|
2838
|
+
async query(query, vars) {
|
|
2839
|
+
return this.db.query(query, vars).collect().catch((err) => {
|
|
2840
|
+
if (err.message.includes("This transaction can be retried")) {
|
|
2841
|
+
return this.db.query(query, vars).collect();
|
|
2842
|
+
}
|
|
2843
|
+
throw err;
|
|
2844
|
+
});
|
|
2845
|
+
}
|
|
2846
|
+
/**
|
|
2847
|
+
* Create a relationship between two records.
|
|
2848
|
+
*/
|
|
2849
|
+
async relate(fromId, edgeId, toId, data) {
|
|
2850
|
+
return this.db.relate(fromId, edgeId, toId, data).catch((err) => {
|
|
2851
|
+
if (err.message.includes("This transaction can be retried")) {
|
|
2852
|
+
return this.db.relate(fromId, edgeId, toId, data);
|
|
2853
|
+
}
|
|
2854
|
+
throw err;
|
|
2855
|
+
});
|
|
2856
|
+
}
|
|
2857
|
+
disconnect() {
|
|
2858
|
+
return this.db.close();
|
|
2859
|
+
}
|
|
2860
|
+
}
|
|
2670
2861
|
const defaults = {
|
|
2671
2862
|
port: 3e3,
|
|
2672
2863
|
hostname: "localhost",
|
|
@@ -2679,9 +2870,15 @@ trace.getTracer("shokupan.application");
|
|
|
2679
2870
|
class Shokupan extends ShokupanRouter {
|
|
2680
2871
|
applicationConfig = {};
|
|
2681
2872
|
openApiSpec;
|
|
2873
|
+
asyncApiSpec;
|
|
2682
2874
|
composedMiddleware;
|
|
2683
2875
|
cpuMonitor;
|
|
2684
2876
|
server;
|
|
2877
|
+
datastore;
|
|
2878
|
+
dbPromise;
|
|
2879
|
+
get db() {
|
|
2880
|
+
return this.datastore;
|
|
2881
|
+
}
|
|
2685
2882
|
get logger() {
|
|
2686
2883
|
return this.applicationConfig.logger;
|
|
2687
2884
|
}
|
|
@@ -2698,6 +2895,19 @@ class Shokupan extends ShokupanRouter {
|
|
|
2698
2895
|
line,
|
|
2699
2896
|
name: "ShokupanApplication"
|
|
2700
2897
|
};
|
|
2898
|
+
this.dbPromise = this.initDatastore();
|
|
2899
|
+
}
|
|
2900
|
+
async initDatastore() {
|
|
2901
|
+
const db = new Surreal({ engines: this.applicationConfig.surreal?.engines ?? (await import("@surrealdb/node")).createNodeEngines() });
|
|
2902
|
+
this.datastore = new SurrealDatastore(db);
|
|
2903
|
+
await db.connect(
|
|
2904
|
+
this.applicationConfig.surreal?.url ?? (process.env.NODE_ENV === "test" ? "mem://" : "surrealkv://database"),
|
|
2905
|
+
this.applicationConfig.surreal?.connectOptions
|
|
2906
|
+
);
|
|
2907
|
+
await db.use({
|
|
2908
|
+
namespace: this.applicationConfig.surreal?.namespace ?? "vendor",
|
|
2909
|
+
database: this.applicationConfig.surreal?.database ?? "shokupan"
|
|
2910
|
+
});
|
|
2701
2911
|
}
|
|
2702
2912
|
/**
|
|
2703
2913
|
* Adds middleware to the application.
|
|
@@ -2777,14 +2987,70 @@ class Shokupan extends ShokupanRouter {
|
|
|
2777
2987
|
*/
|
|
2778
2988
|
async listen(port) {
|
|
2779
2989
|
const finalPort = port ?? this.applicationConfig.port ?? 3e3;
|
|
2780
|
-
if (finalPort < 0 || finalPort > 65535) {
|
|
2990
|
+
if (finalPort < 0 || finalPort > 65535 || finalPort % 1 !== 0) {
|
|
2781
2991
|
throw new Error("Invalid port number");
|
|
2782
2992
|
}
|
|
2783
2993
|
await Promise.all(this.startupHooks.map((hook) => hook()));
|
|
2784
2994
|
if (this.applicationConfig.enableOpenApiGen) {
|
|
2785
2995
|
this.openApiSpec = await generateOpenApi(this);
|
|
2996
|
+
this.get("/.well-known/openapi.yaml", (ctx) => {
|
|
2997
|
+
try {
|
|
2998
|
+
const yaml = dump(this.openApiSpec);
|
|
2999
|
+
return ctx.send(yaml, { status: 200, headers: { "content-type": "application/yaml" } });
|
|
3000
|
+
} catch (e) {
|
|
3001
|
+
this.logger.error("Failed to generate OpenAPI YAML", { error: e });
|
|
3002
|
+
return ctx.text("Internal Server Error", 500);
|
|
3003
|
+
}
|
|
3004
|
+
});
|
|
3005
|
+
if (this.applicationConfig.aiPlugin?.enabled !== false) {
|
|
3006
|
+
this.get("/.well-known/ai-plugin.json", async (ctx) => {
|
|
3007
|
+
const config = this.applicationConfig.aiPlugin || {};
|
|
3008
|
+
let pkg = {};
|
|
3009
|
+
try {
|
|
3010
|
+
pkg = await Bun.file("package.json").json();
|
|
3011
|
+
} catch (e) {
|
|
3012
|
+
}
|
|
3013
|
+
const manifest = {
|
|
3014
|
+
schema_version: "v1",
|
|
3015
|
+
name_for_human: config.name_for_human || this.openApiSpec.info.title || pkg.name || "Shokupan App",
|
|
3016
|
+
name_for_model: config.name_for_model || this.openApiSpec.info.title || pkg.name || "Shokupan App",
|
|
3017
|
+
description_for_human: config.description_for_human || this.openApiSpec.info.description || pkg.description || "Shokupan Application",
|
|
3018
|
+
description_for_model: config.description_for_model || this.openApiSpec.info.description || pkg.description || "Shokupan Application",
|
|
3019
|
+
auth: config.auth || { type: "none" },
|
|
3020
|
+
api: config.api || {
|
|
3021
|
+
type: "openapi",
|
|
3022
|
+
url: `${this.applicationConfig.hostname === "localhost" ? "http" : "https"}://${this.applicationConfig.hostname}:${finalPort}/.well-known/openapi.yaml`,
|
|
3023
|
+
is_user_authenticated: false
|
|
3024
|
+
},
|
|
3025
|
+
logo_url: config.logo_url || `${this.applicationConfig.hostname === "localhost" ? "http" : "https"}://${this.applicationConfig.hostname}:${finalPort}/logo.png`,
|
|
3026
|
+
// Placeholder default
|
|
3027
|
+
contact_email: config.contact_email || pkg.author?.email || "support@example.com",
|
|
3028
|
+
legal_info_url: config.legal_info_url || `${this.applicationConfig.hostname === "localhost" ? "http" : "https"}://${this.applicationConfig.hostname}:${finalPort}/legal`
|
|
3029
|
+
};
|
|
3030
|
+
return ctx.json(manifest);
|
|
3031
|
+
});
|
|
3032
|
+
}
|
|
3033
|
+
if (this.applicationConfig.apiCatalog?.enabled !== false) {
|
|
3034
|
+
this.get("/.well-known/api-catalog", (ctx) => {
|
|
3035
|
+
const config = this.applicationConfig.apiCatalog || {};
|
|
3036
|
+
const catalog = {
|
|
3037
|
+
versions: config.versions || [
|
|
3038
|
+
{
|
|
3039
|
+
name: this.openApiSpec.info.version || "v1",
|
|
3040
|
+
url: `${this.applicationConfig.hostname === "localhost" ? "http" : "https"}://${this.applicationConfig.hostname}:${finalPort}/`,
|
|
3041
|
+
spec_url: `${this.applicationConfig.hostname === "localhost" ? "http" : "https"}://${this.applicationConfig.hostname}:${finalPort}/.well-known/openapi.yaml`
|
|
3042
|
+
}
|
|
3043
|
+
]
|
|
3044
|
+
};
|
|
3045
|
+
return ctx.json(catalog);
|
|
3046
|
+
});
|
|
3047
|
+
}
|
|
2786
3048
|
await Promise.all(this.specAvailableHooks.map((hook) => hook(this.openApiSpec)));
|
|
2787
3049
|
}
|
|
3050
|
+
if (this.applicationConfig.enableAsyncApiGen) {
|
|
3051
|
+
const { generateAsyncApi: generateAsyncApi2 } = await Promise.resolve().then(() => generator);
|
|
3052
|
+
this.asyncApiSpec = await generateAsyncApi2(this);
|
|
3053
|
+
}
|
|
2788
3054
|
if (port === 0 && process.platform === "linux") ;
|
|
2789
3055
|
if (this.applicationConfig.autoBackpressureFeedback === true) {
|
|
2790
3056
|
this.cpuMonitor = new SystemCpuMonitor();
|
|
@@ -2844,6 +3110,8 @@ class Shokupan extends ShokupanRouter {
|
|
|
2844
3110
|
});
|
|
2845
3111
|
const ctx = new ShokupanContext(req, self.server);
|
|
2846
3112
|
ctx[$ws] = ws;
|
|
3113
|
+
ws.data ??= {};
|
|
3114
|
+
ws.data["ctx"] = ctx;
|
|
2847
3115
|
try {
|
|
2848
3116
|
await handler(ctx);
|
|
2849
3117
|
} catch (err) {
|
|
@@ -2863,6 +3131,15 @@ class Shokupan extends ShokupanRouter {
|
|
|
2863
3131
|
},
|
|
2864
3132
|
close(ws, code, reason) {
|
|
2865
3133
|
ws.data?.handler?.close?.(ws, code, reason);
|
|
3134
|
+
const ctx = ws.data?.["ctx"];
|
|
3135
|
+
if (ctx && typeof ctx.getDisconnectCallbacks === "function") {
|
|
3136
|
+
const callbacks = ctx.getDisconnectCallbacks();
|
|
3137
|
+
if (Array.isArray(callbacks) && callbacks.length > 0) {
|
|
3138
|
+
Promise.all(callbacks.map((cb) => cb())).catch((err) => {
|
|
3139
|
+
console.error("Error executing socket disconnect hook:", err);
|
|
3140
|
+
});
|
|
3141
|
+
}
|
|
3142
|
+
}
|
|
2866
3143
|
}
|
|
2867
3144
|
}
|
|
2868
3145
|
};
|
|
@@ -2872,7 +3149,6 @@ class Shokupan extends ShokupanRouter {
|
|
|
2872
3149
|
factory = createHttpServer();
|
|
2873
3150
|
}
|
|
2874
3151
|
this.server = factory ? await factory(serveOptions) : Bun.serve(serveOptions);
|
|
2875
|
-
console.log(`Shokupan server listening on http://${serveOptions.hostname}:${serveOptions.port}`);
|
|
2876
3152
|
return this.server;
|
|
2877
3153
|
}
|
|
2878
3154
|
/**
|
|
@@ -3004,9 +3280,6 @@ class Shokupan extends ShokupanRouter {
|
|
|
3004
3280
|
await bodyParsing;
|
|
3005
3281
|
return match.handler(ctx);
|
|
3006
3282
|
}
|
|
3007
|
-
if (ctx.upgrade()) {
|
|
3008
|
-
return void 0;
|
|
3009
|
-
}
|
|
3010
3283
|
return null;
|
|
3011
3284
|
});
|
|
3012
3285
|
let response;
|
|
@@ -3022,6 +3295,9 @@ class Shokupan extends ShokupanRouter {
|
|
|
3022
3295
|
} else if (ctx[$routeMatched]) {
|
|
3023
3296
|
response = ctx.send(null, { status: ctx.response.status, headers: ctx.response.headers });
|
|
3024
3297
|
} else {
|
|
3298
|
+
if (ctx.upgrade()) {
|
|
3299
|
+
return void 0;
|
|
3300
|
+
}
|
|
3025
3301
|
if (ctx.response.status !== HTTP_STATUS.OK) {
|
|
3026
3302
|
response = ctx.send(null, { status: ctx.response.status, headers: ctx.response.headers });
|
|
3027
3303
|
} else {
|
|
@@ -3034,6 +3310,9 @@ class Shokupan extends ShokupanRouter {
|
|
|
3034
3310
|
response = ctx.text(String(result));
|
|
3035
3311
|
}
|
|
3036
3312
|
await this.runHooks("onRequestEnd", ctx);
|
|
3313
|
+
if (response instanceof Promise) {
|
|
3314
|
+
response = await response;
|
|
3315
|
+
}
|
|
3037
3316
|
await this.runHooks("onResponseStart", ctx, response);
|
|
3038
3317
|
return response;
|
|
3039
3318
|
} catch (err) {
|
|
@@ -3143,8 +3422,7 @@ function RateLimitMiddleware(options = {}) {
|
|
|
3143
3422
|
}
|
|
3144
3423
|
}
|
|
3145
3424
|
const msg = typeof message === "function" ? message(ctx, key) : message;
|
|
3146
|
-
typeof msg === "object" ?
|
|
3147
|
-
const res = typeof msg === "object" ? ctx.json(msg, statusCode) : ctx.text(String(msg), statusCode);
|
|
3425
|
+
const res = await (typeof msg === "object" ? ctx.json(msg, statusCode) : ctx.text(String(msg), statusCode));
|
|
3148
3426
|
if (headers) {
|
|
3149
3427
|
setHeaders(res);
|
|
3150
3428
|
res.headers.set("Retry-After", String(retryAfter));
|
|
@@ -3219,8 +3497,12 @@ function createMethodDecorator(method) {
|
|
|
3219
3497
|
}
|
|
3220
3498
|
target[$routeMethods].set(propertyKey, {
|
|
3221
3499
|
method,
|
|
3222
|
-
path
|
|
3500
|
+
path,
|
|
3501
|
+
source: getCallerInfo(2)
|
|
3223
3502
|
});
|
|
3503
|
+
if (path.includes("/user")) {
|
|
3504
|
+
console.log(`[Decorator] Captured source for ${propertyKey}:`, getCallerInfo());
|
|
3505
|
+
}
|
|
3224
3506
|
};
|
|
3225
3507
|
};
|
|
3226
3508
|
}
|
|
@@ -3243,15 +3525,572 @@ function Event(eventName) {
|
|
|
3243
3525
|
function RateLimit(options) {
|
|
3244
3526
|
return Use(RateLimitMiddleware(options));
|
|
3245
3527
|
}
|
|
3528
|
+
function AsyncApiApp({ spec, serverUrl, base, disableSourceView, navTree }) {
|
|
3529
|
+
return /* @__PURE__ */ jsxs("html", { lang: "en", children: [
|
|
3530
|
+
/* @__PURE__ */ jsxs("head", { children: [
|
|
3531
|
+
/* @__PURE__ */ jsx("meta", { charSet: "UTF-8" }),
|
|
3532
|
+
/* @__PURE__ */ jsx("meta", { name: "viewport", content: "width=device-width, initial-scale=1.0" }),
|
|
3533
|
+
/* @__PURE__ */ jsx("title", { children: "Shokupan AsyncAPI" }),
|
|
3534
|
+
/* @__PURE__ */ jsx("link", { rel: "preconnect", href: "https://fonts.googleapis.com" }),
|
|
3535
|
+
/* @__PURE__ */ jsx("link", { rel: "preconnect", href: "https://fonts.gstatic.com", crossOrigin: "anonymous" }),
|
|
3536
|
+
/* @__PURE__ */ 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" }),
|
|
3537
|
+
/* @__PURE__ */ jsx("link", { rel: "stylesheet", href: `${base}/theme.css` }),
|
|
3538
|
+
/* @__PURE__ */ jsx("link", { rel: "stylesheet", href: `${base}/style.css` }),
|
|
3539
|
+
/* @__PURE__ */ jsx("script", { dangerouslySetInnerHTML: {
|
|
3540
|
+
__html: `
|
|
3541
|
+
window.INITIAL_SPEC = ${JSON.stringify(spec)};
|
|
3542
|
+
window.INITIAL_SERVER_URL = "${serverUrl}";
|
|
3543
|
+
window.DISABLE_SOURCE_VIEW = ${JSON.stringify(disableSourceView)};
|
|
3544
|
+
`
|
|
3545
|
+
} })
|
|
3546
|
+
] }),
|
|
3547
|
+
/* @__PURE__ */ jsxs("body", { children: [
|
|
3548
|
+
/* @__PURE__ */ jsxs("div", { class: "app-container", children: [
|
|
3549
|
+
/* @__PURE__ */ jsx(Sidebar, { navTree, disableSourceView }),
|
|
3550
|
+
/* @__PURE__ */ jsx("div", { class: "resizer", id: "resizer-left" }),
|
|
3551
|
+
/* @__PURE__ */ jsx(MainContent, {}),
|
|
3552
|
+
/* @__PURE__ */ jsx("div", { class: "resizer", id: "resizer-right" }),
|
|
3553
|
+
/* @__PURE__ */ jsx(ConsolePanel, { serverUrl })
|
|
3554
|
+
] }),
|
|
3555
|
+
/* @__PURE__ */ jsx("script", { src: "https://cdn.socket.io/4.7.4/socket.io.min.js" }),
|
|
3556
|
+
/* @__PURE__ */ jsx("script", { src: "https://cdn.jsdelivr.net/npm/monaco-editor@0.45.0/min/vs/loader.js" }),
|
|
3557
|
+
/* @__PURE__ */ jsx("script", { src: `${base}/asyncapi-client.mjs`, type: "module" })
|
|
3558
|
+
] })
|
|
3559
|
+
] });
|
|
3560
|
+
}
|
|
3561
|
+
function Sidebar({ navTree, disableSourceView }) {
|
|
3562
|
+
return /* @__PURE__ */ jsxs("div", { class: "sidebar scroller", id: "sidebar", children: [
|
|
3563
|
+
/* @__PURE__ */ jsxs("div", { class: "sidebar-header", style: "display:flex; justify-content:space-between; align-items:center;", children: [
|
|
3564
|
+
/* @__PURE__ */ jsx("h2", { children: "AsyncAPI" }),
|
|
3565
|
+
/* @__PURE__ */ jsx("button", { id: "btn-collapse-nav", class: "btn-icon", title: "Collapse Sidebar", children: /* @__PURE__ */ jsx("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", children: /* @__PURE__ */ jsx("polyline", { points: "15 18 9 12 15 6" }) }) })
|
|
3566
|
+
] }),
|
|
3567
|
+
/* @__PURE__ */ jsx("div", { class: "nav-list", id: "nav-list", children: /* @__PURE__ */ jsx(NavNode, { node: navTree, level: 0, disableSourceView }) })
|
|
3568
|
+
] });
|
|
3569
|
+
}
|
|
3570
|
+
function NavNode({ node, level, disableSourceView }) {
|
|
3571
|
+
const sortedEntries = Object.entries(node.children || {}).sort((a, b) => {
|
|
3572
|
+
const [aKey, aItem] = a;
|
|
3573
|
+
const [bKey, bItem] = b;
|
|
3574
|
+
const isWarningA = aItem.data?.op?.["x-warning"];
|
|
3575
|
+
const isWarningB = bItem.data?.op?.["x-warning"];
|
|
3576
|
+
if (isWarningA && !isWarningB) return -1;
|
|
3577
|
+
if (!isWarningA && isWarningB) return 1;
|
|
3578
|
+
if (aKey === bKey) return 0;
|
|
3579
|
+
if (aKey === "Warning" || aKey === "Warnings") return -1;
|
|
3580
|
+
if (bKey === "Warning" || bKey === "Warnings") return 1;
|
|
3581
|
+
if (aKey === "Application") return -1;
|
|
3582
|
+
if (bKey === "Application") return 1;
|
|
3583
|
+
if (aKey[0] === "/") return 1;
|
|
3584
|
+
if (bKey[0] === "/") return -1;
|
|
3585
|
+
return aKey.localeCompare(bKey);
|
|
3586
|
+
});
|
|
3587
|
+
return /* @__PURE__ */ jsx(Fragment, { children: sortedEntries.map(([key, item]) => {
|
|
3588
|
+
const hasChildren = Object.keys(item.children || {}).length > 0;
|
|
3589
|
+
if (level === 0) {
|
|
3590
|
+
return /* @__PURE__ */ jsxs("div", { children: [
|
|
3591
|
+
/* @__PURE__ */ jsx("div", { class: "group-label", children: key }),
|
|
3592
|
+
hasChildren && /* @__PURE__ */ jsx("div", { class: "tree-node", style: "margin-left: 0", children: /* @__PURE__ */ jsx(NavNode, { node: item, level: level + 1, disableSourceView }) })
|
|
3593
|
+
] }, key);
|
|
3594
|
+
}
|
|
3595
|
+
const isLeaf = item.isLeaf;
|
|
3596
|
+
return /* @__PURE__ */ jsxs("div", { children: [
|
|
3597
|
+
isLeaf ? /* @__PURE__ */ jsx(LeafNode, { item, label: key, disableSourceView }) : /* @__PURE__ */ jsx("div", { class: "tree-item", style: "color: var(--text-muted)", children: /* @__PURE__ */ jsx("span", { class: "tree-label", children: key }) }),
|
|
3598
|
+
hasChildren && /* @__PURE__ */ jsx("div", { class: "tree-node", children: /* @__PURE__ */ jsx(NavNode, { node: item, level: level + 1, disableSourceView }) })
|
|
3599
|
+
] }, key);
|
|
3600
|
+
}) });
|
|
3601
|
+
}
|
|
3602
|
+
function LeafNode({ item, label, disableSourceView }) {
|
|
3603
|
+
const isWarning = item.data?.op?.["x-warning"];
|
|
3604
|
+
const opId = item.data?.name;
|
|
3605
|
+
const sourceInfo = item.data?.op?.["x-source-info"];
|
|
3606
|
+
let content;
|
|
3607
|
+
if (isWarning) {
|
|
3608
|
+
content = /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
3609
|
+
/* @__PURE__ */ jsx("span", { style: "margin-right: 6px;", children: "⚠️" }),
|
|
3610
|
+
/* @__PURE__ */ jsx("span", { class: "tree-label", children: label })
|
|
3611
|
+
] });
|
|
3612
|
+
} else {
|
|
3613
|
+
const badgeText = item.data.type === "publish" ? "SEND" : "RECV";
|
|
3614
|
+
content = /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
3615
|
+
/* @__PURE__ */ jsx("span", { class: `badge badge-${badgeText}`, children: badgeText }),
|
|
3616
|
+
/* @__PURE__ */ jsx("span", { class: "tree-label", children: label })
|
|
3617
|
+
] });
|
|
3618
|
+
}
|
|
3619
|
+
return /* @__PURE__ */ jsxs("div", { class: "tree-item", "data-event": opId, style: isWarning ? "color: #fbbf24" : "", children: [
|
|
3620
|
+
content,
|
|
3621
|
+
sourceInfo && !disableSourceView && /* @__PURE__ */ jsx(
|
|
3622
|
+
"a",
|
|
3623
|
+
{
|
|
3624
|
+
href: `vscode://file/${sourceInfo.file}:${sourceInfo.line}`,
|
|
3625
|
+
class: "source-link",
|
|
3626
|
+
onClick: (e) => {
|
|
3627
|
+
e.stopPropagation();
|
|
3628
|
+
},
|
|
3629
|
+
title: `${sourceInfo.file}:${sourceInfo.line}`,
|
|
3630
|
+
children: /* @__PURE__ */ jsxs("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", style: "display:block", children: [
|
|
3631
|
+
/* @__PURE__ */ jsx("polyline", { points: "16 18 22 12 16 6" }),
|
|
3632
|
+
/* @__PURE__ */ jsx("polyline", { points: "8 6 2 12 8 18" })
|
|
3633
|
+
] })
|
|
3634
|
+
}
|
|
3635
|
+
)
|
|
3636
|
+
] });
|
|
3637
|
+
}
|
|
3638
|
+
function MainContent() {
|
|
3639
|
+
return /* @__PURE__ */ jsxs("div", { id: "main-wrapper", style: "flex: 1; min-width: 0; position: relative; overflow: hidden;", children: [
|
|
3640
|
+
/* @__PURE__ */ jsx("button", { id: "btn-expand-nav", class: "btn-icon floating-toggle left", title: "Expand Sidebar", style: "display:none;", children: /* @__PURE__ */ jsx("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", children: /* @__PURE__ */ jsx("polyline", { points: "9 18 15 12 9 6" }) }) }),
|
|
3641
|
+
/* @__PURE__ */ jsx("button", { id: "btn-expand-console", class: "btn-icon floating-toggle right", title: "Expand Console", style: "display:none;", children: /* @__PURE__ */ jsx("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", children: /* @__PURE__ */ jsx("polyline", { points: "15 18 9 12 15 6" }) }) }),
|
|
3642
|
+
/* @__PURE__ */ jsx("main", { class: "main-content scroller", id: "doc-panel", style: "height: 100%;", children: /* @__PURE__ */ jsxs("div", { class: "empty-state", children: [
|
|
3643
|
+
/* @__PURE__ */ jsxs("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "1.5", children: [
|
|
3644
|
+
/* @__PURE__ */ jsx("path", { d: "M4 19.5A2.5 2.5 0 0 1 6.5 17H20" }),
|
|
3645
|
+
/* @__PURE__ */ 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" })
|
|
3646
|
+
] }),
|
|
3647
|
+
/* @__PURE__ */ jsx("h3", { children: "Select an event to view details" })
|
|
3648
|
+
] }) })
|
|
3649
|
+
] });
|
|
3650
|
+
}
|
|
3651
|
+
function ConsolePanel({ serverUrl }) {
|
|
3652
|
+
return /* @__PURE__ */ jsxs("div", { class: "console-panel", id: "console-panel", children: [
|
|
3653
|
+
/* @__PURE__ */ jsxs("div", { class: "console-header", children: [
|
|
3654
|
+
/* @__PURE__ */ jsxs("div", { style: "display:flex; justify-content:space-between; align-items:center; margin-bottom: 8px;", children: [
|
|
3655
|
+
/* @__PURE__ */ jsx("h3", { style: "margin:0; font-size:1rem;", children: "Console" }),
|
|
3656
|
+
/* @__PURE__ */ jsxs("div", { style: "display:flex; gap: 4px;", children: [
|
|
3657
|
+
/* @__PURE__ */ jsx("button", { id: "btn-maximize-console", class: "btn-icon", title: "Maximize Console", children: /* @__PURE__ */ jsx("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", children: /* @__PURE__ */ jsx("rect", { x: "3", y: "3", width: "18", height: "18", rx: "2", ry: "2" }) }) }),
|
|
3658
|
+
/* @__PURE__ */ jsx("button", { id: "btn-collapse-console", class: "btn-icon", title: "Collapse Console", children: /* @__PURE__ */ jsx("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", children: /* @__PURE__ */ jsx("polyline", { points: "9 18 15 12 9 6" }) }) })
|
|
3659
|
+
] })
|
|
3660
|
+
] }),
|
|
3661
|
+
/* @__PURE__ */ jsxs("div", { class: "connection-bar", children: [
|
|
3662
|
+
/* @__PURE__ */ jsxs("select", { id: "protocol", children: [
|
|
3663
|
+
/* @__PURE__ */ jsx("option", { value: "ws", children: "WS" }),
|
|
3664
|
+
/* @__PURE__ */ jsx("option", { value: "wss", children: "WSS" }),
|
|
3665
|
+
/* @__PURE__ */ jsx("option", { value: "socket.io", children: "Socket.IO" })
|
|
3666
|
+
] }),
|
|
3667
|
+
/* @__PURE__ */ jsx("div", { style: "width: 1px; background: rgba(255,255,255,0.1); margin: 2px 0;" }),
|
|
3668
|
+
/* @__PURE__ */ jsx("input", { type: "text", id: "url", value: serverUrl })
|
|
3669
|
+
] }),
|
|
3670
|
+
/* @__PURE__ */ jsxs("div", { style: "display: grid; grid-template-columns: 1fr auto; gap: 8px;", children: [
|
|
3671
|
+
/* @__PURE__ */ jsx("button", { id: "connect-btn", class: "btn", children: "Connect" }),
|
|
3672
|
+
/* @__PURE__ */ jsx("button", { id: "clear-logs-btn", class: "btn secondary", title: "Clear Logs", children: /* @__PURE__ */ jsx("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", children: /* @__PURE__ */ 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" }) }) })
|
|
3673
|
+
] }),
|
|
3674
|
+
/* @__PURE__ */ jsxs("div", { class: "status-indicator", children: [
|
|
3675
|
+
/* @__PURE__ */ jsx("div", { id: "status-dot", class: "dot" }),
|
|
3676
|
+
/* @__PURE__ */ jsx("span", { id: "connection-status", children: "Disconnected" })
|
|
3677
|
+
] })
|
|
3678
|
+
] }),
|
|
3679
|
+
/* @__PURE__ */ jsx("div", { class: "logs-container scroller", id: "logs", children: /* @__PURE__ */ jsx("div", { class: "log-shim", id: "log-shim" }) }),
|
|
3680
|
+
/* @__PURE__ */ jsxs("div", { class: "compose-area", children: [
|
|
3681
|
+
/* @__PURE__ */ jsxs("div", { class: "compose-header", children: [
|
|
3682
|
+
/* @__PURE__ */ jsx("span", { children: "Payload" }),
|
|
3683
|
+
/* @__PURE__ */ jsx("span", { id: "target-event", style: "color: var(--primary);", children: "--" })
|
|
3684
|
+
] }),
|
|
3685
|
+
/* @__PURE__ */ jsx("div", { id: "editor-container" }),
|
|
3686
|
+
/* @__PURE__ */ jsx("div", { class: "send-bar", children: /* @__PURE__ */ jsx("button", { id: "send-btn", class: "btn", children: "Send Message" }) })
|
|
3687
|
+
] })
|
|
3688
|
+
] });
|
|
3689
|
+
}
|
|
3690
|
+
function buildNavTree(spec) {
|
|
3691
|
+
if (!spec || !spec.channels) return { children: {} };
|
|
3692
|
+
const root = { children: {} };
|
|
3693
|
+
Object.keys(spec.channels).forEach((name) => {
|
|
3694
|
+
const ch = spec.channels[name];
|
|
3695
|
+
const op = ch.publish || ch.subscribe;
|
|
3696
|
+
const type = ch.publish ? "publish" : "subscribe";
|
|
3697
|
+
const tag = op.tags && op.tags.length > 0 ? op.tags[0].name : "General";
|
|
3698
|
+
if (!root.children[tag]) root.children[tag] = { children: {} };
|
|
3699
|
+
const parts = name.split(/[\.\/]/);
|
|
3700
|
+
let current = root.children[tag];
|
|
3701
|
+
parts.forEach((part, i) => {
|
|
3702
|
+
if (!current.children[part]) current.children[part] = { children: {} };
|
|
3703
|
+
current = current.children[part];
|
|
3704
|
+
if (i === parts.length - 1) {
|
|
3705
|
+
current.isLeaf = true;
|
|
3706
|
+
current.data = { name, op, type };
|
|
3707
|
+
}
|
|
3708
|
+
});
|
|
3709
|
+
});
|
|
3710
|
+
return root;
|
|
3711
|
+
}
|
|
3712
|
+
async function getAstRoutes(applications) {
|
|
3713
|
+
const astRoutes = [];
|
|
3714
|
+
const getExpandedRoutes = (app, prefix = "", seen = /* @__PURE__ */ new Set()) => {
|
|
3715
|
+
if (seen.has(app.name)) return [];
|
|
3716
|
+
const newSeen = new Set(seen);
|
|
3717
|
+
newSeen.add(app.name);
|
|
3718
|
+
const expanded = [];
|
|
3719
|
+
for (const route of app.routes) {
|
|
3720
|
+
expanded.push({
|
|
3721
|
+
...route,
|
|
3722
|
+
// For events, path is the event name
|
|
3723
|
+
path: route.path.startsWith("/") ? route.path.slice(1) : route.path
|
|
3724
|
+
});
|
|
3725
|
+
}
|
|
3726
|
+
if (app.mounted) {
|
|
3727
|
+
for (const mount of app.mounted) {
|
|
3728
|
+
const targetApp = applications.find((a) => a.name === mount.target || a.className === mount.target);
|
|
3729
|
+
if (targetApp) {
|
|
3730
|
+
expanded.push(...getExpandedRoutes(targetApp, "", newSeen));
|
|
3731
|
+
}
|
|
3732
|
+
}
|
|
3733
|
+
}
|
|
3734
|
+
return expanded;
|
|
3735
|
+
};
|
|
3736
|
+
applications.forEach((app) => {
|
|
3737
|
+
astRoutes.push(...getExpandedRoutes(app));
|
|
3738
|
+
});
|
|
3739
|
+
return astRoutes;
|
|
3740
|
+
}
|
|
3741
|
+
async function generateAsyncApi(rootRouter, options = {}) {
|
|
3742
|
+
const channels = {};
|
|
3743
|
+
let astRoutes = [];
|
|
3744
|
+
try {
|
|
3745
|
+
const { OpenAPIAnalyzer: OpenAPIAnalyzer2 } = await import("./analyzer-BqIe1p0R.js");
|
|
3746
|
+
const entrypoint = globalThis.Bun?.main || require.main?.filename || process.argv[1];
|
|
3747
|
+
const analyzer = new OpenAPIAnalyzer2(process.cwd(), entrypoint);
|
|
3748
|
+
const { applications } = await analyzer.analyze();
|
|
3749
|
+
astRoutes = await getAstRoutes(applications);
|
|
3750
|
+
} catch (e) {
|
|
3751
|
+
}
|
|
3752
|
+
const matchedAstRoutes = /* @__PURE__ */ new Set();
|
|
3753
|
+
const collect = async (router, prefix = "") => {
|
|
3754
|
+
const eventHandlers = router.getEventHandlers();
|
|
3755
|
+
let routerTag = "Other";
|
|
3756
|
+
if (router[$isApplication]) {
|
|
3757
|
+
routerTag = "Application";
|
|
3758
|
+
} else if (router.constructor.name && router.constructor.name !== "ShokupanRouter") {
|
|
3759
|
+
routerTag = router.constructor.name;
|
|
3760
|
+
} else {
|
|
3761
|
+
routerTag = router[$mountPath] || "Router";
|
|
3762
|
+
}
|
|
3763
|
+
if (eventHandlers) {
|
|
3764
|
+
for (const [eventName, handlers] of eventHandlers.entries()) {
|
|
3765
|
+
for (const handler of handlers) {
|
|
3766
|
+
const userSpec = handler.spec;
|
|
3767
|
+
let tags = userSpec?.tags;
|
|
3768
|
+
if (!tags && routerTag) {
|
|
3769
|
+
tags = [{ name: routerTag }];
|
|
3770
|
+
}
|
|
3771
|
+
let astMatch = astRoutes.find(
|
|
3772
|
+
(r) => (r.method === "EVENT" || r.method === "ON") && r.path === eventName
|
|
3773
|
+
);
|
|
3774
|
+
if (!astMatch) {
|
|
3775
|
+
const runtimeSource = (handler.originalHandler || handler).toString();
|
|
3776
|
+
const stripComments = (s) => s.replace(/\/\*[\s\S]*?\*\/|([^\\:]|^)\/\/.*$/gm, "$1");
|
|
3777
|
+
const normalize = (s) => stripComments(s).replace(/\s+/g, "");
|
|
3778
|
+
const runtimeHandlerSrc = normalize(runtimeSource);
|
|
3779
|
+
const eventRoutes = astRoutes.filter((r) => r.method === "EVENT" || r.method === "ON");
|
|
3780
|
+
astMatch = eventRoutes.find((r) => {
|
|
3781
|
+
const astHandlerSrc = normalize(r.handlerSource || r.handlerName || "");
|
|
3782
|
+
if (!astHandlerSrc || astHandlerSrc.length < 5) return false;
|
|
3783
|
+
return runtimeHandlerSrc.includes(astHandlerSrc) || astHandlerSrc.includes(runtimeHandlerSrc) || r.handlerSource && runtimeHandlerSrc.includes(normalize(r.handlerSource).substring(0, 50));
|
|
3784
|
+
});
|
|
3785
|
+
}
|
|
3786
|
+
if (astMatch) matchedAstRoutes.add(astMatch);
|
|
3787
|
+
const sourceInfo = handler.source || astMatch?.sourceContext ? {
|
|
3788
|
+
file: handler.source?.file || astMatch?.sourceContext?.file,
|
|
3789
|
+
line: handler.source?.line || astMatch?.sourceContext?.startLine,
|
|
3790
|
+
startLine: handler.source?.line || astMatch?.sourceContext?.startLine,
|
|
3791
|
+
endLine: astMatch?.sourceContext?.endLine,
|
|
3792
|
+
highlightLines: astMatch?.sourceContext ? [astMatch.sourceContext.startLine, astMatch.sourceContext.endLine] : void 0
|
|
3793
|
+
} : void 0;
|
|
3794
|
+
if (!channels[eventName]) {
|
|
3795
|
+
channels[eventName] = {
|
|
3796
|
+
publish: {
|
|
3797
|
+
operationId: `on${eventName.charAt(0).toUpperCase() + eventName.slice(1)}`,
|
|
3798
|
+
tags,
|
|
3799
|
+
message: {
|
|
3800
|
+
payload: { type: "object" },
|
|
3801
|
+
...userSpec?.message ? userSpec.message : {}
|
|
3802
|
+
},
|
|
3803
|
+
...userSpec?.type === "publish" ? userSpec : {},
|
|
3804
|
+
"x-source-info": sourceInfo ? [sourceInfo] : [],
|
|
3805
|
+
"x-shokupan-source": sourceInfo
|
|
3806
|
+
// Simplified
|
|
3807
|
+
}
|
|
3808
|
+
};
|
|
3809
|
+
if (userSpec?.summary) channels[eventName].publish.summary = userSpec.summary;
|
|
3810
|
+
if (userSpec?.description) channels[eventName].publish.description = userSpec.description;
|
|
3811
|
+
} else {
|
|
3812
|
+
if (sourceInfo) {
|
|
3813
|
+
if (!channels[eventName].publish["x-source-info"]) {
|
|
3814
|
+
channels[eventName].publish["x-source-info"] = [];
|
|
3815
|
+
}
|
|
3816
|
+
const exists = channels[eventName].publish["x-source-info"].some(
|
|
3817
|
+
(s) => s.file === sourceInfo.file && s.line === sourceInfo.line
|
|
3818
|
+
);
|
|
3819
|
+
if (!exists) {
|
|
3820
|
+
channels[eventName].publish["x-source-info"].push(sourceInfo);
|
|
3821
|
+
}
|
|
3822
|
+
}
|
|
3823
|
+
}
|
|
3824
|
+
let emits = astMatch?.emits || [];
|
|
3825
|
+
for (const emit of emits) {
|
|
3826
|
+
if (emit.event === "__DYNAMIC_EMIT__") {
|
|
3827
|
+
const warningKey = `${eventName}/Dynamic Emit`;
|
|
3828
|
+
channels[warningKey] = {
|
|
3829
|
+
subscribe: {
|
|
3830
|
+
operationId: `dynamicEmitWarning${eventName}`,
|
|
3831
|
+
summary: "Dynamic Emit Detected",
|
|
3832
|
+
description: "This handler emits an event with a dynamic name that could not be determined statically.",
|
|
3833
|
+
tags,
|
|
3834
|
+
"x-warning": true,
|
|
3835
|
+
"x-source-info": {
|
|
3836
|
+
file: astMatch?.sourceContext?.file,
|
|
3837
|
+
line: emit.location?.startLine,
|
|
3838
|
+
startLine: emit.location?.startLine,
|
|
3839
|
+
endLine: emit.location?.endLine,
|
|
3840
|
+
highlightLines: emit.location ? [emit.location.startLine, emit.location.endLine] : void 0
|
|
3841
|
+
},
|
|
3842
|
+
"x-shokupan-source": {
|
|
3843
|
+
file: astMatch?.sourceContext?.file,
|
|
3844
|
+
line: emit.location?.startLine
|
|
3845
|
+
},
|
|
3846
|
+
message: { payload: { type: "object" } }
|
|
3847
|
+
}
|
|
3848
|
+
};
|
|
3849
|
+
continue;
|
|
3850
|
+
}
|
|
3851
|
+
const emitStart = emit.location?.startLine;
|
|
3852
|
+
const emitEnd = emit.location?.endLine;
|
|
3853
|
+
const newSourceInfo = sourceInfo && emitStart ? {
|
|
3854
|
+
file: sourceInfo.file,
|
|
3855
|
+
line: emitStart,
|
|
3856
|
+
startLine: emitStart,
|
|
3857
|
+
endLine: emitEnd,
|
|
3858
|
+
highlightLines: sourceInfo.highlightLines,
|
|
3859
|
+
emitHighlightLines: [emitStart, emitEnd]
|
|
3860
|
+
} : void 0;
|
|
3861
|
+
if (!channels[emit.event]) {
|
|
3862
|
+
channels[emit.event] = {
|
|
3863
|
+
subscribe: {
|
|
3864
|
+
operationId: `emit${emit.event.charAt(0).toUpperCase() + emit.event.slice(1)}`,
|
|
3865
|
+
tags,
|
|
3866
|
+
message: {
|
|
3867
|
+
payload: emit.payload || { type: "object" }
|
|
3868
|
+
},
|
|
3869
|
+
"x-source-info": newSourceInfo ? [newSourceInfo] : [],
|
|
3870
|
+
"x-shokupan-source": sourceInfo && emitStart ? {
|
|
3871
|
+
file: sourceInfo.file,
|
|
3872
|
+
line: emitStart
|
|
3873
|
+
} : void 0
|
|
3874
|
+
}
|
|
3875
|
+
};
|
|
3876
|
+
} else {
|
|
3877
|
+
if (newSourceInfo) {
|
|
3878
|
+
if (!channels[emit.event].subscribe["x-source-info"]) {
|
|
3879
|
+
channels[emit.event].subscribe["x-source-info"] = [];
|
|
3880
|
+
}
|
|
3881
|
+
const existing = channels[emit.event].subscribe["x-source-info"];
|
|
3882
|
+
const exists = existing.some(
|
|
3883
|
+
(s) => s.file === newSourceInfo.file && s.line === newSourceInfo.line
|
|
3884
|
+
);
|
|
3885
|
+
if (!exists) {
|
|
3886
|
+
existing.push(newSourceInfo);
|
|
3887
|
+
}
|
|
3888
|
+
}
|
|
3889
|
+
}
|
|
3890
|
+
}
|
|
3891
|
+
}
|
|
3892
|
+
}
|
|
3893
|
+
}
|
|
3894
|
+
const httpRoutes = router[$routes];
|
|
3895
|
+
if (httpRoutes) {
|
|
3896
|
+
for (const route of httpRoutes) {
|
|
3897
|
+
const handler = route.handler;
|
|
3898
|
+
let tags = route.handlerSpec?.tags;
|
|
3899
|
+
if (!tags && routerTag) {
|
|
3900
|
+
tags = [{ name: routerTag }];
|
|
3901
|
+
}
|
|
3902
|
+
const methodUpper = route.method.toUpperCase();
|
|
3903
|
+
let astMatch = astRoutes.find(
|
|
3904
|
+
(r) => r.method === methodUpper && (r.path === route.path || r.path === "/" + route.path)
|
|
3905
|
+
);
|
|
3906
|
+
if (!astMatch) {
|
|
3907
|
+
const runtimeSource = (handler.originalHandler || handler).toString();
|
|
3908
|
+
const runtimeHandlerSrc = runtimeSource.replace(/\s+/g, " ");
|
|
3909
|
+
const sameMethodRoutes = astRoutes.filter((r) => r.method === methodUpper);
|
|
3910
|
+
astMatch = sameMethodRoutes.find((r) => {
|
|
3911
|
+
const astHandlerSrc = (r.handlerSource || r.handlerName || "").replace(/\s+/g, " ");
|
|
3912
|
+
if (!astHandlerSrc || astHandlerSrc.length < 20) return false;
|
|
3913
|
+
return runtimeHandlerSrc.includes(astHandlerSrc) || astHandlerSrc.includes(runtimeHandlerSrc) || r.handlerSource && runtimeHandlerSrc.includes(r.handlerSource.substring(0, 50));
|
|
3914
|
+
});
|
|
3915
|
+
}
|
|
3916
|
+
const sourceInfo = handler.source || astMatch?.sourceContext ? {
|
|
3917
|
+
file: handler.source?.file || astMatch?.sourceContext?.file,
|
|
3918
|
+
line: handler.source?.line || astMatch?.sourceContext?.startLine,
|
|
3919
|
+
startLine: handler.source?.line || astMatch?.sourceContext?.startLine,
|
|
3920
|
+
endLine: astMatch?.sourceContext?.endLine,
|
|
3921
|
+
highlightLines: astMatch?.sourceContext ? [astMatch.sourceContext.startLine, astMatch.sourceContext.endLine] : void 0
|
|
3922
|
+
} : void 0;
|
|
3923
|
+
let emits = astMatch?.emits || [];
|
|
3924
|
+
for (const emit of emits) {
|
|
3925
|
+
const emitStart = emit.location?.startLine;
|
|
3926
|
+
const emitEnd = emit.location?.endLine;
|
|
3927
|
+
const newSourceInfo = sourceInfo && emitStart ? {
|
|
3928
|
+
file: sourceInfo.file,
|
|
3929
|
+
line: emitStart,
|
|
3930
|
+
startLine: emitStart,
|
|
3931
|
+
endLine: emitEnd,
|
|
3932
|
+
highlightLines: sourceInfo.highlightLines,
|
|
3933
|
+
emitHighlightLines: [emitStart, emitEnd]
|
|
3934
|
+
} : void 0;
|
|
3935
|
+
if (!channels[emit.event]) {
|
|
3936
|
+
channels[emit.event] = {
|
|
3937
|
+
subscribe: {
|
|
3938
|
+
operationId: `emit${emit.event.charAt(0).toUpperCase() + emit.event.slice(1)}`,
|
|
3939
|
+
tags,
|
|
3940
|
+
message: {
|
|
3941
|
+
payload: emit.payload || { type: "object" }
|
|
3942
|
+
},
|
|
3943
|
+
"x-source-info": newSourceInfo ? [newSourceInfo] : [],
|
|
3944
|
+
"x-shokupan-source": sourceInfo && emitStart ? {
|
|
3945
|
+
file: sourceInfo.file,
|
|
3946
|
+
line: emitStart
|
|
3947
|
+
} : void 0
|
|
3948
|
+
}
|
|
3949
|
+
};
|
|
3950
|
+
} else {
|
|
3951
|
+
if (newSourceInfo) {
|
|
3952
|
+
if (!channels[emit.event].subscribe["x-source-info"]) {
|
|
3953
|
+
channels[emit.event].subscribe["x-source-info"] = [];
|
|
3954
|
+
}
|
|
3955
|
+
const existing = channels[emit.event].subscribe["x-source-info"];
|
|
3956
|
+
const exists = existing.some(
|
|
3957
|
+
(s) => s.file === newSourceInfo.file && s.line === newSourceInfo.line
|
|
3958
|
+
);
|
|
3959
|
+
if (!exists) {
|
|
3960
|
+
existing.push(newSourceInfo);
|
|
3961
|
+
}
|
|
3962
|
+
}
|
|
3963
|
+
}
|
|
3964
|
+
}
|
|
3965
|
+
}
|
|
3966
|
+
}
|
|
3967
|
+
const childRouters = router[$childRouters];
|
|
3968
|
+
for (const child of childRouters) {
|
|
3969
|
+
await collect(child);
|
|
3970
|
+
}
|
|
3971
|
+
};
|
|
3972
|
+
await collect(rootRouter);
|
|
3973
|
+
const dynamicEvents = astRoutes.filter((r) => r.path === "__DYNAMIC_EVENT__" && !matchedAstRoutes.has(r));
|
|
3974
|
+
dynamicEvents.forEach((r, i) => {
|
|
3975
|
+
let prefix = "Anonymous";
|
|
3976
|
+
if (r.handlerName && !r.handlerName.includes("=>") && !r.handlerName.includes("{")) {
|
|
3977
|
+
const parts = r.handlerName.split(".");
|
|
3978
|
+
if (parts.length > 0) prefix = parts[0];
|
|
3979
|
+
}
|
|
3980
|
+
const key = `${prefix}.Dynamic Event ${i + 1}`;
|
|
3981
|
+
channels[key] = {
|
|
3982
|
+
publish: {
|
|
3983
|
+
operationId: `dynamicEventWarning${i}`,
|
|
3984
|
+
summary: "Dynamic Event Detected",
|
|
3985
|
+
description: `A dynamic event listener was detected in your source code but the event name could not be determined statically.`,
|
|
3986
|
+
tags: [{ name: "Warnings" }],
|
|
3987
|
+
"x-warning": true,
|
|
3988
|
+
"x-source-info": {
|
|
3989
|
+
file: r.sourceContext?.file,
|
|
3990
|
+
line: r.sourceContext?.startLine,
|
|
3991
|
+
startLine: r.sourceContext?.startLine,
|
|
3992
|
+
endLine: r.sourceContext?.endLine,
|
|
3993
|
+
highlightLines: r.sourceContext ? [r.sourceContext.startLine, r.sourceContext.endLine] : void 0
|
|
3994
|
+
},
|
|
3995
|
+
"x-shokupan-source": {
|
|
3996
|
+
file: r.sourceContext?.file,
|
|
3997
|
+
line: r.sourceContext?.startLine
|
|
3998
|
+
},
|
|
3999
|
+
message: { payload: { type: "object" } }
|
|
4000
|
+
}
|
|
4001
|
+
};
|
|
4002
|
+
});
|
|
4003
|
+
return {
|
|
4004
|
+
asyncapi: "3.0.0",
|
|
4005
|
+
info: { title: "Shokupan AsyncAPI", version: "1.0.0", ...options.info },
|
|
4006
|
+
channels
|
|
4007
|
+
};
|
|
4008
|
+
}
|
|
4009
|
+
const generator = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
|
|
4010
|
+
__proto__: null,
|
|
4011
|
+
generateAsyncApi
|
|
4012
|
+
}, Symbol.toStringTag, { value: "Module" }));
|
|
4013
|
+
class AsyncApiPlugin extends ShokupanRouter {
|
|
4014
|
+
constructor(pluginOptions = {}) {
|
|
4015
|
+
super({ renderer: renderToString });
|
|
4016
|
+
this.pluginOptions = pluginOptions;
|
|
4017
|
+
this.pluginOptions.path ??= "/asyncapi";
|
|
4018
|
+
this.init();
|
|
4019
|
+
}
|
|
4020
|
+
static getBasePath() {
|
|
4021
|
+
const dir = dirname(fileURLToPath(import.meta.url));
|
|
4022
|
+
if (dir.endsWith("dist")) {
|
|
4023
|
+
return dir + "/plugins/application/asyncapi";
|
|
4024
|
+
}
|
|
4025
|
+
return dir;
|
|
4026
|
+
}
|
|
4027
|
+
onInit(app, options) {
|
|
4028
|
+
const path = this.pluginOptions.path || options?.path || "/asyncapi";
|
|
4029
|
+
app.mount(path, this);
|
|
4030
|
+
if (app.applicationConfig.enableAsyncApiGen !== true) {
|
|
4031
|
+
console.warn("AsyncApiPlugin: enableAsyncApiGen is disabled. AsyncApiPlugin will not generate spec.");
|
|
4032
|
+
}
|
|
4033
|
+
}
|
|
4034
|
+
init() {
|
|
4035
|
+
const serveFile = async (ctx, file, type) => {
|
|
4036
|
+
const content = await readFile(join$1(AsyncApiPlugin.getBasePath(), "static", file), "utf-8");
|
|
4037
|
+
ctx.set("Content-Type", type);
|
|
4038
|
+
return ctx.send(content);
|
|
4039
|
+
};
|
|
4040
|
+
this.get("/style.css", (ctx) => serveFile(ctx, "style.css", "text/css"));
|
|
4041
|
+
this.get("/theme.css", (ctx) => serveFile(ctx, "theme.css", "text/css"));
|
|
4042
|
+
this.get("/asyncapi-client.mjs", (ctx) => serveFile(ctx, "asyncapi-client.mjs", "application/javascript"));
|
|
4043
|
+
this.get("/", async (ctx) => {
|
|
4044
|
+
let spec = ctx.app?.asyncApiSpec;
|
|
4045
|
+
if (!spec) {
|
|
4046
|
+
spec = await generateAsyncApi(ctx.app);
|
|
4047
|
+
}
|
|
4048
|
+
if (this.pluginOptions.spec) {
|
|
4049
|
+
deepMerge(spec, this.pluginOptions.spec);
|
|
4050
|
+
}
|
|
4051
|
+
const serverUrl = `${ctx.hostname}:${ctx.app?.applicationConfig.port}`;
|
|
4052
|
+
const base = this.pluginOptions.path;
|
|
4053
|
+
const disableSourceView = this.pluginOptions.disableSourceView;
|
|
4054
|
+
const navTree = buildNavTree(spec);
|
|
4055
|
+
return ctx.jsx(AsyncApiApp({ spec, serverUrl, base, disableSourceView, navTree }));
|
|
4056
|
+
});
|
|
4057
|
+
this.get("/json", async (ctx) => {
|
|
4058
|
+
let spec = ctx.app?.asyncApiSpec;
|
|
4059
|
+
if (!spec) {
|
|
4060
|
+
spec = await generateAsyncApi(ctx.app);
|
|
4061
|
+
}
|
|
4062
|
+
if (this.pluginOptions.spec) {
|
|
4063
|
+
deepMerge(spec, this.pluginOptions.spec);
|
|
4064
|
+
}
|
|
4065
|
+
return ctx.json(spec);
|
|
4066
|
+
});
|
|
4067
|
+
this.get("/_code", async (ctx) => {
|
|
4068
|
+
const file = ctx.query["file"];
|
|
4069
|
+
if (!file || typeof file !== "string") {
|
|
4070
|
+
return ctx.text("Missing file parameter", 400);
|
|
4071
|
+
}
|
|
4072
|
+
try {
|
|
4073
|
+
const content = await readFile(file, "utf8");
|
|
4074
|
+
return ctx.text(content);
|
|
4075
|
+
} catch (e) {
|
|
4076
|
+
return ctx.text("File not found: " + e.message, 404);
|
|
4077
|
+
}
|
|
4078
|
+
});
|
|
4079
|
+
}
|
|
4080
|
+
}
|
|
3246
4081
|
class AuthPlugin extends ShokupanRouter {
|
|
3247
4082
|
constructor(authConfig) {
|
|
3248
4083
|
super();
|
|
3249
4084
|
this.authConfig = authConfig;
|
|
3250
4085
|
this.secret = typeof authConfig.jwtSecret === "string" ? new TextEncoder().encode(authConfig.jwtSecret) : authConfig.jwtSecret;
|
|
3251
|
-
this.init();
|
|
3252
4086
|
}
|
|
3253
4087
|
secret;
|
|
3254
|
-
|
|
4088
|
+
arctic;
|
|
4089
|
+
jose;
|
|
4090
|
+
async onInit(app, options) {
|
|
4091
|
+
this.arctic = await import("arctic");
|
|
4092
|
+
this.jose = await import("jose");
|
|
4093
|
+
this.init();
|
|
3255
4094
|
if (options?.path) {
|
|
3256
4095
|
app.mount(options.path, this);
|
|
3257
4096
|
} else {
|
|
@@ -3259,6 +4098,7 @@ class AuthPlugin extends ShokupanRouter {
|
|
|
3259
4098
|
}
|
|
3260
4099
|
}
|
|
3261
4100
|
getProviderInstance(name, p) {
|
|
4101
|
+
const { GitHub, Google, MicrosoftEntraId, Apple, Auth0, Okta, OAuth2Client } = this.arctic;
|
|
3262
4102
|
switch (name) {
|
|
3263
4103
|
case "github":
|
|
3264
4104
|
return new GitHub(p.clientId, p.clientSecret, p.redirectUri);
|
|
@@ -3286,7 +4126,7 @@ class AuthPlugin extends ShokupanRouter {
|
|
|
3286
4126
|
}
|
|
3287
4127
|
async createSession(user, ctx) {
|
|
3288
4128
|
const alg = "HS256";
|
|
3289
|
-
const jwt = await new jose.SignJWT({ ...user }).setProtectedHeader({ alg }).setIssuedAt().setExpirationTime(this.authConfig.jwtExpiration || "24h").sign(this.secret);
|
|
4129
|
+
const jwt = await new this.jose.SignJWT({ ...user }).setProtectedHeader({ alg }).setIssuedAt().setExpirationTime(this.authConfig.jwtExpiration || "24h").sign(this.secret);
|
|
3290
4130
|
const opts = this.authConfig.cookieOptions || {};
|
|
3291
4131
|
let cookie = `auth_token=${jwt}; Path=${opts.path || "/"}; HttpOnly`;
|
|
3292
4132
|
if (opts.secure) cookie += "; Secure";
|
|
@@ -3296,6 +4136,7 @@ class AuthPlugin extends ShokupanRouter {
|
|
|
3296
4136
|
return jwt;
|
|
3297
4137
|
}
|
|
3298
4138
|
init() {
|
|
4139
|
+
const { generateState, generateCodeVerifier, GitHub, Google, MicrosoftEntraId, Apple, Auth0, Okta, OAuth2Client } = this.arctic;
|
|
3299
4140
|
const providerEntries = Object.entries(this.authConfig.providers);
|
|
3300
4141
|
for (let i = 0; i < providerEntries.length; i++) {
|
|
3301
4142
|
const [providerName, providerConfig] = providerEntries[i];
|
|
@@ -3427,7 +4268,7 @@ class AuthPlugin extends ShokupanRouter {
|
|
|
3427
4268
|
};
|
|
3428
4269
|
} else if (provider === "apple") {
|
|
3429
4270
|
if (idToken) {
|
|
3430
|
-
const payload = jose.decodeJwt(idToken);
|
|
4271
|
+
const payload = this.jose.decodeJwt(idToken);
|
|
3431
4272
|
user = {
|
|
3432
4273
|
id: payload.sub,
|
|
3433
4274
|
email: payload["email"],
|
|
@@ -3458,6 +4299,9 @@ class AuthPlugin extends ShokupanRouter {
|
|
|
3458
4299
|
*/
|
|
3459
4300
|
getMiddleware() {
|
|
3460
4301
|
return async (ctx, next) => {
|
|
4302
|
+
if (!this.jose) {
|
|
4303
|
+
this.jose = await import("jose");
|
|
4304
|
+
}
|
|
3461
4305
|
const authHeader = ctx.req.headers.get("Authorization");
|
|
3462
4306
|
let token = authHeader?.startsWith("Bearer ") ? authHeader.substring(7) : null;
|
|
3463
4307
|
if (!token) {
|
|
@@ -3466,7 +4310,7 @@ class AuthPlugin extends ShokupanRouter {
|
|
|
3466
4310
|
}
|
|
3467
4311
|
if (token) {
|
|
3468
4312
|
try {
|
|
3469
|
-
const { payload } = await jose.jwtVerify(token, this.secret);
|
|
4313
|
+
const { payload } = await this.jose.jwtVerify(token, this.secret);
|
|
3470
4314
|
ctx.user = payload;
|
|
3471
4315
|
} catch {
|
|
3472
4316
|
}
|
|
@@ -3583,6 +4427,187 @@ class ClusterPlugin {
|
|
|
3583
4427
|
}
|
|
3584
4428
|
}
|
|
3585
4429
|
}
|
|
4430
|
+
function DashboardApp({ metrics, uptime, integrations, base, getRequestHeadersSource }) {
|
|
4431
|
+
return /* @__PURE__ */ jsxs("html", { lang: "en", children: [
|
|
4432
|
+
/* @__PURE__ */ jsxs("head", { children: [
|
|
4433
|
+
/* @__PURE__ */ jsx("meta", { charSet: "UTF-8" }),
|
|
4434
|
+
/* @__PURE__ */ jsx("meta", { name: "viewport", content: "width=device-width, initial-scale=1.0" }),
|
|
4435
|
+
/* @__PURE__ */ jsx("title", { children: "Shokupan Debug Dashboard" }),
|
|
4436
|
+
/* @__PURE__ */ jsx("link", { href: "https://unpkg.com/tabulator-tables@5.5.0/dist/css/tabulator_bootstrap5.min.css", rel: "stylesheet" }),
|
|
4437
|
+
/* @__PURE__ */ jsx("link", { rel: "stylesheet", href: "https://esm.sh/@xyflow/react@12.3.6/dist/style.css" }),
|
|
4438
|
+
/* @__PURE__ */ jsx("link", { rel: "stylesheet", href: `${base}/theme.css` }),
|
|
4439
|
+
/* @__PURE__ */ jsx("link", { rel: "stylesheet", href: `${base}/styles.css` }),
|
|
4440
|
+
/* @__PURE__ */ jsx("link", { rel: "stylesheet", href: `${base}/reactflow.css` }),
|
|
4441
|
+
/* @__PURE__ */ jsx("link", { rel: "stylesheet", href: `${base}/registry.css` }),
|
|
4442
|
+
/* @__PURE__ */ jsx("link", { rel: "stylesheet", href: `${base}/tabulator.css` }),
|
|
4443
|
+
/* @__PURE__ */ jsx("script", { src: "https://cdn.jsdelivr.net/npm/chart.js" }),
|
|
4444
|
+
/* @__PURE__ */ jsx("script", { type: "text/javascript", src: "https://unpkg.com/tabulator-tables@5.5.0/dist/js/tabulator.min.js" })
|
|
4445
|
+
] }),
|
|
4446
|
+
/* @__PURE__ */ jsxs("body", { children: [
|
|
4447
|
+
/* @__PURE__ */ jsxs("div", { class: "container", children: [
|
|
4448
|
+
/* @__PURE__ */ jsxs("header", { children: [
|
|
4449
|
+
/* @__PURE__ */ jsxs("div", { children: [
|
|
4450
|
+
/* @__PURE__ */ jsx("h1", { children: "Dashboard" }),
|
|
4451
|
+
/* @__PURE__ */ jsxs("div", { style: "color: var(--text-secondary)", children: [
|
|
4452
|
+
"Uptime: ",
|
|
4453
|
+
/* @__PURE__ */ jsx("span", { id: "uptime", children: uptime })
|
|
4454
|
+
] })
|
|
4455
|
+
] }),
|
|
4456
|
+
/* @__PURE__ */ jsxs("div", { class: "tabs", children: [
|
|
4457
|
+
/* @__PURE__ */ jsx("button", { class: "tab-btn active", onclick: "switchTab('overview')", children: "Overview" }),
|
|
4458
|
+
/* @__PURE__ */ jsx("button", { class: "tab-btn", onclick: "switchTab('registry')", children: "Registry" }),
|
|
4459
|
+
/* @__PURE__ */ jsx("button", { class: "tab-btn", onclick: "switchTab('graph')", children: "Graph" }),
|
|
4460
|
+
/* @__PURE__ */ jsx("button", { class: "tab-btn", onclick: "switchTab('requests')", children: "Requests" }),
|
|
4461
|
+
/* @__PURE__ */ jsx("button", { class: "tab-btn", onclick: "switchTab('failures')", children: "Failures" }),
|
|
4462
|
+
integrations.scalar && /* @__PURE__ */ jsx("button", { class: "tab-btn", onclick: "switchTab('scalar')", children: "API Reference" }),
|
|
4463
|
+
integrations.asyncapi && /* @__PURE__ */ jsx("button", { class: "tab-btn", onclick: "switchTab('asyncapi')", children: "AsyncAPI" })
|
|
4464
|
+
] })
|
|
4465
|
+
] }),
|
|
4466
|
+
/* @__PURE__ */ jsxs("div", { id: "tab-overview", class: "tab-content active", children: [
|
|
4467
|
+
/* @__PURE__ */ jsx(MetricsGrid, { metrics }),
|
|
4468
|
+
/* @__PURE__ */ jsxs("div", { id: "chart-container", style: "display: flex; flex-direction: column; gap: 1rem;", children: [
|
|
4469
|
+
/* @__PURE__ */ jsx("div", { style: "display: flex; justify-content: flex-end;", children: /* @__PURE__ */ jsxs("select", { id: "time-range-selector", onchange: "updateCharts(); updateDashboard(); fetchTopStats();", style: "background: var(--bg-primary); color: var(--text-primary); border: 1px solid var(--card-border); padding: 5px; border-radius: 4px;", children: [
|
|
4470
|
+
/* @__PURE__ */ jsx("option", { value: "1m", children: "1 Minute" }),
|
|
4471
|
+
/* @__PURE__ */ jsx("option", { value: "5m", children: "5 Minutes" }),
|
|
4472
|
+
/* @__PURE__ */ jsx("option", { value: "30m", children: "30 Minutes" }),
|
|
4473
|
+
/* @__PURE__ */ jsx("option", { value: "1h", children: "1 Hour" }),
|
|
4474
|
+
/* @__PURE__ */ jsx("option", { value: "2h", children: "2 Hours" }),
|
|
4475
|
+
/* @__PURE__ */ jsx("option", { value: "6h", children: "6 Hours" }),
|
|
4476
|
+
/* @__PURE__ */ jsx("option", { value: "12h", children: "12 Hours" }),
|
|
4477
|
+
/* @__PURE__ */ jsx("option", { value: "1d", children: "1 Day" }),
|
|
4478
|
+
/* @__PURE__ */ jsx("option", { value: "3d", children: "3 Days" }),
|
|
4479
|
+
/* @__PURE__ */ jsx("option", { value: "7d", children: "7 Days" }),
|
|
4480
|
+
/* @__PURE__ */ jsx("option", { value: "30d", children: "30 Days" })
|
|
4481
|
+
] }) }),
|
|
4482
|
+
/* @__PURE__ */ jsxs("div", { class: "card-container", children: [
|
|
4483
|
+
/* @__PURE__ */ jsx(ChartCard, { title: "Response Time", id: "latencyChart" }),
|
|
4484
|
+
/* @__PURE__ */ jsx(ChartCard, { title: "Requests / Second", id: "rpsChart" }),
|
|
4485
|
+
/* @__PURE__ */ jsx(ChartCard, { title: "CPU & Load", id: "cpuChart" }),
|
|
4486
|
+
/* @__PURE__ */ jsx(ChartCard, { title: "Memory", id: "memoryChart" }),
|
|
4487
|
+
/* @__PURE__ */ jsx(ChartCard, { title: "Heap Usage", id: "heapChart" }),
|
|
4488
|
+
/* @__PURE__ */ jsx(ChartCard, { title: "Event Loop Latency", id: "eventLoopChart" }),
|
|
4489
|
+
/* @__PURE__ */ jsx(ChartCard, { title: "Error Rate", id: "errorRateChart" })
|
|
4490
|
+
] }),
|
|
4491
|
+
/* @__PURE__ */ jsx("div", { class: "card-title", style: "margin-top: 1rem;", children: "Top Statistics" }),
|
|
4492
|
+
/* @__PURE__ */ jsxs("div", { class: "card-container", children: [
|
|
4493
|
+
/* @__PURE__ */ jsx(Card, { title: "Top Requests", contentId: "top-requests-table" }),
|
|
4494
|
+
/* @__PURE__ */ jsx(Card, { title: "Top Errors", contentId: "top-errors-table" }),
|
|
4495
|
+
/* @__PURE__ */ jsx(Card, { title: "Most Frequent Failures", contentId: "failing-requests-table" }),
|
|
4496
|
+
/* @__PURE__ */ jsx(Card, { title: "Slowest Requests", contentId: "slowest-requests-table" })
|
|
4497
|
+
] }),
|
|
4498
|
+
/* @__PURE__ */ jsx("div", { id: "table-container", style: "padding: 0; margin-top: 1rem;", children: /* @__PURE__ */ jsx("div", { id: "requests-table", class: "table-dark" }) })
|
|
4499
|
+
] })
|
|
4500
|
+
] }),
|
|
4501
|
+
/* @__PURE__ */ jsx("div", { id: "tab-registry", class: "tab-content", children: /* @__PURE__ */ jsxs("div", { id: "registry-container", class: "card", style: "margin-top: 2rem;", children: [
|
|
4502
|
+
/* @__PURE__ */ jsx("div", { class: "card-title", children: "Component Registry" }),
|
|
4503
|
+
/* @__PURE__ */ jsx("div", { id: "registry-tree", style: "padding: 0 1rem 1rem 1rem; font-family: monospace; font-size: 0.9rem;" })
|
|
4504
|
+
] }) }),
|
|
4505
|
+
/* @__PURE__ */ jsxs("div", { id: "tab-graph", class: "tab-content", children: [
|
|
4506
|
+
/* @__PURE__ */ jsx("div", { class: "card", style: "margin-bottom: 1rem;", children: /* @__PURE__ */ jsx("div", { style: "display: flex; gap: 1rem;", children: /* @__PURE__ */ jsx("input", { type: "text", id: "graph-search", placeholder: "Search routes or middleware...", "aria-label": "Search routes or middleware", style: "flex:1; padding: 0.5rem; border-radius: 0.5rem; background: var(--bg-primary); border: 1px solid var(--card-border); color: var(--text-primary);" }) }) }),
|
|
4507
|
+
/* @__PURE__ */ jsx("div", { id: "cy" })
|
|
4508
|
+
] }),
|
|
4509
|
+
/* @__PURE__ */ jsxs("div", { id: "tab-requests", class: "tab-content", children: [
|
|
4510
|
+
/* @__PURE__ */ jsxs("div", { class: "card", style: "margin-bottom: 1rem; display: flex; justify-content: space-between; align-items: center;", children: [
|
|
4511
|
+
/* @__PURE__ */ jsx("div", { class: "card-title", children: "Recent Requests (Last 100)" }),
|
|
4512
|
+
/* @__PURE__ */ jsx("div", { children: /* @__PURE__ */ jsx("button", { onclick: "fetchRequests()", style: "background: var(--bg-primary); color: var(--text-primary); border: 1px solid var(--card-border); padding: 5px 10px; border-radius: 4px; cursor: pointer;", children: "Refresh" }) })
|
|
4513
|
+
] }),
|
|
4514
|
+
/* @__PURE__ */ jsx("div", { id: "requests-list-container", style: "height: calc(100vh - 300px); margin-bottom: 2rem;" }),
|
|
4515
|
+
/* @__PURE__ */ jsxs("div", { id: "request-details-container", class: "card", style: "display: none;", children: [
|
|
4516
|
+
/* @__PURE__ */ jsx("div", { class: "card-title", children: "Request Details" }),
|
|
4517
|
+
/* @__PURE__ */ jsx("div", { id: "request-details-content" }),
|
|
4518
|
+
/* @__PURE__ */ jsx("div", { class: "card-title", style: "margin-top: 1rem;", children: "Middleware Trace" }),
|
|
4519
|
+
/* @__PURE__ */ jsx("div", { id: "middleware-trace-container" })
|
|
4520
|
+
] })
|
|
4521
|
+
] }),
|
|
4522
|
+
/* @__PURE__ */ jsxs("div", { id: "tab-failures", class: "tab-content", children: [
|
|
4523
|
+
/* @__PURE__ */ jsxs("div", { class: "card", style: "margin-bottom: 1rem; display: flex; justify-content: space-between; align-items: center;", children: [
|
|
4524
|
+
/* @__PURE__ */ jsx("div", { class: "card-title", children: "Failed Requests (Last 50)" }),
|
|
4525
|
+
/* @__PURE__ */ jsxs("div", { children: [
|
|
4526
|
+
/* @__PURE__ */ 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" }),
|
|
4527
|
+
/* @__PURE__ */ 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" })
|
|
4528
|
+
] })
|
|
4529
|
+
] }),
|
|
4530
|
+
/* @__PURE__ */ jsx("div", { id: "failures-table-container" })
|
|
4531
|
+
] }),
|
|
4532
|
+
integrations.scalar && /* @__PURE__ */ jsx("div", { id: "tab-scalar", class: "tab-content", style: "margin: 0; overflow: hidden; height: 100%; max-width: unset", children: /* @__PURE__ */ jsx("iframe", { src: integrations.scalar, style: "width: 100%; height: 100%; border: none;" }) }),
|
|
4533
|
+
integrations.asyncapi && /* @__PURE__ */ jsx("div", { id: "tab-asyncapi", class: "tab-content", style: "margin: 0; overflow: hidden; height: 100%; max-width: unset", children: /* @__PURE__ */ jsx("iframe", { src: integrations.asyncapi, style: "width: 100%; height: 100%; border: none;" }) })
|
|
4534
|
+
] }),
|
|
4535
|
+
/* @__PURE__ */ jsx("script", { dangerouslySetInnerHTML: {
|
|
4536
|
+
__html: `
|
|
4537
|
+
// Injected function from server config
|
|
4538
|
+
const getRequestHeaders = ${getRequestHeadersSource};
|
|
4539
|
+
`
|
|
4540
|
+
} }),
|
|
4541
|
+
/* @__PURE__ */ jsx("script", { src: `${base}/poll.js` }),
|
|
4542
|
+
/* @__PURE__ */ jsx("script", { src: `${base}/graph.mjs`, type: "module" }),
|
|
4543
|
+
/* @__PURE__ */ jsx("script", { src: `${base}/charts.js` }),
|
|
4544
|
+
/* @__PURE__ */ jsx("script", { src: `${base}/tables.js` }),
|
|
4545
|
+
/* @__PURE__ */ jsx("script", { src: `${base}/registry.js` }),
|
|
4546
|
+
/* @__PURE__ */ jsx("script", { src: `${base}/failures.js` }),
|
|
4547
|
+
/* @__PURE__ */ jsx("script", { src: `${base}/requests.js` }),
|
|
4548
|
+
/* @__PURE__ */ jsx("script", { src: `${base}/tabs.js` })
|
|
4549
|
+
] })
|
|
4550
|
+
] });
|
|
4551
|
+
}
|
|
4552
|
+
function MetricsGrid({ metrics }) {
|
|
4553
|
+
const total = metrics.totalRequests;
|
|
4554
|
+
const active = metrics.activeRequests;
|
|
4555
|
+
const finished = total - active;
|
|
4556
|
+
const successRate = finished ? Math.round(metrics.successfulRequests / finished * 100) : 100;
|
|
4557
|
+
const failRate = finished ? Math.round(metrics.failedRequests / finished * 100) : 0;
|
|
4558
|
+
return /* @__PURE__ */ jsxs("div", { class: "metrics-grid", children: [
|
|
4559
|
+
/* @__PURE__ */ jsxs("div", { class: "card", children: [
|
|
4560
|
+
/* @__PURE__ */ jsx("div", { class: "card-title", children: "Total Requests" }),
|
|
4561
|
+
/* @__PURE__ */ jsx("div", { class: "card-value", id: "total-requests", children: metrics.totalRequests })
|
|
4562
|
+
] }),
|
|
4563
|
+
/* @__PURE__ */ jsxs("div", { class: "card", children: [
|
|
4564
|
+
/* @__PURE__ */ jsx("div", { class: "card-title", children: "Active Requests" }),
|
|
4565
|
+
/* @__PURE__ */ jsx("div", { class: "card-value", style: "color: var(--accent)", id: "active-requests", children: metrics.activeRequests })
|
|
4566
|
+
] }),
|
|
4567
|
+
/* @__PURE__ */ jsxs("div", { class: "card", children: [
|
|
4568
|
+
/* @__PURE__ */ jsx("div", { class: "card-title", children: "Success Rate" }),
|
|
4569
|
+
/* @__PURE__ */ jsx("div", { class: "card-value text-success", children: /* @__PURE__ */ jsxs("span", { id: "success-rate", children: [
|
|
4570
|
+
successRate,
|
|
4571
|
+
"%"
|
|
4572
|
+
] }) }),
|
|
4573
|
+
/* @__PURE__ */ jsxs("div", { style: "color: var(--text-secondary); margin-top: 0.5rem", children: [
|
|
4574
|
+
/* @__PURE__ */ jsx("span", { id: "successful-requests", children: metrics.successfulRequests }),
|
|
4575
|
+
" successful"
|
|
4576
|
+
] })
|
|
4577
|
+
] }),
|
|
4578
|
+
/* @__PURE__ */ jsxs("div", { class: "card", children: [
|
|
4579
|
+
/* @__PURE__ */ jsx("div", { class: "card-title", children: "Fail Rate" }),
|
|
4580
|
+
/* @__PURE__ */ jsx("div", { class: "card-value text-error", children: /* @__PURE__ */ jsxs("span", { id: "fail-rate", children: [
|
|
4581
|
+
failRate,
|
|
4582
|
+
"%"
|
|
4583
|
+
] }) }),
|
|
4584
|
+
/* @__PURE__ */ jsxs("div", { style: "color: var(--text-secondary); margin-top: 0.5rem", children: [
|
|
4585
|
+
/* @__PURE__ */ jsx("span", { id: "failed-requests", children: metrics.failedRequests }),
|
|
4586
|
+
" failed"
|
|
4587
|
+
] })
|
|
4588
|
+
] }),
|
|
4589
|
+
/* @__PURE__ */ jsxs("div", { class: "card", children: [
|
|
4590
|
+
/* @__PURE__ */ jsx("div", { class: "card-title", children: "Avg Latency" }),
|
|
4591
|
+
/* @__PURE__ */ jsxs("div", { class: "card-value", children: [
|
|
4592
|
+
/* @__PURE__ */ jsx("span", { id: "avg-latency", children: metrics.averageTotalTime_ms.toFixed(2) }),
|
|
4593
|
+
" ",
|
|
4594
|
+
/* @__PURE__ */ jsx("span", { style: "font-size: 1rem; color: var(--text-secondary)", children: "ms" })
|
|
4595
|
+
] })
|
|
4596
|
+
] })
|
|
4597
|
+
] });
|
|
4598
|
+
}
|
|
4599
|
+
function ChartCard({ title, id }) {
|
|
4600
|
+
return /* @__PURE__ */ jsxs("div", { class: "card", style: "height: 300px;", children: [
|
|
4601
|
+
/* @__PURE__ */ jsx("div", { class: "card-title", children: title }),
|
|
4602
|
+
/* @__PURE__ */ jsx("div", { class: "card-chart", children: /* @__PURE__ */ jsx("canvas", { id }) })
|
|
4603
|
+
] });
|
|
4604
|
+
}
|
|
4605
|
+
function Card({ title, contentId }) {
|
|
4606
|
+
return /* @__PURE__ */ jsxs("div", { class: "card", children: [
|
|
4607
|
+
/* @__PURE__ */ jsx("div", { class: "card-title", children: title }),
|
|
4608
|
+
/* @__PURE__ */ jsx("div", { id: contentId })
|
|
4609
|
+
] });
|
|
4610
|
+
}
|
|
3586
4611
|
const INTERVALS = [
|
|
3587
4612
|
{ label: "10s", ms: 10 * 1e3 },
|
|
3588
4613
|
{ label: "1m", ms: 60 * 1e3 },
|
|
@@ -3597,19 +4622,19 @@ const INTERVALS = [
|
|
|
3597
4622
|
{ label: "30d", ms: 30 * 24 * 60 * 60 * 1e3 }
|
|
3598
4623
|
];
|
|
3599
4624
|
class MetricsCollector {
|
|
3600
|
-
|
|
3601
|
-
|
|
3602
|
-
eventLoopHistogram = monitorEventLoopDelay({ resolution: 10 });
|
|
3603
|
-
timer = null;
|
|
3604
|
-
constructor() {
|
|
4625
|
+
constructor(db) {
|
|
4626
|
+
this.db = db;
|
|
3605
4627
|
this.eventLoopHistogram.enable();
|
|
3606
4628
|
const now = Date.now();
|
|
3607
4629
|
INTERVALS.forEach((int) => {
|
|
3608
4630
|
this.currentIntervalStart[int.label] = this.alignTimestamp(now, int.ms);
|
|
3609
4631
|
this.pendingDetails[int.label] = [];
|
|
3610
4632
|
});
|
|
3611
|
-
this.timer = setInterval(() => this.collect(), 1e4);
|
|
3612
4633
|
}
|
|
4634
|
+
currentIntervalStart = {};
|
|
4635
|
+
pendingDetails = {};
|
|
4636
|
+
eventLoopHistogram = monitorEventLoopDelay({ resolution: 10 });
|
|
4637
|
+
timer = null;
|
|
3613
4638
|
recordRequest(duration, isError) {
|
|
3614
4639
|
INTERVALS.forEach((int) => {
|
|
3615
4640
|
this.pendingDetails[int.label].push({ duration, isError });
|
|
@@ -3621,11 +4646,9 @@ class MetricsCollector {
|
|
|
3621
4646
|
async collect() {
|
|
3622
4647
|
try {
|
|
3623
4648
|
const now = Date.now();
|
|
3624
|
-
console.log("[MetricsCollector] collect() called at", new Date(now).toISOString());
|
|
3625
4649
|
for (const int of INTERVALS) {
|
|
3626
4650
|
const start = this.currentIntervalStart[int.label];
|
|
3627
4651
|
if (now >= start + int.ms) {
|
|
3628
|
-
console.log(`[MetricsCollector] Flushing ${int.label} interval (boundary crossed)`);
|
|
3629
4652
|
await this.flushInterval(int.label, start, int.ms);
|
|
3630
4653
|
this.currentIntervalStart[int.label] = this.alignTimestamp(now, int.ms);
|
|
3631
4654
|
}
|
|
@@ -3636,10 +4659,8 @@ class MetricsCollector {
|
|
|
3636
4659
|
}
|
|
3637
4660
|
async flushInterval(label, timestamp, durationMs) {
|
|
3638
4661
|
const reqs = this.pendingDetails[label];
|
|
3639
|
-
console.log(`[MetricsCollector] flushInterval(${label}) - ${reqs.length} requests pending`);
|
|
3640
4662
|
this.pendingDetails[label] = [];
|
|
3641
4663
|
if (reqs.length === 0) {
|
|
3642
|
-
console.log(`[MetricsCollector] No requests for ${label}, skipping persist`);
|
|
3643
4664
|
return;
|
|
3644
4665
|
}
|
|
3645
4666
|
const totalReqs = reqs.length;
|
|
@@ -3689,15 +4710,11 @@ class MetricsCollector {
|
|
|
3689
4710
|
p99: getP(0.99)
|
|
3690
4711
|
}
|
|
3691
4712
|
};
|
|
3692
|
-
console.log(`[MetricsCollector] Persisting ${label} metric at timestamp ${timestamp}`);
|
|
3693
4713
|
try {
|
|
3694
4714
|
const recordId = new RecordId("metrics", timestamp);
|
|
3695
|
-
await
|
|
3696
|
-
|
|
3697
|
-
const
|
|
3698
|
-
console.log(`[MetricsCollector] DEBUG: Immediate .get() returned:`, test ? "DATA" : "NULL");
|
|
3699
|
-
const queryTest = await datastore.query("SELECT * FROM metrics WHERE id = $id", { id: recordId });
|
|
3700
|
-
console.log(`[MetricsCollector] DEBUG: Query by id returned ${queryTest[0]?.length || 0} records`);
|
|
4715
|
+
await this.db.upsert(recordId, metric);
|
|
4716
|
+
const test = await this.db.select(recordId);
|
|
4717
|
+
const queryTest = await this.db.query("SELECT * FROM metrics WHERE id = $id", { id: recordId });
|
|
3701
4718
|
} catch (e) {
|
|
3702
4719
|
console.error(`[MetricsCollector] ✗ Failed to save metrics for ${label}:`, e);
|
|
3703
4720
|
}
|
|
@@ -3732,16 +4749,8 @@ class Dashboard {
|
|
|
3732
4749
|
constructor(dashboardConfig = {}) {
|
|
3733
4750
|
this.dashboardConfig = dashboardConfig;
|
|
3734
4751
|
}
|
|
3735
|
-
|
|
3736
|
-
|
|
3737
|
-
static getBasePath() {
|
|
3738
|
-
const dir = dirname(fileURLToPath(import.meta.url));
|
|
3739
|
-
if (dir.endsWith("dist")) {
|
|
3740
|
-
return dir + "/plugins/application/dashboard";
|
|
3741
|
-
}
|
|
3742
|
-
return dir;
|
|
3743
|
-
}
|
|
3744
|
-
router = new ShokupanRouter();
|
|
4752
|
+
[$appRoot];
|
|
4753
|
+
router = new ShokupanRouter({ renderer: renderToString });
|
|
3745
4754
|
metrics = {
|
|
3746
4755
|
totalRequests: 0,
|
|
3747
4756
|
successfulRequests: 0,
|
|
@@ -3754,16 +4763,16 @@ class Dashboard {
|
|
|
3754
4763
|
nodeMetrics: {},
|
|
3755
4764
|
edgeMetrics: {}
|
|
3756
4765
|
};
|
|
3757
|
-
eta = new Eta({
|
|
3758
|
-
views: Dashboard.getBasePath() + "/static",
|
|
3759
|
-
cache: false
|
|
3760
|
-
});
|
|
3761
4766
|
startTime = Date.now();
|
|
3762
4767
|
instrumented = false;
|
|
3763
|
-
metricsCollector
|
|
4768
|
+
metricsCollector;
|
|
4769
|
+
get db() {
|
|
4770
|
+
return this[$appRoot].db;
|
|
4771
|
+
}
|
|
3764
4772
|
// ShokupanPlugin interface implementation
|
|
3765
4773
|
onInit(app, options) {
|
|
3766
4774
|
this[$appRoot] = app;
|
|
4775
|
+
this.metricsCollector = new MetricsCollector(this.db);
|
|
3767
4776
|
const mountPath = options?.path || this.dashboardConfig.path || "/dashboard";
|
|
3768
4777
|
const hooks = this.getHooks();
|
|
3769
4778
|
if (!app.middleware) {
|
|
@@ -3783,6 +4792,47 @@ class Dashboard {
|
|
|
3783
4792
|
app.mount(mountPath, this.router);
|
|
3784
4793
|
this.setupRoutes();
|
|
3785
4794
|
}
|
|
4795
|
+
detectIntegrations() {
|
|
4796
|
+
const integrations = {};
|
|
4797
|
+
const routers = this[$appRoot]?.[$childRouters] || [];
|
|
4798
|
+
const checkConfig = (key) => {
|
|
4799
|
+
const conf = this.dashboardConfig.integrations?.[key];
|
|
4800
|
+
if (conf === false) return { enabled: false };
|
|
4801
|
+
if (typeof conf === "object" && conf.path) return { enabled: true, path: conf.path };
|
|
4802
|
+
return { enabled: true };
|
|
4803
|
+
};
|
|
4804
|
+
const scalarConf = checkConfig("scalar");
|
|
4805
|
+
if (scalarConf.enabled) {
|
|
4806
|
+
if (scalarConf.path) {
|
|
4807
|
+
integrations["scalar"] = scalarConf.path;
|
|
4808
|
+
} else {
|
|
4809
|
+
const plugin = routers.find((r) => r.constructor.name === "ScalarPlugin");
|
|
4810
|
+
if (plugin) {
|
|
4811
|
+
integrations["scalar"] = plugin[$mountPath];
|
|
4812
|
+
}
|
|
4813
|
+
}
|
|
4814
|
+
}
|
|
4815
|
+
const asyncApiConf = checkConfig("asyncapi");
|
|
4816
|
+
if (asyncApiConf.enabled) {
|
|
4817
|
+
if (asyncApiConf.path) {
|
|
4818
|
+
integrations["asyncapi"] = asyncApiConf.path;
|
|
4819
|
+
} else {
|
|
4820
|
+
const plugin = routers.find((r) => r.constructor.name === "AsyncApiPlugin");
|
|
4821
|
+
if (plugin) {
|
|
4822
|
+
integrations["asyncapi"] = plugin[$mountPath];
|
|
4823
|
+
}
|
|
4824
|
+
}
|
|
4825
|
+
}
|
|
4826
|
+
return integrations;
|
|
4827
|
+
}
|
|
4828
|
+
// Get base path for dashboard files - works in both dev (src/) and production (dist/)
|
|
4829
|
+
static getBasePath() {
|
|
4830
|
+
const dir = dirname(fileURLToPath(import.meta.url));
|
|
4831
|
+
if (dir.endsWith("dist")) {
|
|
4832
|
+
return dir + "/plugins/application/dashboard";
|
|
4833
|
+
}
|
|
4834
|
+
return dir;
|
|
4835
|
+
}
|
|
3786
4836
|
setupRoutes() {
|
|
3787
4837
|
this.router.get("/metrics", async (ctx) => {
|
|
3788
4838
|
const uptimeSeconds = Math.floor((Date.now() - this.startTime) / 1e3);
|
|
@@ -3807,7 +4857,7 @@ class Dashboard {
|
|
|
3807
4857
|
const startTime = Date.now() - ms;
|
|
3808
4858
|
let stats;
|
|
3809
4859
|
try {
|
|
3810
|
-
stats = await
|
|
4860
|
+
stats = await this.db.query(`
|
|
3811
4861
|
SELECT
|
|
3812
4862
|
count() as total,
|
|
3813
4863
|
count(IF status < 400 THEN 1 END) as success,
|
|
@@ -3868,7 +4918,7 @@ class Dashboard {
|
|
|
3868
4918
|
const periodMs = intervalMap[interval] || 60 * 1e3;
|
|
3869
4919
|
const startTime = Date.now() - periodMs * 3;
|
|
3870
4920
|
const endTime = Date.now();
|
|
3871
|
-
const result = await
|
|
4921
|
+
const result = await this.db.query(
|
|
3872
4922
|
"SELECT * FROM metrics WHERE timestamp >= $start AND timestamp <= $end AND interval = $interval ORDER BY timestamp ASC",
|
|
3873
4923
|
{ start: startTime, end: endTime, interval }
|
|
3874
4924
|
);
|
|
@@ -3897,7 +4947,7 @@ class Dashboard {
|
|
|
3897
4947
|
};
|
|
3898
4948
|
this.router.get("/requests/top", async (ctx) => {
|
|
3899
4949
|
const startTime = getIntervalStartTime(ctx.query["interval"]);
|
|
3900
|
-
const result = await
|
|
4950
|
+
const result = await this.db.query(
|
|
3901
4951
|
"SELECT method, url, count() as count FROM requests WHERE timestamp >= $start GROUP BY method, url ORDER BY count DESC LIMIT 10",
|
|
3902
4952
|
{ start: startTime }
|
|
3903
4953
|
);
|
|
@@ -3905,7 +4955,7 @@ class Dashboard {
|
|
|
3905
4955
|
});
|
|
3906
4956
|
this.router.get("/errors/top", async (ctx) => {
|
|
3907
4957
|
const startTime = getIntervalStartTime(ctx.query["interval"]);
|
|
3908
|
-
const result = await
|
|
4958
|
+
const result = await this.db.query(
|
|
3909
4959
|
"SELECT status, count() as count FROM failed_requests WHERE timestamp >= $start GROUP BY status ORDER BY count DESC LIMIT 10",
|
|
3910
4960
|
{ start: startTime }
|
|
3911
4961
|
);
|
|
@@ -3913,7 +4963,7 @@ class Dashboard {
|
|
|
3913
4963
|
});
|
|
3914
4964
|
this.router.get("/requests/failing", async (ctx) => {
|
|
3915
4965
|
const startTime = getIntervalStartTime(ctx.query["interval"]);
|
|
3916
|
-
const result = await
|
|
4966
|
+
const result = await this.db.query(
|
|
3917
4967
|
"SELECT method, url, count() as count FROM failed_requests WHERE timestamp >= $start GROUP BY method, url ORDER BY count DESC LIMIT 10",
|
|
3918
4968
|
{ start: startTime }
|
|
3919
4969
|
);
|
|
@@ -3921,7 +4971,7 @@ class Dashboard {
|
|
|
3921
4971
|
});
|
|
3922
4972
|
this.router.get("/requests/slowest", async (ctx) => {
|
|
3923
4973
|
const startTime = getIntervalStartTime(ctx.query["interval"]);
|
|
3924
|
-
const result = await
|
|
4974
|
+
const result = await this.db.query(
|
|
3925
4975
|
"SELECT method, url, duration, status, timestamp FROM requests WHERE timestamp >= $start ORDER BY duration DESC LIMIT 10",
|
|
3926
4976
|
{ start: startTime }
|
|
3927
4977
|
);
|
|
@@ -3939,15 +4989,15 @@ class Dashboard {
|
|
|
3939
4989
|
return ctx.json({ registry: registry || {} });
|
|
3940
4990
|
});
|
|
3941
4991
|
this.router.get("/requests", async (ctx) => {
|
|
3942
|
-
const result = await
|
|
4992
|
+
const result = await this.db.query("SELECT * FROM requests ORDER BY timestamp DESC LIMIT 100");
|
|
3943
4993
|
return ctx.json({ requests: result[0] || [] });
|
|
3944
4994
|
});
|
|
3945
4995
|
this.router.get("/requests/:id", async (ctx) => {
|
|
3946
|
-
const result = await
|
|
4996
|
+
const result = await this.db.query("SELECT * FROM requests WHERE id = $id", { id: ctx.params["id"] });
|
|
3947
4997
|
return ctx.json({ request: result[0]?.[0] });
|
|
3948
4998
|
});
|
|
3949
4999
|
this.router.get("/failures", async (ctx) => {
|
|
3950
|
-
const result = await
|
|
5000
|
+
const result = await this.db.query("SELECT * FROM failed_requests ORDER BY timestamp DESC LIMIT 50");
|
|
3951
5001
|
return ctx.json({ failures: result[0] });
|
|
3952
5002
|
});
|
|
3953
5003
|
this.router.post("/replay", async (ctx) => {
|
|
@@ -3971,18 +5021,51 @@ class Dashboard {
|
|
|
3971
5021
|
return ctx.json({ error: String(e) }, 500);
|
|
3972
5022
|
}
|
|
3973
5023
|
});
|
|
3974
|
-
this.router.get("
|
|
5024
|
+
this.router.get("/**", async (ctx) => {
|
|
5025
|
+
const mountPath = this.router[$mountPath] || this.dashboardConfig.path || "/dashboard";
|
|
5026
|
+
let relativePath = ctx.path;
|
|
5027
|
+
if (relativePath.startsWith(mountPath)) {
|
|
5028
|
+
relativePath = relativePath.slice(mountPath.length);
|
|
5029
|
+
}
|
|
5030
|
+
if (relativePath.startsWith("/")) {
|
|
5031
|
+
relativePath = relativePath.slice(1);
|
|
5032
|
+
}
|
|
5033
|
+
const path = relativePath;
|
|
5034
|
+
const staticFiles = [
|
|
5035
|
+
"charts.js",
|
|
5036
|
+
"failures.js",
|
|
5037
|
+
"graph.mjs",
|
|
5038
|
+
"poll.js",
|
|
5039
|
+
"reactflow.css",
|
|
5040
|
+
"registry.css",
|
|
5041
|
+
"registry.js",
|
|
5042
|
+
"requests.js",
|
|
5043
|
+
"styles.css",
|
|
5044
|
+
"tables.js",
|
|
5045
|
+
"tabs.js",
|
|
5046
|
+
"tabulator.css",
|
|
5047
|
+
"theme.css"
|
|
5048
|
+
];
|
|
5049
|
+
if (staticFiles.includes(path)) {
|
|
5050
|
+
const content = await readFile(join$1(Dashboard.getBasePath(), "static", path), "utf-8");
|
|
5051
|
+
if (path.endsWith(".css")) ctx.set("Content-Type", "text/css");
|
|
5052
|
+
else if (path.endsWith(".js") || path.endsWith(".mjs")) ctx.set("Content-Type", "application/javascript");
|
|
5053
|
+
return ctx.send(content);
|
|
5054
|
+
}
|
|
3975
5055
|
const uptimeSeconds = Math.floor((Date.now() - this.startTime) / 1e3);
|
|
3976
5056
|
const uptime = `${Math.floor(uptimeSeconds / 3600)}h ${Math.floor(uptimeSeconds % 3600 / 60)}m ${uptimeSeconds % 60}s`;
|
|
3977
|
-
|
|
3978
|
-
const
|
|
3979
|
-
|
|
5057
|
+
this.getLinkPattern();
|
|
5058
|
+
const integrations = this.detectIntegrations();
|
|
5059
|
+
const getRequestHeadersSource = this.dashboardConfig.getRequestHeaders ? this.dashboardConfig.getRequestHeaders.toString() : "undefined";
|
|
5060
|
+
const html = renderToString(DashboardApp({
|
|
3980
5061
|
metrics: this.metrics,
|
|
3981
5062
|
uptime,
|
|
3982
5063
|
rootPath: process.cwd(),
|
|
3983
|
-
|
|
3984
|
-
|
|
5064
|
+
integrations,
|
|
5065
|
+
base: mountPath,
|
|
5066
|
+
getRequestHeadersSource
|
|
3985
5067
|
}));
|
|
5068
|
+
return ctx.html(`<!DOCTYPE html>${html}`);
|
|
3986
5069
|
});
|
|
3987
5070
|
}
|
|
3988
5071
|
instrumentApp(app) {
|
|
@@ -4078,7 +5161,7 @@ class Dashboard {
|
|
|
4078
5161
|
headers[k] = v;
|
|
4079
5162
|
});
|
|
4080
5163
|
}
|
|
4081
|
-
await
|
|
5164
|
+
await this.db.upsert(new RecordId("failed_requests", ctx.requestId), {
|
|
4082
5165
|
method: ctx.method,
|
|
4083
5166
|
url: ctx.url.toString(),
|
|
4084
5167
|
headers,
|
|
@@ -4103,7 +5186,7 @@ class Dashboard {
|
|
|
4103
5186
|
};
|
|
4104
5187
|
this.metrics.logs.push(logEntry);
|
|
4105
5188
|
try {
|
|
4106
|
-
await
|
|
5189
|
+
await this.db.upsert(new RecordId("requests", ctx.requestId), logEntry);
|
|
4107
5190
|
} catch (e) {
|
|
4108
5191
|
console.error("Failed to record request log", e);
|
|
4109
5192
|
}
|
|
@@ -4131,32 +5214,169 @@ class Dashboard {
|
|
|
4131
5214
|
function unknownError(ctx) {
|
|
4132
5215
|
return ctx.json({ error: "Unknown Error" }, 500);
|
|
4133
5216
|
}
|
|
4134
|
-
|
|
5217
|
+
class GraphQLApolloPlugin extends ShokupanRouter {
|
|
5218
|
+
// Use generic any or verify type
|
|
5219
|
+
constructor(pluginOptions) {
|
|
5220
|
+
super();
|
|
5221
|
+
this.pluginOptions = pluginOptions;
|
|
5222
|
+
this.pluginOptions.path ??= "/graphql";
|
|
5223
|
+
}
|
|
5224
|
+
apolloServer;
|
|
5225
|
+
async onInit(app, options) {
|
|
5226
|
+
const { ApolloServer, HeaderMap } = await import("@apollo/server");
|
|
5227
|
+
this.apolloServer = new ApolloServer({
|
|
5228
|
+
typeDefs: this.pluginOptions.typeDefs,
|
|
5229
|
+
resolvers: this.pluginOptions.resolvers,
|
|
5230
|
+
...this.pluginOptions.apolloConfig || {}
|
|
5231
|
+
});
|
|
5232
|
+
const path = options?.path || this.pluginOptions.path || "/graphql";
|
|
5233
|
+
app.mount(path, this);
|
|
5234
|
+
app.onStart(async () => {
|
|
5235
|
+
await this.apolloServer.start();
|
|
5236
|
+
});
|
|
5237
|
+
this.post("/", async (ctx) => {
|
|
5238
|
+
const body = await ctx.body();
|
|
5239
|
+
const httpGraphQLResponse = await this.apolloServer.executeHTTPGraphQLRequest({
|
|
5240
|
+
httpGraphQLRequest: {
|
|
5241
|
+
body,
|
|
5242
|
+
method: ctx.req.method,
|
|
5243
|
+
search: ctx.url.search,
|
|
5244
|
+
headers: new HeaderMap(ctx.req.headers)
|
|
5245
|
+
},
|
|
5246
|
+
// Pass the Shokupan Context as the GraphQL Context
|
|
5247
|
+
context: async () => ({ ...ctx, shokupan: ctx })
|
|
5248
|
+
});
|
|
5249
|
+
for (const [key, value] of httpGraphQLResponse.headers) {
|
|
5250
|
+
ctx.set(key, value);
|
|
5251
|
+
}
|
|
5252
|
+
if (httpGraphQLResponse.body.kind === "complete") {
|
|
5253
|
+
return ctx.send(httpGraphQLResponse.body.string, {
|
|
5254
|
+
status: httpGraphQLResponse.status ?? 200
|
|
5255
|
+
});
|
|
5256
|
+
} else {
|
|
5257
|
+
let string = "";
|
|
5258
|
+
for await (const chunk of httpGraphQLResponse.body.asyncIterator) {
|
|
5259
|
+
string += chunk;
|
|
5260
|
+
}
|
|
5261
|
+
return ctx.send(string, {
|
|
5262
|
+
status: httpGraphQLResponse.status ?? 200
|
|
5263
|
+
});
|
|
5264
|
+
}
|
|
5265
|
+
});
|
|
5266
|
+
this.get("/", async (ctx) => {
|
|
5267
|
+
const httpGraphQLResponse = await this.apolloServer.executeHTTPGraphQLRequest({
|
|
5268
|
+
httpGraphQLRequest: {
|
|
5269
|
+
body: Object.keys(ctx.query).length > 0 ? ctx.query : void 0,
|
|
5270
|
+
method: ctx.req.method,
|
|
5271
|
+
search: ctx.url.search,
|
|
5272
|
+
headers: new HeaderMap(ctx.req.headers)
|
|
5273
|
+
},
|
|
5274
|
+
context: async () => ({ ...ctx, shokupan: ctx })
|
|
5275
|
+
});
|
|
5276
|
+
for (const [key, value] of httpGraphQLResponse.headers) {
|
|
5277
|
+
ctx.set(key, value);
|
|
5278
|
+
}
|
|
5279
|
+
if (httpGraphQLResponse.body.kind === "complete") {
|
|
5280
|
+
return ctx.html(httpGraphQLResponse.body.string, httpGraphQLResponse.status ?? 200);
|
|
5281
|
+
} else {
|
|
5282
|
+
let string = "";
|
|
5283
|
+
for await (const chunk of httpGraphQLResponse.body.asyncIterator) {
|
|
5284
|
+
string += chunk;
|
|
5285
|
+
}
|
|
5286
|
+
return ctx.html(string, httpGraphQLResponse.status ?? 200);
|
|
5287
|
+
}
|
|
5288
|
+
});
|
|
5289
|
+
}
|
|
5290
|
+
}
|
|
4135
5291
|
class ScalarPlugin extends ShokupanRouter {
|
|
4136
5292
|
constructor(pluginOptions = {}) {
|
|
4137
5293
|
pluginOptions.config ??= {};
|
|
4138
5294
|
super();
|
|
4139
5295
|
this.pluginOptions = pluginOptions;
|
|
4140
|
-
this.
|
|
5296
|
+
this.initRoutes();
|
|
5297
|
+
}
|
|
5298
|
+
eta;
|
|
5299
|
+
async onInit(app, options) {
|
|
5300
|
+
const { Eta: Eta2 } = await import("eta");
|
|
5301
|
+
this.eta = new Eta2();
|
|
5302
|
+
const path = options?.path || this.pluginOptions.path || "/reference";
|
|
5303
|
+
app.mount(path, this);
|
|
5304
|
+
this.onMount(app);
|
|
4141
5305
|
}
|
|
4142
|
-
|
|
4143
|
-
if (
|
|
4144
|
-
|
|
4145
|
-
|
|
4146
|
-
app.mount(options.path ?? "/", this);
|
|
5306
|
+
async ensureEta() {
|
|
5307
|
+
if (!this.eta) {
|
|
5308
|
+
const { Eta: Eta2 } = await import("eta");
|
|
5309
|
+
this.eta = new Eta2();
|
|
4147
5310
|
}
|
|
4148
|
-
this.onMount(app);
|
|
4149
5311
|
}
|
|
4150
|
-
|
|
4151
|
-
|
|
5312
|
+
initRoutes() {
|
|
5313
|
+
const bootId = Date.now().toString();
|
|
5314
|
+
this.get("/_lifecycle", (ctx) => ctx.json({ boot: bootId }));
|
|
5315
|
+
this.get("/", async (ctx) => {
|
|
5316
|
+
await this.ensureEta();
|
|
4152
5317
|
let path = ctx.url.toString();
|
|
4153
5318
|
if (!path.endsWith("/")) path += "/";
|
|
4154
|
-
|
|
4155
|
-
<
|
|
5319
|
+
const devScript = ctx.app?.applicationConfig.development ? `
|
|
5320
|
+
<script>
|
|
5321
|
+
(function() {
|
|
5322
|
+
const bootId = "${bootId}";
|
|
5323
|
+
let isDown = false;
|
|
5324
|
+
|
|
5325
|
+
setInterval(async () => {
|
|
5326
|
+
try {
|
|
5327
|
+
const res = await fetch('${path}_lifecycle');
|
|
5328
|
+
if (!res.ok) throw new Error('Down');
|
|
5329
|
+
const data = await res.json();
|
|
5330
|
+
if (data.boot !== bootId) {
|
|
5331
|
+
console.log('Server restarted, reloading...');
|
|
5332
|
+
window.location.reload();
|
|
5333
|
+
}
|
|
5334
|
+
else if (isDown) {
|
|
5335
|
+
isDown = false;
|
|
5336
|
+
}
|
|
5337
|
+
} catch (e) {
|
|
5338
|
+
isDown = true;
|
|
5339
|
+
console.log('Connection lost...');
|
|
5340
|
+
}
|
|
5341
|
+
}, 2000);
|
|
5342
|
+
})();
|
|
5343
|
+
<\/script>
|
|
5344
|
+
` : "";
|
|
5345
|
+
let themeCss = "";
|
|
5346
|
+
try {
|
|
5347
|
+
try {
|
|
5348
|
+
themeCss = readFileSync(join$1(process.cwd(), "src/theme.css"), "utf-8");
|
|
5349
|
+
} catch {
|
|
5350
|
+
}
|
|
5351
|
+
} catch (e) {
|
|
5352
|
+
}
|
|
5353
|
+
if (!this.eta) throw new Error("Eta not initialized");
|
|
5354
|
+
return ctx.html(this.eta.renderString(`<!doctype html>
|
|
5355
|
+
<html lang="en">
|
|
4156
5356
|
<head>
|
|
4157
5357
|
<title>API Reference</title>
|
|
4158
5358
|
<meta charset = "utf-8" />
|
|
4159
5359
|
<meta name="viewport" content = "width=device-width, initial-scale=1" />
|
|
5360
|
+
<style>
|
|
5361
|
+
${themeCss}
|
|
5362
|
+
|
|
5363
|
+
:root {
|
|
5364
|
+
--scalar-color-1: var(--primary);
|
|
5365
|
+
--scalar-color-2: var(--secondary);
|
|
5366
|
+
--scalar-color-3: var(--accent);
|
|
5367
|
+
--scalar-color-accent: var(--accent);
|
|
5368
|
+
|
|
5369
|
+
--scalar-background-1: var(--bg-primary);
|
|
5370
|
+
--scalar-background-2: var(--bg-secondary);
|
|
5371
|
+
--scalar-background-3: var(--bg-card);
|
|
5372
|
+
|
|
5373
|
+
--scalar-text-1: var(--text-primary);
|
|
5374
|
+
--scalar-text-2: var(--text-secondary);
|
|
5375
|
+
--scalar-text-3: var(--text-muted);
|
|
5376
|
+
|
|
5377
|
+
--scalar-border-color: var(--border-color);
|
|
5378
|
+
}
|
|
5379
|
+
</style>
|
|
4160
5380
|
</head>
|
|
4161
5381
|
|
|
4162
5382
|
<body>
|
|
@@ -4168,9 +5388,10 @@ class ScalarPlugin extends ShokupanRouter {
|
|
|
4168
5388
|
}
|
|
4169
5389
|
])
|
|
4170
5390
|
<\/script>
|
|
5391
|
+
<%~ it.devScript %>
|
|
4171
5392
|
</body>
|
|
4172
5393
|
|
|
4173
|
-
</html>`, { path, config: this.pluginOptions }));
|
|
5394
|
+
</html>`, { path, config: this.pluginOptions, devScript }));
|
|
4174
5395
|
});
|
|
4175
5396
|
this.get("/openapi.json", async (ctx) => {
|
|
4176
5397
|
let spec;
|
|
@@ -4373,7 +5594,7 @@ function Cors(options = {}) {
|
|
|
4373
5594
|
}
|
|
4374
5595
|
const response = await next();
|
|
4375
5596
|
if (response instanceof Response) {
|
|
4376
|
-
const headerEntries =
|
|
5597
|
+
const headerEntries = Object.entries(headers);
|
|
4377
5598
|
for (let i = 0; i < headerEntries.length; i++) {
|
|
4378
5599
|
const [key, value] = headerEntries[i];
|
|
4379
5600
|
response.headers.set(key, value);
|
|
@@ -5145,6 +6366,7 @@ export {
|
|
|
5145
6366
|
$url,
|
|
5146
6367
|
$ws,
|
|
5147
6368
|
All,
|
|
6369
|
+
AsyncApiPlugin,
|
|
5148
6370
|
AuthPlugin,
|
|
5149
6371
|
Body,
|
|
5150
6372
|
ClusterPlugin,
|
|
@@ -5157,6 +6379,7 @@ export {
|
|
|
5157
6379
|
Delete,
|
|
5158
6380
|
Event,
|
|
5159
6381
|
Get,
|
|
6382
|
+
GraphQLApolloPlugin,
|
|
5160
6383
|
HTTPMethods,
|
|
5161
6384
|
Head,
|
|
5162
6385
|
Headers$1 as Headers,
|
|
@@ -5173,13 +6396,16 @@ export {
|
|
|
5173
6396
|
RateLimitMiddleware,
|
|
5174
6397
|
Req,
|
|
5175
6398
|
RouteParamType,
|
|
6399
|
+
RouterRegistry,
|
|
5176
6400
|
ScalarPlugin,
|
|
5177
6401
|
SecurityHeaders,
|
|
5178
6402
|
Session,
|
|
5179
6403
|
Shokupan,
|
|
6404
|
+
ShokupanApplicationTree,
|
|
5180
6405
|
ShokupanContext,
|
|
5181
6406
|
ShokupanRequest,
|
|
5182
6407
|
ShokupanResponse,
|
|
6408
|
+
ShokupanRouter,
|
|
5183
6409
|
Spec,
|
|
5184
6410
|
Use,
|
|
5185
6411
|
ValidationError,
|