shokupan 0.7.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 (66) hide show
  1. package/README.md +53 -0
  2. package/dist/analyzer-BqIe1p0R.js +35 -0
  3. package/dist/analyzer-BqIe1p0R.js.map +1 -0
  4. package/dist/analyzer-CKLGLFtx.cjs +35 -0
  5. package/dist/analyzer-CKLGLFtx.cjs.map +1 -0
  6. package/dist/{analyzer-Ce_7JxZh.js → analyzer.impl-CV6W1Eq7.js} +238 -21
  7. package/dist/analyzer.impl-CV6W1Eq7.js.map +1 -0
  8. package/dist/{analyzer-Bei1sVWp.cjs → analyzer.impl-D9Yi1Hax.cjs} +237 -20
  9. package/dist/analyzer.impl-D9Yi1Hax.cjs.map +1 -0
  10. package/dist/cli.cjs +1 -1
  11. package/dist/cli.js +1 -1
  12. package/dist/context.d.ts +69 -22
  13. package/dist/{http-server-DFhwlK8e.cjs → http-server-BEMPIs33.cjs} +4 -2
  14. package/dist/http-server-BEMPIs33.cjs.map +1 -0
  15. package/dist/{http-server-0xH174zz.js → http-server-CCeagTyU.js} +4 -2
  16. package/dist/http-server-CCeagTyU.js.map +1 -0
  17. package/dist/index.cjs +2411 -329
  18. package/dist/index.cjs.map +1 -1
  19. package/dist/index.d.ts +3 -0
  20. package/dist/index.js +2390 -308
  21. package/dist/index.js.map +1 -1
  22. package/dist/plugins/application/api-explorer/plugin.d.ts +9 -0
  23. package/dist/plugins/application/api-explorer/static/explorer-client.mjs +880 -0
  24. package/dist/plugins/application/api-explorer/static/style.css +767 -0
  25. package/dist/plugins/application/api-explorer/static/theme.css +128 -0
  26. package/dist/plugins/application/asyncapi/generator.d.ts +3 -0
  27. package/dist/plugins/application/asyncapi/plugin.d.ts +15 -0
  28. package/dist/plugins/application/asyncapi/static/asyncapi-client.mjs +748 -0
  29. package/dist/plugins/application/asyncapi/static/style.css +565 -0
  30. package/dist/plugins/application/asyncapi/static/theme.css +128 -0
  31. package/dist/plugins/application/auth.d.ts +3 -1
  32. package/dist/plugins/application/dashboard/metrics-collector.d.ts +14 -0
  33. package/dist/plugins/application/dashboard/plugin.d.ts +25 -9
  34. package/dist/plugins/application/dashboard/static/charts.js +328 -0
  35. package/dist/plugins/application/dashboard/static/failures.js +85 -0
  36. package/dist/plugins/application/dashboard/static/graph.mjs +523 -0
  37. package/dist/plugins/application/dashboard/static/poll.js +146 -0
  38. package/dist/plugins/application/dashboard/static/reactflow.css +18 -0
  39. package/dist/plugins/application/dashboard/static/registry.css +78 -0
  40. package/dist/plugins/application/dashboard/static/registry.js +269 -0
  41. package/dist/plugins/application/dashboard/static/requests.js +118 -0
  42. package/dist/plugins/application/dashboard/static/styles.css +184 -0
  43. package/dist/plugins/application/dashboard/static/tables.js +92 -0
  44. package/dist/plugins/application/dashboard/static/tabs.js +113 -0
  45. package/dist/plugins/application/dashboard/static/tabulator.css +118 -0
  46. package/dist/plugins/application/dashboard/static/theme.css +128 -0
  47. package/dist/plugins/application/graphql-apollo.d.ts +33 -0
  48. package/dist/plugins/application/graphql-yoga.d.ts +25 -0
  49. package/dist/plugins/application/openapi/analyzer.d.ts +12 -119
  50. package/dist/plugins/application/openapi/analyzer.impl.d.ts +167 -0
  51. package/dist/plugins/application/scalar.d.ts +9 -2
  52. package/dist/plugins/application/socket-io.d.ts +14 -0
  53. package/dist/router.d.ts +92 -51
  54. package/dist/shokupan.d.ts +34 -8
  55. package/dist/util/datastore.d.ts +71 -6
  56. package/dist/util/decorators.d.ts +7 -2
  57. package/dist/util/http-error.d.ts +38 -0
  58. package/dist/util/http-status.d.ts +30 -0
  59. package/dist/util/request.d.ts +1 -1
  60. package/dist/util/symbol.d.ts +19 -0
  61. package/dist/util/types.d.ts +126 -4
  62. package/package.json +38 -15
  63. package/dist/analyzer-Bei1sVWp.cjs.map +0 -1
  64. package/dist/analyzer-Ce_7JxZh.js.map +0 -1
  65. package/dist/http-server-0xH174zz.js.map +0 -1
  66. package/dist/http-server-DFhwlK8e.cjs.map +0 -1
package/dist/index.js CHANGED
@@ -1,21 +1,59 @@
1
+ import { nanoid } from "nanoid";
1
2
  import { readFile } from "node:fs/promises";
3
+ import { inspect } from "node:util";
2
4
  import { trace, SpanKind, SpanStatusCode, context } from "@opentelemetry/api";
5
+ import { dump } from "js-yaml";
3
6
  import { AsyncLocalStorage } from "node:async_hooks";
7
+ import { RecordId, Surreal } from "surrealdb";
4
8
  import { Eta } from "eta";
5
9
  import { stat, readdir, readFile as readFile$1 } from "fs/promises";
6
10
  import { resolve, join, sep, basename } from "path";
7
11
  import * as os from "node:os";
8
12
  import os__default from "node:os";
9
- import { OAuth2Client, Okta, Auth0, Apple, MicrosoftEntraId, Google, GitHub, generateState, generateCodeVerifier } from "arctic";
10
- 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";
11
17
  import cluster from "node:cluster";
12
18
  import net from "node:net";
13
- import { OpenAPIAnalyzer } from "./analyzer-Ce_7JxZh.js";
19
+ import { monitorEventLoopDelay } from "node:perf_hooks";
20
+ import { readFileSync } from "node:fs";
21
+ import { OpenAPIAnalyzer } from "./analyzer-BqIe1p0R.js";
14
22
  import * as zlib from "node:zlib";
15
23
  import Ajv from "ajv";
16
24
  import addFormats from "ajv-formats";
17
25
  import { randomUUID, createHmac } from "crypto";
18
26
  import { EventEmitter } from "events";
