shokupan 0.9.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/analyzer-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 +1459 -239
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1441 -220
- 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 +33 -13
- 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 { inspect } from "node:util";
|
|
3
4
|
import { trace, SpanKind, SpanStatusCode, context } from "@opentelemetry/api";
|
|
5
|
+
import { dump } from "js-yaml";
|
|
4
6
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
5
|
-
import {
|
|
7
|
+
import { RecordId, Surreal } from "surrealdb";
|
|
6
8
|
import { Eta } from "eta";
|
|
7
9
|
import { stat, readdir, readFile as readFile$1 } from "fs/promises";
|
|
8
10
|
import { resolve, join, sep, basename } from "path";
|
|
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);
|
|
@@ -1265,7 +1351,7 @@ class EventError extends HttpError {
|
|
|
1265
1351
|
this.name = "EventError";
|
|
1266
1352
|
}
|
|
1267
1353
|
}
|
|
1268
|
-
const eta
|
|
1354
|
+
const eta = new Eta();
|
|
1269
1355
|
function serveStatic(config, prefix) {
|
|
1270
1356
|
const rootPath = resolve(config.root || ".");
|
|
1271
1357
|
const normalizedPrefix = prefix.endsWith("/") && prefix !== "/" ? prefix.slice(0, -1) : prefix;
|
|
@@ -1364,7 +1450,7 @@ function serveStatic(config, prefix) {
|
|
|
1364
1450
|
if (config.listDirectory) {
|
|
1365
1451
|
try {
|
|
1366
1452
|
const files = await readdir(requestPath);
|
|
1367
|
-
const listing = eta
|
|
1453
|
+
const listing = eta.renderString(`
|
|
1368
1454
|
<!DOCTYPE html>
|
|
1369
1455
|
<html>
|
|
1370
1456
|
<head>
|
|
@@ -1404,7 +1490,7 @@ function serveStatic(config, prefix) {
|
|
|
1404
1490
|
if (typeof Bun !== "undefined") {
|
|
1405
1491
|
response = new Response(Bun.file(finalPath));
|
|
1406
1492
|
} else {
|
|
1407
|
-
const fileBuffer = await readFile$1(finalPath);
|
|
1493
|
+
const fileBuffer = await readFile$1(finalPath, { encoding: "binary" });
|
|
1408
1494
|
response = new Response(fileBuffer);
|
|
1409
1495
|
}
|
|
1410
1496
|
if (config.hooks?.onResponse) {
|
|
@@ -1417,67 +1503,6 @@ function serveStatic(config, prefix) {
|
|
|
1417
1503
|
serveStaticMiddleware.pluginName = "ServeStatic";
|
|
1418
1504
|
return serveStaticMiddleware;
|
|
1419
1505
|
}
|
|
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
1506
|
class Container {
|
|
1482
1507
|
static services = /* @__PURE__ */ new Map();
|
|
1483
1508
|
static register(target, instance) {
|
|
@@ -1511,6 +1536,29 @@ function Inject(token) {
|
|
|
1511
1536
|
});
|
|
1512
1537
|
};
|
|
1513
1538
|
}
|
|
1539
|
+
const tracer = trace.getTracer("shokupan.middleware");
|
|
1540
|
+
function traceHandler(fn, name) {
|
|
1541
|
+
return async function(...args) {
|
|
1542
|
+
return tracer.startActiveSpan(`route handler - ${name}`, {
|
|
1543
|
+
kind: SpanKind.INTERNAL,
|
|
1544
|
+
attributes: {
|
|
1545
|
+
"http.route": name,
|
|
1546
|
+
"component": "shokupan.route"
|
|
1547
|
+
}
|
|
1548
|
+
}, async (span) => {
|
|
1549
|
+
try {
|
|
1550
|
+
const result = await fn.apply(this, args);
|
|
1551
|
+
return result;
|
|
1552
|
+
} catch (err) {
|
|
1553
|
+
span.recordException(err);
|
|
1554
|
+
span.setStatus({ code: SpanStatusCode.ERROR, message: err.message });
|
|
1555
|
+
throw err;
|
|
1556
|
+
} finally {
|
|
1557
|
+
span.end();
|
|
1558
|
+
}
|
|
1559
|
+
});
|
|
1560
|
+
};
|
|
1561
|
+
}
|
|
1514
1562
|
class ShokupanRequestBase {
|
|
1515
1563
|
method;
|
|
1516
1564
|
url;
|
|
@@ -1548,8 +1596,10 @@ function getCallerInfo(skipFrames = 1) {
|
|
|
1548
1596
|
if (!l.includes(":")) continue;
|
|
1549
1597
|
if (l.includes("node_modules")) continue;
|
|
1550
1598
|
if (l.includes("bun:main")) continue;
|
|
1599
|
+
if (l.includes("bun:wrap")) continue;
|
|
1551
1600
|
if (l.includes("src/util/stack.ts")) continue;
|
|
1552
1601
|
if (l.includes("src/router.ts")) continue;
|
|
1602
|
+
if (l.includes("src/util/decorators.ts")) continue;
|
|
1553
1603
|
if (l.includes("src/shokupan.ts")) continue;
|
|
1554
1604
|
found++;
|
|
1555
1605
|
if (found >= skipFrames) {
|
|
@@ -1692,6 +1742,9 @@ class ShokupanRouter {
|
|
|
1692
1742
|
[$parent] = null;
|
|
1693
1743
|
[$childRouters] = [];
|
|
1694
1744
|
[$childControllers] = [];
|
|
1745
|
+
get db() {
|
|
1746
|
+
return this.root?.db;
|
|
1747
|
+
}
|
|
1695
1748
|
hookCache = /* @__PURE__ */ new Map();
|
|
1696
1749
|
hooksInitialized = false;
|
|
1697
1750
|
middleware = [];
|
|
@@ -1708,6 +1761,14 @@ class ShokupanRouter {
|
|
|
1708
1761
|
// Metadata for the router itself
|
|
1709
1762
|
currentGuards = [];
|
|
1710
1763
|
eventHandlers = /* @__PURE__ */ new Map();
|
|
1764
|
+
/**
|
|
1765
|
+
* Registers middleware for this router.
|
|
1766
|
+
* Middleware will run for all routes matched by this router.
|
|
1767
|
+
*/
|
|
1768
|
+
use(middleware) {
|
|
1769
|
+
this.middleware.push(middleware);
|
|
1770
|
+
return this;
|
|
1771
|
+
}
|
|
1711
1772
|
// Registry Accessor
|
|
1712
1773
|
getComponentRegistry() {
|
|
1713
1774
|
const controllerRoutesMap = /* @__PURE__ */ new Map();
|
|
@@ -1772,6 +1833,8 @@ class ShokupanRouter {
|
|
|
1772
1833
|
* Registers an event handler for WebSocket.
|
|
1773
1834
|
*/
|
|
1774
1835
|
event(name, handler) {
|
|
1836
|
+
const info = getCallerInfo();
|
|
1837
|
+
handler.source = { file: info.file, line: info.line };
|
|
1775
1838
|
if (this.eventHandlers.has(name)) {
|
|
1776
1839
|
const err = new EventError(`Event handler \`${name}\` already exists.`);
|
|
1777
1840
|
console.warn(err);
|
|
@@ -1796,6 +1859,12 @@ class ShokupanRouter {
|
|
|
1796
1859
|
}
|
|
1797
1860
|
return null;
|
|
1798
1861
|
}
|
|
1862
|
+
/**
|
|
1863
|
+
* Returns all registered event handlers.
|
|
1864
|
+
*/
|
|
1865
|
+
getEventHandlers() {
|
|
1866
|
+
return this.eventHandlers;
|
|
1867
|
+
}
|
|
1799
1868
|
/**
|
|
1800
1869
|
* Mounts a controller instance to a path prefix.
|
|
1801
1870
|
*
|
|
@@ -2039,10 +2108,12 @@ class ShokupanRouter {
|
|
|
2039
2108
|
if (typeof originalHandler !== "function") continue;
|
|
2040
2109
|
let method;
|
|
2041
2110
|
let subPath = "";
|
|
2111
|
+
let methodSource;
|
|
2042
2112
|
if (decoratedRoutes && decoratedRoutes.has(name)) {
|
|
2043
2113
|
const config = decoratedRoutes.get(name);
|
|
2044
2114
|
method = config.method;
|
|
2045
2115
|
subPath = config.path;
|
|
2116
|
+
methodSource = config.source;
|
|
2046
2117
|
} else {
|
|
2047
2118
|
for (let j = 0; j < HTTPMethods.length; j++) {
|
|
2048
2119
|
const m = HTTPMethods[j];
|
|
@@ -2172,7 +2243,16 @@ class ShokupanRouter {
|
|
|
2172
2243
|
const decoratedSpecs = instance[$routeSpec] || proto && proto[$routeSpec];
|
|
2173
2244
|
const userSpec = decoratedSpecs && decoratedSpecs.get(name);
|
|
2174
2245
|
const spec = { tags: [tagName], ...userSpec };
|
|
2175
|
-
this.add({
|
|
2246
|
+
this.add({
|
|
2247
|
+
method,
|
|
2248
|
+
path: normalizedPath,
|
|
2249
|
+
handler: finalHandler,
|
|
2250
|
+
spec,
|
|
2251
|
+
controller: instance,
|
|
2252
|
+
metadata: methodSource || instance.metadata,
|
|
2253
|
+
middleware: allMiddleware
|
|
2254
|
+
// Capture all resolved middleware
|
|
2255
|
+
});
|
|
2176
2256
|
}
|
|
2177
2257
|
if (decoratedEvents?.has(name)) {
|
|
2178
2258
|
routesAttached++;
|
|
@@ -2205,6 +2285,11 @@ class ShokupanRouter {
|
|
|
2205
2285
|
}
|
|
2206
2286
|
return originalHandler.apply(instance, args);
|
|
2207
2287
|
};
|
|
2288
|
+
const decoratedSpecs = instance[$routeSpec] || proto && proto[$routeSpec];
|
|
2289
|
+
const userSpec = decoratedSpecs && decoratedSpecs.get(name);
|
|
2290
|
+
const spec = { tags: [{ name: instance.constructor.name }], ...userSpec };
|
|
2291
|
+
wrappedHandler.spec = spec;
|
|
2292
|
+
wrappedHandler.originalHandler = originalHandler;
|
|
2208
2293
|
this.event(config.eventName, wrappedHandler);
|
|
2209
2294
|
}
|
|
2210
2295
|
}
|
|
@@ -2274,7 +2359,7 @@ class ShokupanRouter {
|
|
|
2274
2359
|
* @param arg.renderer - JSX renderer for the route
|
|
2275
2360
|
* @param arg.controller - Controller for the route
|
|
2276
2361
|
*/
|
|
2277
|
-
add({ method, path, spec, handler, regex: customRegex, group, requestTimeout, renderer, controller }) {
|
|
2362
|
+
add({ method, path, spec, handler, regex: customRegex, group, requestTimeout, renderer, controller, metadata, middleware }) {
|
|
2278
2363
|
const { regex, keys } = customRegex ? { regex: customRegex, keys: [] } : this.parsePath(path);
|
|
2279
2364
|
if (this.currentGuards.length > 0) {
|
|
2280
2365
|
spec = spec || {};
|
|
@@ -2292,7 +2377,13 @@ class ShokupanRouter {
|
|
|
2292
2377
|
}
|
|
2293
2378
|
}
|
|
2294
2379
|
}
|
|
2295
|
-
let wrappedHandler =
|
|
2380
|
+
let wrappedHandler = async (ctx) => {
|
|
2381
|
+
if (ctx.upgrade()) {
|
|
2382
|
+
return void 0;
|
|
2383
|
+
}
|
|
2384
|
+
return handler(ctx);
|
|
2385
|
+
};
|
|
2386
|
+
wrappedHandler.originalHandler = handler.originalHandler || handler;
|
|
2296
2387
|
const routeGuards = [...this.currentGuards];
|
|
2297
2388
|
const effectiveTimeout = requestTimeout ?? this.requestTimeout ?? this.rootConfig?.requestTimeout;
|
|
2298
2389
|
if (effectiveTimeout !== void 0 && effectiveTimeout > 0) {
|
|
@@ -2343,7 +2434,7 @@ class ShokupanRouter {
|
|
|
2343
2434
|
return innerHandler(ctx);
|
|
2344
2435
|
};
|
|
2345
2436
|
}
|
|
2346
|
-
const { file, line } = getCallerInfo();
|
|
2437
|
+
const { file, line } = metadata || getCallerInfo();
|
|
2347
2438
|
const trackingHandler = wrappedHandler;
|
|
2348
2439
|
wrappedHandler = async (ctx) => {
|
|
2349
2440
|
if (!ctx.app?.applicationConfig.enableMiddlewareTracking) {
|
|
@@ -2369,8 +2460,10 @@ class ShokupanRouter {
|
|
|
2369
2460
|
const config = ctx.app.applicationConfig;
|
|
2370
2461
|
Promise.resolve().then(async () => {
|
|
2371
2462
|
try {
|
|
2463
|
+
const db = ctx.app?.db;
|
|
2464
|
+
if (!db) return;
|
|
2372
2465
|
const timestamp = Date.now();
|
|
2373
|
-
await
|
|
2466
|
+
await db.upsert(new RecordId("middleware_tracking", {
|
|
2374
2467
|
timestamp,
|
|
2375
2468
|
name: handler.name || "anonymous"
|
|
2376
2469
|
}), {
|
|
@@ -2389,11 +2482,11 @@ class ShokupanRouter {
|
|
|
2389
2482
|
const ttl = config.middlewareTrackingTTL ?? 864e5;
|
|
2390
2483
|
const maxCapacity = config.middlewareTrackingMaxCapacity ?? 1e4;
|
|
2391
2484
|
const cutoff = Date.now() - ttl;
|
|
2392
|
-
await
|
|
2393
|
-
const results = await
|
|
2485
|
+
await db.query(`DELETE middleware_tracking WHERE timestamp < ${cutoff}`);
|
|
2486
|
+
const results = await db.query("SELECT count() FROM middleware_tracking GROUP ALL");
|
|
2394
2487
|
if (results?.[0]?.count > maxCapacity) {
|
|
2395
2488
|
const toDelete = results[0].count - maxCapacity;
|
|
2396
|
-
await
|
|
2489
|
+
await db.query(`DELETE middleware_tracking ORDER BY timestamp ASC LIMIT ${toDelete}`);
|
|
2397
2490
|
}
|
|
2398
2491
|
} catch (datastoreError) {
|
|
2399
2492
|
console.error("Failed to store middleware tracking:", datastoreError);
|
|
@@ -2423,7 +2516,8 @@ class ShokupanRouter {
|
|
|
2423
2516
|
file,
|
|
2424
2517
|
line
|
|
2425
2518
|
},
|
|
2426
|
-
controller
|
|
2519
|
+
controller,
|
|
2520
|
+
middleware: middleware || []
|
|
2427
2521
|
});
|
|
2428
2522
|
this.trie.insert(method, path, bakedHandler);
|
|
2429
2523
|
return this;
|
|
@@ -2552,7 +2646,8 @@ class ShokupanRouter {
|
|
|
2552
2646
|
method,
|
|
2553
2647
|
path,
|
|
2554
2648
|
spec,
|
|
2555
|
-
handler: finalHandler
|
|
2649
|
+
handler: finalHandler,
|
|
2650
|
+
middleware: handlers.slice(0, handlers.length - 1)
|
|
2556
2651
|
});
|
|
2557
2652
|
}
|
|
2558
2653
|
/**
|
|
@@ -2592,7 +2687,7 @@ class ShokupanRouter {
|
|
|
2592
2687
|
}
|
|
2593
2688
|
this.hooksInitialized = true;
|
|
2594
2689
|
}
|
|
2595
|
-
|
|
2690
|
+
runHooks(name, ...args) {
|
|
2596
2691
|
if (!this.hooksInitialized) {
|
|
2597
2692
|
this.ensureHooksInitialized();
|
|
2598
2693
|
}
|
|
@@ -2601,7 +2696,7 @@ class ShokupanRouter {
|
|
|
2601
2696
|
const ctx = args?.[0] instanceof ShokupanContext ? args[0] : void 0;
|
|
2602
2697
|
const debug = ctx?.[$debug];
|
|
2603
2698
|
if (debug) {
|
|
2604
|
-
|
|
2699
|
+
return Promise.all(fns.map(async (fn, index) => {
|
|
2605
2700
|
const hookId = `hook_${name}_${fn.name || index}`;
|
|
2606
2701
|
const previousNode = debug.getCurrentNode();
|
|
2607
2702
|
debug.trackEdge(previousNode, hookId);
|
|
@@ -2620,7 +2715,7 @@ class ShokupanRouter {
|
|
|
2620
2715
|
}
|
|
2621
2716
|
}));
|
|
2622
2717
|
} else {
|
|
2623
|
-
|
|
2718
|
+
return Promise.all(fns.map((fn) => fn(...args)));
|
|
2624
2719
|
}
|
|
2625
2720
|
}
|
|
2626
2721
|
}
|
|
@@ -2667,6 +2762,100 @@ class SystemCpuMonitor {
|
|
|
2667
2762
|
this.currentUsage = total === 0 ? 0 : (1 - idle / total) * 100;
|
|
2668
2763
|
}
|
|
2669
2764
|
}
|
|
2765
|
+
class SurrealDatastore {
|
|
2766
|
+
constructor(db) {
|
|
2767
|
+
this.db = db;
|
|
2768
|
+
process.on("exit", async () => {
|
|
2769
|
+
await this.disconnect();
|
|
2770
|
+
});
|
|
2771
|
+
}
|
|
2772
|
+
createSchema() {
|
|
2773
|
+
this.db.query(`
|
|
2774
|
+
DEFINE TABLE OVERWRITE failed_requests SCHEMALESS COMMENT "Created by Shokupan";
|
|
2775
|
+
DEFINE TABLE OVERWRITE sessions SCHEMALESS COMMENT "Created by Shokupan";
|
|
2776
|
+
DEFINE TABLE OVERWRITE users SCHEMALESS COMMENT "Created by Shokupan";
|
|
2777
|
+
DEFINE TABLE OVERWRITE idempotency_keys SCHEMALESS COMMENT "Created by Shokupan";
|
|
2778
|
+
DEFINE TABLE OVERWRITE middleware_tracking SCHEMALESS COMMENT "Created by Shokupan";
|
|
2779
|
+
DEFINE TABLE OVERWRITE requests SCHEMALESS COMMENT "Created by Shokupan";
|
|
2780
|
+
DEFINE TABLE OVERWRITE metrics SCHEMALESS COMMENT "Created by Shokupan";
|
|
2781
|
+
`).collect();
|
|
2782
|
+
}
|
|
2783
|
+
/**
|
|
2784
|
+
* Select a record or contents of a table by its ID.
|
|
2785
|
+
*/
|
|
2786
|
+
async select(id) {
|
|
2787
|
+
return this.db.select(id);
|
|
2788
|
+
}
|
|
2789
|
+
/**
|
|
2790
|
+
* Merge update data into a record by its ID.
|
|
2791
|
+
*/
|
|
2792
|
+
async merge(id, data) {
|
|
2793
|
+
return this.db.update(id).merge(data).catch((err) => {
|
|
2794
|
+
if (err.message.includes("This transaction can be retried")) {
|
|
2795
|
+
return this.db.update(id).merge(data);
|
|
2796
|
+
}
|
|
2797
|
+
throw err;
|
|
2798
|
+
});
|
|
2799
|
+
}
|
|
2800
|
+
/**
|
|
2801
|
+
* Create a record by its ID.
|
|
2802
|
+
*/
|
|
2803
|
+
async create(id, data) {
|
|
2804
|
+
return this.db.create(id).content(data).catch((err) => {
|
|
2805
|
+
if (err.message.includes("This transaction can be retried")) {
|
|
2806
|
+
return this.db.create(id).content(data);
|
|
2807
|
+
}
|
|
2808
|
+
throw err;
|
|
2809
|
+
});
|
|
2810
|
+
}
|
|
2811
|
+
/**
|
|
2812
|
+
* Upsert a record by its ID.
|
|
2813
|
+
*/
|
|
2814
|
+
async upsert(id, data) {
|
|
2815
|
+
return this.db.upsert(id).content(data).catch((err) => {
|
|
2816
|
+
if (err.message.includes("This transaction can be retried")) {
|
|
2817
|
+
return this.db.upsert(id).content(data);
|
|
2818
|
+
}
|
|
2819
|
+
throw err;
|
|
2820
|
+
});
|
|
2821
|
+
}
|
|
2822
|
+
/**
|
|
2823
|
+
* Delete a record by its ID.
|
|
2824
|
+
*/
|
|
2825
|
+
async delete(id) {
|
|
2826
|
+
return this.db.delete(id).catch((err) => {
|
|
2827
|
+
if (err.message.includes("This transaction can be retried")) {
|
|
2828
|
+
return this.db.delete(id);
|
|
2829
|
+
}
|
|
2830
|
+
throw err;
|
|
2831
|
+
});
|
|
2832
|
+
}
|
|
2833
|
+
/**
|
|
2834
|
+
* Run a SurrealDB query.
|
|
2835
|
+
*/
|
|
2836
|
+
async query(query, vars) {
|
|
2837
|
+
return this.db.query(query, vars).collect().catch((err) => {
|
|
2838
|
+
if (err.message.includes("This transaction can be retried")) {
|
|
2839
|
+
return this.db.query(query, vars).collect();
|
|
2840
|
+
}
|
|
2841
|
+
throw err;
|
|
2842
|
+
});
|
|
2843
|
+
}
|
|
2844
|
+
/**
|
|
2845
|
+
* Create a relationship between two records.
|
|
2846
|
+
*/
|
|
2847
|
+
async relate(fromId, edgeId, toId, data) {
|
|
2848
|
+
return this.db.relate(fromId, edgeId, toId, data).catch((err) => {
|
|
2849
|
+
if (err.message.includes("This transaction can be retried")) {
|
|
2850
|
+
return this.db.relate(fromId, edgeId, toId, data);
|
|
2851
|
+
}
|
|
2852
|
+
throw err;
|
|
2853
|
+
});
|
|
2854
|
+
}
|
|
2855
|
+
disconnect() {
|
|
2856
|
+
return this.db.close();
|
|
2857
|
+
}
|
|
2858
|
+
}
|
|
2670
2859
|
const defaults = {
|
|
2671
2860
|
port: 3e3,
|
|
2672
2861
|
hostname: "localhost",
|
|
@@ -2679,9 +2868,15 @@ trace.getTracer("shokupan.application");
|
|
|
2679
2868
|
class Shokupan extends ShokupanRouter {
|
|
2680
2869
|
applicationConfig = {};
|
|
2681
2870
|
openApiSpec;
|
|
2871
|
+
asyncApiSpec;
|
|
2682
2872
|
composedMiddleware;
|
|
2683
2873
|
cpuMonitor;
|
|
2684
2874
|
server;
|
|
2875
|
+
datastore;
|
|
2876
|
+
dbPromise;
|
|
2877
|
+
get db() {
|
|
2878
|
+
return this.datastore;
|
|
2879
|
+
}
|
|
2685
2880
|
get logger() {
|
|
2686
2881
|
return this.applicationConfig.logger;
|
|
2687
2882
|
}
|
|
@@ -2698,6 +2893,19 @@ class Shokupan extends ShokupanRouter {
|
|
|
2698
2893
|
line,
|
|
2699
2894
|
name: "ShokupanApplication"
|
|
2700
2895
|
};
|
|
2896
|
+
this.dbPromise = this.initDatastore();
|
|
2897
|
+
}
|
|
2898
|
+
async initDatastore() {
|
|
2899
|
+
const db = new Surreal({ engines: this.applicationConfig.surreal?.engines ?? (await import("@surrealdb/node")).createNodeEngines() });
|
|
2900
|
+
this.datastore = new SurrealDatastore(db);
|
|
2901
|
+
await db.connect(
|
|
2902
|
+
this.applicationConfig.surreal?.url ?? (process.env.NODE_ENV === "test" ? "mem://" : "surrealkv://database"),
|
|
2903
|
+
this.applicationConfig.surreal?.connectOptions
|
|
2904
|
+
);
|
|
2905
|
+
await db.use({
|
|
2906
|
+
namespace: this.applicationConfig.surreal?.namespace ?? "vendor",
|
|
2907
|
+
database: this.applicationConfig.surreal?.database ?? "shokupan"
|
|
2908
|
+
});
|
|
2701
2909
|
}
|
|
2702
2910
|
/**
|
|
2703
2911
|
* Adds middleware to the application.
|
|
@@ -2777,14 +2985,70 @@ class Shokupan extends ShokupanRouter {
|
|
|
2777
2985
|
*/
|
|
2778
2986
|
async listen(port) {
|
|
2779
2987
|
const finalPort = port ?? this.applicationConfig.port ?? 3e3;
|
|
2780
|
-
if (finalPort < 0 || finalPort > 65535) {
|
|
2988
|
+
if (finalPort < 0 || finalPort > 65535 || finalPort % 1 !== 0) {
|
|
2781
2989
|
throw new Error("Invalid port number");
|
|
2782
2990
|
}
|
|
2783
2991
|
await Promise.all(this.startupHooks.map((hook) => hook()));
|
|
2784
2992
|
if (this.applicationConfig.enableOpenApiGen) {
|
|
2785
2993
|
this.openApiSpec = await generateOpenApi(this);
|
|
2994
|
+
this.get("/.well-known/openapi.yaml", (ctx) => {
|
|
2995
|
+
try {
|
|
2996
|
+
const yaml = dump(this.openApiSpec);
|
|
2997
|
+
return ctx.send(yaml, { status: 200, headers: { "content-type": "application/yaml" } });
|
|
2998
|
+
} catch (e) {
|
|
2999
|
+
this.logger.error("Failed to generate OpenAPI YAML", { error: e });
|
|
3000
|
+
return ctx.text("Internal Server Error", 500);
|
|
3001
|
+
}
|
|
3002
|
+
});
|
|
3003
|
+
if (this.applicationConfig.aiPlugin?.enabled !== false) {
|
|
3004
|
+
this.get("/.well-known/ai-plugin.json", async (ctx) => {
|
|
3005
|
+
const config = this.applicationConfig.aiPlugin || {};
|
|
3006
|
+
let pkg = {};
|
|
3007
|
+
try {
|
|
3008
|
+
pkg = await Bun.file("package.json").json();
|
|
3009
|
+
} catch (e) {
|
|
3010
|
+
}
|
|
3011
|
+
const manifest = {
|
|
3012
|
+
schema_version: "v1",
|
|
3013
|
+
name_for_human: config.name_for_human || this.openApiSpec.info.title || pkg.name || "Shokupan App",
|
|
3014
|
+
name_for_model: config.name_for_model || this.openApiSpec.info.title || pkg.name || "Shokupan App",
|
|
3015
|
+
description_for_human: config.description_for_human || this.openApiSpec.info.description || pkg.description || "Shokupan Application",
|
|
3016
|
+
description_for_model: config.description_for_model || this.openApiSpec.info.description || pkg.description || "Shokupan Application",
|
|
3017
|
+
auth: config.auth || { type: "none" },
|
|
3018
|
+
api: config.api || {
|
|
3019
|
+
type: "openapi",
|
|
3020
|
+
url: `${this.applicationConfig.hostname === "localhost" ? "http" : "https"}://${this.applicationConfig.hostname}:${finalPort}/.well-known/openapi.yaml`,
|
|
3021
|
+
is_user_authenticated: false
|
|
3022
|
+
},
|
|
3023
|
+
logo_url: config.logo_url || `${this.applicationConfig.hostname === "localhost" ? "http" : "https"}://${this.applicationConfig.hostname}:${finalPort}/logo.png`,
|
|
3024
|
+
// Placeholder default
|
|
3025
|
+
contact_email: config.contact_email || pkg.author?.email || "support@example.com",
|
|
3026
|
+
legal_info_url: config.legal_info_url || `${this.applicationConfig.hostname === "localhost" ? "http" : "https"}://${this.applicationConfig.hostname}:${finalPort}/legal`
|
|
3027
|
+
};
|
|
3028
|
+
return ctx.json(manifest);
|
|
3029
|
+
});
|
|
3030
|
+
}
|
|
3031
|
+
if (this.applicationConfig.apiCatalog?.enabled !== false) {
|
|
3032
|
+
this.get("/.well-known/api-catalog", (ctx) => {
|
|
3033
|
+
const config = this.applicationConfig.apiCatalog || {};
|
|
3034
|
+
const catalog = {
|
|
3035
|
+
versions: config.versions || [
|
|
3036
|
+
{
|
|
3037
|
+
name: this.openApiSpec.info.version || "v1",
|
|
3038
|
+
url: `${this.applicationConfig.hostname === "localhost" ? "http" : "https"}://${this.applicationConfig.hostname}:${finalPort}/`,
|
|
3039
|
+
spec_url: `${this.applicationConfig.hostname === "localhost" ? "http" : "https"}://${this.applicationConfig.hostname}:${finalPort}/.well-known/openapi.yaml`
|
|
3040
|
+
}
|
|
3041
|
+
]
|
|
3042
|
+
};
|
|
3043
|
+
return ctx.json(catalog);
|
|
3044
|
+
});
|
|
3045
|
+
}
|
|
2786
3046
|
await Promise.all(this.specAvailableHooks.map((hook) => hook(this.openApiSpec)));
|
|
2787
3047
|
}
|
|
3048
|
+
if (this.applicationConfig.enableAsyncApiGen) {
|
|
3049
|
+
const { generateAsyncApi: generateAsyncApi2 } = await Promise.resolve().then(() => generator);
|
|
3050
|
+
this.asyncApiSpec = await generateAsyncApi2(this);
|
|
3051
|
+
}
|
|
2788
3052
|
if (port === 0 && process.platform === "linux") ;
|
|
2789
3053
|
if (this.applicationConfig.autoBackpressureFeedback === true) {
|
|
2790
3054
|
this.cpuMonitor = new SystemCpuMonitor();
|
|
@@ -2844,6 +3108,8 @@ class Shokupan extends ShokupanRouter {
|
|
|
2844
3108
|
});
|
|
2845
3109
|
const ctx = new ShokupanContext(req, self.server);
|
|
2846
3110
|
ctx[$ws] = ws;
|
|
3111
|
+
ws.data ??= {};
|
|
3112
|
+
ws.data["ctx"] = ctx;
|
|
2847
3113
|
try {
|
|
2848
3114
|
await handler(ctx);
|
|
2849
3115
|
} catch (err) {
|
|
@@ -2863,6 +3129,15 @@ class Shokupan extends ShokupanRouter {
|
|
|
2863
3129
|
},
|
|
2864
3130
|
close(ws, code, reason) {
|
|
2865
3131
|
ws.data?.handler?.close?.(ws, code, reason);
|
|
3132
|
+
const ctx = ws.data?.["ctx"];
|
|
3133
|
+
if (ctx && typeof ctx.getDisconnectCallbacks === "function") {
|
|
3134
|
+
const callbacks = ctx.getDisconnectCallbacks();
|
|
3135
|
+
if (Array.isArray(callbacks) && callbacks.length > 0) {
|
|
3136
|
+
Promise.all(callbacks.map((cb) => cb())).catch((err) => {
|
|
3137
|
+
console.error("Error executing socket disconnect hook:", err);
|
|
3138
|
+
});
|
|
3139
|
+
}
|
|
3140
|
+
}
|
|
2866
3141
|
}
|
|
2867
3142
|
}
|
|
2868
3143
|
};
|
|
@@ -2872,7 +3147,6 @@ class Shokupan extends ShokupanRouter {
|
|
|
2872
3147
|
factory = createHttpServer();
|
|
2873
3148
|
}
|
|
2874
3149
|
this.server = factory ? await factory(serveOptions) : Bun.serve(serveOptions);
|
|
2875
|
-
console.log(`Shokupan server listening on http://${serveOptions.hostname}:${serveOptions.port}`);
|
|
2876
3150
|
return this.server;
|
|
2877
3151
|
}
|
|
2878
3152
|
/**
|
|
@@ -3004,9 +3278,6 @@ class Shokupan extends ShokupanRouter {
|
|
|
3004
3278
|
await bodyParsing;
|
|
3005
3279
|
return match.handler(ctx);
|
|
3006
3280
|
}
|
|
3007
|
-
if (ctx.upgrade()) {
|
|
3008
|
-
return void 0;
|
|
3009
|
-
}
|
|
3010
3281
|
return null;
|
|
3011
3282
|
});
|
|
3012
3283
|
let response;
|
|
@@ -3022,6 +3293,9 @@ class Shokupan extends ShokupanRouter {
|
|
|
3022
3293
|
} else if (ctx[$routeMatched]) {
|
|
3023
3294
|
response = ctx.send(null, { status: ctx.response.status, headers: ctx.response.headers });
|
|
3024
3295
|
} else {
|
|
3296
|
+
if (ctx.upgrade()) {
|
|
3297
|
+
return void 0;
|
|
3298
|
+
}
|
|
3025
3299
|
if (ctx.response.status !== HTTP_STATUS.OK) {
|
|
3026
3300
|
response = ctx.send(null, { status: ctx.response.status, headers: ctx.response.headers });
|
|
3027
3301
|
} else {
|
|
@@ -3034,6 +3308,9 @@ class Shokupan extends ShokupanRouter {
|
|
|
3034
3308
|
response = ctx.text(String(result));
|
|
3035
3309
|
}
|
|
3036
3310
|
await this.runHooks("onRequestEnd", ctx);
|
|
3311
|
+
if (response instanceof Promise) {
|
|
3312
|
+
response = await response;
|
|
3313
|
+
}
|
|
3037
3314
|
await this.runHooks("onResponseStart", ctx, response);
|
|
3038
3315
|
return response;
|
|
3039
3316
|
} catch (err) {
|
|
@@ -3143,8 +3420,7 @@ function RateLimitMiddleware(options = {}) {
|
|
|
3143
3420
|
}
|
|
3144
3421
|
}
|
|
3145
3422
|
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);
|
|
3423
|
+
const res = await (typeof msg === "object" ? ctx.json(msg, statusCode) : ctx.text(String(msg), statusCode));
|
|
3148
3424
|
if (headers) {
|
|
3149
3425
|
setHeaders(res);
|
|
3150
3426
|
res.headers.set("Retry-After", String(retryAfter));
|
|
@@ -3219,8 +3495,12 @@ function createMethodDecorator(method) {
|
|
|
3219
3495
|
}
|
|
3220
3496
|
target[$routeMethods].set(propertyKey, {
|
|
3221
3497
|
method,
|
|
3222
|
-
path
|
|
3498
|
+
path,
|
|
3499
|
+
source: getCallerInfo(2)
|
|
3223
3500
|
});
|
|
3501
|
+
if (path.includes("/user")) {
|
|
3502
|
+
console.log(`[Decorator] Captured source for ${propertyKey}:`, getCallerInfo());
|
|
3503
|
+
}
|
|
3224
3504
|
};
|
|
3225
3505
|
};
|
|
3226
3506
|
}
|
|
@@ -3243,15 +3523,572 @@ function Event(eventName) {
|
|
|
3243
3523
|
function RateLimit(options) {
|
|
3244
3524
|
return Use(RateLimitMiddleware(options));
|
|
3245
3525
|
}
|
|
3526
|
+
function AsyncApiApp({ spec, serverUrl, base, disableSourceView, navTree }) {
|
|
3527
|
+
return /* @__PURE__ */ jsxs("html", { lang: "en", children: [
|
|
3528
|
+
/* @__PURE__ */ jsxs("head", { children: [
|
|
3529
|
+
/* @__PURE__ */ jsx("meta", { charSet: "UTF-8" }),
|
|
3530
|
+
/* @__PURE__ */ jsx("meta", { name: "viewport", content: "width=device-width, initial-scale=1.0" }),
|
|
3531
|
+
/* @__PURE__ */ jsx("title", { children: "Shokupan AsyncAPI" }),
|
|
3532
|
+
/* @__PURE__ */ jsx("link", { rel: "preconnect", href: "https://fonts.googleapis.com" }),
|
|
3533
|
+
/* @__PURE__ */ jsx("link", { rel: "preconnect", href: "https://fonts.gstatic.com", crossOrigin: "anonymous" }),
|
|
3534
|
+
/* @__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" }),
|
|
3535
|
+
/* @__PURE__ */ jsx("link", { rel: "stylesheet", href: `${base}/theme.css` }),
|
|
3536
|
+
/* @__PURE__ */ jsx("link", { rel: "stylesheet", href: `${base}/style.css` }),
|
|
3537
|
+
/* @__PURE__ */ jsx("script", { dangerouslySetInnerHTML: {
|
|
3538
|
+
__html: `
|
|
3539
|
+
window.INITIAL_SPEC = ${JSON.stringify(spec)};
|
|
3540
|
+
window.INITIAL_SERVER_URL = "${serverUrl}";
|
|
3541
|
+
window.DISABLE_SOURCE_VIEW = ${JSON.stringify(disableSourceView)};
|
|
3542
|
+
`
|
|
3543
|
+
} })
|
|
3544
|
+
] }),
|
|
3545
|
+
/* @__PURE__ */ jsxs("body", { children: [
|
|
3546
|
+
/* @__PURE__ */ jsxs("div", { class: "app-container", children: [
|
|
3547
|
+
/* @__PURE__ */ jsx(Sidebar, { navTree, disableSourceView }),
|
|
3548
|
+
/* @__PURE__ */ jsx("div", { class: "resizer", id: "resizer-left" }),
|
|
3549
|
+
/* @__PURE__ */ jsx(MainContent, {}),
|
|
3550
|
+
/* @__PURE__ */ jsx("div", { class: "resizer", id: "resizer-right" }),
|
|
3551
|
+
/* @__PURE__ */ jsx(ConsolePanel, { serverUrl })
|
|
3552
|
+
] }),
|
|
3553
|
+
/* @__PURE__ */ jsx("script", { src: "https://cdn.socket.io/4.7.4/socket.io.min.js" }),
|
|
3554
|
+
/* @__PURE__ */ jsx("script", { src: "https://cdn.jsdelivr.net/npm/monaco-editor@0.45.0/min/vs/loader.js" }),
|
|
3555
|
+
/* @__PURE__ */ jsx("script", { src: `${base}/asyncapi-client.mjs`, type: "module" })
|
|
3556
|
+
] })
|
|
3557
|
+
] });
|
|
3558
|
+
}
|
|
3559
|
+
function Sidebar({ navTree, disableSourceView }) {
|
|
3560
|
+
return /* @__PURE__ */ jsxs("div", { class: "sidebar scroller", id: "sidebar", children: [
|
|
3561
|
+
/* @__PURE__ */ jsxs("div", { class: "sidebar-header", style: "display:flex; justify-content:space-between; align-items:center;", children: [
|
|
3562
|
+
/* @__PURE__ */ jsx("h2", { children: "AsyncAPI" }),
|
|
3563
|
+
/* @__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" }) }) })
|
|
3564
|
+
] }),
|
|
3565
|
+
/* @__PURE__ */ jsx("div", { class: "nav-list", id: "nav-list", children: /* @__PURE__ */ jsx(NavNode, { node: navTree, level: 0, disableSourceView }) })
|
|
3566
|
+
] });
|
|
3567
|
+
}
|
|
3568
|
+
function NavNode({ node, level, disableSourceView }) {
|
|
3569
|
+
const sortedEntries = Object.entries(node.children || {}).sort((a, b) => {
|
|
3570
|
+
const [aKey, aItem] = a;
|
|
3571
|
+
const [bKey, bItem] = b;
|
|
3572
|
+
const isWarningA = aItem.data?.op?.["x-warning"];
|
|
3573
|
+
const isWarningB = bItem.data?.op?.["x-warning"];
|
|
3574
|
+
if (isWarningA && !isWarningB) return -1;
|
|
3575
|
+
if (!isWarningA && isWarningB) return 1;
|
|
3576
|
+
if (aKey === bKey) return 0;
|
|
3577
|
+
if (aKey === "Warning" || aKey === "Warnings") return -1;
|
|
3578
|
+
if (bKey === "Warning" || bKey === "Warnings") return 1;
|
|
3579
|
+
if (aKey === "Application") return -1;
|
|
3580
|
+
if (bKey === "Application") return 1;
|
|
3581
|
+
if (aKey[0] === "/") return 1;
|
|
3582
|
+
if (bKey[0] === "/") return -1;
|
|
3583
|
+
return aKey.localeCompare(bKey);
|
|
3584
|
+
});
|
|
3585
|
+
return /* @__PURE__ */ jsx(Fragment, { children: sortedEntries.map(([key, item]) => {
|
|
3586
|
+
const hasChildren = Object.keys(item.children || {}).length > 0;
|
|
3587
|
+
if (level === 0) {
|
|
3588
|
+
return /* @__PURE__ */ jsxs("div", { children: [
|
|
3589
|
+
/* @__PURE__ */ jsx("div", { class: "group-label", children: key }),
|
|
3590
|
+
hasChildren && /* @__PURE__ */ jsx("div", { class: "tree-node", style: "margin-left: 0", children: /* @__PURE__ */ jsx(NavNode, { node: item, level: level + 1, disableSourceView }) })
|
|
3591
|
+
] }, key);
|
|
3592
|
+
}
|
|
3593
|
+
const isLeaf = item.isLeaf;
|
|
3594
|
+
return /* @__PURE__ */ jsxs("div", { children: [
|
|
3595
|
+
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 }) }),
|
|
3596
|
+
hasChildren && /* @__PURE__ */ jsx("div", { class: "tree-node", children: /* @__PURE__ */ jsx(NavNode, { node: item, level: level + 1, disableSourceView }) })
|
|
3597
|
+
] }, key);
|
|
3598
|
+
}) });
|
|
3599
|
+
}
|
|
3600
|
+
function LeafNode({ item, label, disableSourceView }) {
|
|
3601
|
+
const isWarning = item.data?.op?.["x-warning"];
|
|
3602
|
+
const opId = item.data?.name;
|
|
3603
|
+
const sourceInfo = item.data?.op?.["x-source-info"];
|
|
3604
|
+
let content;
|
|
3605
|
+
if (isWarning) {
|
|
3606
|
+
content = /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
3607
|
+
/* @__PURE__ */ jsx("span", { style: "margin-right: 6px;", children: "⚠️" }),
|
|
3608
|
+
/* @__PURE__ */ jsx("span", { class: "tree-label", children: label })
|
|
3609
|
+
] });
|
|
3610
|
+
} else {
|
|
3611
|
+
const badgeText = item.data.type === "publish" ? "SEND" : "RECV";
|
|
3612
|
+
content = /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
3613
|
+
/* @__PURE__ */ jsx("span", { class: `badge badge-${badgeText}`, children: badgeText }),
|
|
3614
|
+
/* @__PURE__ */ jsx("span", { class: "tree-label", children: label })
|
|
3615
|
+
] });
|
|
3616
|
+
}
|
|
3617
|
+
return /* @__PURE__ */ jsxs("div", { class: "tree-item", "data-event": opId, style: isWarning ? "color: #fbbf24" : "", children: [
|
|
3618
|
+
content,
|
|
3619
|
+
sourceInfo && !disableSourceView && /* @__PURE__ */ jsx(
|
|
3620
|
+
"a",
|
|
3621
|
+
{
|
|
3622
|
+
href: `vscode://file/${sourceInfo.file}:${sourceInfo.line}`,
|
|
3623
|
+
class: "source-link",
|
|
3624
|
+
onClick: (e) => {
|
|
3625
|
+
e.stopPropagation();
|
|
3626
|
+
},
|
|
3627
|
+
title: `${sourceInfo.file}:${sourceInfo.line}`,
|
|
3628
|
+
children: /* @__PURE__ */ jsxs("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", style: "display:block", children: [
|
|
3629
|
+
/* @__PURE__ */ jsx("polyline", { points: "16 18 22 12 16 6" }),
|
|
3630
|
+
/* @__PURE__ */ jsx("polyline", { points: "8 6 2 12 8 18" })
|
|
3631
|
+
] })
|
|
3632
|
+
}
|
|
3633
|
+
)
|
|
3634
|
+
] });
|
|
3635
|
+
}
|
|
3636
|
+
function MainContent() {
|
|
3637
|
+
return /* @__PURE__ */ jsxs("div", { id: "main-wrapper", style: "flex: 1; min-width: 0; position: relative; overflow: hidden;", children: [
|
|
3638
|
+
/* @__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" }) }) }),
|
|
3639
|
+
/* @__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" }) }) }),
|
|
3640
|
+
/* @__PURE__ */ jsx("main", { class: "main-content scroller", id: "doc-panel", style: "height: 100%;", children: /* @__PURE__ */ jsxs("div", { class: "empty-state", children: [
|
|
3641
|
+
/* @__PURE__ */ jsxs("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "1.5", children: [
|
|
3642
|
+
/* @__PURE__ */ jsx("path", { d: "M4 19.5A2.5 2.5 0 0 1 6.5 17H20" }),
|
|
3643
|
+
/* @__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" })
|
|
3644
|
+
] }),
|
|
3645
|
+
/* @__PURE__ */ jsx("h3", { children: "Select an event to view details" })
|
|
3646
|
+
] }) })
|
|
3647
|
+
] });
|
|
3648
|
+
}
|
|
3649
|
+
function ConsolePanel({ serverUrl }) {
|
|
3650
|
+
return /* @__PURE__ */ jsxs("div", { class: "console-panel", id: "console-panel", children: [
|
|
3651
|
+
/* @__PURE__ */ jsxs("div", { class: "console-header", children: [
|
|
3652
|
+
/* @__PURE__ */ jsxs("div", { style: "display:flex; justify-content:space-between; align-items:center; margin-bottom: 8px;", children: [
|
|
3653
|
+
/* @__PURE__ */ jsx("h3", { style: "margin:0; font-size:1rem;", children: "Console" }),
|
|
3654
|
+
/* @__PURE__ */ jsxs("div", { style: "display:flex; gap: 4px;", children: [
|
|
3655
|
+
/* @__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" }) }) }),
|
|
3656
|
+
/* @__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" }) }) })
|
|
3657
|
+
] })
|
|
3658
|
+
] }),
|
|
3659
|
+
/* @__PURE__ */ jsxs("div", { class: "connection-bar", children: [
|
|
3660
|
+
/* @__PURE__ */ jsxs("select", { id: "protocol", children: [
|
|
3661
|
+
/* @__PURE__ */ jsx("option", { value: "ws", children: "WS" }),
|
|
3662
|
+
/* @__PURE__ */ jsx("option", { value: "wss", children: "WSS" }),
|
|
3663
|
+
/* @__PURE__ */ jsx("option", { value: "socket.io", children: "Socket.IO" })
|
|
3664
|
+
] }),
|
|
3665
|
+
/* @__PURE__ */ jsx("div", { style: "width: 1px; background: rgba(255,255,255,0.1); margin: 2px 0;" }),
|
|
3666
|
+
/* @__PURE__ */ jsx("input", { type: "text", id: "url", value: serverUrl })
|
|
3667
|
+
] }),
|
|
3668
|
+
/* @__PURE__ */ jsxs("div", { style: "display: grid; grid-template-columns: 1fr auto; gap: 8px;", children: [
|
|
3669
|
+
/* @__PURE__ */ jsx("button", { id: "connect-btn", class: "btn", children: "Connect" }),
|
|
3670
|
+
/* @__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" }) }) })
|
|
3671
|
+
] }),
|
|
3672
|
+
/* @__PURE__ */ jsxs("div", { class: "status-indicator", children: [
|
|
3673
|
+
/* @__PURE__ */ jsx("div", { id: "status-dot", class: "dot" }),
|
|
3674
|
+
/* @__PURE__ */ jsx("span", { id: "connection-status", children: "Disconnected" })
|
|
3675
|
+
] })
|
|
3676
|
+
] }),
|
|
3677
|
+
/* @__PURE__ */ jsx("div", { class: "logs-container scroller", id: "logs", children: /* @__PURE__ */ jsx("div", { class: "log-shim", id: "log-shim" }) }),
|
|
3678
|
+
/* @__PURE__ */ jsxs("div", { class: "compose-area", children: [
|
|
3679
|
+
/* @__PURE__ */ jsxs("div", { class: "compose-header", children: [
|
|
3680
|
+
/* @__PURE__ */ jsx("span", { children: "Payload" }),
|
|
3681
|
+
/* @__PURE__ */ jsx("span", { id: "target-event", style: "color: var(--primary);", children: "--" })
|
|
3682
|
+
] }),
|
|
3683
|
+
/* @__PURE__ */ jsx("div", { id: "editor-container" }),
|
|
3684
|
+
/* @__PURE__ */ jsx("div", { class: "send-bar", children: /* @__PURE__ */ jsx("button", { id: "send-btn", class: "btn", children: "Send Message" }) })
|
|
3685
|
+
] })
|
|
3686
|
+
] });
|
|
3687
|
+
}
|
|
3688
|
+
function buildNavTree(spec) {
|
|
3689
|
+
if (!spec || !spec.channels) return { children: {} };
|
|
3690
|
+
const root = { children: {} };
|
|
3691
|
+
Object.keys(spec.channels).forEach((name) => {
|
|
3692
|
+
const ch = spec.channels[name];
|
|
3693
|
+
const op = ch.publish || ch.subscribe;
|
|
3694
|
+
const type = ch.publish ? "publish" : "subscribe";
|
|
3695
|
+
const tag = op.tags && op.tags.length > 0 ? op.tags[0].name : "General";
|
|
3696
|
+
if (!root.children[tag]) root.children[tag] = { children: {} };
|
|
3697
|
+
const parts = name.split(/[\.\/]/);
|
|
3698
|
+
let current = root.children[tag];
|
|
3699
|
+
parts.forEach((part, i) => {
|
|
3700
|
+
if (!current.children[part]) current.children[part] = { children: {} };
|
|
3701
|
+
current = current.children[part];
|
|
3702
|
+
if (i === parts.length - 1) {
|
|
3703
|
+
current.isLeaf = true;
|
|
3704
|
+
current.data = { name, op, type };
|
|
3705
|
+
}
|
|
3706
|
+
});
|
|
3707
|
+
});
|
|
3708
|
+
return root;
|
|
3709
|
+
}
|
|
3710
|
+
async function getAstRoutes(applications) {
|
|
3711
|
+
const astRoutes = [];
|
|
3712
|
+
const getExpandedRoutes = (app, prefix = "", seen = /* @__PURE__ */ new Set()) => {
|
|
3713
|
+
if (seen.has(app.name)) return [];
|
|
3714
|
+
const newSeen = new Set(seen);
|
|
3715
|
+
newSeen.add(app.name);
|
|
3716
|
+
const expanded = [];
|
|
3717
|
+
for (const route of app.routes) {
|
|
3718
|
+
expanded.push({
|
|
3719
|
+
...route,
|
|
3720
|
+
// For events, path is the event name
|
|
3721
|
+
path: route.path.startsWith("/") ? route.path.slice(1) : route.path
|
|
3722
|
+
});
|
|
3723
|
+
}
|
|
3724
|
+
if (app.mounted) {
|
|
3725
|
+
for (const mount of app.mounted) {
|
|
3726
|
+
const targetApp = applications.find((a) => a.name === mount.target || a.className === mount.target);
|
|
3727
|
+
if (targetApp) {
|
|
3728
|
+
expanded.push(...getExpandedRoutes(targetApp, "", newSeen));
|
|
3729
|
+
}
|
|
3730
|
+
}
|
|
3731
|
+
}
|
|
3732
|
+
return expanded;
|
|
3733
|
+
};
|
|
3734
|
+
applications.forEach((app) => {
|
|
3735
|
+
astRoutes.push(...getExpandedRoutes(app));
|
|
3736
|
+
});
|
|
3737
|
+
return astRoutes;
|
|
3738
|
+
}
|
|
3739
|
+
async function generateAsyncApi(rootRouter, options = {}) {
|
|
3740
|
+
const channels = {};
|
|
3741
|
+
let astRoutes = [];
|
|
3742
|
+
try {
|
|
3743
|
+
const { OpenAPIAnalyzer: OpenAPIAnalyzer2 } = await import("./analyzer-BqIe1p0R.js");
|
|
3744
|
+
const entrypoint = globalThis.Bun?.main || require.main?.filename || process.argv[1];
|
|
3745
|
+
const analyzer = new OpenAPIAnalyzer2(process.cwd(), entrypoint);
|
|
3746
|
+
const { applications } = await analyzer.analyze();
|
|
3747
|
+
astRoutes = await getAstRoutes(applications);
|
|
3748
|
+
} catch (e) {
|
|
3749
|
+
}
|
|
3750
|
+
const matchedAstRoutes = /* @__PURE__ */ new Set();
|
|
3751
|
+
const collect = async (router, prefix = "") => {
|
|
3752
|
+
const eventHandlers = router.getEventHandlers();
|
|
3753
|
+
let routerTag = "Other";
|
|
3754
|
+
if (router[$isApplication]) {
|
|
3755
|
+
routerTag = "Application";
|
|
3756
|
+
} else if (router.constructor.name && router.constructor.name !== "ShokupanRouter") {
|
|
3757
|
+
routerTag = router.constructor.name;
|
|
3758
|
+
} else {
|
|
3759
|
+
routerTag = router[$mountPath] || "Router";
|
|
3760
|
+
}
|
|
3761
|
+
if (eventHandlers) {
|
|
3762
|
+
for (const [eventName, handlers] of eventHandlers.entries()) {
|
|
3763
|
+
for (const handler of handlers) {
|
|
3764
|
+
const userSpec = handler.spec;
|
|
3765
|
+
let tags = userSpec?.tags;
|
|
3766
|
+
if (!tags && routerTag) {
|
|
3767
|
+
tags = [{ name: routerTag }];
|
|
3768
|
+
}
|
|
3769
|
+
let astMatch = astRoutes.find(
|
|
3770
|
+
(r) => (r.method === "EVENT" || r.method === "ON") && r.path === eventName
|
|
3771
|
+
);
|
|
3772
|
+
if (!astMatch) {
|
|
3773
|
+
const runtimeSource = (handler.originalHandler || handler).toString();
|
|
3774
|
+
const stripComments = (s) => s.replace(/\/\*[\s\S]*?\*\/|([^\\:]|^)\/\/.*$/gm, "$1");
|
|
3775
|
+
const normalize = (s) => stripComments(s).replace(/\s+/g, "");
|
|
3776
|
+
const runtimeHandlerSrc = normalize(runtimeSource);
|
|
3777
|
+
const eventRoutes = astRoutes.filter((r) => r.method === "EVENT" || r.method === "ON");
|
|
3778
|
+
astMatch = eventRoutes.find((r) => {
|
|
3779
|
+
const astHandlerSrc = normalize(r.handlerSource || r.handlerName || "");
|
|
3780
|
+
if (!astHandlerSrc || astHandlerSrc.length < 5) return false;
|
|
3781
|
+
return runtimeHandlerSrc.includes(astHandlerSrc) || astHandlerSrc.includes(runtimeHandlerSrc) || r.handlerSource && runtimeHandlerSrc.includes(normalize(r.handlerSource).substring(0, 50));
|
|
3782
|
+
});
|
|
3783
|
+
}
|
|
3784
|
+
if (astMatch) matchedAstRoutes.add(astMatch);
|
|
3785
|
+
const sourceInfo = handler.source || astMatch?.sourceContext ? {
|
|
3786
|
+
file: handler.source?.file || astMatch?.sourceContext?.file,
|
|
3787
|
+
line: handler.source?.line || astMatch?.sourceContext?.startLine,
|
|
3788
|
+
startLine: handler.source?.line || astMatch?.sourceContext?.startLine,
|
|
3789
|
+
endLine: astMatch?.sourceContext?.endLine,
|
|
3790
|
+
highlightLines: astMatch?.sourceContext ? [astMatch.sourceContext.startLine, astMatch.sourceContext.endLine] : void 0
|
|
3791
|
+
} : void 0;
|
|
3792
|
+
if (!channels[eventName]) {
|
|
3793
|
+
channels[eventName] = {
|
|
3794
|
+
publish: {
|
|
3795
|
+
operationId: `on${eventName.charAt(0).toUpperCase() + eventName.slice(1)}`,
|
|
3796
|
+
tags,
|
|
3797
|
+
message: {
|
|
3798
|
+
payload: { type: "object" },
|
|
3799
|
+
...userSpec?.message ? userSpec.message : {}
|
|
3800
|
+
},
|
|
3801
|
+
...userSpec?.type === "publish" ? userSpec : {},
|
|
3802
|
+
"x-source-info": sourceInfo ? [sourceInfo] : [],
|
|
3803
|
+
"x-shokupan-source": sourceInfo
|
|
3804
|
+
// Simplified
|
|
3805
|
+
}
|
|
3806
|
+
};
|
|
3807
|
+
if (userSpec?.summary) channels[eventName].publish.summary = userSpec.summary;
|
|
3808
|
+
if (userSpec?.description) channels[eventName].publish.description = userSpec.description;
|
|
3809
|
+
} else {
|
|
3810
|
+
if (sourceInfo) {
|
|
3811
|
+
if (!channels[eventName].publish["x-source-info"]) {
|
|
3812
|
+
channels[eventName].publish["x-source-info"] = [];
|
|
3813
|
+
}
|
|
3814
|
+
const exists = channels[eventName].publish["x-source-info"].some(
|
|
3815
|
+
(s) => s.file === sourceInfo.file && s.line === sourceInfo.line
|
|
3816
|
+
);
|
|
3817
|
+
if (!exists) {
|
|
3818
|
+
channels[eventName].publish["x-source-info"].push(sourceInfo);
|
|
3819
|
+
}
|
|
3820
|
+
}
|
|
3821
|
+
}
|
|
3822
|
+
let emits = astMatch?.emits || [];
|
|
3823
|
+
for (const emit of emits) {
|
|
3824
|
+
if (emit.event === "__DYNAMIC_EMIT__") {
|
|
3825
|
+
const warningKey = `${eventName}/Dynamic Emit`;
|
|
3826
|
+
channels[warningKey] = {
|
|
3827
|
+
subscribe: {
|
|
3828
|
+
operationId: `dynamicEmitWarning${eventName}`,
|
|
3829
|
+
summary: "Dynamic Emit Detected",
|
|
3830
|
+
description: "This handler emits an event with a dynamic name that could not be determined statically.",
|
|
3831
|
+
tags,
|
|
3832
|
+
"x-warning": true,
|
|
3833
|
+
"x-source-info": {
|
|
3834
|
+
file: astMatch?.sourceContext?.file,
|
|
3835
|
+
line: emit.location?.startLine,
|
|
3836
|
+
startLine: emit.location?.startLine,
|
|
3837
|
+
endLine: emit.location?.endLine,
|
|
3838
|
+
highlightLines: emit.location ? [emit.location.startLine, emit.location.endLine] : void 0
|
|
3839
|
+
},
|
|
3840
|
+
"x-shokupan-source": {
|
|
3841
|
+
file: astMatch?.sourceContext?.file,
|
|
3842
|
+
line: emit.location?.startLine
|
|
3843
|
+
},
|
|
3844
|
+
message: { payload: { type: "object" } }
|
|
3845
|
+
}
|
|
3846
|
+
};
|
|
3847
|
+
continue;
|
|
3848
|
+
}
|
|
3849
|
+
const emitStart = emit.location?.startLine;
|
|
3850
|
+
const emitEnd = emit.location?.endLine;
|
|
3851
|
+
const newSourceInfo = sourceInfo && emitStart ? {
|
|
3852
|
+
file: sourceInfo.file,
|
|
3853
|
+
line: emitStart,
|
|
3854
|
+
startLine: emitStart,
|
|
3855
|
+
endLine: emitEnd,
|
|
3856
|
+
highlightLines: sourceInfo.highlightLines,
|
|
3857
|
+
emitHighlightLines: [emitStart, emitEnd]
|
|
3858
|
+
} : void 0;
|
|
3859
|
+
if (!channels[emit.event]) {
|
|
3860
|
+
channels[emit.event] = {
|
|
3861
|
+
subscribe: {
|
|
3862
|
+
operationId: `emit${emit.event.charAt(0).toUpperCase() + emit.event.slice(1)}`,
|
|
3863
|
+
tags,
|
|
3864
|
+
message: {
|
|
3865
|
+
payload: emit.payload || { type: "object" }
|
|
3866
|
+
},
|
|
3867
|
+
"x-source-info": newSourceInfo ? [newSourceInfo] : [],
|
|
3868
|
+
"x-shokupan-source": sourceInfo && emitStart ? {
|
|
3869
|
+
file: sourceInfo.file,
|
|
3870
|
+
line: emitStart
|
|
3871
|
+
} : void 0
|
|
3872
|
+
}
|
|
3873
|
+
};
|
|
3874
|
+
} else {
|
|
3875
|
+
if (newSourceInfo) {
|
|
3876
|
+
if (!channels[emit.event].subscribe["x-source-info"]) {
|
|
3877
|
+
channels[emit.event].subscribe["x-source-info"] = [];
|
|
3878
|
+
}
|
|
3879
|
+
const existing = channels[emit.event].subscribe["x-source-info"];
|
|
3880
|
+
const exists = existing.some(
|
|
3881
|
+
(s) => s.file === newSourceInfo.file && s.line === newSourceInfo.line
|
|
3882
|
+
);
|
|
3883
|
+
if (!exists) {
|
|
3884
|
+
existing.push(newSourceInfo);
|
|
3885
|
+
}
|
|
3886
|
+
}
|
|
3887
|
+
}
|
|
3888
|
+
}
|
|
3889
|
+
}
|
|
3890
|
+
}
|
|
3891
|
+
}
|
|
3892
|
+
const httpRoutes = router[$routes];
|
|
3893
|
+
if (httpRoutes) {
|
|
3894
|
+
for (const route of httpRoutes) {
|
|
3895
|
+
const handler = route.handler;
|
|
3896
|
+
let tags = route.handlerSpec?.tags;
|
|
3897
|
+
if (!tags && routerTag) {
|
|
3898
|
+
tags = [{ name: routerTag }];
|
|
3899
|
+
}
|
|
3900
|
+
const methodUpper = route.method.toUpperCase();
|
|
3901
|
+
let astMatch = astRoutes.find(
|
|
3902
|
+
(r) => r.method === methodUpper && (r.path === route.path || r.path === "/" + route.path)
|
|
3903
|
+
);
|
|
3904
|
+
if (!astMatch) {
|
|
3905
|
+
const runtimeSource = (handler.originalHandler || handler).toString();
|
|
3906
|
+
const runtimeHandlerSrc = runtimeSource.replace(/\s+/g, " ");
|
|
3907
|
+
const sameMethodRoutes = astRoutes.filter((r) => r.method === methodUpper);
|
|
3908
|
+
astMatch = sameMethodRoutes.find((r) => {
|
|
3909
|
+
const astHandlerSrc = (r.handlerSource || r.handlerName || "").replace(/\s+/g, " ");
|
|
3910
|
+
if (!astHandlerSrc || astHandlerSrc.length < 20) return false;
|
|
3911
|
+
return runtimeHandlerSrc.includes(astHandlerSrc) || astHandlerSrc.includes(runtimeHandlerSrc) || r.handlerSource && runtimeHandlerSrc.includes(r.handlerSource.substring(0, 50));
|
|
3912
|
+
});
|
|
3913
|
+
}
|
|
3914
|
+
const sourceInfo = handler.source || astMatch?.sourceContext ? {
|
|
3915
|
+
file: handler.source?.file || astMatch?.sourceContext?.file,
|
|
3916
|
+
line: handler.source?.line || astMatch?.sourceContext?.startLine,
|
|
3917
|
+
startLine: handler.source?.line || astMatch?.sourceContext?.startLine,
|
|
3918
|
+
endLine: astMatch?.sourceContext?.endLine,
|
|
3919
|
+
highlightLines: astMatch?.sourceContext ? [astMatch.sourceContext.startLine, astMatch.sourceContext.endLine] : void 0
|
|
3920
|
+
} : void 0;
|
|
3921
|
+
let emits = astMatch?.emits || [];
|
|
3922
|
+
for (const emit of emits) {
|
|
3923
|
+
const emitStart = emit.location?.startLine;
|
|
3924
|
+
const emitEnd = emit.location?.endLine;
|
|
3925
|
+
const newSourceInfo = sourceInfo && emitStart ? {
|
|
3926
|
+
file: sourceInfo.file,
|
|
3927
|
+
line: emitStart,
|
|
3928
|
+
startLine: emitStart,
|
|
3929
|
+
endLine: emitEnd,
|
|
3930
|
+
highlightLines: sourceInfo.highlightLines,
|
|
3931
|
+
emitHighlightLines: [emitStart, emitEnd]
|
|
3932
|
+
} : void 0;
|
|
3933
|
+
if (!channels[emit.event]) {
|
|
3934
|
+
channels[emit.event] = {
|
|
3935
|
+
subscribe: {
|
|
3936
|
+
operationId: `emit${emit.event.charAt(0).toUpperCase() + emit.event.slice(1)}`,
|
|
3937
|
+
tags,
|
|
3938
|
+
message: {
|
|
3939
|
+
payload: emit.payload || { type: "object" }
|
|
3940
|
+
},
|
|
3941
|
+
"x-source-info": newSourceInfo ? [newSourceInfo] : [],
|
|
3942
|
+
"x-shokupan-source": sourceInfo && emitStart ? {
|
|
3943
|
+
file: sourceInfo.file,
|
|
3944
|
+
line: emitStart
|
|
3945
|
+
} : void 0
|
|
3946
|
+
}
|
|
3947
|
+
};
|
|
3948
|
+
} else {
|
|
3949
|
+
if (newSourceInfo) {
|
|
3950
|
+
if (!channels[emit.event].subscribe["x-source-info"]) {
|
|
3951
|
+
channels[emit.event].subscribe["x-source-info"] = [];
|
|
3952
|
+
}
|
|
3953
|
+
const existing = channels[emit.event].subscribe["x-source-info"];
|
|
3954
|
+
const exists = existing.some(
|
|
3955
|
+
(s) => s.file === newSourceInfo.file && s.line === newSourceInfo.line
|
|
3956
|
+
);
|
|
3957
|
+
if (!exists) {
|
|
3958
|
+
existing.push(newSourceInfo);
|
|
3959
|
+
}
|
|
3960
|
+
}
|
|
3961
|
+
}
|
|
3962
|
+
}
|
|
3963
|
+
}
|
|
3964
|
+
}
|
|
3965
|
+
const childRouters = router[$childRouters];
|
|
3966
|
+
for (const child of childRouters) {
|
|
3967
|
+
await collect(child);
|
|
3968
|
+
}
|
|
3969
|
+
};
|
|
3970
|
+
await collect(rootRouter);
|
|
3971
|
+
const dynamicEvents = astRoutes.filter((r) => r.path === "__DYNAMIC_EVENT__" && !matchedAstRoutes.has(r));
|
|
3972
|
+
dynamicEvents.forEach((r, i) => {
|
|
3973
|
+
let prefix = "Anonymous";
|
|
3974
|
+
if (r.handlerName && !r.handlerName.includes("=>") && !r.handlerName.includes("{")) {
|
|
3975
|
+
const parts = r.handlerName.split(".");
|
|
3976
|
+
if (parts.length > 0) prefix = parts[0];
|
|
3977
|
+
}
|
|
3978
|
+
const key = `${prefix}.Dynamic Event ${i + 1}`;
|
|
3979
|
+
channels[key] = {
|
|
3980
|
+
publish: {
|
|
3981
|
+
operationId: `dynamicEventWarning${i}`,
|
|
3982
|
+
summary: "Dynamic Event Detected",
|
|
3983
|
+
description: `A dynamic event listener was detected in your source code but the event name could not be determined statically.`,
|
|
3984
|
+
tags: [{ name: "Warnings" }],
|
|
3985
|
+
"x-warning": true,
|
|
3986
|
+
"x-source-info": {
|
|
3987
|
+
file: r.sourceContext?.file,
|
|
3988
|
+
line: r.sourceContext?.startLine,
|
|
3989
|
+
startLine: r.sourceContext?.startLine,
|
|
3990
|
+
endLine: r.sourceContext?.endLine,
|
|
3991
|
+
highlightLines: r.sourceContext ? [r.sourceContext.startLine, r.sourceContext.endLine] : void 0
|
|
3992
|
+
},
|
|
3993
|
+
"x-shokupan-source": {
|
|
3994
|
+
file: r.sourceContext?.file,
|
|
3995
|
+
line: r.sourceContext?.startLine
|
|
3996
|
+
},
|
|
3997
|
+
message: { payload: { type: "object" } }
|
|
3998
|
+
}
|
|
3999
|
+
};
|
|
4000
|
+
});
|
|
4001
|
+
return {
|
|
4002
|
+
asyncapi: "3.0.0",
|
|
4003
|
+
info: { title: "Shokupan AsyncAPI", version: "1.0.0", ...options.info },
|
|
4004
|
+
channels
|
|
4005
|
+
};
|
|
4006
|
+
}
|
|
4007
|
+
const generator = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
|
|
4008
|
+
__proto__: null,
|
|
4009
|
+
generateAsyncApi
|
|
4010
|
+
}, Symbol.toStringTag, { value: "Module" }));
|
|
4011
|
+
class AsyncApiPlugin extends ShokupanRouter {
|
|
4012
|
+
constructor(pluginOptions = {}) {
|
|
4013
|
+
super({ renderer: renderToString });
|
|
4014
|
+
this.pluginOptions = pluginOptions;
|
|
4015
|
+
this.pluginOptions.path ??= "/asyncapi";
|
|
4016
|
+
this.init();
|
|
4017
|
+
}
|
|
4018
|
+
static getBasePath() {
|
|
4019
|
+
const dir = dirname(fileURLToPath(import.meta.url));
|
|
4020
|
+
if (dir.endsWith("dist")) {
|
|
4021
|
+
return dir + "/plugins/application/asyncapi";
|
|
4022
|
+
}
|
|
4023
|
+
return dir;
|
|
4024
|
+
}
|
|
4025
|
+
onInit(app, options) {
|
|
4026
|
+
const path = this.pluginOptions.path || options?.path || "/asyncapi";
|
|
4027
|
+
app.mount(path, this);
|
|
4028
|
+
if (app.applicationConfig.enableAsyncApiGen !== true) {
|
|
4029
|
+
console.warn("AsyncApiPlugin: enableAsyncApiGen is disabled. AsyncApiPlugin will not generate spec.");
|
|
4030
|
+
}
|
|
4031
|
+
}
|
|
4032
|
+
init() {
|
|
4033
|
+
const serveFile = async (ctx, file, type) => {
|
|
4034
|
+
const content = await readFile(join$1(AsyncApiPlugin.getBasePath(), "static", file), "utf-8");
|
|
4035
|
+
ctx.set("Content-Type", type);
|
|
4036
|
+
return ctx.send(content);
|
|
4037
|
+
};
|
|
4038
|
+
this.get("/style.css", (ctx) => serveFile(ctx, "style.css", "text/css"));
|
|
4039
|
+
this.get("/theme.css", (ctx) => serveFile(ctx, "theme.css", "text/css"));
|
|
4040
|
+
this.get("/asyncapi-client.mjs", (ctx) => serveFile(ctx, "asyncapi-client.mjs", "application/javascript"));
|
|
4041
|
+
this.get("/", async (ctx) => {
|
|
4042
|
+
let spec = ctx.app?.asyncApiSpec;
|
|
4043
|
+
if (!spec) {
|
|
4044
|
+
spec = await generateAsyncApi(ctx.app);
|
|
4045
|
+
}
|
|
4046
|
+
if (this.pluginOptions.spec) {
|
|
4047
|
+
deepMerge(spec, this.pluginOptions.spec);
|
|
4048
|
+
}
|
|
4049
|
+
const serverUrl = `${ctx.hostname}:${ctx.app?.applicationConfig.port}`;
|
|
4050
|
+
const base = this.pluginOptions.path;
|
|
4051
|
+
const disableSourceView = this.pluginOptions.disableSourceView;
|
|
4052
|
+
const navTree = buildNavTree(spec);
|
|
4053
|
+
return ctx.jsx(AsyncApiApp({ spec, serverUrl, base, disableSourceView, navTree }));
|
|
4054
|
+
});
|
|
4055
|
+
this.get("/json", async (ctx) => {
|
|
4056
|
+
let spec = ctx.app?.asyncApiSpec;
|
|
4057
|
+
if (!spec) {
|
|
4058
|
+
spec = await generateAsyncApi(ctx.app);
|
|
4059
|
+
}
|
|
4060
|
+
if (this.pluginOptions.spec) {
|
|
4061
|
+
deepMerge(spec, this.pluginOptions.spec);
|
|
4062
|
+
}
|
|
4063
|
+
return ctx.json(spec);
|
|
4064
|
+
});
|
|
4065
|
+
this.get("/_code", async (ctx) => {
|
|
4066
|
+
const file = ctx.query["file"];
|
|
4067
|
+
if (!file || typeof file !== "string") {
|
|
4068
|
+
return ctx.text("Missing file parameter", 400);
|
|
4069
|
+
}
|
|
4070
|
+
try {
|
|
4071
|
+
const content = await readFile(file, "utf8");
|
|
4072
|
+
return ctx.text(content);
|
|
4073
|
+
} catch (e) {
|
|
4074
|
+
return ctx.text("File not found: " + e.message, 404);
|
|
4075
|
+
}
|
|
4076
|
+
});
|
|
4077
|
+
}
|
|
4078
|
+
}
|
|
3246
4079
|
class AuthPlugin extends ShokupanRouter {
|
|
3247
4080
|
constructor(authConfig) {
|
|
3248
4081
|
super();
|
|
3249
4082
|
this.authConfig = authConfig;
|
|
3250
4083
|
this.secret = typeof authConfig.jwtSecret === "string" ? new TextEncoder().encode(authConfig.jwtSecret) : authConfig.jwtSecret;
|
|
3251
|
-
this.init();
|
|
3252
4084
|
}
|
|
3253
4085
|
secret;
|
|
3254
|
-
|
|
4086
|
+
arctic;
|
|
4087
|
+
jose;
|
|
4088
|
+
async onInit(app, options) {
|
|
4089
|
+
this.arctic = await import("arctic");
|
|
4090
|
+
this.jose = await import("jose");
|
|
4091
|
+
this.init();
|
|
3255
4092
|
if (options?.path) {
|
|
3256
4093
|
app.mount(options.path, this);
|
|
3257
4094
|
} else {
|
|
@@ -3259,6 +4096,7 @@ class AuthPlugin extends ShokupanRouter {
|
|
|
3259
4096
|
}
|
|
3260
4097
|
}
|
|
3261
4098
|
getProviderInstance(name, p) {
|
|
4099
|
+
const { GitHub, Google, MicrosoftEntraId, Apple, Auth0, Okta, OAuth2Client } = this.arctic;
|
|
3262
4100
|
switch (name) {
|
|
3263
4101
|
case "github":
|
|
3264
4102
|
return new GitHub(p.clientId, p.clientSecret, p.redirectUri);
|
|
@@ -3286,7 +4124,7 @@ class AuthPlugin extends ShokupanRouter {
|
|
|
3286
4124
|
}
|
|
3287
4125
|
async createSession(user, ctx) {
|
|
3288
4126
|
const alg = "HS256";
|
|
3289
|
-
const jwt = await new jose.SignJWT({ ...user }).setProtectedHeader({ alg }).setIssuedAt().setExpirationTime(this.authConfig.jwtExpiration || "24h").sign(this.secret);
|
|
4127
|
+
const jwt = await new this.jose.SignJWT({ ...user }).setProtectedHeader({ alg }).setIssuedAt().setExpirationTime(this.authConfig.jwtExpiration || "24h").sign(this.secret);
|
|
3290
4128
|
const opts = this.authConfig.cookieOptions || {};
|
|
3291
4129
|
let cookie = `auth_token=${jwt}; Path=${opts.path || "/"}; HttpOnly`;
|
|
3292
4130
|
if (opts.secure) cookie += "; Secure";
|
|
@@ -3296,6 +4134,7 @@ class AuthPlugin extends ShokupanRouter {
|
|
|
3296
4134
|
return jwt;
|
|
3297
4135
|
}
|
|
3298
4136
|
init() {
|
|
4137
|
+
const { generateState, generateCodeVerifier, GitHub, Google, MicrosoftEntraId, Apple, Auth0, Okta, OAuth2Client } = this.arctic;
|
|
3299
4138
|
const providerEntries = Object.entries(this.authConfig.providers);
|
|
3300
4139
|
for (let i = 0; i < providerEntries.length; i++) {
|
|
3301
4140
|
const [providerName, providerConfig] = providerEntries[i];
|
|
@@ -3427,7 +4266,7 @@ class AuthPlugin extends ShokupanRouter {
|
|
|
3427
4266
|
};
|
|
3428
4267
|
} else if (provider === "apple") {
|
|
3429
4268
|
if (idToken) {
|
|
3430
|
-
const payload = jose.decodeJwt(idToken);
|
|
4269
|
+
const payload = this.jose.decodeJwt(idToken);
|
|
3431
4270
|
user = {
|
|
3432
4271
|
id: payload.sub,
|
|
3433
4272
|
email: payload["email"],
|
|
@@ -3458,6 +4297,9 @@ class AuthPlugin extends ShokupanRouter {
|
|
|
3458
4297
|
*/
|
|
3459
4298
|
getMiddleware() {
|
|
3460
4299
|
return async (ctx, next) => {
|
|
4300
|
+
if (!this.jose) {
|
|
4301
|
+
this.jose = await import("jose");
|
|
4302
|
+
}
|
|
3461
4303
|
const authHeader = ctx.req.headers.get("Authorization");
|
|
3462
4304
|
let token = authHeader?.startsWith("Bearer ") ? authHeader.substring(7) : null;
|
|
3463
4305
|
if (!token) {
|
|
@@ -3466,7 +4308,7 @@ class AuthPlugin extends ShokupanRouter {
|
|
|
3466
4308
|
}
|
|
3467
4309
|
if (token) {
|
|
3468
4310
|
try {
|
|
3469
|
-
const { payload } = await jose.jwtVerify(token, this.secret);
|
|
4311
|
+
const { payload } = await this.jose.jwtVerify(token, this.secret);
|
|
3470
4312
|
ctx.user = payload;
|
|
3471
4313
|
} catch {
|
|
3472
4314
|
}
|
|
@@ -3583,6 +4425,187 @@ class ClusterPlugin {
|
|
|
3583
4425
|
}
|
|
3584
4426
|
}
|
|
3585
4427
|
}
|
|
4428
|
+
function DashboardApp({ metrics, uptime, integrations, base, getRequestHeadersSource }) {
|
|
4429
|
+
return /* @__PURE__ */ jsxs("html", { lang: "en", children: [
|
|
4430
|
+
/* @__PURE__ */ jsxs("head", { children: [
|
|
4431
|
+
/* @__PURE__ */ jsx("meta", { charSet: "UTF-8" }),
|
|
4432
|
+
/* @__PURE__ */ jsx("meta", { name: "viewport", content: "width=device-width, initial-scale=1.0" }),
|
|
4433
|
+
/* @__PURE__ */ jsx("title", { children: "Shokupan Debug Dashboard" }),
|
|
4434
|
+
/* @__PURE__ */ jsx("link", { href: "https://unpkg.com/tabulator-tables@5.5.0/dist/css/tabulator_bootstrap5.min.css", rel: "stylesheet" }),
|
|
4435
|
+
/* @__PURE__ */ jsx("link", { rel: "stylesheet", href: "https://esm.sh/@xyflow/react@12.3.6/dist/style.css" }),
|
|
4436
|
+
/* @__PURE__ */ jsx("link", { rel: "stylesheet", href: `${base}/theme.css` }),
|
|
4437
|
+
/* @__PURE__ */ jsx("link", { rel: "stylesheet", href: `${base}/styles.css` }),
|
|
4438
|
+
/* @__PURE__ */ jsx("link", { rel: "stylesheet", href: `${base}/reactflow.css` }),
|
|
4439
|
+
/* @__PURE__ */ jsx("link", { rel: "stylesheet", href: `${base}/registry.css` }),
|
|
4440
|
+
/* @__PURE__ */ jsx("link", { rel: "stylesheet", href: `${base}/tabulator.css` }),
|
|
4441
|
+
/* @__PURE__ */ jsx("script", { src: "https://cdn.jsdelivr.net/npm/chart.js" }),
|
|
4442
|
+
/* @__PURE__ */ jsx("script", { type: "text/javascript", src: "https://unpkg.com/tabulator-tables@5.5.0/dist/js/tabulator.min.js" })
|
|
4443
|
+
] }),
|
|
4444
|
+
/* @__PURE__ */ jsxs("body", { children: [
|
|
4445
|
+
/* @__PURE__ */ jsxs("div", { class: "container", children: [
|
|
4446
|
+
/* @__PURE__ */ jsxs("header", { children: [
|
|
4447
|
+
/* @__PURE__ */ jsxs("div", { children: [
|
|
4448
|
+
/* @__PURE__ */ jsx("h1", { children: "Dashboard" }),
|
|
4449
|
+
/* @__PURE__ */ jsxs("div", { style: "color: var(--text-secondary)", children: [
|
|
4450
|
+
"Uptime: ",
|
|
4451
|
+
/* @__PURE__ */ jsx("span", { id: "uptime", children: uptime })
|
|
4452
|
+
] })
|
|
4453
|
+
] }),
|
|
4454
|
+
/* @__PURE__ */ jsxs("div", { class: "tabs", children: [
|
|
4455
|
+
/* @__PURE__ */ jsx("button", { class: "tab-btn active", onclick: "switchTab('overview')", children: "Overview" }),
|
|
4456
|
+
/* @__PURE__ */ jsx("button", { class: "tab-btn", onclick: "switchTab('registry')", children: "Registry" }),
|
|
4457
|
+
/* @__PURE__ */ jsx("button", { class: "tab-btn", onclick: "switchTab('graph')", children: "Graph" }),
|
|
4458
|
+
/* @__PURE__ */ jsx("button", { class: "tab-btn", onclick: "switchTab('requests')", children: "Requests" }),
|
|
4459
|
+
/* @__PURE__ */ jsx("button", { class: "tab-btn", onclick: "switchTab('failures')", children: "Failures" }),
|
|
4460
|
+
integrations.scalar && /* @__PURE__ */ jsx("button", { class: "tab-btn", onclick: "switchTab('scalar')", children: "API Reference" }),
|
|
4461
|
+
integrations.asyncapi && /* @__PURE__ */ jsx("button", { class: "tab-btn", onclick: "switchTab('asyncapi')", children: "AsyncAPI" })
|
|
4462
|
+
] })
|
|
4463
|
+
] }),
|
|
4464
|
+
/* @__PURE__ */ jsxs("div", { id: "tab-overview", class: "tab-content active", children: [
|
|
4465
|
+
/* @__PURE__ */ jsx(MetricsGrid, { metrics }),
|
|
4466
|
+
/* @__PURE__ */ jsxs("div", { id: "chart-container", style: "display: flex; flex-direction: column; gap: 1rem;", children: [
|
|
4467
|
+
/* @__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: [
|
|
4468
|
+
/* @__PURE__ */ jsx("option", { value: "1m", children: "1 Minute" }),
|
|
4469
|
+
/* @__PURE__ */ jsx("option", { value: "5m", children: "5 Minutes" }),
|
|
4470
|
+
/* @__PURE__ */ jsx("option", { value: "30m", children: "30 Minutes" }),
|
|
4471
|
+
/* @__PURE__ */ jsx("option", { value: "1h", children: "1 Hour" }),
|
|
4472
|
+
/* @__PURE__ */ jsx("option", { value: "2h", children: "2 Hours" }),
|
|
4473
|
+
/* @__PURE__ */ jsx("option", { value: "6h", children: "6 Hours" }),
|
|
4474
|
+
/* @__PURE__ */ jsx("option", { value: "12h", children: "12 Hours" }),
|
|
4475
|
+
/* @__PURE__ */ jsx("option", { value: "1d", children: "1 Day" }),
|
|
4476
|
+
/* @__PURE__ */ jsx("option", { value: "3d", children: "3 Days" }),
|
|
4477
|
+
/* @__PURE__ */ jsx("option", { value: "7d", children: "7 Days" }),
|
|
4478
|
+
/* @__PURE__ */ jsx("option", { value: "30d", children: "30 Days" })
|
|
4479
|
+
] }) }),
|
|
4480
|
+
/* @__PURE__ */ jsxs("div", { class: "card-container", children: [
|
|
4481
|
+
/* @__PURE__ */ jsx(ChartCard, { title: "Response Time", id: "latencyChart" }),
|
|
4482
|
+
/* @__PURE__ */ jsx(ChartCard, { title: "Requests / Second", id: "rpsChart" }),
|
|
4483
|
+
/* @__PURE__ */ jsx(ChartCard, { title: "CPU & Load", id: "cpuChart" }),
|
|
4484
|
+
/* @__PURE__ */ jsx(ChartCard, { title: "Memory", id: "memoryChart" }),
|
|
4485
|
+
/* @__PURE__ */ jsx(ChartCard, { title: "Heap Usage", id: "heapChart" }),
|
|
4486
|
+
/* @__PURE__ */ jsx(ChartCard, { title: "Event Loop Latency", id: "eventLoopChart" }),
|
|
4487
|
+
/* @__PURE__ */ jsx(ChartCard, { title: "Error Rate", id: "errorRateChart" })
|
|
4488
|
+
] }),
|
|
4489
|
+
/* @__PURE__ */ jsx("div", { class: "card-title", style: "margin-top: 1rem;", children: "Top Statistics" }),
|
|
4490
|
+
/* @__PURE__ */ jsxs("div", { class: "card-container", children: [
|
|
4491
|
+
/* @__PURE__ */ jsx(Card, { title: "Top Requests", contentId: "top-requests-table" }),
|
|
4492
|
+
/* @__PURE__ */ jsx(Card, { title: "Top Errors", contentId: "top-errors-table" }),
|
|
4493
|
+
/* @__PURE__ */ jsx(Card, { title: "Most Frequent Failures", contentId: "failing-requests-table" }),
|
|
4494
|
+
/* @__PURE__ */ jsx(Card, { title: "Slowest Requests", contentId: "slowest-requests-table" })
|
|
4495
|
+
] }),
|
|
4496
|
+
/* @__PURE__ */ jsx("div", { id: "table-container", style: "padding: 0; margin-top: 1rem;", children: /* @__PURE__ */ jsx("div", { id: "requests-table", class: "table-dark" }) })
|
|
4497
|
+
] })
|
|
4498
|
+
] }),
|
|
4499
|
+
/* @__PURE__ */ jsx("div", { id: "tab-registry", class: "tab-content", children: /* @__PURE__ */ jsxs("div", { id: "registry-container", class: "card", style: "margin-top: 2rem;", children: [
|
|
4500
|
+
/* @__PURE__ */ jsx("div", { class: "card-title", children: "Component Registry" }),
|
|
4501
|
+
/* @__PURE__ */ jsx("div", { id: "registry-tree", style: "padding: 0 1rem 1rem 1rem; font-family: monospace; font-size: 0.9rem;" })
|
|
4502
|
+
] }) }),
|
|
4503
|
+
/* @__PURE__ */ jsxs("div", { id: "tab-graph", class: "tab-content", children: [
|
|
4504
|
+
/* @__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);" }) }) }),
|
|
4505
|
+
/* @__PURE__ */ jsx("div", { id: "cy" })
|
|
4506
|
+
] }),
|
|
4507
|
+
/* @__PURE__ */ jsxs("div", { id: "tab-requests", class: "tab-content", children: [
|
|
4508
|
+
/* @__PURE__ */ jsxs("div", { class: "card", style: "margin-bottom: 1rem; display: flex; justify-content: space-between; align-items: center;", children: [
|
|
4509
|
+
/* @__PURE__ */ jsx("div", { class: "card-title", children: "Recent Requests (Last 100)" }),
|
|
4510
|
+
/* @__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" }) })
|
|
4511
|
+
] }),
|
|
4512
|
+
/* @__PURE__ */ jsx("div", { id: "requests-list-container", style: "height: calc(100vh - 300px); margin-bottom: 2rem;" }),
|
|
4513
|
+
/* @__PURE__ */ jsxs("div", { id: "request-details-container", class: "card", style: "display: none;", children: [
|
|
4514
|
+
/* @__PURE__ */ jsx("div", { class: "card-title", children: "Request Details" }),
|
|
4515
|
+
/* @__PURE__ */ jsx("div", { id: "request-details-content" }),
|
|
4516
|
+
/* @__PURE__ */ jsx("div", { class: "card-title", style: "margin-top: 1rem;", children: "Middleware Trace" }),
|
|
4517
|
+
/* @__PURE__ */ jsx("div", { id: "middleware-trace-container" })
|
|
4518
|
+
] })
|
|
4519
|
+
] }),
|
|
4520
|
+
/* @__PURE__ */ jsxs("div", { id: "tab-failures", class: "tab-content", children: [
|
|
4521
|
+
/* @__PURE__ */ jsxs("div", { class: "card", style: "margin-bottom: 1rem; display: flex; justify-content: space-between; align-items: center;", children: [
|
|
4522
|
+
/* @__PURE__ */ jsx("div", { class: "card-title", children: "Failed Requests (Last 50)" }),
|
|
4523
|
+
/* @__PURE__ */ jsxs("div", { children: [
|
|
4524
|
+
/* @__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" }),
|
|
4525
|
+
/* @__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" })
|
|
4526
|
+
] })
|
|
4527
|
+
] }),
|
|
4528
|
+
/* @__PURE__ */ jsx("div", { id: "failures-table-container" })
|
|
4529
|
+
] }),
|
|
4530
|
+
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;" }) }),
|
|
4531
|
+
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;" }) })
|
|
4532
|
+
] }),
|
|
4533
|
+
/* @__PURE__ */ jsx("script", { dangerouslySetInnerHTML: {
|
|
4534
|
+
__html: `
|
|
4535
|
+
// Injected function from server config
|
|
4536
|
+
const getRequestHeaders = ${getRequestHeadersSource};
|
|
4537
|
+
`
|
|
4538
|
+
} }),
|
|
4539
|
+
/* @__PURE__ */ jsx("script", { src: `${base}/poll.js` }),
|
|
4540
|
+
/* @__PURE__ */ jsx("script", { src: `${base}/graph.mjs`, type: "module" }),
|
|
4541
|
+
/* @__PURE__ */ jsx("script", { src: `${base}/charts.js` }),
|
|
4542
|
+
/* @__PURE__ */ jsx("script", { src: `${base}/tables.js` }),
|
|
4543
|
+
/* @__PURE__ */ jsx("script", { src: `${base}/registry.js` }),
|
|
4544
|
+
/* @__PURE__ */ jsx("script", { src: `${base}/failures.js` }),
|
|
4545
|
+
/* @__PURE__ */ jsx("script", { src: `${base}/requests.js` }),
|
|
4546
|
+
/* @__PURE__ */ jsx("script", { src: `${base}/tabs.js` })
|
|
4547
|
+
] })
|
|
4548
|
+
] });
|
|
4549
|
+
}
|
|
4550
|
+
function MetricsGrid({ metrics }) {
|
|
4551
|
+
const total = metrics.totalRequests;
|
|
4552
|
+
const active = metrics.activeRequests;
|
|
4553
|
+
const finished = total - active;
|
|
4554
|
+
const successRate = finished ? Math.round(metrics.successfulRequests / finished * 100) : 100;
|
|
4555
|
+
const failRate = finished ? Math.round(metrics.failedRequests / finished * 100) : 0;
|
|
4556
|
+
return /* @__PURE__ */ jsxs("div", { class: "metrics-grid", children: [
|
|
4557
|
+
/* @__PURE__ */ jsxs("div", { class: "card", children: [
|
|
4558
|
+
/* @__PURE__ */ jsx("div", { class: "card-title", children: "Total Requests" }),
|
|
4559
|
+
/* @__PURE__ */ jsx("div", { class: "card-value", id: "total-requests", children: metrics.totalRequests })
|
|
4560
|
+
] }),
|
|
4561
|
+
/* @__PURE__ */ jsxs("div", { class: "card", children: [
|
|
4562
|
+
/* @__PURE__ */ jsx("div", { class: "card-title", children: "Active Requests" }),
|
|
4563
|
+
/* @__PURE__ */ jsx("div", { class: "card-value", style: "color: var(--accent)", id: "active-requests", children: metrics.activeRequests })
|
|
4564
|
+
] }),
|
|
4565
|
+
/* @__PURE__ */ jsxs("div", { class: "card", children: [
|
|
4566
|
+
/* @__PURE__ */ jsx("div", { class: "card-title", children: "Success Rate" }),
|
|
4567
|
+
/* @__PURE__ */ jsx("div", { class: "card-value text-success", children: /* @__PURE__ */ jsxs("span", { id: "success-rate", children: [
|
|
4568
|
+
successRate,
|
|
4569
|
+
"%"
|
|
4570
|
+
] }) }),
|
|
4571
|
+
/* @__PURE__ */ jsxs("div", { style: "color: var(--text-secondary); margin-top: 0.5rem", children: [
|
|
4572
|
+
/* @__PURE__ */ jsx("span", { id: "successful-requests", children: metrics.successfulRequests }),
|
|
4573
|
+
" successful"
|
|
4574
|
+
] })
|
|
4575
|
+
] }),
|
|
4576
|
+
/* @__PURE__ */ jsxs("div", { class: "card", children: [
|
|
4577
|
+
/* @__PURE__ */ jsx("div", { class: "card-title", children: "Fail Rate" }),
|
|
4578
|
+
/* @__PURE__ */ jsx("div", { class: "card-value text-error", children: /* @__PURE__ */ jsxs("span", { id: "fail-rate", children: [
|
|
4579
|
+
failRate,
|
|
4580
|
+
"%"
|
|
4581
|
+
] }) }),
|
|
4582
|
+
/* @__PURE__ */ jsxs("div", { style: "color: var(--text-secondary); margin-top: 0.5rem", children: [
|
|
4583
|
+
/* @__PURE__ */ jsx("span", { id: "failed-requests", children: metrics.failedRequests }),
|
|
4584
|
+
" failed"
|
|
4585
|
+
] })
|
|
4586
|
+
] }),
|
|
4587
|
+
/* @__PURE__ */ jsxs("div", { class: "card", children: [
|
|
4588
|
+
/* @__PURE__ */ jsx("div", { class: "card-title", children: "Avg Latency" }),
|
|
4589
|
+
/* @__PURE__ */ jsxs("div", { class: "card-value", children: [
|
|
4590
|
+
/* @__PURE__ */ jsx("span", { id: "avg-latency", children: metrics.averageTotalTime_ms.toFixed(2) }),
|
|
4591
|
+
" ",
|
|
4592
|
+
/* @__PURE__ */ jsx("span", { style: "font-size: 1rem; color: var(--text-secondary)", children: "ms" })
|
|
4593
|
+
] })
|
|
4594
|
+
] })
|
|
4595
|
+
] });
|
|
4596
|
+
}
|
|
4597
|
+
function ChartCard({ title, id }) {
|
|
4598
|
+
return /* @__PURE__ */ jsxs("div", { class: "card", style: "height: 300px;", children: [
|
|
4599
|
+
/* @__PURE__ */ jsx("div", { class: "card-title", children: title }),
|
|
4600
|
+
/* @__PURE__ */ jsx("div", { class: "card-chart", children: /* @__PURE__ */ jsx("canvas", { id }) })
|
|
4601
|
+
] });
|
|
4602
|
+
}
|
|
4603
|
+
function Card({ title, contentId }) {
|
|
4604
|
+
return /* @__PURE__ */ jsxs("div", { class: "card", children: [
|
|
4605
|
+
/* @__PURE__ */ jsx("div", { class: "card-title", children: title }),
|
|
4606
|
+
/* @__PURE__ */ jsx("div", { id: contentId })
|
|
4607
|
+
] });
|
|
4608
|
+
}
|
|
3586
4609
|
const INTERVALS = [
|
|
3587
4610
|
{ label: "10s", ms: 10 * 1e3 },
|
|
3588
4611
|
{ label: "1m", ms: 60 * 1e3 },
|
|
@@ -3597,19 +4620,19 @@ const INTERVALS = [
|
|
|
3597
4620
|
{ label: "30d", ms: 30 * 24 * 60 * 60 * 1e3 }
|
|
3598
4621
|
];
|
|
3599
4622
|
class MetricsCollector {
|
|
3600
|
-
|
|
3601
|
-
|
|
3602
|
-
eventLoopHistogram = monitorEventLoopDelay({ resolution: 10 });
|
|
3603
|
-
timer = null;
|
|
3604
|
-
constructor() {
|
|
4623
|
+
constructor(db) {
|
|
4624
|
+
this.db = db;
|
|
3605
4625
|
this.eventLoopHistogram.enable();
|
|
3606
4626
|
const now = Date.now();
|
|
3607
4627
|
INTERVALS.forEach((int) => {
|
|
3608
4628
|
this.currentIntervalStart[int.label] = this.alignTimestamp(now, int.ms);
|
|
3609
4629
|
this.pendingDetails[int.label] = [];
|
|
3610
4630
|
});
|
|
3611
|
-
this.timer = setInterval(() => this.collect(), 1e4);
|
|
3612
4631
|
}
|
|
4632
|
+
currentIntervalStart = {};
|
|
4633
|
+
pendingDetails = {};
|
|
4634
|
+
eventLoopHistogram = monitorEventLoopDelay({ resolution: 10 });
|
|
4635
|
+
timer = null;
|
|
3613
4636
|
recordRequest(duration, isError) {
|
|
3614
4637
|
INTERVALS.forEach((int) => {
|
|
3615
4638
|
this.pendingDetails[int.label].push({ duration, isError });
|
|
@@ -3621,11 +4644,9 @@ class MetricsCollector {
|
|
|
3621
4644
|
async collect() {
|
|
3622
4645
|
try {
|
|
3623
4646
|
const now = Date.now();
|
|
3624
|
-
console.log("[MetricsCollector] collect() called at", new Date(now).toISOString());
|
|
3625
4647
|
for (const int of INTERVALS) {
|
|
3626
4648
|
const start = this.currentIntervalStart[int.label];
|
|
3627
4649
|
if (now >= start + int.ms) {
|
|
3628
|
-
console.log(`[MetricsCollector] Flushing ${int.label} interval (boundary crossed)`);
|
|
3629
4650
|
await this.flushInterval(int.label, start, int.ms);
|
|
3630
4651
|
this.currentIntervalStart[int.label] = this.alignTimestamp(now, int.ms);
|
|
3631
4652
|
}
|
|
@@ -3636,10 +4657,8 @@ class MetricsCollector {
|
|
|
3636
4657
|
}
|
|
3637
4658
|
async flushInterval(label, timestamp, durationMs) {
|
|
3638
4659
|
const reqs = this.pendingDetails[label];
|
|
3639
|
-
console.log(`[MetricsCollector] flushInterval(${label}) - ${reqs.length} requests pending`);
|
|
3640
4660
|
this.pendingDetails[label] = [];
|
|
3641
4661
|
if (reqs.length === 0) {
|
|
3642
|
-
console.log(`[MetricsCollector] No requests for ${label}, skipping persist`);
|
|
3643
4662
|
return;
|
|
3644
4663
|
}
|
|
3645
4664
|
const totalReqs = reqs.length;
|
|
@@ -3689,15 +4708,11 @@ class MetricsCollector {
|
|
|
3689
4708
|
p99: getP(0.99)
|
|
3690
4709
|
}
|
|
3691
4710
|
};
|
|
3692
|
-
console.log(`[MetricsCollector] Persisting ${label} metric at timestamp ${timestamp}`);
|
|
3693
4711
|
try {
|
|
3694
4712
|
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`);
|
|
4713
|
+
await this.db.upsert(recordId, metric);
|
|
4714
|
+
const test = await this.db.select(recordId);
|
|
4715
|
+
const queryTest = await this.db.query("SELECT * FROM metrics WHERE id = $id", { id: recordId });
|
|
3701
4716
|
} catch (e) {
|
|
3702
4717
|
console.error(`[MetricsCollector] ✗ Failed to save metrics for ${label}:`, e);
|
|
3703
4718
|
}
|
|
@@ -3732,16 +4747,8 @@ class Dashboard {
|
|
|
3732
4747
|
constructor(dashboardConfig = {}) {
|
|
3733
4748
|
this.dashboardConfig = dashboardConfig;
|
|
3734
4749
|
}
|
|
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();
|
|
4750
|
+
[$appRoot];
|
|
4751
|
+
router = new ShokupanRouter({ renderer: renderToString });
|
|
3745
4752
|
metrics = {
|
|
3746
4753
|
totalRequests: 0,
|
|
3747
4754
|
successfulRequests: 0,
|
|
@@ -3754,16 +4761,16 @@ class Dashboard {
|
|
|
3754
4761
|
nodeMetrics: {},
|
|
3755
4762
|
edgeMetrics: {}
|
|
3756
4763
|
};
|
|
3757
|
-
eta = new Eta({
|
|
3758
|
-
views: Dashboard.getBasePath() + "/static",
|
|
3759
|
-
cache: false
|
|
3760
|
-
});
|
|
3761
4764
|
startTime = Date.now();
|
|
3762
4765
|
instrumented = false;
|
|
3763
|
-
metricsCollector
|
|
4766
|
+
metricsCollector;
|
|
4767
|
+
get db() {
|
|
4768
|
+
return this[$appRoot].db;
|
|
4769
|
+
}
|
|
3764
4770
|
// ShokupanPlugin interface implementation
|
|
3765
4771
|
onInit(app, options) {
|
|
3766
4772
|
this[$appRoot] = app;
|
|
4773
|
+
this.metricsCollector = new MetricsCollector(this.db);
|
|
3767
4774
|
const mountPath = options?.path || this.dashboardConfig.path || "/dashboard";
|
|
3768
4775
|
const hooks = this.getHooks();
|
|
3769
4776
|
if (!app.middleware) {
|
|
@@ -3783,6 +4790,47 @@ class Dashboard {
|
|
|
3783
4790
|
app.mount(mountPath, this.router);
|
|
3784
4791
|
this.setupRoutes();
|
|
3785
4792
|
}
|
|
4793
|
+
detectIntegrations() {
|
|
4794
|
+
const integrations = {};
|
|
4795
|
+
const routers = this[$appRoot]?.[$childRouters] || [];
|
|
4796
|
+
const checkConfig = (key) => {
|
|
4797
|
+
const conf = this.dashboardConfig.integrations?.[key];
|
|
4798
|
+
if (conf === false) return { enabled: false };
|
|
4799
|
+
if (typeof conf === "object" && conf.path) return { enabled: true, path: conf.path };
|
|
4800
|
+
return { enabled: true };
|
|
4801
|
+
};
|
|
4802
|
+
const scalarConf = checkConfig("scalar");
|
|
4803
|
+
if (scalarConf.enabled) {
|
|
4804
|
+
if (scalarConf.path) {
|
|
4805
|
+
integrations["scalar"] = scalarConf.path;
|
|
4806
|
+
} else {
|
|
4807
|
+
const plugin = routers.find((r) => r.constructor.name === "ScalarPlugin");
|
|
4808
|
+
if (plugin) {
|
|
4809
|
+
integrations["scalar"] = plugin[$mountPath];
|
|
4810
|
+
}
|
|
4811
|
+
}
|
|
4812
|
+
}
|
|
4813
|
+
const asyncApiConf = checkConfig("asyncapi");
|
|
4814
|
+
if (asyncApiConf.enabled) {
|
|
4815
|
+
if (asyncApiConf.path) {
|
|
4816
|
+
integrations["asyncapi"] = asyncApiConf.path;
|
|
4817
|
+
} else {
|
|
4818
|
+
const plugin = routers.find((r) => r.constructor.name === "AsyncApiPlugin");
|
|
4819
|
+
if (plugin) {
|
|
4820
|
+
integrations["asyncapi"] = plugin[$mountPath];
|
|
4821
|
+
}
|
|
4822
|
+
}
|
|
4823
|
+
}
|
|
4824
|
+
return integrations;
|
|
4825
|
+
}
|
|
4826
|
+
// Get base path for dashboard files - works in both dev (src/) and production (dist/)
|
|
4827
|
+
static getBasePath() {
|
|
4828
|
+
const dir = dirname(fileURLToPath(import.meta.url));
|
|
4829
|
+
if (dir.endsWith("dist")) {
|
|
4830
|
+
return dir + "/plugins/application/dashboard";
|
|
4831
|
+
}
|
|
4832
|
+
return dir;
|
|
4833
|
+
}
|
|
3786
4834
|
setupRoutes() {
|
|
3787
4835
|
this.router.get("/metrics", async (ctx) => {
|
|
3788
4836
|
const uptimeSeconds = Math.floor((Date.now() - this.startTime) / 1e3);
|
|
@@ -3807,7 +4855,7 @@ class Dashboard {
|
|
|
3807
4855
|
const startTime = Date.now() - ms;
|
|
3808
4856
|
let stats;
|
|
3809
4857
|
try {
|
|
3810
|
-
stats = await
|
|
4858
|
+
stats = await this.db.query(`
|
|
3811
4859
|
SELECT
|
|
3812
4860
|
count() as total,
|
|
3813
4861
|
count(IF status < 400 THEN 1 END) as success,
|
|
@@ -3868,7 +4916,7 @@ class Dashboard {
|
|
|
3868
4916
|
const periodMs = intervalMap[interval] || 60 * 1e3;
|
|
3869
4917
|
const startTime = Date.now() - periodMs * 3;
|
|
3870
4918
|
const endTime = Date.now();
|
|
3871
|
-
const result = await
|
|
4919
|
+
const result = await this.db.query(
|
|
3872
4920
|
"SELECT * FROM metrics WHERE timestamp >= $start AND timestamp <= $end AND interval = $interval ORDER BY timestamp ASC",
|
|
3873
4921
|
{ start: startTime, end: endTime, interval }
|
|
3874
4922
|
);
|
|
@@ -3897,7 +4945,7 @@ class Dashboard {
|
|
|
3897
4945
|
};
|
|
3898
4946
|
this.router.get("/requests/top", async (ctx) => {
|
|
3899
4947
|
const startTime = getIntervalStartTime(ctx.query["interval"]);
|
|
3900
|
-
const result = await
|
|
4948
|
+
const result = await this.db.query(
|
|
3901
4949
|
"SELECT method, url, count() as count FROM requests WHERE timestamp >= $start GROUP BY method, url ORDER BY count DESC LIMIT 10",
|
|
3902
4950
|
{ start: startTime }
|
|
3903
4951
|
);
|
|
@@ -3905,7 +4953,7 @@ class Dashboard {
|
|
|
3905
4953
|
});
|
|
3906
4954
|
this.router.get("/errors/top", async (ctx) => {
|
|
3907
4955
|
const startTime = getIntervalStartTime(ctx.query["interval"]);
|
|
3908
|
-
const result = await
|
|
4956
|
+
const result = await this.db.query(
|
|
3909
4957
|
"SELECT status, count() as count FROM failed_requests WHERE timestamp >= $start GROUP BY status ORDER BY count DESC LIMIT 10",
|
|
3910
4958
|
{ start: startTime }
|
|
3911
4959
|
);
|
|
@@ -3913,7 +4961,7 @@ class Dashboard {
|
|
|
3913
4961
|
});
|
|
3914
4962
|
this.router.get("/requests/failing", async (ctx) => {
|
|
3915
4963
|
const startTime = getIntervalStartTime(ctx.query["interval"]);
|
|
3916
|
-
const result = await
|
|
4964
|
+
const result = await this.db.query(
|
|
3917
4965
|
"SELECT method, url, count() as count FROM failed_requests WHERE timestamp >= $start GROUP BY method, url ORDER BY count DESC LIMIT 10",
|
|
3918
4966
|
{ start: startTime }
|
|
3919
4967
|
);
|
|
@@ -3921,7 +4969,7 @@ class Dashboard {
|
|
|
3921
4969
|
});
|
|
3922
4970
|
this.router.get("/requests/slowest", async (ctx) => {
|
|
3923
4971
|
const startTime = getIntervalStartTime(ctx.query["interval"]);
|
|
3924
|
-
const result = await
|
|
4972
|
+
const result = await this.db.query(
|
|
3925
4973
|
"SELECT method, url, duration, status, timestamp FROM requests WHERE timestamp >= $start ORDER BY duration DESC LIMIT 10",
|
|
3926
4974
|
{ start: startTime }
|
|
3927
4975
|
);
|
|
@@ -3939,15 +4987,15 @@ class Dashboard {
|
|
|
3939
4987
|
return ctx.json({ registry: registry || {} });
|
|
3940
4988
|
});
|
|
3941
4989
|
this.router.get("/requests", async (ctx) => {
|
|
3942
|
-
const result = await
|
|
4990
|
+
const result = await this.db.query("SELECT * FROM requests ORDER BY timestamp DESC LIMIT 100");
|
|
3943
4991
|
return ctx.json({ requests: result[0] || [] });
|
|
3944
4992
|
});
|
|
3945
4993
|
this.router.get("/requests/:id", async (ctx) => {
|
|
3946
|
-
const result = await
|
|
4994
|
+
const result = await this.db.query("SELECT * FROM requests WHERE id = $id", { id: ctx.params["id"] });
|
|
3947
4995
|
return ctx.json({ request: result[0]?.[0] });
|
|
3948
4996
|
});
|
|
3949
4997
|
this.router.get("/failures", async (ctx) => {
|
|
3950
|
-
const result = await
|
|
4998
|
+
const result = await this.db.query("SELECT * FROM failed_requests ORDER BY timestamp DESC LIMIT 50");
|
|
3951
4999
|
return ctx.json({ failures: result[0] });
|
|
3952
5000
|
});
|
|
3953
5001
|
this.router.post("/replay", async (ctx) => {
|
|
@@ -3971,18 +5019,51 @@ class Dashboard {
|
|
|
3971
5019
|
return ctx.json({ error: String(e) }, 500);
|
|
3972
5020
|
}
|
|
3973
5021
|
});
|
|
3974
|
-
this.router.get("
|
|
5022
|
+
this.router.get("/**", async (ctx) => {
|
|
5023
|
+
const mountPath = this.router[$mountPath] || this.dashboardConfig.path || "/dashboard";
|
|
5024
|
+
let relativePath = ctx.path;
|
|
5025
|
+
if (relativePath.startsWith(mountPath)) {
|
|
5026
|
+
relativePath = relativePath.slice(mountPath.length);
|
|
5027
|
+
}
|
|
5028
|
+
if (relativePath.startsWith("/")) {
|
|
5029
|
+
relativePath = relativePath.slice(1);
|
|
5030
|
+
}
|
|
5031
|
+
const path = relativePath;
|
|
5032
|
+
const staticFiles = [
|
|
5033
|
+
"charts.js",
|
|
5034
|
+
"failures.js",
|
|
5035
|
+
"graph.mjs",
|
|
5036
|
+
"poll.js",
|
|
5037
|
+
"reactflow.css",
|
|
5038
|
+
"registry.css",
|
|
5039
|
+
"registry.js",
|
|
5040
|
+
"requests.js",
|
|
5041
|
+
"styles.css",
|
|
5042
|
+
"tables.js",
|
|
5043
|
+
"tabs.js",
|
|
5044
|
+
"tabulator.css",
|
|
5045
|
+
"theme.css"
|
|
5046
|
+
];
|
|
5047
|
+
if (staticFiles.includes(path)) {
|
|
5048
|
+
const content = await readFile(join$1(Dashboard.getBasePath(), "static", path), "utf-8");
|
|
5049
|
+
if (path.endsWith(".css")) ctx.set("Content-Type", "text/css");
|
|
5050
|
+
else if (path.endsWith(".js") || path.endsWith(".mjs")) ctx.set("Content-Type", "application/javascript");
|
|
5051
|
+
return ctx.send(content);
|
|
5052
|
+
}
|
|
3975
5053
|
const uptimeSeconds = Math.floor((Date.now() - this.startTime) / 1e3);
|
|
3976
5054
|
const uptime = `${Math.floor(uptimeSeconds / 3600)}h ${Math.floor(uptimeSeconds % 3600 / 60)}m ${uptimeSeconds % 60}s`;
|
|
3977
|
-
|
|
3978
|
-
const
|
|
3979
|
-
|
|
5055
|
+
this.getLinkPattern();
|
|
5056
|
+
const integrations = this.detectIntegrations();
|
|
5057
|
+
const getRequestHeadersSource = this.dashboardConfig.getRequestHeaders ? this.dashboardConfig.getRequestHeaders.toString() : "undefined";
|
|
5058
|
+
const html = renderToString(DashboardApp({
|
|
3980
5059
|
metrics: this.metrics,
|
|
3981
5060
|
uptime,
|
|
3982
5061
|
rootPath: process.cwd(),
|
|
3983
|
-
|
|
3984
|
-
|
|
5062
|
+
integrations,
|
|
5063
|
+
base: mountPath,
|
|
5064
|
+
getRequestHeadersSource
|
|
3985
5065
|
}));
|
|
5066
|
+
return ctx.html(`<!DOCTYPE html>${html}`);
|
|
3986
5067
|
});
|
|
3987
5068
|
}
|
|
3988
5069
|
instrumentApp(app) {
|
|
@@ -4078,7 +5159,7 @@ class Dashboard {
|
|
|
4078
5159
|
headers[k] = v;
|
|
4079
5160
|
});
|
|
4080
5161
|
}
|
|
4081
|
-
await
|
|
5162
|
+
await this.db.upsert(new RecordId("failed_requests", ctx.requestId), {
|
|
4082
5163
|
method: ctx.method,
|
|
4083
5164
|
url: ctx.url.toString(),
|
|
4084
5165
|
headers,
|
|
@@ -4103,7 +5184,7 @@ class Dashboard {
|
|
|
4103
5184
|
};
|
|
4104
5185
|
this.metrics.logs.push(logEntry);
|
|
4105
5186
|
try {
|
|
4106
|
-
await
|
|
5187
|
+
await this.db.upsert(new RecordId("requests", ctx.requestId), logEntry);
|
|
4107
5188
|
} catch (e) {
|
|
4108
5189
|
console.error("Failed to record request log", e);
|
|
4109
5190
|
}
|
|
@@ -4131,32 +5212,169 @@ class Dashboard {
|
|
|
4131
5212
|
function unknownError(ctx) {
|
|
4132
5213
|
return ctx.json({ error: "Unknown Error" }, 500);
|
|
4133
5214
|
}
|
|
4134
|
-
|
|
5215
|
+
class GraphQLApolloPlugin extends ShokupanRouter {
|
|
5216
|
+
// Use generic any or verify type
|
|
5217
|
+
constructor(pluginOptions) {
|
|
5218
|
+
super();
|
|
5219
|
+
this.pluginOptions = pluginOptions;
|
|
5220
|
+
this.pluginOptions.path ??= "/graphql";
|
|
5221
|
+
}
|
|
5222
|
+
apolloServer;
|
|
5223
|
+
async onInit(app, options) {
|
|
5224
|
+
const { ApolloServer, HeaderMap } = await import("@apollo/server");
|
|
5225
|
+
this.apolloServer = new ApolloServer({
|
|
5226
|
+
typeDefs: this.pluginOptions.typeDefs,
|
|
5227
|
+
resolvers: this.pluginOptions.resolvers,
|
|
5228
|
+
...this.pluginOptions.apolloConfig || {}
|
|
5229
|
+
});
|
|
5230
|
+
const path = options?.path || this.pluginOptions.path || "/graphql";
|
|
5231
|
+
app.mount(path, this);
|
|
5232
|
+
app.onStart(async () => {
|
|
5233
|
+
await this.apolloServer.start();
|
|
5234
|
+
});
|
|
5235
|
+
this.post("/", async (ctx) => {
|
|
5236
|
+
const body = await ctx.body();
|
|
5237
|
+
const httpGraphQLResponse = await this.apolloServer.executeHTTPGraphQLRequest({
|
|
5238
|
+
httpGraphQLRequest: {
|
|
5239
|
+
body,
|
|
5240
|
+
method: ctx.req.method,
|
|
5241
|
+
search: ctx.url.search,
|
|
5242
|
+
headers: new HeaderMap(ctx.req.headers)
|
|
5243
|
+
},
|
|
5244
|
+
// Pass the Shokupan Context as the GraphQL Context
|
|
5245
|
+
context: async () => ({ ...ctx, shokupan: ctx })
|
|
5246
|
+
});
|
|
5247
|
+
for (const [key, value] of httpGraphQLResponse.headers) {
|
|
5248
|
+
ctx.set(key, value);
|
|
5249
|
+
}
|
|
5250
|
+
if (httpGraphQLResponse.body.kind === "complete") {
|
|
5251
|
+
return ctx.send(httpGraphQLResponse.body.string, {
|
|
5252
|
+
status: httpGraphQLResponse.status ?? 200
|
|
5253
|
+
});
|
|
5254
|
+
} else {
|
|
5255
|
+
let string = "";
|
|
5256
|
+
for await (const chunk of httpGraphQLResponse.body.asyncIterator) {
|
|
5257
|
+
string += chunk;
|
|
5258
|
+
}
|
|
5259
|
+
return ctx.send(string, {
|
|
5260
|
+
status: httpGraphQLResponse.status ?? 200
|
|
5261
|
+
});
|
|
5262
|
+
}
|
|
5263
|
+
});
|
|
5264
|
+
this.get("/", async (ctx) => {
|
|
5265
|
+
const httpGraphQLResponse = await this.apolloServer.executeHTTPGraphQLRequest({
|
|
5266
|
+
httpGraphQLRequest: {
|
|
5267
|
+
body: Object.keys(ctx.query).length > 0 ? ctx.query : void 0,
|
|
5268
|
+
method: ctx.req.method,
|
|
5269
|
+
search: ctx.url.search,
|
|
5270
|
+
headers: new HeaderMap(ctx.req.headers)
|
|
5271
|
+
},
|
|
5272
|
+
context: async () => ({ ...ctx, shokupan: ctx })
|
|
5273
|
+
});
|
|
5274
|
+
for (const [key, value] of httpGraphQLResponse.headers) {
|
|
5275
|
+
ctx.set(key, value);
|
|
5276
|
+
}
|
|
5277
|
+
if (httpGraphQLResponse.body.kind === "complete") {
|
|
5278
|
+
return ctx.html(httpGraphQLResponse.body.string, httpGraphQLResponse.status ?? 200);
|
|
5279
|
+
} else {
|
|
5280
|
+
let string = "";
|
|
5281
|
+
for await (const chunk of httpGraphQLResponse.body.asyncIterator) {
|
|
5282
|
+
string += chunk;
|
|
5283
|
+
}
|
|
5284
|
+
return ctx.html(string, httpGraphQLResponse.status ?? 200);
|
|
5285
|
+
}
|
|
5286
|
+
});
|
|
5287
|
+
}
|
|
5288
|
+
}
|
|
4135
5289
|
class ScalarPlugin extends ShokupanRouter {
|
|
4136
5290
|
constructor(pluginOptions = {}) {
|
|
4137
5291
|
pluginOptions.config ??= {};
|
|
4138
5292
|
super();
|
|
4139
5293
|
this.pluginOptions = pluginOptions;
|
|
4140
|
-
this.
|
|
5294
|
+
this.initRoutes();
|
|
5295
|
+
}
|
|
5296
|
+
eta;
|
|
5297
|
+
async onInit(app, options) {
|
|
5298
|
+
const { Eta: Eta2 } = await import("eta");
|
|
5299
|
+
this.eta = new Eta2();
|
|
5300
|
+
const path = options?.path || this.pluginOptions.path || "/reference";
|
|
5301
|
+
app.mount(path, this);
|
|
5302
|
+
this.onMount(app);
|
|
4141
5303
|
}
|
|
4142
|
-
|
|
4143
|
-
if (
|
|
4144
|
-
|
|
4145
|
-
|
|
4146
|
-
app.mount(options.path ?? "/", this);
|
|
5304
|
+
async ensureEta() {
|
|
5305
|
+
if (!this.eta) {
|
|
5306
|
+
const { Eta: Eta2 } = await import("eta");
|
|
5307
|
+
this.eta = new Eta2();
|
|
4147
5308
|
}
|
|
4148
|
-
this.onMount(app);
|
|
4149
5309
|
}
|
|
4150
|
-
|
|
4151
|
-
|
|
5310
|
+
initRoutes() {
|
|
5311
|
+
const bootId = Date.now().toString();
|
|
5312
|
+
this.get("/_lifecycle", (ctx) => ctx.json({ boot: bootId }));
|
|
5313
|
+
this.get("/", async (ctx) => {
|
|
5314
|
+
await this.ensureEta();
|
|
4152
5315
|
let path = ctx.url.toString();
|
|
4153
5316
|
if (!path.endsWith("/")) path += "/";
|
|
4154
|
-
|
|
4155
|
-
<
|
|
5317
|
+
const devScript = ctx.app?.applicationConfig.development ? `
|
|
5318
|
+
<script>
|
|
5319
|
+
(function() {
|
|
5320
|
+
const bootId = "${bootId}";
|
|
5321
|
+
let isDown = false;
|
|
5322
|
+
|
|
5323
|
+
setInterval(async () => {
|
|
5324
|
+
try {
|
|
5325
|
+
const res = await fetch('${path}_lifecycle');
|
|
5326
|
+
if (!res.ok) throw new Error('Down');
|
|
5327
|
+
const data = await res.json();
|
|
5328
|
+
if (data.boot !== bootId) {
|
|
5329
|
+
console.log('Server restarted, reloading...');
|
|
5330
|
+
window.location.reload();
|
|
5331
|
+
}
|
|
5332
|
+
else if (isDown) {
|
|
5333
|
+
isDown = false;
|
|
5334
|
+
}
|
|
5335
|
+
} catch (e) {
|
|
5336
|
+
isDown = true;
|
|
5337
|
+
console.log('Connection lost...');
|
|
5338
|
+
}
|
|
5339
|
+
}, 2000);
|
|
5340
|
+
})();
|
|
5341
|
+
<\/script>
|
|
5342
|
+
` : "";
|
|
5343
|
+
let themeCss = "";
|
|
5344
|
+
try {
|
|
5345
|
+
try {
|
|
5346
|
+
themeCss = readFileSync(join$1(process.cwd(), "src/theme.css"), "utf-8");
|
|
5347
|
+
} catch {
|
|
5348
|
+
}
|
|
5349
|
+
} catch (e) {
|
|
5350
|
+
}
|
|
5351
|
+
if (!this.eta) throw new Error("Eta not initialized");
|
|
5352
|
+
return ctx.html(this.eta.renderString(`<!doctype html>
|
|
5353
|
+
<html lang="en">
|
|
4156
5354
|
<head>
|
|
4157
5355
|
<title>API Reference</title>
|
|
4158
5356
|
<meta charset = "utf-8" />
|
|
4159
5357
|
<meta name="viewport" content = "width=device-width, initial-scale=1" />
|
|
5358
|
+
<style>
|
|
5359
|
+
${themeCss}
|
|
5360
|
+
|
|
5361
|
+
:root {
|
|
5362
|
+
--scalar-color-1: var(--primary);
|
|
5363
|
+
--scalar-color-2: var(--secondary);
|
|
5364
|
+
--scalar-color-3: var(--accent);
|
|
5365
|
+
--scalar-color-accent: var(--accent);
|
|
5366
|
+
|
|
5367
|
+
--scalar-background-1: var(--bg-primary);
|
|
5368
|
+
--scalar-background-2: var(--bg-secondary);
|
|
5369
|
+
--scalar-background-3: var(--bg-card);
|
|
5370
|
+
|
|
5371
|
+
--scalar-text-1: var(--text-primary);
|
|
5372
|
+
--scalar-text-2: var(--text-secondary);
|
|
5373
|
+
--scalar-text-3: var(--text-muted);
|
|
5374
|
+
|
|
5375
|
+
--scalar-border-color: var(--border-color);
|
|
5376
|
+
}
|
|
5377
|
+
</style>
|
|
4160
5378
|
</head>
|
|
4161
5379
|
|
|
4162
5380
|
<body>
|
|
@@ -4168,9 +5386,10 @@ class ScalarPlugin extends ShokupanRouter {
|
|
|
4168
5386
|
}
|
|
4169
5387
|
])
|
|
4170
5388
|
<\/script>
|
|
5389
|
+
<%~ it.devScript %>
|
|
4171
5390
|
</body>
|
|
4172
5391
|
|
|
4173
|
-
</html>`, { path, config: this.pluginOptions }));
|
|
5392
|
+
</html>`, { path, config: this.pluginOptions, devScript }));
|
|
4174
5393
|
});
|
|
4175
5394
|
this.get("/openapi.json", async (ctx) => {
|
|
4176
5395
|
let spec;
|
|
@@ -4373,7 +5592,7 @@ function Cors(options = {}) {
|
|
|
4373
5592
|
}
|
|
4374
5593
|
const response = await next();
|
|
4375
5594
|
if (response instanceof Response) {
|
|
4376
|
-
const headerEntries =
|
|
5595
|
+
const headerEntries = Object.entries(headers);
|
|
4377
5596
|
for (let i = 0; i < headerEntries.length; i++) {
|
|
4378
5597
|
const [key, value] = headerEntries[i];
|
|
4379
5598
|
response.headers.set(key, value);
|
|
@@ -5145,6 +6364,7 @@ export {
|
|
|
5145
6364
|
$url,
|
|
5146
6365
|
$ws,
|
|
5147
6366
|
All,
|
|
6367
|
+
AsyncApiPlugin,
|
|
5148
6368
|
AuthPlugin,
|
|
5149
6369
|
Body,
|
|
5150
6370
|
ClusterPlugin,
|
|
@@ -5157,6 +6377,7 @@ export {
|
|
|
5157
6377
|
Delete,
|
|
5158
6378
|
Event,
|
|
5159
6379
|
Get,
|
|
6380
|
+
GraphQLApolloPlugin,
|
|
5160
6381
|
HTTPMethods,
|
|
5161
6382
|
Head,
|
|
5162
6383
|
Headers$1 as Headers,
|