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.
Files changed (49) hide show
  1. package/dist/analyzer-BqIe1p0R.js +35 -0
  2. package/dist/analyzer-BqIe1p0R.js.map +1 -0
  3. package/dist/analyzer-CKLGLFtx.cjs +35 -0
  4. package/dist/analyzer-CKLGLFtx.cjs.map +1 -0
  5. package/dist/{analyzer-Ce_7JxZh.js → analyzer.impl-CV6W1Eq7.js} +238 -21
  6. package/dist/analyzer.impl-CV6W1Eq7.js.map +1 -0
  7. package/dist/{analyzer-Bei1sVWp.cjs → analyzer.impl-D9Yi1Hax.cjs} +237 -20
  8. package/dist/analyzer.impl-D9Yi1Hax.cjs.map +1 -0
  9. package/dist/cli.cjs +1 -1
  10. package/dist/cli.js +1 -1
  11. package/dist/context.d.ts +19 -7
  12. package/dist/http-server-BEMPIs33.cjs.map +1 -1
  13. package/dist/http-server-CCeagTyU.js.map +1 -1
  14. package/dist/index.cjs +1459 -239
  15. package/dist/index.cjs.map +1 -1
  16. package/dist/index.d.ts +2 -0
  17. package/dist/index.js +1441 -220
  18. package/dist/index.js.map +1 -1
  19. package/dist/plugins/application/api-explorer/plugin.d.ts +9 -0
  20. package/dist/plugins/application/api-explorer/static/explorer-client.mjs +880 -0
  21. package/dist/plugins/application/api-explorer/static/style.css +767 -0
  22. package/dist/plugins/application/api-explorer/static/theme.css +128 -0
  23. package/dist/plugins/application/asyncapi/generator.d.ts +3 -0
  24. package/dist/plugins/application/asyncapi/plugin.d.ts +15 -0
  25. package/dist/plugins/application/asyncapi/static/asyncapi-client.mjs +748 -0
  26. package/dist/plugins/application/asyncapi/static/style.css +565 -0
  27. package/dist/plugins/application/asyncapi/static/theme.css +128 -0
  28. package/dist/plugins/application/auth.d.ts +3 -1
  29. package/dist/plugins/application/dashboard/metrics-collector.d.ts +3 -1
  30. package/dist/plugins/application/dashboard/plugin.d.ts +13 -3
  31. package/dist/plugins/application/dashboard/static/registry.css +0 -53
  32. package/dist/plugins/application/dashboard/static/styles.css +29 -20
  33. package/dist/plugins/application/dashboard/static/tabulator.css +83 -31
  34. package/dist/plugins/application/dashboard/static/theme.css +128 -0
  35. package/dist/plugins/application/graphql-apollo.d.ts +33 -0
  36. package/dist/plugins/application/graphql-yoga.d.ts +25 -0
  37. package/dist/plugins/application/openapi/analyzer.d.ts +12 -119
  38. package/dist/plugins/application/openapi/analyzer.impl.d.ts +167 -0
  39. package/dist/plugins/application/scalar.d.ts +9 -2
  40. package/dist/router.d.ts +80 -51
  41. package/dist/shokupan.d.ts +14 -8
  42. package/dist/util/datastore.d.ts +71 -7
  43. package/dist/util/decorators.d.ts +2 -2
  44. package/dist/util/types.d.ts +96 -3
  45. package/package.json +33 -13
  46. package/dist/analyzer-Bei1sVWp.cjs.map +0 -1
  47. package/dist/analyzer-Ce_7JxZh.js.map +0 -1
  48. package/dist/plugins/application/dashboard/static/scrollbar.css +0 -24
  49. 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 { Surreal, RecordId } from "surrealdb";
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 { OAuth2Client, Okta, Auth0, Apple, MicrosoftEntraId, Google, GitHub, generateState, generateCodeVerifier } from "arctic";
12
- import * as jose from "jose";
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 { OpenAPIAnalyzer } from "./analyzer-Ce_7JxZh.js";
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
- const entries = Object.entries(this.url.searchParams);
337
- for (let i = 0; i < entries.length; i++) {
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(data, {
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(data, { status: finalStatus, headers: finalHeaders });
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(html, { status: finalStatus, headers: finalHeaders });
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 headers = this.mergeHeaders();
704
- headers.set("Location", url);
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(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 headers = this.mergeHeaders();
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 headers = this.mergeHeaders(responseOptions?.headers);
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
- headers.set("content-type", fileOptions.type);
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 = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
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
- expanded.push({
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
- expanded.push(...getExpandedRoutes(targetApp, cleanPrefix + mountPrefix, newSeen));
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-Ce_7JxZh.js");
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
- const contentType = astMatch.responseType === "string" ? "text/plain" : "application/json";
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: astMatch.responseType } } }
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$1 = new 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$1.renderString(`
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({ method, path: normalizedPath, handler: finalHandler, spec, controller: instance });
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 = handler;
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 datastore.set(new RecordId("middleware_tracking", {
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 datastore.query(`DELETE middleware_tracking WHERE timestamp < ${cutoff}`);
2393
- const results = await datastore.query("SELECT count() FROM middleware_tracking GROUP ALL");
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 datastore.query(`DELETE middleware_tracking ORDER BY timestamp ASC LIMIT ${toDelete}`);
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
- async runHooks(name, ...args) {
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
- await Promise.all(fns.map(async (fn, index) => {
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
- await Promise.all(fns.map((fn) => fn(...args)));
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" ? JSON.stringify(msg) : String(msg);
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
- onInit(app, options) {
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
- currentIntervalStart = {};
3601
- pendingDetails = {};
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 datastore.set(recordId, metric);
3696
- console.log(`[MetricsCollector] Successfully saved ${label} metric to datastore`);
3697
- const test = await datastore.get(recordId);
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
- static __dirname = dirname(fileURLToPath(import.meta.url));
3736
- // Get base path for dashboard files - works in both dev (src/) and production (dist/)
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 = new 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 datastore.query(`
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 datastore.query(
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 datastore.query(
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 datastore.query(
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 datastore.query(
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 datastore.query(
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 datastore.query("SELECT * FROM requests ORDER BY timestamp DESC LIMIT 100");
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 datastore.query("SELECT * FROM requests WHERE id = $id", { id: ctx.params["id"] });
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 datastore.query("SELECT * FROM failed_requests ORDER BY timestamp DESC LIMIT 50");
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("/", async (ctx) => {
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
- const linkPattern = this.getLinkPattern();
3978
- const template = await readFile(Dashboard.getBasePath() + "/template.eta", "utf8");
3979
- return ctx.html(this.eta.renderString(template, {
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
- linkPattern,
3984
- headers: this.dashboardConfig.getRequestHeaders?.()
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 datastore.set(new RecordId("failed_requests", ctx.requestId), {
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 datastore.set(new RecordId("requests", ctx.requestId), logEntry);
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
- const eta = new Eta();
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.init();
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
- onInit(app, options) {
4143
- if (options?.path) {
4144
- app.mount(options.path, this);
4145
- } else {
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
- init() {
4151
- this.get("/", (ctx) => {
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
- return ctx.html(eta.renderString(`<!doctype html>
4155
- <html>
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 = Array.from(headers.entries());
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,