27
+ const HTTP_STATUS = {
28
+ // 2xx Success
29
+ OK: 200,
30
+ CREATED: 201,
31
+ ACCEPTED: 202,
32
+ NO_CONTENT: 204,
33
+ // 3xx Redirection
34
+ MOVED_PERMANENTLY: 301,
35
+ FOUND: 302,
36
+ SEE_OTHER: 303,
37
+ NOT_MODIFIED: 304,
38
+ TEMPORARY_REDIRECT: 307,
39
+ PERMANENT_REDIRECT: 308,
40
+ // 4xx Client Errors
41
+ BAD_REQUEST: 400,
42
+ UNAUTHORIZED: 401,
43
+ FORBIDDEN: 403,
44
+ NOT_FOUND: 404,
45
+ METHOD_NOT_ALLOWED: 405,
46
+ REQUEST_TIMEOUT: 408,
47
+ CONFLICT: 409,
48
+ UNPROCESSABLE_ENTITY: 422,
49
+ TOO_MANY_REQUESTS: 429,
50
+ // 5xx Server Errors
51
+ INTERNAL_SERVER_ERROR: 500,
52
+ NOT_IMPLEMENTED: 501,
53
+ BAD_GATEWAY: 502,
54
+ SERVICE_UNAVAILABLE: 503,
55
+ GATEWAY_TIMEOUT: 504
56
+ };
19
57
  const VALID_HTTP_STATUSES = /* @__PURE__ */ new Set([
20
58
  100,
21
59
  101,
@@ -145,6 +183,40 @@ class ShokupanResponse {
145
183
  return this._headers !== null;
146
184
  }
147
185
  }
186
+ const $isApplication = /* @__PURE__ */ Symbol.for("Shokupan.app");
187
+ const $appRoot = /* @__PURE__ */ Symbol.for("Shokupan.app-root");
188
+ const $isMounted = /* @__PURE__ */ Symbol("Shokupan.isMounted");
189
+ const $routeMethods = /* @__PURE__ */ Symbol("Shokupan.routeMethods");
190
+ const $eventMethods = /* @__PURE__ */ Symbol("Shokupan.eventMethods");
191
+ const $routeArgs = /* @__PURE__ */ Symbol("Shokupan.routeArgs");
192
+ const $controllerPath = /* @__PURE__ */ Symbol("Shokupan.controllerPath");
193
+ const $middleware = /* @__PURE__ */ Symbol("Shokupan.middleware");
194
+ const $isRouter = /* @__PURE__ */ Symbol.for("Shokupan.router");
195
+ const $parent = /* @__PURE__ */ Symbol.for("Shokupan.parent");
196
+ const $childRouters = /* @__PURE__ */ Symbol.for("Shokupan.child-routers");
197
+ const $childControllers = /* @__PURE__ */ Symbol.for("Shokupan.child-controllers");
198
+ const $mountPath = /* @__PURE__ */ Symbol.for("Shokupan.mount-path");
199
+ const $dispatch = /* @__PURE__ */ Symbol.for("Shokupan.dispatch");
200
+ const $routes = /* @__PURE__ */ Symbol.for("Shokupan.routes");
201
+ const $routeSpec = /* @__PURE__ */ Symbol.for("Shokupan.routeSpec");
202
+ const $url = /* @__PURE__ */ Symbol.for("Shokupan.ctx.url");
203
+ const $requestId = /* @__PURE__ */ Symbol.for("Shokupan.ctx.requestId");
204
+ const $debug = /* @__PURE__ */ Symbol.for("Shokupan.ctx.debug");
205
+ const $finalResponse = /* @__PURE__ */ Symbol.for("Shokupan.ctx.finalResponse");
206
+ const $rawBody = /* @__PURE__ */ Symbol.for("Shokupan.ctx.rawBody");
207
+ const $cachedBody = /* @__PURE__ */ Symbol.for("Shokupan.ctx.cachedBody");
208
+ const $bodyType = /* @__PURE__ */ Symbol.for("Shokupan.ctx.bodyType");
209
+ const $bodyParsed = /* @__PURE__ */ Symbol.for("Shokupan.ctx.bodyParsed");
210
+ const $bodyParseError = /* @__PURE__ */ Symbol.for("Shokupan.ctx.bodyParseError");
211
+ const $routeMatched = /* @__PURE__ */ Symbol.for("Shokupan.ctx.routeMatched");
212
+ const $cachedHostname = /* @__PURE__ */ Symbol.for("Shokupan.ctx.cachedHostname");
213
+ const $cachedProtocol = /* @__PURE__ */ Symbol.for("Shokupan.ctx.cachedProtocol");
214
+ const $cachedHost = /* @__PURE__ */ Symbol.for("Shokupan.ctx.cachedHost");
215
+ const $cachedOrigin = /* @__PURE__ */ Symbol.for("Shokupan.ctx.cachedOrigin");
216
+ const $cachedQuery = /* @__PURE__ */ Symbol.for("Shokupan.ctx.cachedQuery");
217
+ const $ws = /* @__PURE__ */ Symbol.for("Shokupan.ctx.ws");
218
+ const $socket = /* @__PURE__ */ Symbol.for("Shokupan.ctx.socket");
219
+ const $io = /* @__PURE__ */ Symbol.for("Shokupan.ctx.io");
148
220
  function isValidCookieDomain(domain, currentHost) {
149
221
  const hostWithoutPort = currentHost.split(":")[0];
150
222
  if (domain === hostWithoutPort) return true;
@@ -182,23 +254,41 @@ class ShokupanContext {
182
254
  state;
183
255
  handlerStack = [];
184
256
  response;
185
- _debug;
186
- _finalResponse;
187
- _rawBody;
257
+ [$debug];
258
+ [$finalResponse];
259
+ [$rawBody];
188
260
  // Raw body for compression optimization
189
261
  // Body caching to avoid double parsing
190
- _url;
191
- _cachedBody;
192
- _bodyType;
193
- _bodyParsed = false;
194
- _bodyParseError;
195
- _routeMatched = false;
262
+ [$url];
263
+ [$cachedBody];
264
+ [$bodyType];
265
+ [$bodyParsed] = false;
266
+ [$bodyParseError];
267
+ [$routeMatched] = false;
196
268
  // Cached URL properties to avoid repeated parsing
197
- _cachedHostname;
198
- _cachedProtocol;
199
- _cachedHost;
200
- _cachedOrigin;
201
- _cachedQuery;
269
+ [$cachedHostname];
270
+ [$cachedProtocol];
271
+ [$cachedHost];
272
+ [$cachedOrigin];
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
+ }
289
+ [$ws];
290
+ [$socket];
291
+ [$io];
202
292
  /**
203
293
  * JSX Rendering Function
204
294
  */
@@ -206,12 +296,30 @@ class ShokupanContext {
206
296
  setRenderer(renderer) {
207
297
  this.renderer = renderer;
208
298
  }
299
+ [$requestId];
300
+ get requestId() {
301
+ return this[$requestId] ??= this.app?.applicationConfig?.idGenerator?.() ?? nanoid();
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
+ }
209
317
  get url() {
210
- if (!this._url) {
318
+ if (!this[$url]) {
211
319
  const urlString = this.request.url || "http://localhost/";
212
- this._url = new URL(urlString);
320
+ this[$url] = new URL(urlString);
213
321
  }
214
- return this._url;
322
+ return this[$url];
215
323
  }
216
324
  /**
217
325
  * Base request
@@ -229,7 +337,7 @@ class ShokupanContext {
229
337
  * Request path
230
338
  */
231
339
  get path() {
232
- if (this._url) return this._url.pathname;
340
+ if (this[$url]) return this[$url].pathname;
233
341
  const url = this.request.url;
234
342
  let queryIndex = url.indexOf("?");
235
343
  const end = queryIndex === -1 ? url.length : queryIndex;
@@ -254,13 +362,11 @@ class ShokupanContext {
254
362
  * Request query params
255
363
  */
256
364
  get query() {
257
- if (this._cachedQuery) return this._cachedQuery;
365
+ if (this[$cachedQuery]) return this[$cachedQuery];
258
366
  const q = /* @__PURE__ */ Object.create(null);
259
367
  const blocklist = ["__proto__", "constructor", "prototype"];
260
- const entries = Object.entries(this.url.searchParams);
261
- for (let i = 0; i < entries.length; i++) {
262
- const [key, value] = entries[i];
263
- if (blocklist.includes(key)) continue;
368
+ this.url.searchParams.forEach((value, key) => {
369
+ if (blocklist.includes(key)) return;
264
370
  if (Object.prototype.hasOwnProperty.call(q, key)) {
265
371
  if (Array.isArray(q[key])) {
266
372
  q[key].push(value);
@@ -270,8 +376,8 @@ class ShokupanContext {
270
376
  } else {
271
377
  q[key] = value;
272
378
  }
273
- }
274
- this._cachedQuery = q;
379
+ });
380
+ this[$cachedQuery] = q;
275
381
  return q;
276
382
  }
277
383
  /**
@@ -284,19 +390,19 @@ class ShokupanContext {
284
390
  * Request hostname (e.g. "localhost")
285
391
  */
286
392
  get hostname() {
287
- return this._cachedHostname ??= this.url.hostname;
393
+ return this[$cachedHostname] ??= this.url.hostname;
288
394
  }
289
395
  /**
290
396
  * Request host (e.g. "localhost:3000")
291
397
  */
292
398
  get host() {
293
- return this._cachedHost ??= this.url.host;
399
+ return this[$cachedHost] ??= this.url.host;
294
400
  }
295
401
  /**
296
402
  * Request protocol (e.g. "http:", "https:")
297
403
  */
298
404
  get protocol() {
299
- return this._cachedProtocol ??= this.url.protocol;
405
+ return this[$cachedProtocol] ??= this.url.protocol;
300
406
  }
301
407
  /**
302
408
  * Whether request is secure (https)
@@ -308,7 +414,7 @@ class ShokupanContext {
308
414
  * Request origin (e.g. "http://localhost:3000")
309
415
  */
310
416
  get origin() {
311
- return this._cachedOrigin ??= this.url.origin;
417
+ return this[$cachedOrigin] ??= this.url.origin;
312
418
  }
313
419
  /**
314
420
  * Request headers
@@ -329,6 +435,24 @@ class ShokupanContext {
329
435
  get res() {
330
436
  return this.response;
331
437
  }
438
+ /**
439
+ * Raw WebSocket connection
440
+ */
441
+ get ws() {
442
+ return this[$ws];
443
+ }
444
+ /**
445
+ * Socket.io socket
446
+ */
447
+ get socket() {
448
+ return this[$socket];
449
+ }
450
+ /**
451
+ * Socket.io server
452
+ */
453
+ get io() {
454
+ return this[$io];
455
+ }
332
456
  /**
333
457
  * Helper to set a header on the response
334
458
  * @param key Header key
@@ -338,6 +462,20 @@ class ShokupanContext {
338
462
  this.response.set(key, value);
339
463
  return this;
340
464
  }
465
+ isUpgraded = false;
466
+ /**
467
+ * Upgrades the request to a WebSocket connection.
468
+ * @param options Upgrade options
469
+ * @returns true if upgraded, false otherwise
470
+ */
471
+ upgrade(options) {
472
+ if (!this.server) return false;
473
+ const success = this.server.upgrade(this.req, options);
474
+ if (success) {
475
+ this.isUpgraded = true;
476
+ }
477
+ return success;
478
+ }
341
479
  /**
342
480
  * Set a cookie
343
481
  * @param name Cookie name
@@ -415,18 +553,18 @@ class ShokupanContext {
415
553
  * The body is only parsed once and cached for subsequent reads.
416
554
  */
417
555
  async body() {
418
- if (this._bodyParseError) {
419
- throw this._bodyParseError;
556
+ if (this[$bodyParseError]) {
557
+ throw this[$bodyParseError];
420
558
  }
421
- if (this._bodyParsed) {
422
- return this._cachedBody;
559
+ if (this[$bodyParsed]) {
560
+ return this[$cachedBody];
423
561
  }
424
562
  const contentType = this.request.headers.get("content-type") || "";
425
563
  if (contentType.includes("application/json") || contentType.includes("+json")) {
426
564
  const parserType = this.app?.applicationConfig?.jsonParser || "native";
427
565
  if (parserType === "native") {
428
566
  try {
429
- this._cachedBody = await this.request.json();
567
+ this[$cachedBody] = await this.request.json();
430
568
  } catch (e) {
431
569
  throw e;
432
570
  }
@@ -434,18 +572,18 @@ class ShokupanContext {
434
572
  const rawText = await this.request.text();
435
573
  const { getJSONParser } = await import("./json-parser-B3dnQmCC.js");
436
574
  const parser = getJSONParser(parserType);
437
- this._cachedBody = parser(rawText);
575
+ this[$cachedBody] = parser(rawText);
438
576
  }
439
- this._bodyType = "json";
577
+ this[$bodyType] = "json";
440
578
  } else if (contentType.includes("multipart/form-data") || contentType.includes("application/x-www-form-urlencoded")) {
441
- this._cachedBody = await this.request.formData();
442
- this._bodyType = "formData";
579
+ this[$cachedBody] = await this.request.formData();
580
+ this[$bodyType] = "formData";
443
581
  } else {
444
- this._cachedBody = await this.request.text();
445
- this._bodyType = "text";
582
+ this[$cachedBody] = await this.request.text();
583
+ this[$bodyType] = "text";
446
584
  }
447
- this._bodyParsed = true;
448
- return this._cachedBody;
585
+ this[$bodyParsed] = true;
586
+ return this[$cachedBody];
449
587
  }
450
588
  /**
451
589
  * Pre-parse the request body before handler execution.
@@ -453,7 +591,7 @@ class ShokupanContext {
453
591
  * Errors are deferred until the body is actually accessed in the handler.
454
592
  */
455
593
  async parseBody() {
456
- if (this._bodyParsed) {
594
+ if (this[$bodyParsed]) {
457
595
  return;
458
596
  }
459
597
  if (this.request.method === "GET" || this.request.method === "HEAD") {
@@ -462,7 +600,7 @@ class ShokupanContext {
462
600
  try {
463
601
  await this.body();
464
602
  } catch (error) {
465
- this._bodyParseError = error;
603
+ this[$bodyParseError] = error;
466
604
  }
467
605
  }
468
606
  /**
@@ -512,116 +650,129 @@ class ShokupanContext {
512
650
  throw new Error(`Invalid HTTP status code: ${status}`);
513
651
  }
514
652
  if (typeof body === "string" || body instanceof ArrayBuffer || body instanceof Uint8Array) {
515
- this._rawBody = body;
653
+ this[$rawBody] = body;
654
+ }
655
+ return this[$finalResponse] ??= new Response(body, { status, headers });
656
+ }
657
+ /**
658
+ * Emit an event to the client (WebSocket only)
659
+ * @param event Event name
660
+ * @param data Event data (Must be JSON serializable)
661
+ */
662
+ emit(event, data) {
663
+ if (this[$ws]) {
664
+ this[$ws].send(JSON.stringify({ event, data }));
665
+ } else if (this[$socket]) {
666
+ this[$socket].emit(event, data);
516
667
  }
517
- this._finalResponse = new Response(body, { status, headers });
518
- return this._finalResponse;
519
668
  }
520
669
  /**
521
670
  * Respond with a JSON object
522
671
  */
523
- json(data, status, headers) {
672
+ async json(data, status, headers) {
524
673
  const finalStatus = status ?? this.response.status ?? 200;
525
674
  if (!VALID_HTTP_STATUSES.has(finalStatus)) {
526
675
  throw new Error(`Invalid HTTP status code: ${finalStatus}`);
527
676
  }
528
- const jsonString = JSON.stringify(data);
529
- this._rawBody = jsonString;
677
+ const jsonString = JSON.stringify(data instanceof Promise ? await data : data);
678
+ this[$rawBody] = jsonString;
530
679
  if (!headers && !this.response.hasPopulatedHeaders) {
531
- this._finalResponse = new Response(jsonString, {
680
+ this[$finalResponse] = new Response(jsonString, {
532
681
  status: finalStatus,
533
682
  headers: { "content-type": "application/json" }
534
683
  });
535
- return this._finalResponse;
684
+ return this[$finalResponse];
536
685
  }
537
686
  const finalHeaders = this.mergeHeaders(headers);
538
687
  finalHeaders.set("content-type", "application/json");
539
- this._finalResponse = new Response(jsonString, { status: finalStatus, headers: finalHeaders });
540
- return this._finalResponse;
688
+ this[$finalResponse] = new Response(jsonString, { status: finalStatus, headers: finalHeaders });
689
+ return this[$finalResponse];
541
690
  }
542
691
  /**
543
692
  * Respond with a text string
544
693
  */
545
- text(data, status, headers) {
694
+ async text(data, status, headers) {
546
695
  const finalStatus = status ?? this.response.status ?? 200;
547
696
  if (this.app.applicationConfig.validateStatusCodes && !VALID_HTTP_STATUSES.has(finalStatus)) {
548
697
  throw new Error(`Invalid HTTP status code: ${finalStatus}`);
549
698
  }
550
- this._rawBody = data;
699
+ this[$rawBody] = data instanceof Promise ? await data : data;
551
700
  if (!headers && !this.response.hasPopulatedHeaders) {
552
- this._finalResponse = new Response(data, {
701
+ this[$finalResponse] = new Response(this[$rawBody], {
553
702
  status: finalStatus,
554
703
  headers: { "content-type": "text/plain; charset=utf-8" }
555
704
  });
556
- return this._finalResponse;
705
+ return this[$finalResponse];
557
706
  }
558
707
  const finalHeaders = this.mergeHeaders(headers);
559
708
  finalHeaders.set("content-type", "text/plain; charset=utf-8");
560
- this._finalResponse = new Response(data, { status: finalStatus, headers: finalHeaders });
561
- return this._finalResponse;
709
+ this[$finalResponse] = new Response(this[$rawBody], { status: finalStatus, headers: finalHeaders });
710
+ return this[$finalResponse];
562
711
  }
563
712
  /**
564
713
  * Respond with HTML content
565
714
  */
566
- html(html, status, headers) {
715
+ async html(html, status, headers) {
567
716
  const finalStatus = status ?? this.response.status ?? 200;
568
717
  if (this.app.applicationConfig.validateStatusCodes && !VALID_HTTP_STATUSES.has(finalStatus)) {
569
718
  throw new Error(`Invalid HTTP status code: ${finalStatus}`);
570
719
  }
571
720
  const finalHeaders = this.mergeHeaders(headers);
572
721
  finalHeaders.set("content-type", "text/html; charset=utf-8");
573
- this._rawBody = html;
574
- this._finalResponse = new Response(html, { status: finalStatus, headers: finalHeaders });
575
- return this._finalResponse;
722
+ this[$rawBody] = html instanceof Promise ? await html : html;
723
+ this[$finalResponse] = new Response(this[$rawBody], { status: finalStatus, headers: finalHeaders });
724
+ return this[$finalResponse];
576
725
  }
577
726
  /**
578
727
  * Respond with a redirect
579
728
  */
580
- redirect(url, status = 302) {
729
+ async redirect(url, status = 302) {
581
730
  if (this.app.applicationConfig.validateStatusCodes && !VALID_REDIRECT_STATUSES.has(status)) {
582
731
  throw new Error(`Invalid redirect status code: ${status}`);
583
732
  }
584
- const headers = this.mergeHeaders();
585
- headers.set("Location", url);
586
- this._finalResponse = new Response(null, { status, headers });
587
- return this._finalResponse;
733
+ const finalHeaders = this.mergeHeaders();
734
+ finalHeaders.set("Location", url instanceof Promise ? await url : url);
735
+ this[$finalResponse] = new Response(null, { status, headers: finalHeaders });
736
+ return this[$finalResponse];
588
737
  }
589
738
  /**
590
739
  * Respond with a status code
591
740
  * DOES NOT CHAIN!
592
741
  */
593
- status(status) {
742
+ async status(statusCode) {
743
+ const status = statusCode instanceof Promise ? await statusCode : statusCode;
594
744
  if (this.app.applicationConfig.validateStatusCodes && !VALID_HTTP_STATUSES.has(status)) {
595
745
  throw new Error(`Invalid HTTP status code: ${status}`);
596
746
  }
597
- const headers = this.mergeHeaders();
598
- this._finalResponse = new Response(null, { status, headers });
599
- return this._finalResponse;
747
+ const finalHeaders = this.mergeHeaders();
748
+ this[$finalResponse] = new Response(null, { status, headers: finalHeaders });
749
+ return this[$finalResponse];
600
750
  }
601
751
  /**
602
752
  * Respond with a file
603
753
  */
604
754
  async file(path, fileOptions, responseOptions) {
605
- const headers = this.mergeHeaders(responseOptions?.headers);
755
+ const finalHeaders = this.mergeHeaders(responseOptions?.headers);
606
756
  const status = responseOptions?.status ?? this.response.status;
607
757
  if (this.app.applicationConfig.validateStatusCodes && !VALID_HTTP_STATUSES.has(status)) {
608
758
  throw new Error(`Invalid HTTP status code: ${status}`);
609
759
  }
610
760
  if (typeof Bun !== "undefined") {
611
- this._finalResponse = new Response(Bun.file(path, fileOptions), { status, headers });
612
- return this._finalResponse;
761
+ this[$finalResponse] = new Response(Bun.file(path, fileOptions), { status, headers: finalHeaders });
762
+ return this[$finalResponse];
613
763
  } else {
614
764
  const fileBuffer = await readFile(path);
615
765
  if (fileOptions?.type) {
616
- headers.set("content-type", fileOptions.type);
766
+ finalHeaders.set("content-type", fileOptions.type);
617
767
  }
618
- this._finalResponse = new Response(fileBuffer, { status, headers });
619
- return this._finalResponse;
768
+ this[$finalResponse] = new Response(fileBuffer, { status, headers: finalHeaders });
769
+ return this[$finalResponse];
620
770
  }
621
771
  }
622
772
  /**
623
773
  * Render a JSX element
624
774
  * @param element JSX Element
775
+ * @param args JSX Element Args/Props
625
776
  * @param status HTTP Status
626
777
  * @param headers HTTP Headers
627
778
  */
@@ -652,10 +803,10 @@ const compose = (middleware) => {
652
803
  return next ? next() : Promise.resolve();
653
804
  }
654
805
  const fn = middleware[i];
655
- if (!context2._debug) {
806
+ if (!context2[$debug]) {
656
807
  return fn(context2, () => runner(i + 1));
657
808
  }
658
- const debug = context2._debug;
809
+ const debug = context2[$debug];
659
810
  const debugId = fn._debugId || fn.name || "anonymous";
660
811
  const previousNode = debug.getCurrentNode();
661
812
  debug.trackEdge(previousNode, debugId);
@@ -675,29 +826,6 @@ const compose = (middleware) => {
675
826
  return runner(0);
676
827
  };
677
828
  };
678
- const tracer = trace.getTracer("shokupan.middleware");
679
- function traceHandler(fn, name) {
680
- return async function(...args) {
681
- return tracer.startActiveSpan(`route handler - ${name}`, {
682
- kind: SpanKind.INTERNAL,
683
- attributes: {
684
- "http.route": name,
685
- "component": "shokupan.route"
686
- }
687
- }, async (span) => {
688
- try {
689
- const result = await fn.apply(this, args);
690
- return result;
691
- } catch (err) {
692
- span.recordException(err);
693
- span.setStatus({ code: SpanStatusCode.ERROR, message: err.message });
694
- throw err;
695
- } finally {
696
- span.end();
697
- }
698
- });
699
- };
700
- }
701
829
  function isObject(item) {
702
830
  return item && typeof item === "object" && !Array.isArray(item);
703
831
  }
@@ -731,21 +859,6 @@ function deepMerge(target, ...sources) {
731
859
  }
732
860
  return deepMerge(target, ...sources);
733
861
  }
734
- const $isApplication = /* @__PURE__ */ Symbol.for("Shokupan.app");
735
- const $appRoot = /* @__PURE__ */ Symbol.for("Shokupan.app-root");
736
- const $isMounted = /* @__PURE__ */ Symbol("Shokupan.isMounted");
737
- const $routeMethods = /* @__PURE__ */ Symbol("Shokupan.routeMethods");
738
- const $routeArgs = /* @__PURE__ */ Symbol("Shokupan.routeArgs");
739
- const $controllerPath = /* @__PURE__ */ Symbol("Shokupan.controllerPath");
740
- const $middleware = /* @__PURE__ */ Symbol("Shokupan.middleware");
741
- const $isRouter = /* @__PURE__ */ Symbol.for("Shokupan.router");
742
- const $parent = /* @__PURE__ */ Symbol.for("Shokupan.parent");
743
- const $childRouters = /* @__PURE__ */ Symbol.for("Shokupan.child-routers");
744
- const $childControllers = /* @__PURE__ */ Symbol.for("Shokupan.child-controllers");
745
- const $mountPath = /* @__PURE__ */ Symbol.for("Shokupan.mount-path");
746
- const $dispatch = /* @__PURE__ */ Symbol.for("Shokupan.dispatch");
747
- const $routes = /* @__PURE__ */ Symbol.for("Shokupan.routes");
748
- const $routeSpec = /* @__PURE__ */ Symbol.for("Shokupan.routeSpec");
749
862
  const REGEX_PATTERNS = {
750
863
  QUERY_INT: /parseInt\(ctx\.query\.(\w+)\)/g,
751
864
  QUERY_FLOAT: /parseFloat\(ctx\.query\.(\w+)\)/g,
@@ -885,24 +998,34 @@ function analyzeHandler(handler) {
885
998
  }
886
999
  return { inferredSpec };
887
1000
  }
888
- async function getAstRoutes(applications) {
1001
+ async function getAstRoutes$1(applications) {
889
1002
  const astRoutes = [];
890
- const getExpandedRoutes = (app, prefix = "", seen = /* @__PURE__ */ new Set()) => {
1003
+ const getExpandedRoutes = (app, prefix = "", seen = /* @__PURE__ */ new Set(), sourceOverride) => {
891
1004
  if (seen.has(app.name)) return [];
892
1005
  const newSeen = new Set(seen);
893
1006
  newSeen.add(app.name);
894
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
+ }
895
1014
  for (const route of app.routes) {
896
- const cleanPrefix = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
1015
+ const cleanPrefix = currentPrefix.endsWith("/") ? currentPrefix.slice(0, -1) : currentPrefix;
897
1016
  const cleanPath = route.path.startsWith("/") ? route.path : "/" + route.path;
898
1017
  let joined = cleanPrefix + cleanPath;
899
1018
  if (joined.length > 1 && joined.endsWith("/")) {
900
1019
  joined = joined.slice(0, -1);
901
1020
  }
902
- expanded.push({
1021
+ const expandedRoute = {
903
1022
  ...route,
904
1023
  path: joined || "/"
905
- });
1024
+ };
1025
+ if (sourceOverride) {
1026
+ expandedRoute.sourceContext = sourceOverride;
1027
+ }
1028
+ expanded.push(expandedRoute);
906
1029
  }
907
1030
  if (app.mounted) {
908
1031
  for (const mount of app.mounted) {
@@ -910,7 +1033,23 @@ async function getAstRoutes(applications) {
910
1033
  if (targetApp) {
911
1034
  const cleanPrefix = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
912
1035
  const mountPrefix = mount.prefix.startsWith("/") ? mount.prefix : "/" + mount.prefix;
913
- 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));
914
1053
  }
915
1054
  }
916
1055
  }
@@ -938,13 +1077,13 @@ async function generateOpenApi(rootRouter, options = {}) {
938
1077
  const defaultTagName = options.defaultTag || "Application";
939
1078
  let astRoutes = [];
940
1079
  try {
941
- const { OpenAPIAnalyzer: OpenAPIAnalyzer2 } = await import("./analyzer-Ce_7JxZh.js");
1080
+ const { OpenAPIAnalyzer: OpenAPIAnalyzer2 } = await import("./analyzer-BqIe1p0R.js");
942
1081
  const analyzer = new OpenAPIAnalyzer2(process.cwd());
943
1082
  const { applications } = await analyzer.analyze();
944
- astRoutes = await getAstRoutes(applications);
1083
+ astRoutes = await getAstRoutes$1(applications);
945
1084
  } catch (e) {
946
1085
  }
947
- const collect = (router, prefix = "", currentGroup = defaultTagGroup, defaultTag = defaultTagName) => {
1086
+ const collect = (router, prefix = "", currentGroup = defaultTagGroup, defaultTag = defaultTagName, inheritedMiddleware = []) => {
948
1087
  let group = currentGroup;
949
1088
  let tag = defaultTag;
950
1089
  if (router.config?.group) group = router.config.group;
@@ -961,21 +1100,33 @@ async function generateOpenApi(rootRouter, options = {}) {
961
1100
  }
962
1101
  }
963
1102
  if (!tagGroups.has(group)) tagGroups.set(group, /* @__PURE__ */ new Set());
1103
+ const routerMiddleware = router.middleware || [];
964
1104
  const routes = router[$routes] || [];
965
1105
  for (const route of routes) {
1106
+ if (!["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS", "HEAD"].includes(route.method.toUpperCase())) {
1107
+ continue;
1108
+ }
966
1109
  const routeGroup = route.group || group;
967
1110
  const cleanPrefix = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
968
1111
  const cleanSubPath = route.path.startsWith("/") ? route.path : "/" + route.path;
969
1112
  let fullPath = cleanPrefix + cleanSubPath || "/";
970
- fullPath = fullPath.replace(/:([a-zA-Z0-9_]+)/g, "{$1}");
971
1113
  if (fullPath.length > 1 && fullPath.endsWith("/")) {
972
1114
  fullPath = fullPath.slice(0, -1);
973
1115
  }
1116
+ fullPath = fullPath.replace(/:([a-zA-Z0-9_]+)/g, "{$1}");
974
1117
  if (!paths[fullPath]) paths[fullPath] = {};
975
1118
  const operation = {
976
1119
  responses: { "200": { description: "Successful response" } },
977
1120
  tags: [tag]
978
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
+ }
979
1130
  if (route.guards) {
980
1131
  for (const guard of route.guards) {
981
1132
  if (guard.spec) {
@@ -1013,6 +1164,23 @@ async function generateOpenApi(rootRouter, options = {}) {
1013
1164
  if (astMatch.description) operation.description = astMatch.description;
1014
1165
  if (astMatch.tags) operation.tags = astMatch.tags;
1015
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
+ }
1016
1184
  if (astMatch.requestTypes?.body) {
1017
1185
  operation.requestBody = {
1018
1186
  content: { "application/json": { schema: astMatch.requestTypes.body } }
@@ -1024,10 +1192,12 @@ async function generateOpenApi(rootRouter, options = {}) {
1024
1192
  content: { "application/json": { schema: astMatch.responseSchema } }
1025
1193
  };
1026
1194
  } else if (astMatch.responseType) {
1027
- 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";
1028
1198
  operation.responses["200"] = {
1029
1199
  description: "Successful response",
1030
- content: { [contentType]: { schema: { type: astMatch.responseType } } }
1200
+ content: { [contentType]: { schema: { type: "string" } } }
1031
1201
  };
1032
1202
  }
1033
1203
  const params = [];
@@ -1039,6 +1209,26 @@ async function generateOpenApi(rootRouter, options = {}) {
1039
1209
  if (params.length > 0) {
1040
1210
  operation.parameters = params;
1041
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
+ }
1042
1232
  }
1043
1233
  if (route.keys.length > 0) {
1044
1234
  const pathParams = route.keys.map((key) => ({
@@ -1108,7 +1298,7 @@ async function generateOpenApi(rootRouter, options = {}) {
1108
1298
  const cleanPrefix = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
1109
1299
  const cleanMount = mountPath.startsWith("/") ? mountPath : "/" + mountPath;
1110
1300
  const nextPrefix = cleanPrefix + cleanMount || "/";
1111
- collect(child, nextPrefix, group, tag);
1301
+ collect(child, nextPrefix, group, tag, [...inheritedMiddleware, ...routerMiddleware]);
1112
1302
  }
1113
1303
  };
1114
1304
  collect(rootRouter);
@@ -1132,7 +1322,36 @@ class RequestContextStore {
1132
1322
  span;
1133
1323
  }
1134
1324
  const asyncContext = new AsyncLocalStorage();
1135
- const eta$1 = new Eta();
1325
+ class HttpError extends Error {
1326
+ status;
1327
+ constructor(message, status) {
1328
+ super(message);
1329
+ this.name = "HttpError";
1330
+ this.status = status;
1331
+ if (Error.captureStackTrace) {
1332
+ Error.captureStackTrace(this, HttpError);
1333
+ }
1334
+ }
1335
+ }
1336
+ function getErrorStatus(err) {
1337
+ if (!err || typeof err !== "object") {
1338
+ return 500;
1339
+ }
1340
+ if (typeof err.status === "number") {
1341
+ return err.status;
1342
+ }
1343
+ if (typeof err.statusCode === "number") {
1344
+ return err.statusCode;
1345
+ }
1346
+ return 500;
1347
+ }
1348
+ class EventError extends HttpError {
1349
+ constructor(message = "Event Error") {
1350
+ super(message, 500);
1351
+ this.name = "EventError";
1352
+ }
1353
+ }
1354
+ const eta = new Eta();
1136
1355
  function serveStatic(config, prefix) {
1137
1356
  const rootPath = resolve(config.root || ".");
1138
1357
  const normalizedPrefix = prefix.endsWith("/") && prefix !== "/" ? prefix.slice(0, -1) : prefix;
@@ -1231,7 +1450,7 @@ function serveStatic(config, prefix) {
1231
1450
  if (config.listDirectory) {
1232
1451
  try {
1233
1452
  const files = await readdir(requestPath);
1234
- const listing = eta$1.renderString(`
1453
+ const listing = eta.renderString(`
1235
1454
  <!DOCTYPE html>
1236
1455
  <html>
1237
1456
  <head>
@@ -1271,7 +1490,7 @@ function serveStatic(config, prefix) {
1271
1490
  if (typeof Bun !== "undefined") {
1272
1491
  response = new Response(Bun.file(finalPath));
1273
1492
  } else {
1274
- const fileBuffer = await readFile$1(finalPath);
1493
+ const fileBuffer = await readFile$1(finalPath, { encoding: "binary" });
1275
1494
  response = new Response(fileBuffer);
1276
1495
  }
1277
1496
  if (config.hooks?.onResponse) {
@@ -1284,69 +1503,6 @@ function serveStatic(config, prefix) {
1284
1503
  serveStaticMiddleware.pluginName = "ServeStatic";
1285
1504
  return serveStaticMiddleware;
1286
1505
  }
1287
- let db;
1288
- let dbPromise = null;
1289
- let RecordId;
1290
- async function ensureDb() {
1291
- if (db) return db;
1292
- if (dbPromise) return dbPromise;
1293
- dbPromise = (async () => {
1294
- try {
1295
- const { createNodeEngines } = await import("@surrealdb/node");
1296
- const surreal = await import("surrealdb");
1297
- const Surreal = surreal.Surreal;
1298
- RecordId = surreal.RecordId;
1299
- const engine = process.env["SHOKUPAN_DB_ENGINE"] === "memory" ? "mem://" : "rocksdb://database";
1300
- const _db = new Surreal({
1301
- engines: createNodeEngines()
1302
- });
1303
- await _db.connect(engine, { namespace: "vendor", database: "shokupan" });
1304
- await _db.query(`
1305
- DEFINE TABLE OVERWRITE failed_requests SCHEMALESS COMMENT "Created by Shokupan";
1306
- DEFINE TABLE OVERWRITE sessions SCHEMALESS COMMENT "Created by Shokupan";
1307
- DEFINE TABLE OVERWRITE users SCHEMALESS COMMENT "Created by Shokupan";
1308
- DEFINE TABLE OVERWRITE idempotency_keys SCHEMALESS COMMENT "Created by Shokupan";
1309
- DEFINE TABLE OVERWRITE middleware_tracking SCHEMALESS COMMENT "Created by Shokupan";
1310
- DEFINE TABLE OVERWRITE requests SCHEMALESS COMMENT "Created by Shokupan";
1311
- `);
1312
- db = _db;
1313
- return db;
1314
- } catch (e) {
1315
- dbPromise = null;
1316
- if (e.code === "ERR_MODULE_NOT_FOUND" || e.message.includes("Cannot find module")) {
1317
- throw new Error("SurrealDB dependencies not found. To use the datastore, please install 'surrealdb' and '@surrealdb/node'.");
1318
- }
1319
- throw e;
1320
- }
1321
- })();
1322
- return dbPromise;
1323
- }
1324
- const datastore = {
1325
- async get(store, key) {
1326
- await ensureDb();
1327
- return db.select(new RecordId(store, key));
1328
- },
1329
- async set(store, key, value) {
1330
- await ensureDb();
1331
- return db.create(new RecordId(store, key)).content(value);
1332
- },
1333
- async query(query, vars) {
1334
- await ensureDb();
1335
- try {
1336
- const r = await db.query(query, vars);
1337
- return Array.isArray(r) ? r : r?.collect ? await r.collect() : r;
1338
- } catch (e) {
1339
- console.error("DS ERROR:", e);
1340
- throw e;
1341
- }
1342
- },
1343
- get ready() {
1344
- return ensureDb().then(() => void 0);
1345
- }
1346
- };
1347
- process.on("exit", async () => {
1348
- if (db) await db.close();
1349
- });
1350
1506
  class Container {
1351
1507
  static services = /* @__PURE__ */ new Map();
1352
1508
  static register(target, instance) {
@@ -1380,6 +1536,29 @@ function Inject(token) {
1380
1536
  });
1381
1537
  };
1382
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
+ }
1383
1562
  class ShokupanRequestBase {
1384
1563
  method;
1385
1564
  url;
@@ -1417,8 +1596,10 @@ function getCallerInfo(skipFrames = 1) {
1417
1596
  if (!l.includes(":")) continue;
1418
1597
  if (l.includes("node_modules")) continue;
1419
1598
  if (l.includes("bun:main")) continue;
1599
+ if (l.includes("bun:wrap")) continue;
1420
1600
  if (l.includes("src/util/stack.ts")) continue;
1421
1601
  if (l.includes("src/router.ts")) continue;
1602
+ if (l.includes("src/util/decorators.ts")) continue;
1422
1603
  if (l.includes("src/shokupan.ts")) continue;
1423
1604
  found++;
1424
1605
  if (found >= skipFrames) {
@@ -1561,6 +1742,9 @@ class ShokupanRouter {
1561
1742
  [$parent] = null;
1562
1743
  [$childRouters] = [];
1563
1744
  [$childControllers] = [];
1745
+ get db() {
1746
+ return this.root?.db;
1747
+ }
1564
1748
  hookCache = /* @__PURE__ */ new Map();
1565
1749
  hooksInitialized = false;
1566
1750
  middleware = [];
@@ -1576,6 +1760,15 @@ class ShokupanRouter {
1576
1760
  metadata;
1577
1761
  // Metadata for the router itself
1578
1762
  currentGuards = [];
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
+ }
1579
1772
  // Registry Accessor
1580
1773
  getComponentRegistry() {
1581
1774
  const controllerRoutesMap = /* @__PURE__ */ new Map();
@@ -1636,6 +1829,42 @@ class ShokupanRouter {
1636
1829
  isRouterInstance(target) {
1637
1830
  return typeof target === "object" && target !== null && $isRouter in target;
1638
1831
  }
1832
+ /**
1833
+ * Registers an event handler for WebSocket.
1834
+ */
1835
+ event(name, handler) {
1836
+ const info = getCallerInfo();
1837
+ handler.source = { file: info.file, line: info.line };
1838
+ if (this.eventHandlers.has(name)) {
1839
+ const err = new EventError(`Event handler \`${name}\` already exists.`);
1840
+ console.warn(err);
1841
+ const handlers = this.eventHandlers.get(name);
1842
+ handlers.push(handler);
1843
+ this.eventHandlers.set(name, handlers);
1844
+ } else {
1845
+ this.eventHandlers.set(name, [handler]);
1846
+ }
1847
+ return this;
1848
+ }
1849
+ /**
1850
+ * Finds an event handler(s) by name.
1851
+ */
1852
+ findEvent(name) {
1853
+ if (this.eventHandlers.has(name)) {
1854
+ return this.eventHandlers.get(name);
1855
+ }
1856
+ for (const child of this[$childRouters]) {
1857
+ const handler = child.findEvent(name);
1858
+ if (handler) return handler;
1859
+ }
1860
+ return null;
1861
+ }
1862
+ /**
1863
+ * Returns all registered event handlers.
1864
+ */
1865
+ getEventHandlers() {
1866
+ return this.eventHandlers;
1867
+ }
1639
1868
  /**
1640
1869
  * Mounts a controller instance to a path prefix.
1641
1870
  *
@@ -1736,7 +1965,7 @@ class ShokupanRouter {
1736
1965
  });
1737
1966
  const ctx = new ShokupanContext(req);
1738
1967
  let result = null;
1739
- let status = 200;
1968
+ let status = HTTP_STATUS.OK;
1740
1969
  const headers = {};
1741
1970
  const match = this.find(req.method, ctx.path);
1742
1971
  if (match) {
@@ -1745,12 +1974,12 @@ class ShokupanRouter {
1745
1974
  result = await match.handler(ctx);
1746
1975
  } catch (err) {
1747
1976
  console.error(err);
1748
- status = err.status || err.statusCode || 500;
1977
+ status = getErrorStatus(err);
1749
1978
  result = { error: err.message || "Internal Server Error" };
1750
1979
  if (err.errors) result.errors = err.errors;
1751
1980
  }
1752
1981
  } else {
1753
- status = 404;
1982
+ status = HTTP_STATUS.NOT_FOUND;
1754
1983
  result = "Not Found";
1755
1984
  }
1756
1985
  if (result instanceof Response) {
@@ -1779,7 +2008,7 @@ class ShokupanRouter {
1779
2008
  const originalHandler = handler;
1780
2009
  const wrapped = async (ctx) => {
1781
2010
  await this.runHooks("onRequestStart", ctx);
1782
- const debug = ctx._debug;
2011
+ const debug = ctx[$debug];
1783
2012
  let debugId;
1784
2013
  let previousNode;
1785
2014
  if (debug) {
@@ -1869,6 +2098,7 @@ class ShokupanRouter {
1869
2098
  const decoratedRoutes = instance[$routeMethods] || proto && proto[$routeMethods];
1870
2099
  const decoratedArgs = instance[$routeArgs] || proto && proto[$routeArgs];
1871
2100
  const methodMiddlewareMap = instance[$middleware] || proto && proto[$middleware];
2101
+ const decoratedEvents = instance[$eventMethods] || proto && proto[$eventMethods];
1872
2102
  let routesAttached = 0;
1873
2103
  for (let i = 0; i < Array.from(methods).length; i++) {
1874
2104
  const name = Array.from(methods)[i];
@@ -1878,10 +2108,12 @@ class ShokupanRouter {
1878
2108
  if (typeof originalHandler !== "function") continue;
1879
2109
  let method;
1880
2110
  let subPath = "";
2111
+ let methodSource;
1881
2112
  if (decoratedRoutes && decoratedRoutes.has(name)) {
1882
2113
  const config = decoratedRoutes.get(name);
1883
2114
  method = config.method;
1884
2115
  subPath = config.path;
2116
+ methodSource = config.source;
1885
2117
  } else {
1886
2118
  for (let j = 0; j < HTTPMethods.length; j++) {
1887
2119
  const m = HTTPMethods[j];
@@ -2011,7 +2243,54 @@ class ShokupanRouter {
2011
2243
  const decoratedSpecs = instance[$routeSpec] || proto && proto[$routeSpec];
2012
2244
  const userSpec = decoratedSpecs && decoratedSpecs.get(name);
2013
2245
  const spec = { tags: [tagName], ...userSpec };
2014
- 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
+ });
2256
+ }
2257
+ if (decoratedEvents?.has(name)) {
2258
+ routesAttached++;
2259
+ const config = decoratedEvents.get(name);
2260
+ const routeArgs = decoratedArgs?.get(name);
2261
+ const wrappedHandler = async (ctx) => {
2262
+ let args = [ctx];
2263
+ if (routeArgs?.length > 0) {
2264
+ args = [];
2265
+ const sortedArgs = [...routeArgs].sort((a, b) => a.index - b.index);
2266
+ for (let k = 0; k < sortedArgs.length; k++) {
2267
+ const arg = sortedArgs[k];
2268
+ switch (arg.type) {
2269
+ case RouteParamType.BODY:
2270
+ args[arg.index] = await ctx.body();
2271
+ break;
2272
+ case RouteParamType.CONTEXT:
2273
+ args[arg.index] = ctx;
2274
+ break;
2275
+ case RouteParamType.REQUEST:
2276
+ args[arg.index] = ctx.req;
2277
+ break;
2278
+ case RouteParamType.HEADER:
2279
+ args[arg.index] = arg.name ? ctx.req.headers.get(arg.name) : ctx.req.headers;
2280
+ break;
2281
+ default:
2282
+ args[arg.index] = void 0;
2283
+ }
2284
+ }
2285
+ }
2286
+ return originalHandler.apply(instance, args);
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;
2293
+ this.event(config.eventName, wrappedHandler);
2015
2294
  }
2016
2295
  }
2017
2296
  if (routesAttached === 0) {
@@ -2080,7 +2359,7 @@ class ShokupanRouter {
2080
2359
  * @param arg.renderer - JSX renderer for the route
2081
2360
  * @param arg.controller - Controller for the route
2082
2361
  */
2083
- 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 }) {
2084
2363
  const { regex, keys } = customRegex ? { regex: customRegex, keys: [] } : this.parsePath(path);
2085
2364
  if (this.currentGuards.length > 0) {
2086
2365
  spec = spec || {};
@@ -2098,7 +2377,13 @@ class ShokupanRouter {
2098
2377
  }
2099
2378
  }
2100
2379
  }
2101
- 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;
2102
2387
  const routeGuards = [...this.currentGuards];
2103
2388
  const effectiveTimeout = requestTimeout ?? this.requestTimeout ?? this.rootConfig?.requestTimeout;
2104
2389
  if (effectiveTimeout !== void 0 && effectiveTimeout > 0) {
@@ -2149,7 +2434,7 @@ class ShokupanRouter {
2149
2434
  return innerHandler(ctx);
2150
2435
  };
2151
2436
  }
2152
- const { file, line } = getCallerInfo();
2437
+ const { file, line } = metadata || getCallerInfo();
2153
2438
  const trackingHandler = wrappedHandler;
2154
2439
  wrappedHandler = async (ctx) => {
2155
2440
  if (!ctx.app?.applicationConfig.enableMiddlewareTracking) {
@@ -2175,9 +2460,13 @@ class ShokupanRouter {
2175
2460
  const config = ctx.app.applicationConfig;
2176
2461
  Promise.resolve().then(async () => {
2177
2462
  try {
2463
+ const db = ctx.app?.db;
2464
+ if (!db) return;
2178
2465
  const timestamp = Date.now();
2179
- const key = `${timestamp}-${handler.name || "anonymous"}-${Math.random().toString(36).substring(7)}`;
2180
- await datastore.set("middleware_tracking", key, {
2466
+ await db.upsert(new RecordId("middleware_tracking", {
2467
+ timestamp,
2468
+ name: handler.name || "anonymous"
2469
+ }), {
2181
2470
  name: handler.name || "anonymous",
2182
2471
  path: ctx.path,
2183
2472
  timestamp,
@@ -2193,11 +2482,11 @@ class ShokupanRouter {
2193
2482
  const ttl = config.middlewareTrackingTTL ?? 864e5;
2194
2483
  const maxCapacity = config.middlewareTrackingMaxCapacity ?? 1e4;
2195
2484
  const cutoff = Date.now() - ttl;
2196
- await datastore.query(`DELETE middleware_tracking WHERE timestamp < ${cutoff}`);
2197
- const results = await datastore.query("SELECT count() FROM middleware_tracking GROUP ALL");
2198
- if (results && results[0] && results[0].count > maxCapacity) {
2485
+ await db.query(`DELETE middleware_tracking WHERE timestamp < ${cutoff}`);
2486
+ const results = await db.query("SELECT count() FROM middleware_tracking GROUP ALL");
2487
+ if (results?.[0]?.count > maxCapacity) {
2199
2488
  const toDelete = results[0].count - maxCapacity;
2200
- 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}`);
2201
2490
  }
2202
2491
  } catch (datastoreError) {
2203
2492
  console.error("Failed to store middleware tracking:", datastoreError);
@@ -2227,7 +2516,8 @@ class ShokupanRouter {
2227
2516
  file,
2228
2517
  line
2229
2518
  },
2230
- controller
2519
+ controller,
2520
+ middleware: middleware || []
2231
2521
  });
2232
2522
  this.trie.insert(method, path, bakedHandler);
2233
2523
  return this;
@@ -2272,7 +2562,7 @@ class ShokupanRouter {
2272
2562
  (l) => l.includes(":") && !l.includes("router.ts") && !l.includes("shokupan.ts") && !l.includes("node_modules") && !l.includes("bun:main")
2273
2563
  );
2274
2564
  if (callerLine) {
2275
- const match = callerLine.match(/\((.*):(\d+):(\d+)\)/) || callerLine.match(/at (.*):(\d+):(\d+)/);
2565
+ const match = callerLine.match(/\((.{0,1000}):(\d{1,10}):(?:\d{1,10})\)/) || callerLine.match(/at (.{0,1000}):(\d{1,10}):(?:\d{1,10})/);
2276
2566
  if (match) {
2277
2567
  file = match[1];
2278
2568
  line = parseInt(match[2], 10);
@@ -2290,7 +2580,7 @@ class ShokupanRouter {
2290
2580
  }
2291
2581
  return guardHandler(ctx, next);
2292
2582
  };
2293
- trackedGuard.originalHandler = guardHandler.originalHandler || guardHandler;
2583
+ trackedGuard.originalHandler = guardHandler.originalHandler ?? guardHandler;
2294
2584
  this.currentGuards.push({ handler: trackedGuard, spec });
2295
2585
  return this;
2296
2586
  }
@@ -2356,7 +2646,8 @@ class ShokupanRouter {
2356
2646
  method,
2357
2647
  path,
2358
2648
  spec,
2359
- handler: finalHandler
2649
+ handler: finalHandler,
2650
+ middleware: handlers.slice(0, handlers.length - 1)
2360
2651
  });
2361
2652
  }
2362
2653
  /**
@@ -2396,16 +2687,16 @@ class ShokupanRouter {
2396
2687
  }
2397
2688
  this.hooksInitialized = true;
2398
2689
  }
2399
- async runHooks(name, ...args) {
2690
+ runHooks(name, ...args) {
2400
2691
  if (!this.hooksInitialized) {
2401
2692
  this.ensureHooksInitialized();
2402
2693
  }
2403
2694
  const fns = this.hookCache.get(name);
2404
2695
  if (!fns) return;
2405
2696
  const ctx = args?.[0] instanceof ShokupanContext ? args[0] : void 0;
2406
- const debug = ctx?._debug;
2697
+ const debug = ctx?.[$debug];
2407
2698
  if (debug) {
2408
- await Promise.all(fns.map(async (fn, index) => {
2699
+ return Promise.all(fns.map(async (fn, index) => {
2409
2700
  const hookId = `hook_${name}_${fn.name || index}`;
2410
2701
  const previousNode = debug.getCurrentNode();
2411
2702
  debug.trackEdge(previousNode, hookId);
@@ -2424,7 +2715,7 @@ class ShokupanRouter {
2424
2715
  }
2425
2716
  }));
2426
2717
  } else {
2427
- await Promise.all(fns.map((fn) => fn(...args)));
2718
+ return Promise.all(fns.map((fn) => fn(...args)));
2428
2719
  }
2429
2720
  }
2430
2721
  }
@@ -2471,45 +2762,160 @@ class SystemCpuMonitor {
2471
2762
  this.currentUsage = total === 0 ? 0 : (1 - idle / total) * 100;
2472
2763
  }
2473
2764
  }
2474
- const defaults = {
2475
- port: 3e3,
2476
- hostname: "localhost",
2477
- development: process.env.NODE_ENV !== "production",
2478
- enableAsyncLocalStorage: false,
2479
- reusePort: false
2480
- };
2481
- trace.getTracer("shokupan.application");
2482
- class Shokupan extends ShokupanRouter {
2483
- applicationConfig = {};
2484
- openApiSpec;
2485
- composedMiddleware;
2486
- cpuMonitor;
2487
- get logger() {
2488
- return this.applicationConfig.logger;
2765
+ class SurrealDatastore {
2766
+ constructor(db) {
2767
+ this.db = db;
2768
+ process.on("exit", async () => {
2769
+ await this.disconnect();
2770
+ });
2489
2771
  }
2490
- constructor(applicationConfig = {}) {
2491
- const config = Object.assign({}, defaults, applicationConfig);
2492
- const { hooks, ...routerConfig } = config;
2493
- super({ ...routerConfig, hooks });
2494
- this[$isApplication] = true;
2495
- this[$appRoot] = this;
2496
- this.applicationConfig = config;
2497
- const { file, line } = getCallerInfo();
2498
- this.metadata = {
2499
- file,
2500
- line,
2501
- name: "ShokupanApplication"
2502
- };
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();
2503
2782
  }
2504
2783
  /**
2505
- * Adds middleware to the application.
2784
+ * Select a record or contents of a table by its ID.
2506
2785
  */
2507
- use(middleware) {
2508
- const { file, line } = getCallerInfo();
2509
- if (!middleware.metadata) {
2510
- middleware.metadata = {
2511
- file,
2512
- line,
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
+ }
2859
+ const defaults = {
2860
+ port: 3e3,
2861
+ hostname: "localhost",
2862
+ development: process.env.NODE_ENV !== "production",
2863
+ enableAsyncLocalStorage: false,
2864
+ enableHttpBridge: false,
2865
+ reusePort: false
2866
+ };
2867
+ trace.getTracer("shokupan.application");
2868
+ class Shokupan extends ShokupanRouter {
2869
+ applicationConfig = {};
2870
+ openApiSpec;
2871
+ asyncApiSpec;
2872
+ composedMiddleware;
2873
+ cpuMonitor;
2874
+ server;
2875
+ datastore;
2876
+ dbPromise;
2877
+ get db() {
2878
+ return this.datastore;
2879
+ }
2880
+ get logger() {
2881
+ return this.applicationConfig.logger;
2882
+ }
2883
+ constructor(applicationConfig = {}) {
2884
+ const config = Object.assign({}, defaults, applicationConfig);
2885
+ const { hooks, ...routerConfig } = config;
2886
+ super({ ...routerConfig, hooks });
2887
+ this[$isApplication] = true;
2888
+ this[$appRoot] = this;
2889
+ this.applicationConfig = config;
2890
+ const { file, line } = getCallerInfo();
2891
+ this.metadata = {
2892
+ file,
2893
+ line,
2894
+ name: "ShokupanApplication"
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
+ });
2909
+ }
2910
+ /**
2911
+ * Adds middleware to the application.
2912
+ */
2913
+ use(middleware) {
2914
+ const { file, line } = getCallerInfo();
2915
+ if (!middleware.metadata) {
2916
+ middleware.metadata = {
2917
+ file,
2918
+ line,
2513
2919
  name: middleware.name || "middleware",
2514
2920
  isBuiltin: middleware.isBuiltin,
2515
2921
  pluginName: middleware.pluginName
@@ -2579,19 +2985,76 @@ class Shokupan extends ShokupanRouter {
2579
2985
  */
2580
2986
  async listen(port) {
2581
2987
  const finalPort = port ?? this.applicationConfig.port ?? 3e3;
2582
- if (finalPort < 0 || finalPort > 65535) {
2988
+ if (finalPort < 0 || finalPort > 65535 || finalPort % 1 !== 0) {
2583
2989
  throw new Error("Invalid port number");
2584
2990
  }
2585
2991
  await Promise.all(this.startupHooks.map((hook) => hook()));
2586
2992
  if (this.applicationConfig.enableOpenApiGen) {
2587
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
+ }
2588
3046
  await Promise.all(this.specAvailableHooks.map((hook) => hook(this.openApiSpec)));
2589
3047
  }
3048
+ if (this.applicationConfig.enableAsyncApiGen) {
3049
+ const { generateAsyncApi: generateAsyncApi2 } = await Promise.resolve().then(() => generator);
3050
+ this.asyncApiSpec = await generateAsyncApi2(this);
3051
+ }
2590
3052
  if (port === 0 && process.platform === "linux") ;
2591
3053
  if (this.applicationConfig.autoBackpressureFeedback === true) {
2592
3054
  this.cpuMonitor = new SystemCpuMonitor();
2593
3055
  this.cpuMonitor.start();
2594
3056
  }
3057
+ const self = this;
2595
3058
  const serveOptions = {
2596
3059
  port: finalPort,
2597
3060
  hostname: this.applicationConfig.hostname,
@@ -2603,25 +3066,116 @@ class Shokupan extends ShokupanRouter {
2603
3066
  open(ws) {
2604
3067
  ws.data?.handler?.open?.(ws);
2605
3068
  },
2606
- message(ws, message) {
2607
- ws.data?.handler?.message?.(ws, message);
3069
+ async message(ws, message) {
3070
+ if (ws.data?.handler?.message) {
3071
+ return ws.data.handler.message(ws, message);
3072
+ }
3073
+ if (typeof message !== "string") return;
3074
+ try {
3075
+ const payload = JSON.parse(message);
3076
+ if (self.applicationConfig["enableHttpBridge"] && payload.type === "HTTP") {
3077
+ const { id, method, path, headers, body } = payload;
3078
+ const url = new URL(path, `http://${self.applicationConfig.hostname || "localhost"}:${finalPort}`);
3079
+ const req = new Request(url.toString(), {
3080
+ method,
3081
+ headers,
3082
+ body: typeof body === "object" ? JSON.stringify(body) : body
3083
+ });
3084
+ const res = await self.fetch(req);
3085
+ const resBody = await res.json().catch((err) => res.text());
3086
+ const resHeaders = {};
3087
+ res.headers.forEach((v, k) => resHeaders[k] = v);
3088
+ ws.send(JSON.stringify({
3089
+ type: "RESPONSE",
3090
+ id,
3091
+ status: res.status,
3092
+ headers: resHeaders,
3093
+ body: resBody
3094
+ }));
3095
+ return;
3096
+ }
3097
+ const eventName = payload.event || (payload.type === "EVENT" ? payload.name : void 0);
3098
+ if (eventName) {
3099
+ const handlers = self.findEvent(eventName);
3100
+ const handler = handlers?.length == 1 ? handlers[0] : compose(handlers);
3101
+ if (handler) {
3102
+ const data = payload.data || payload.payload;
3103
+ const req = new ShokupanRequest({
3104
+ url: `http://${self.applicationConfig.hostname || "localhost"}/event/${eventName}`,
3105
+ method: "POST",
3106
+ headers: new Headers({ "content-type": "application/json" }),
3107
+ body: JSON.stringify(data)
3108
+ });
3109
+ const ctx = new ShokupanContext(req, self.server);
3110
+ ctx[$ws] = ws;
3111
+ ws.data ??= {};
3112
+ ws.data["ctx"] = ctx;
3113
+ try {
3114
+ await handler(ctx);
3115
+ } catch (err) {
3116
+ if (self.applicationConfig["websocketErrorHandler"]) {
3117
+ await self.applicationConfig["websocketErrorHandler"](err, ctx);
3118
+ } else {
3119
+ console.error(`Error in event ${eventName}:`, err);
3120
+ }
3121
+ }
3122
+ }
3123
+ }
3124
+ } catch (e) {
3125
+ }
2608
3126
  },
2609
3127
  drain(ws) {
2610
3128
  ws.data?.handler?.drain?.(ws);
2611
3129
  },
2612
3130
  close(ws, code, reason) {
2613
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
+ }
2614
3141
  }
2615
3142
  }
2616
3143
  };
2617
3144
  let factory = this.applicationConfig.serverFactory;
2618
3145
  if (!factory && typeof Bun === "undefined") {
2619
- const { createHttpServer } = await import("./http-server-0xH174zz.js");
3146
+ const { createHttpServer } = await import("./http-server-CCeagTyU.js");
2620
3147
  factory = createHttpServer();
2621
3148
  }
2622
- const server = factory ? await factory(serveOptions) : Bun.serve(serveOptions);
2623
- console.log(`Shokupan server listening on http://${serveOptions.hostname}:${serveOptions.port}`);
2624
- return server;
3149
+ this.server = factory ? await factory(serveOptions) : Bun.serve(serveOptions);
3150
+ return this.server;
3151
+ }
3152
+ /**
3153
+ * Stops the application server.
3154
+ *
3155
+ * This method gracefully shuts down the server and stops any running monitors.
3156
+ * Works transparently in both Bun and Node.js runtimes.
3157
+ *
3158
+ * @returns A promise that resolves when the server has been stopped.
3159
+ *
3160
+ * @example
3161
+ * ```typescript
3162
+ * const app = new Shokupan();
3163
+ * const server = await app.listen(3000);
3164
+ *
3165
+ * // Later, when you want to stop the server
3166
+ * await app.stop();
3167
+ * ```
3168
+ * @param closeActiveConnections — Immediately terminate in-flight requests, websockets, and stop accepting new connections.
3169
+ */
3170
+ async stop(closeActiveConnections) {
3171
+ if (this.cpuMonitor) {
3172
+ this.cpuMonitor.stop();
3173
+ this.cpuMonitor = void 0;
3174
+ }
3175
+ if (this.server) {
3176
+ await this.server.stop(closeActiveConnections);
3177
+ this.server = void 0;
3178
+ }
2625
3179
  }
2626
3180
  [$dispatch](req) {
2627
3181
  return this.fetch(req);
@@ -2719,7 +3273,7 @@ class Shokupan extends ShokupanRouter {
2719
3273
  const bodyParsing = ["POST", "PUT", "PATCH", "DELETE"].includes(req.method) ? ctx.parseBody() : Promise.resolve();
2720
3274
  const match = this.find(req.method, ctx.path);
2721
3275
  if (match) {
2722
- ctx._routeMatched = true;
3276
+ ctx[$routeMatched] = true;
2723
3277
  ctx.params = match.params;
2724
3278
  await bodyParsing;
2725
3279
  return match.handler(ctx);
@@ -2729,18 +3283,23 @@ class Shokupan extends ShokupanRouter {
2729
3283
  let response;
2730
3284
  if (result instanceof Response) {
2731
3285
  response = result;
2732
- } else if ((result === null || result === void 0) && ctx._finalResponse instanceof Response) {
2733
- response = ctx._finalResponse;
3286
+ } else if ((result === null || result === void 0) && ctx[$finalResponse] instanceof Response) {
3287
+ response = ctx[$finalResponse];
2734
3288
  } else if (result === null || result === void 0) {
2735
- if (ctx._finalResponse instanceof Response) {
2736
- response = ctx._finalResponse;
2737
- } else if (ctx._routeMatched) {
3289
+ if (ctx[$finalResponse] instanceof Response) {
3290
+ response = ctx[$finalResponse];
3291
+ } else if (ctx.isUpgraded) {
3292
+ return void 0;
3293
+ } else if (ctx[$routeMatched]) {
2738
3294
  response = ctx.send(null, { status: ctx.response.status, headers: ctx.response.headers });
2739
3295
  } else {
2740
- if (ctx.response.status !== 200) {
3296
+ if (ctx.upgrade()) {
3297
+ return void 0;
3298
+ }
3299
+ if (ctx.response.status !== HTTP_STATUS.OK) {
2741
3300
  response = ctx.send(null, { status: ctx.response.status, headers: ctx.response.headers });
2742
3301
  } else {
2743
- response = ctx.text("Not Found", 404);
3302
+ response = ctx.text("Not Found", HTTP_STATUS.NOT_FOUND);
2744
3303
  }
2745
3304
  }
2746
3305
  } else if (typeof result === "object") {
@@ -2749,13 +3308,15 @@ class Shokupan extends ShokupanRouter {
2749
3308
  response = ctx.text(String(result));
2750
3309
  }
2751
3310
  await this.runHooks("onRequestEnd", ctx);
3311
+ if (response instanceof Promise) {
3312
+ response = await response;
3313
+ }
2752
3314
  await this.runHooks("onResponseStart", ctx, response);
2753
3315
  return response;
2754
3316
  } catch (err) {
2755
- console.error(err);
2756
3317
  const span = asyncContext.getStore()?.span;
2757
3318
  if (span) span.setStatus({ code: 2 });
2758
- const status = err.status || err.statusCode || 500;
3319
+ const status = getErrorStatus(err);
2759
3320
  const body = { error: err.message || "Internal Server Error" };
2760
3321
  if (err.errors) body.errors = err.errors;
2761
3322
  await this.runHooks("onError", ctx, err);
@@ -2777,10 +3338,10 @@ class Shokupan extends ShokupanRouter {
2777
3338
  }
2778
3339
  return executionPromise.catch((err) => {
2779
3340
  if (err.message === "Request Timeout") {
2780
- return ctx.text("Request Timeout", 408);
3341
+ return ctx.text("Request Timeout", HTTP_STATUS.REQUEST_TIMEOUT);
2781
3342
  }
2782
3343
  console.error("Unexpected error in request execution:", err);
2783
- return ctx.text("Internal Server Error", 500);
3344
+ return ctx.text("Internal Server Error", HTTP_STATUS.INTERNAL_SERVER_ERROR);
2784
3345
  }).then(async (res) => {
2785
3346
  await this.runHooks("onResponseEnd", ctx, res);
2786
3347
  return res;
@@ -2859,8 +3420,7 @@ function RateLimitMiddleware(options = {}) {
2859
3420
  }
2860
3421
  }
2861
3422
  const msg = typeof message === "function" ? message(ctx, key) : message;
2862
- typeof msg === "object" ? JSON.stringify(msg) : String(msg);
2863
- 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));
2864
3424
  if (headers) {
2865
3425
  setHeaders(res);
2866
3426
  res.headers.set("Retry-After", String(retryAfter));
@@ -2935,8 +3495,12 @@ function createMethodDecorator(method) {
2935
3495
  }
2936
3496
  target[$routeMethods].set(propertyKey, {
2937
3497
  method,
2938
- path
3498
+ path,
3499
+ source: getCallerInfo(2)
2939
3500
  });
3501
+ if (path.includes("/user")) {
3502
+ console.log(`[Decorator] Captured source for ${propertyKey}:`, getCallerInfo());
3503
+ }
2940
3504
  };
2941
3505
  };
2942
3506
  }
@@ -2948,18 +3512,583 @@ const Patch = createMethodDecorator("PATCH");
2948
3512
  const Options = createMethodDecorator("OPTIONS");
2949
3513
  const Head = createMethodDecorator("HEAD");
2950
3514
  const All = createMethodDecorator("ALL");
3515
+ function Event(eventName) {
3516
+ return (target, propertyKey, descriptor) => {
3517
+ target[$eventMethods] ??= /* @__PURE__ */ new Map();
3518
+ target[$eventMethods].set(propertyKey, {
3519
+ eventName
3520
+ });
3521
+ };
3522
+ }
2951
3523
  function RateLimit(options) {
2952
3524
  return Use(RateLimitMiddleware(options));
2953
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
+ }
2954
4079
  class AuthPlugin extends ShokupanRouter {
2955
4080
  constructor(authConfig) {
2956
4081
  super();
2957
4082
  this.authConfig = authConfig;
2958
4083
  this.secret = typeof authConfig.jwtSecret === "string" ? new TextEncoder().encode(authConfig.jwtSecret) : authConfig.jwtSecret;
2959
- this.init();
2960
4084
  }
2961
4085
  secret;
2962
- 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();
2963
4092
  if (options?.path) {
2964
4093
  app.mount(options.path, this);
2965
4094
  } else {
@@ -2967,6 +4096,7 @@ class AuthPlugin extends ShokupanRouter {
2967
4096
  }
2968
4097
  }
2969
4098
  getProviderInstance(name, p) {
4099
+ const { GitHub, Google, MicrosoftEntraId, Apple, Auth0, Okta, OAuth2Client } = this.arctic;
2970
4100
  switch (name) {
2971
4101
  case "github":
2972
4102
  return new GitHub(p.clientId, p.clientSecret, p.redirectUri);
@@ -2994,7 +4124,7 @@ class AuthPlugin extends ShokupanRouter {
2994
4124
  }
2995
4125
  async createSession(user, ctx) {
2996
4126
  const alg = "HS256";
2997
- 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);
2998
4128
  const opts = this.authConfig.cookieOptions || {};
2999
4129
  let cookie = `auth_token=${jwt}; Path=${opts.path || "/"}; HttpOnly`;
3000
4130
  if (opts.secure) cookie += "; Secure";
@@ -3004,6 +4134,7 @@ class AuthPlugin extends ShokupanRouter {
3004
4134
  return jwt;
3005
4135
  }
3006
4136
  init() {
4137
+ const { generateState, generateCodeVerifier, GitHub, Google, MicrosoftEntraId, Apple, Auth0, Okta, OAuth2Client } = this.arctic;
3007
4138
  const providerEntries = Object.entries(this.authConfig.providers);
3008
4139
  for (let i = 0; i < providerEntries.length; i++) {
3009
4140
  const [providerName, providerConfig] = providerEntries[i];
@@ -3135,7 +4266,7 @@ class AuthPlugin extends ShokupanRouter {
3135
4266
  };
3136
4267
  } else if (provider === "apple") {
3137
4268
  if (idToken) {
3138
- const payload = jose.decodeJwt(idToken);
4269
+ const payload = this.jose.decodeJwt(idToken);
3139
4270
  user = {
3140
4271
  id: payload.sub,
3141
4272
  email: payload["email"],
@@ -3166,6 +4297,9 @@ class AuthPlugin extends ShokupanRouter {
3166
4297
  */
3167
4298
  getMiddleware() {
3168
4299
  return async (ctx, next) => {
4300
+ if (!this.jose) {
4301
+ this.jose = await import("jose");
4302
+ }
3169
4303
  const authHeader = ctx.req.headers.get("Authorization");
3170
4304
  let token = authHeader?.startsWith("Bearer ") ? authHeader.substring(7) : null;
3171
4305
  if (!token) {
@@ -3174,7 +4308,7 @@ class AuthPlugin extends ShokupanRouter {
3174
4308
  }
3175
4309
  if (token) {
3176
4310
  try {
3177
- const { payload } = await jose.jwtVerify(token, this.secret);
4311
+ const { payload } = await this.jose.jwtVerify(token, this.secret);
3178
4312
  ctx.user = payload;
3179
4313
  } catch {
3180
4314
  }
@@ -3291,32 +4425,956 @@ class ClusterPlugin {
3291
4425
  }
3292
4426
  }
3293
4427
  }
3294
- const eta = new Eta();
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
+ }
4609
+ const INTERVALS = [
4610
+ { label: "10s", ms: 10 * 1e3 },
4611
+ { label: "1m", ms: 60 * 1e3 },
4612
+ { label: "5m", ms: 5 * 60 * 1e3 },
4613
+ { label: "1h", ms: 60 * 60 * 1e3 },
4614
+ { label: "2h", ms: 2 * 60 * 60 * 1e3 },
4615
+ { label: "6h", ms: 6 * 60 * 60 * 1e3 },
4616
+ { label: "12h", ms: 12 * 60 * 60 * 1e3 },
4617
+ { label: "1d", ms: 24 * 60 * 60 * 1e3 },
4618
+ { label: "3d", ms: 3 * 24 * 60 * 60 * 1e3 },
4619
+ { label: "7d", ms: 7 * 24 * 60 * 60 * 1e3 },
4620
+ { label: "30d", ms: 30 * 24 * 60 * 60 * 1e3 }
4621
+ ];
4622
+ class MetricsCollector {
4623
+ constructor(db) {
4624
+ this.db = db;
4625
+ this.eventLoopHistogram.enable();
4626
+ const now = Date.now();
4627
+ INTERVALS.forEach((int) => {
4628
+ this.currentIntervalStart[int.label] = this.alignTimestamp(now, int.ms);
4629
+ this.pendingDetails[int.label] = [];
4630
+ });
4631
+ }
4632
+ currentIntervalStart = {};
4633
+ pendingDetails = {};
4634
+ eventLoopHistogram = monitorEventLoopDelay({ resolution: 10 });
4635
+ timer = null;
4636
+ recordRequest(duration, isError) {
4637
+ INTERVALS.forEach((int) => {
4638
+ this.pendingDetails[int.label].push({ duration, isError });
4639
+ });
4640
+ }
4641
+ alignTimestamp(ts, intervalMs) {
4642
+ return Math.floor(ts / intervalMs) * intervalMs;
4643
+ }
4644
+ async collect() {
4645
+ try {
4646
+ const now = Date.now();
4647
+ for (const int of INTERVALS) {
4648
+ const start = this.currentIntervalStart[int.label];
4649
+ if (now >= start + int.ms) {
4650
+ await this.flushInterval(int.label, start, int.ms);
4651
+ this.currentIntervalStart[int.label] = this.alignTimestamp(now, int.ms);
4652
+ }
4653
+ }
4654
+ } catch (error) {
4655
+ console.error("[MetricsCollector] Error in collect():", error);
4656
+ }
4657
+ }
4658
+ async flushInterval(label, timestamp, durationMs) {
4659
+ const reqs = this.pendingDetails[label];
4660
+ this.pendingDetails[label] = [];
4661
+ if (reqs.length === 0) {
4662
+ return;
4663
+ }
4664
+ const totalReqs = reqs.length;
4665
+ const errorReqs = reqs.filter((r) => r.isError).length;
4666
+ const successReqs = totalReqs - errorReqs;
4667
+ const duratons = reqs.map((r) => r.duration).sort((a, b) => a - b);
4668
+ const rps = totalReqs / (durationMs / 1e3);
4669
+ const sum = duratons.reduce((a, b) => a + b, 0);
4670
+ const avg = totalReqs > 0 ? sum / totalReqs : 0;
4671
+ const getP = (p) => {
4672
+ if (duratons.length === 0) return 0;
4673
+ const idx = Math.floor(duratons.length * p);
4674
+ return duratons[idx];
4675
+ };
4676
+ const metric = {
4677
+ timestamp,
4678
+ interval: label,
4679
+ cpu: os.loadavg()[0],
4680
+ // Using load avg for simplicity as per requirements (Load)
4681
+ load: os.loadavg(),
4682
+ memory: {
4683
+ used: process.memoryUsage().rss,
4684
+ total: os.totalmem(),
4685
+ heapUsed: process.memoryUsage().heapUsed,
4686
+ heapTotal: process.memoryUsage().heapTotal
4687
+ },
4688
+ eventLoopLatency: {
4689
+ min: this.eventLoopHistogram.min / 1e6,
4690
+ max: this.eventLoopHistogram.max / 1e6,
4691
+ mean: this.eventLoopHistogram.mean / 1e6,
4692
+ p50: this.eventLoopHistogram.percentile(50) / 1e6,
4693
+ p95: this.eventLoopHistogram.percentile(95) / 1e6,
4694
+ p99: this.eventLoopHistogram.percentile(99) / 1e6
4695
+ },
4696
+ requests: {
4697
+ total: totalReqs,
4698
+ rps,
4699
+ success: successReqs,
4700
+ error: errorReqs
4701
+ },
4702
+ responseTime: {
4703
+ min: duratons[0] || 0,
4704
+ max: duratons[duratons.length - 1] || 0,
4705
+ avg,
4706
+ p50: getP(0.5),
4707
+ p95: getP(0.95),
4708
+ p99: getP(0.99)
4709
+ }
4710
+ };
4711
+ try {
4712
+ const recordId = new RecordId("metrics", timestamp);
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 });
4716
+ } catch (e) {
4717
+ console.error(`[MetricsCollector] ✗ Failed to save metrics for ${label}:`, e);
4718
+ }
4719
+ }
4720
+ // Cleanup if needed
4721
+ stop() {
4722
+ if (this.timer) clearInterval(this.timer);
4723
+ this.eventLoopHistogram.disable();
4724
+ }
4725
+ }
4726
+ class Collector {
4727
+ constructor(dashboard) {
4728
+ this.dashboard = dashboard;
4729
+ }
4730
+ currentNode;
4731
+ trackStep(id, type, duration, status, error) {
4732
+ if (!id) return;
4733
+ this.dashboard.recordNodeMetric(id, type, duration, status === "error");
4734
+ }
4735
+ trackEdge(fromId, toId) {
4736
+ if (!fromId || !toId) return;
4737
+ this.dashboard.recordEdgeMetric(fromId, toId);
4738
+ }
4739
+ setNode(id) {
4740
+ this.currentNode = id;
4741
+ }
4742
+ getCurrentNode() {
4743
+ return this.currentNode;
4744
+ }
4745
+ }
4746
+ class Dashboard {
4747
+ constructor(dashboardConfig = {}) {
4748
+ this.dashboardConfig = dashboardConfig;
4749
+ }
4750
+ [$appRoot];
4751
+ router = new ShokupanRouter({ renderer: renderToString });
4752
+ metrics = {
4753
+ totalRequests: 0,
4754
+ successfulRequests: 0,
4755
+ failedRequests: 0,
4756
+ activeRequests: 0,
4757
+ averageTotalTime_ms: 0,
4758
+ recentTimings: [],
4759
+ logs: [],
4760
+ rateLimitedCounts: {},
4761
+ nodeMetrics: {},
4762
+ edgeMetrics: {}
4763
+ };
4764
+ startTime = Date.now();
4765
+ instrumented = false;
4766
+ metricsCollector;
4767
+ get db() {
4768
+ return this[$appRoot].db;
4769
+ }
4770
+ // ShokupanPlugin interface implementation
4771
+ onInit(app, options) {
4772
+ this[$appRoot] = app;
4773
+ this.metricsCollector = new MetricsCollector(this.db);
4774
+ const mountPath = options?.path || this.dashboardConfig.path || "/dashboard";
4775
+ const hooks = this.getHooks();
4776
+ if (!app.middleware) {
4777
+ app.middleware = [];
4778
+ }
4779
+ const hooksMiddleware = async (ctx, next) => {
4780
+ if (hooks.onRequestStart) {
4781
+ await hooks.onRequestStart(ctx);
4782
+ }
4783
+ await next();
4784
+ if (hooks.onResponseEnd) {
4785
+ const effectiveResponse = ctx._finalResponse || ctx.response || {};
4786
+ await hooks.onResponseEnd(ctx, effectiveResponse);
4787
+ }
4788
+ };
4789
+ app.use(hooksMiddleware);
4790
+ app.mount(mountPath, this.router);
4791
+ this.setupRoutes();
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
+ }
4834
+ setupRoutes() {
4835
+ this.router.get("/metrics", async (ctx) => {
4836
+ const uptimeSeconds = Math.floor((Date.now() - this.startTime) / 1e3);
4837
+ const uptime = `${Math.floor(uptimeSeconds / 3600)}h ${Math.floor(uptimeSeconds % 3600 / 60)}m ${uptimeSeconds % 60}s`;
4838
+ const interval = ctx.query["interval"];
4839
+ if (interval) {
4840
+ const intervalMap = {
4841
+ "10s": 10 * 1e3,
4842
+ "1m": 60 * 1e3,
4843
+ "5m": 5 * 60 * 1e3,
4844
+ "30m": 30 * 60 * 1e3,
4845
+ "1h": 60 * 60 * 1e3,
4846
+ "2h": 2 * 60 * 60 * 1e3,
4847
+ "6h": 6 * 60 * 60 * 1e3,
4848
+ "12h": 12 * 60 * 60 * 1e3,
4849
+ "1d": 24 * 60 * 60 * 1e3,
4850
+ "3d": 3 * 24 * 60 * 60 * 1e3,
4851
+ "7d": 7 * 24 * 60 * 60 * 1e3,
4852
+ "30d": 30 * 24 * 60 * 60 * 1e3
4853
+ };
4854
+ const ms = intervalMap[interval] || 60 * 1e3;
4855
+ const startTime = Date.now() - ms;
4856
+ let stats;
4857
+ try {
4858
+ stats = await this.db.query(`
4859
+ SELECT
4860
+ count() as total,
4861
+ count(IF status < 400 THEN 1 END) as success,
4862
+ count(IF status >= 400 THEN 1 END) as failed,
4863
+ math::mean(duration) as avg_latency
4864
+ FROM requests
4865
+ WHERE timestamp >= $start
4866
+ GROUP ALL
4867
+ `, { start: startTime });
4868
+ } catch (error) {
4869
+ console.error("[Dashboard] Query failed at plugin.ts:180-191", {
4870
+ error,
4871
+ interval,
4872
+ startTime,
4873
+ query: "metrics interval stats",
4874
+ stack: new Error().stack
4875
+ });
4876
+ throw error;
4877
+ }
4878
+ const s = stats[0] || { total: 0, success: 0, failed: 0, avg_latency: 0 };
4879
+ return ctx.json({
4880
+ metrics: {
4881
+ totalRequests: s.total || 0,
4882
+ successfulRequests: s.success || 0,
4883
+ failedRequests: s.failed || 0,
4884
+ activeRequests: this.metrics.activeRequests,
4885
+ averageTotalTime_ms: s.avg_latency || 0,
4886
+ recentTimings: this.metrics.recentTimings,
4887
+ logs: [],
4888
+ rateLimitedCounts: this.metrics.rateLimitedCounts,
4889
+ nodeMetrics: this.metrics.nodeMetrics,
4890
+ edgeMetrics: this.metrics.edgeMetrics
4891
+ },
4892
+ uptime
4893
+ });
4894
+ }
4895
+ return ctx.json({
4896
+ metrics: this.metrics,
4897
+ uptime
4898
+ });
4899
+ });
4900
+ this.router.get("/metrics/history", async (ctx) => {
4901
+ const interval = ctx.query["interval"] || "1m";
4902
+ const intervalMap = {
4903
+ "10s": 10 * 1e3,
4904
+ "1m": 60 * 1e3,
4905
+ "5m": 5 * 60 * 1e3,
4906
+ "30m": 30 * 60 * 1e3,
4907
+ "1h": 60 * 60 * 1e3,
4908
+ "2h": 2 * 60 * 60 * 1e3,
4909
+ "6h": 6 * 60 * 60 * 1e3,
4910
+ "12h": 12 * 60 * 60 * 1e3,
4911
+ "1d": 24 * 60 * 60 * 1e3,
4912
+ "3d": 3 * 24 * 60 * 60 * 1e3,
4913
+ "7d": 7 * 24 * 60 * 60 * 1e3,
4914
+ "30d": 30 * 24 * 60 * 60 * 1e3
4915
+ };
4916
+ const periodMs = intervalMap[interval] || 60 * 1e3;
4917
+ const startTime = Date.now() - periodMs * 3;
4918
+ const endTime = Date.now();
4919
+ const result = await this.db.query(
4920
+ "SELECT * FROM metrics WHERE timestamp >= $start AND timestamp <= $end AND interval = $interval ORDER BY timestamp ASC",
4921
+ { start: startTime, end: endTime, interval }
4922
+ );
4923
+ return ctx.json({
4924
+ metrics: result[0] || []
4925
+ });
4926
+ });
4927
+ const getIntervalStartTime = (interval) => {
4928
+ if (!interval) return 0;
4929
+ const intervalMap = {
4930
+ "10s": 10 * 1e3,
4931
+ "1m": 60 * 1e3,
4932
+ "5m": 5 * 60 * 1e3,
4933
+ "30m": 30 * 60 * 1e3,
4934
+ "1h": 60 * 60 * 1e3,
4935
+ "2h": 2 * 60 * 60 * 1e3,
4936
+ "6h": 6 * 60 * 60 * 1e3,
4937
+ "12h": 12 * 60 * 60 * 1e3,
4938
+ "1d": 24 * 60 * 60 * 1e3,
4939
+ "3d": 3 * 24 * 60 * 60 * 1e3,
4940
+ "7d": 7 * 24 * 60 * 60 * 1e3,
4941
+ "30d": 30 * 24 * 60 * 60 * 1e3
4942
+ };
4943
+ const ms = intervalMap[interval] || 0;
4944
+ return ms ? Date.now() - ms : 0;
4945
+ };
4946
+ this.router.get("/requests/top", async (ctx) => {
4947
+ const startTime = getIntervalStartTime(ctx.query["interval"]);
4948
+ const result = await this.db.query(
4949
+ "SELECT method, url, count() as count FROM requests WHERE timestamp >= $start GROUP BY method, url ORDER BY count DESC LIMIT 10",
4950
+ { start: startTime }
4951
+ );
4952
+ return ctx.json({ top: result[0] || [] });
4953
+ });
4954
+ this.router.get("/errors/top", async (ctx) => {
4955
+ const startTime = getIntervalStartTime(ctx.query["interval"]);
4956
+ const result = await this.db.query(
4957
+ "SELECT status, count() as count FROM failed_requests WHERE timestamp >= $start GROUP BY status ORDER BY count DESC LIMIT 10",
4958
+ { start: startTime }
4959
+ );
4960
+ return ctx.json({ top: result[0] || [] });
4961
+ });
4962
+ this.router.get("/requests/failing", async (ctx) => {
4963
+ const startTime = getIntervalStartTime(ctx.query["interval"]);
4964
+ const result = await this.db.query(
4965
+ "SELECT method, url, count() as count FROM failed_requests WHERE timestamp >= $start GROUP BY method, url ORDER BY count DESC LIMIT 10",
4966
+ { start: startTime }
4967
+ );
4968
+ return ctx.json({ top: result[0] || [] });
4969
+ });
4970
+ this.router.get("/requests/slowest", async (ctx) => {
4971
+ const startTime = getIntervalStartTime(ctx.query["interval"]);
4972
+ const result = await this.db.query(
4973
+ "SELECT method, url, duration, status, timestamp FROM requests WHERE timestamp >= $start ORDER BY duration DESC LIMIT 10",
4974
+ { start: startTime }
4975
+ );
4976
+ return ctx.json({ slowest: result[0] || [] });
4977
+ });
4978
+ this.router.get("/registry", (ctx) => {
4979
+ const app = this[$appRoot];
4980
+ if (!this.instrumented && app) {
4981
+ this.instrumentApp(app);
4982
+ }
4983
+ const registry = app?.getComponentRegistry?.();
4984
+ if (registry) {
4985
+ this.assignIdsToRegistry(registry, "root");
4986
+ }
4987
+ return ctx.json({ registry: registry || {} });
4988
+ });
4989
+ this.router.get("/requests", async (ctx) => {
4990
+ const result = await this.db.query("SELECT * FROM requests ORDER BY timestamp DESC LIMIT 100");
4991
+ return ctx.json({ requests: result[0] || [] });
4992
+ });
4993
+ this.router.get("/requests/:id", async (ctx) => {
4994
+ const result = await this.db.query("SELECT * FROM requests WHERE id = $id", { id: ctx.params["id"] });
4995
+ return ctx.json({ request: result[0]?.[0] });
4996
+ });
4997
+ this.router.get("/failures", async (ctx) => {
4998
+ const result = await this.db.query("SELECT * FROM failed_requests ORDER BY timestamp DESC LIMIT 50");
4999
+ return ctx.json({ failures: result[0] });
5000
+ });
5001
+ this.router.post("/replay", async (ctx) => {
5002
+ const body = await ctx.body();
5003
+ const app = this[$appRoot];
5004
+ if (!app) return unknownError(ctx);
5005
+ try {
5006
+ const result = await app.processRequest({
5007
+ method: body.method,
5008
+ path: body.url,
5009
+ // or path
5010
+ headers: body.headers,
5011
+ body: body.body
5012
+ });
5013
+ return ctx.json({
5014
+ status: result.status,
5015
+ headers: result.headers,
5016
+ data: result.data
5017
+ });
5018
+ } catch (e) {
5019
+ return ctx.json({ error: String(e) }, 500);
5020
+ }
5021
+ });
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
+ }
5053
+ const uptimeSeconds = Math.floor((Date.now() - this.startTime) / 1e3);
5054
+ const uptime = `${Math.floor(uptimeSeconds / 3600)}h ${Math.floor(uptimeSeconds % 3600 / 60)}m ${uptimeSeconds % 60}s`;
5055
+ this.getLinkPattern();
5056
+ const integrations = this.detectIntegrations();
5057
+ const getRequestHeadersSource = this.dashboardConfig.getRequestHeaders ? this.dashboardConfig.getRequestHeaders.toString() : "undefined";
5058
+ const html = renderToString(DashboardApp({
5059
+ metrics: this.metrics,
5060
+ uptime,
5061
+ rootPath: process.cwd(),
5062
+ integrations,
5063
+ base: mountPath,
5064
+ getRequestHeadersSource
5065
+ }));
5066
+ return ctx.html(`<!DOCTYPE html>${html}`);
5067
+ });
5068
+ }
5069
+ instrumentApp(app) {
5070
+ if (!app.getComponentRegistry) return;
5071
+ const registry = app.getComponentRegistry();
5072
+ this.assignIdsToRegistry(registry, "root");
5073
+ this.instrumented = true;
5074
+ }
5075
+ // Traverses registry, generates IDs, and attaches them to the actual function objects
5076
+ assignIdsToRegistry(node, parentId) {
5077
+ if (!node) return;
5078
+ const makeId = (type, parent, idx, name) => `${type}_${parent}_${idx}_${name.replace(/[^a-zA-Z0-9]/g, "")}`;
5079
+ node.middleware?.forEach((mw, idx) => {
5080
+ const id = makeId("mw", parentId, idx, mw.name);
5081
+ mw.id = id;
5082
+ if (mw._fn) mw._fn._debugId = id;
5083
+ });
5084
+ node.controllers?.forEach((ctrl, idx) => {
5085
+ const id = makeId("ctrl", parentId, idx, ctrl.name);
5086
+ ctrl.id = id;
5087
+ });
5088
+ node.routes?.forEach((r, idx) => {
5089
+ const id = makeId("route", parentId, idx, r.handlerName || "handler");
5090
+ r.id = id;
5091
+ if (r._fn) r._fn._debugId = id;
5092
+ });
5093
+ node.routers?.forEach((r, idx) => {
5094
+ const id = makeId("router", parentId, idx, r.path);
5095
+ r.id = id;
5096
+ this.assignIdsToRegistry(r.children, id);
5097
+ });
5098
+ }
5099
+ recordNodeMetric(id, type, duration, isError) {
5100
+ if (!this.metrics.nodeMetrics[id]) {
5101
+ this.metrics.nodeMetrics[id] = {
5102
+ id,
5103
+ type,
5104
+ requests: 0,
5105
+ totalTime: 0,
5106
+ failures: 0,
5107
+ name: id
5108
+ // simplify
5109
+ };
5110
+ }
5111
+ const m = this.metrics.nodeMetrics[id];
5112
+ m.requests++;
5113
+ m.totalTime += duration;
5114
+ if (isError) m.failures++;
5115
+ }
5116
+ recordEdgeMetric(from, to) {
5117
+ const key = `${from}|${to}`;
5118
+ this.metrics.edgeMetrics[key] = (this.metrics.edgeMetrics[key] || 0) + 1;
5119
+ }
5120
+ getLinkPattern() {
5121
+ const term = process.env["TERM_PROGRAM"] || "";
5122
+ if (["vscode", "cursor", "antigravity"].some((t) => term.includes(t))) {
5123
+ return "vscode://file/{{absolute}}:{{line}}";
5124
+ }
5125
+ return "file:///{{absolute}}:{{line}}";
5126
+ }
5127
+ getHooks() {
5128
+ return {
5129
+ onRequestStart: (ctx) => {
5130
+ const app = this[$appRoot];
5131
+ if (!this.instrumented && app) {
5132
+ this.instrumentApp(app);
5133
+ }
5134
+ this.metrics.totalRequests++;
5135
+ this.metrics.activeRequests++;
5136
+ ctx._debugStartTime = performance.now();
5137
+ ctx[$debug] = new Collector(this);
5138
+ },
5139
+ onResponseEnd: async (ctx, response) => {
5140
+ this.metrics.activeRequests = Math.max(0, this.metrics.activeRequests - 1);
5141
+ const start = ctx._debugStartTime;
5142
+ let duration = 0;
5143
+ if (start) {
5144
+ duration = performance.now() - start;
5145
+ this.updateTiming(duration);
5146
+ }
5147
+ const isError = response.status >= 400;
5148
+ this.metricsCollector.recordRequest(duration, isError);
5149
+ if (response.status >= 400) {
5150
+ this.metrics.failedRequests++;
5151
+ if (response.status === 429) {
5152
+ const path = ctx.path;
5153
+ this.metrics.rateLimitedCounts[path] = (this.metrics.rateLimitedCounts[path] || 0) + 1;
5154
+ }
5155
+ try {
5156
+ const headers = {};
5157
+ if (ctx.request.headers && typeof ctx.request.headers.forEach === "function") {
5158
+ ctx.request.headers.forEach((v, k) => {
5159
+ headers[k] = v;
5160
+ });
5161
+ }
5162
+ await this.db.upsert(new RecordId("failed_requests", ctx.requestId), {
5163
+ method: ctx.method,
5164
+ url: ctx.url.toString(),
5165
+ headers,
5166
+ status: response.status,
5167
+ timestamp: Date.now(),
5168
+ state: ctx.state
5169
+ // body?
5170
+ });
5171
+ } catch (e) {
5172
+ console.error("Failed to record failed request", e);
5173
+ }
5174
+ } else {
5175
+ this.metrics.successfulRequests++;
5176
+ }
5177
+ const logEntry = {
5178
+ method: ctx.method,
5179
+ url: ctx.url.toString(),
5180
+ status: response.status,
5181
+ duration,
5182
+ timestamp: Date.now(),
5183
+ handlerStack: ctx.handlerStack
5184
+ };
5185
+ this.metrics.logs.push(logEntry);
5186
+ try {
5187
+ await this.db.upsert(new RecordId("requests", ctx.requestId), logEntry);
5188
+ } catch (e) {
5189
+ console.error("Failed to record request log", e);
5190
+ }
5191
+ const retention = this.dashboardConfig.retentionMs ?? 72e5;
5192
+ const cutoff = Date.now() - retention;
5193
+ if (this.metrics.logs.length > 0 && this.metrics.logs[0].timestamp < cutoff) {
5194
+ this.metrics.logs = this.metrics.logs.filter((log) => log.timestamp >= cutoff);
5195
+ }
5196
+ }
5197
+ };
5198
+ }
5199
+ updateTiming(duration) {
5200
+ const alpha = 0.1;
5201
+ if (this.metrics.averageTotalTime_ms === 0) {
5202
+ this.metrics.averageTotalTime_ms = duration;
5203
+ } else {
5204
+ this.metrics.averageTotalTime_ms = alpha * duration + (1 - alpha) * this.metrics.averageTotalTime_ms;
5205
+ }
5206
+ this.metrics.recentTimings.push(duration);
5207
+ if (this.metrics.recentTimings.length > 50) {
5208
+ this.metrics.recentTimings.shift();
5209
+ }
5210
+ }
5211
+ }
5212
+ function unknownError(ctx) {
5213
+ return ctx.json({ error: "Unknown Error" }, 500);
5214
+ }
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
+ }
3295
5289
  class ScalarPlugin extends ShokupanRouter {
3296
5290
  constructor(pluginOptions = {}) {
3297
5291
  pluginOptions.config ??= {};
3298
5292
  super();
3299
5293
  this.pluginOptions = pluginOptions;
3300
- 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);
3301
5303
  }
3302
- onInit(app, options) {
3303
- if (options?.path) {
3304
- app.mount(options.path, this);
3305
- } else {
3306
- app.mount(options.path ?? "/", this);
5304
+ async ensureEta() {
5305
+ if (!this.eta) {
5306
+ const { Eta: Eta2 } = await import("eta");
5307
+ this.eta = new Eta2();
3307
5308
  }
3308
- this.onMount(app);
3309
5309
  }
3310
- init() {
3311
- 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();
3312
5315
  let path = ctx.url.toString();
3313
5316
  if (!path.endsWith("/")) path += "/";
3314
- return ctx.html(eta.renderString(`<!doctype html>
3315
- <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">
3316
5354
  <head>
3317
5355
  <title>API Reference</title>
3318
5356
  <meta charset = "utf-8" />
3319
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>
3320
5378
  </head>
3321
5379
 
3322
5380
  <body>
@@ -3328,9 +5386,10 @@ class ScalarPlugin extends ShokupanRouter {
3328
5386
  }
3329
5387
  ])
3330
5388
  <\/script>
5389
+ <%~ it.devScript %>
3331
5390
  </body>
3332
5391
 
3333
- </html>`, { path, config: this.pluginOptions }));
5392
+ </html>`, { path, config: this.pluginOptions, devScript }));
3334
5393
  });
3335
5394
  this.get("/openapi.json", async (ctx) => {
3336
5395
  let spec;
@@ -3389,23 +5448,23 @@ function Compression(options = {}) {
3389
5448
  return next();
3390
5449
  }
3391
5450
  let response = await next();
3392
- if (!(response instanceof Response) && ctx._finalResponse instanceof Response) {
3393
- response = ctx._finalResponse;
5451
+ if (!(response instanceof Response) && ctx[$finalResponse] instanceof Response) {
5452
+ response = ctx[$finalResponse];
3394
5453
  }
3395
5454
  if (response instanceof Response) {
3396
5455
  if (response.headers.has("Content-Encoding")) return response;
3397
5456
  let body;
3398
5457
  let bodySize;
3399
- if (ctx._rawBody !== void 0) {
3400
- if (typeof ctx._rawBody === "string") {
3401
- const encoded = new TextEncoder().encode(ctx._rawBody);
5458
+ if (ctx[$rawBody] !== void 0) {
5459
+ if (typeof ctx[$rawBody] === "string") {
5460
+ const encoded = new TextEncoder().encode(ctx[$rawBody]);
3402
5461
  body = encoded;
3403
5462
  bodySize = encoded.byteLength;
3404
- } else if (ctx._rawBody instanceof Uint8Array) {
3405
- body = ctx._rawBody;
3406
- bodySize = ctx._rawBody.byteLength;
5463
+ } else if (ctx[$rawBody] instanceof Uint8Array) {
5464
+ body = ctx[$rawBody];
5465
+ bodySize = ctx[$rawBody].byteLength;
3407
5466
  } else {
3408
- body = ctx._rawBody;
5467
+ body = ctx[$rawBody];
3409
5468
  bodySize = body.byteLength;
3410
5469
  }
3411
5470
  } else {
@@ -3533,7 +5592,7 @@ function Cors(options = {}) {
3533
5592
  }
3534
5593
  const response = await next();
3535
5594
  if (response instanceof Response) {
3536
- const headerEntries = Array.from(headers.entries());
5595
+ const headerEntries = Object.entries(headers);
3537
5596
  for (let i = 0; i < headerEntries.length; i++) {
3538
5597
  const [key, value] = headerEntries[i];
3539
5598
  response.headers.set(key, value);
@@ -4271,21 +6330,41 @@ function Session(options) {
4271
6330
  }
4272
6331
  export {
4273
6332
  $appRoot,
6333
+ $bodyParseError,
6334
+ $bodyParsed,
6335
+ $bodyType,
6336
+ $cachedBody,
6337
+ $cachedHost,
6338
+ $cachedHostname,
6339
+ $cachedOrigin,
6340
+ $cachedProtocol,
6341
+ $cachedQuery,
4274
6342
  $childControllers,
4275
6343
  $childRouters,
4276
6344
  $controllerPath,
6345
+ $debug,
4277
6346
  $dispatch,
6347
+ $eventMethods,
6348
+ $finalResponse,
6349
+ $io,
4278
6350
  $isApplication,
4279
6351
  $isMounted,
4280
6352
  $isRouter,
4281
6353
  $middleware,
4282
6354
  $mountPath,
4283
6355
  $parent,
6356
+ $rawBody,
6357
+ $requestId,
4284
6358
  $routeArgs,
6359
+ $routeMatched,
4285
6360
  $routeMethods,
4286
6361
  $routeSpec,
4287
6362
  $routes,
6363
+ $socket,
6364
+ $url,
6365
+ $ws,
4288
6366
  All,
6367
+ AsyncApiPlugin,
4289
6368
  AuthPlugin,
4290
6369
  Body,
4291
6370
  ClusterPlugin,
@@ -4294,8 +6373,11 @@ export {
4294
6373
  Controller,
4295
6374
  Cors,
4296
6375
  Ctx,
6376
+ Dashboard,
4297
6377
  Delete,
6378
+ Event,
4298
6379
  Get,
6380
+ GraphQLApolloPlugin,
4299
6381
  HTTPMethods,
4300
6382
  Head,
4301
6383
  Headers$1 as Headers,