shokupan 0.2.0 → 0.4.4

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 (59) hide show
  1. package/README.md +197 -24
  2. package/dist/benchmarking/advanced-cases/elysia.d.ts +1 -0
  3. package/dist/benchmarking/advanced-cases/express.d.ts +1 -0
  4. package/dist/benchmarking/advanced-cases/fastify.d.ts +1 -0
  5. package/dist/benchmarking/advanced-cases/hapi.d.ts +1 -0
  6. package/dist/benchmarking/advanced-cases/hono.d.ts +1 -0
  7. package/dist/benchmarking/advanced-cases/koa.d.ts +1 -0
  8. package/dist/benchmarking/advanced-cases/nest.d.ts +1 -0
  9. package/dist/benchmarking/advanced-cases/shokupan.d.ts +1 -0
  10. package/dist/benchmarking/advanced-data.d.ts +33 -0
  11. package/dist/benchmarking/advanced-runner.d.ts +1 -0
  12. package/dist/benchmarking/advanced-worker.d.ts +0 -0
  13. package/dist/benchmarking/cases/elysia.d.ts +1 -0
  14. package/dist/benchmarking/cases/express.d.ts +1 -0
  15. package/dist/benchmarking/cases/fastify.d.ts +1 -0
  16. package/dist/benchmarking/cases/hapi.d.ts +1 -0
  17. package/dist/benchmarking/cases/hono.d.ts +1 -0
  18. package/dist/benchmarking/cases/koa.d.ts +1 -0
  19. package/dist/benchmarking/cases/nest.d.ts +1 -0
  20. package/dist/benchmarking/cases/shokupan.d.ts +1 -0
  21. package/dist/benchmarking/data.d.ts +15 -0
  22. package/dist/benchmarking/quick_bench.d.ts +1 -0
  23. package/dist/benchmarking/runner.d.ts +1 -0
  24. package/dist/benchmarking/worker.d.ts +0 -0
  25. package/dist/buntest.d.ts +1 -0
  26. package/dist/cli.cjs +1 -1
  27. package/dist/cli.js +1 -1
  28. package/dist/context.d.ts +17 -7
  29. package/dist/decorators.d.ts +47 -0
  30. package/dist/index.cjs +939 -385
  31. package/dist/index.cjs.map +1 -1
  32. package/dist/index.js +931 -379
  33. package/dist/index.js.map +1 -1
  34. package/dist/{openapi-analyzer-BTExMLX4.js → openapi-analyzer-BtIaHIfe.js} +11 -6
  35. package/dist/openapi-analyzer-BtIaHIfe.js.map +1 -0
  36. package/dist/{openapi-analyzer-BN0wFCML.cjs → openapi-analyzer-D9YB3IkV.cjs} +11 -6
  37. package/dist/openapi-analyzer-D9YB3IkV.cjs.map +1 -0
  38. package/dist/plugins/auth.d.ts +1 -1
  39. package/dist/plugins/debugview/plugin.d.ts +7 -12
  40. package/dist/plugins/failed-request-recorder.d.ts +14 -0
  41. package/dist/plugins/idempotency/plugin.d.ts +14 -0
  42. package/dist/plugins/openapi-validator.d.ts +28 -0
  43. package/dist/plugins/rate-limit.d.ts +3 -1
  44. package/dist/plugins/serve-static.d.ts +2 -3
  45. package/dist/router/trie.d.ts +14 -0
  46. package/dist/router.d.ts +60 -4
  47. package/dist/{server-adapter-CnQFr4P7.js → server-adapter-BWrEJbKL.js} +5 -5
  48. package/dist/server-adapter-BWrEJbKL.js.map +1 -0
  49. package/dist/{server-adapter-BD6oKEto.cjs → server-adapter-fVKP60e0.cjs} +5 -5
  50. package/dist/server-adapter-fVKP60e0.cjs.map +1 -0
  51. package/dist/shokupan.d.ts +14 -3
  52. package/dist/types.d.ts +100 -4
  53. package/dist/util/cpu-monitor.d.ts +11 -0
  54. package/dist/util/stack.d.ts +8 -0
  55. package/package.json +12 -5
  56. package/dist/openapi-analyzer-BN0wFCML.cjs.map +0 -1
  57. package/dist/openapi-analyzer-BTExMLX4.js.map +0 -1
  58. package/dist/server-adapter-BD6oKEto.cjs.map +0 -1
  59. package/dist/server-adapter-CnQFr4P7.js.map +0 -1
package/dist/index.cjs CHANGED
@@ -1,17 +1,22 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
+ const promises = require("node:fs/promises");
3
4
  const eta$2 = require("eta");
4
- const promises = require("fs/promises");
5
+ const promises$1 = require("fs/promises");
5
6
  const path = require("path");
6
7
  const node_async_hooks = require("node:async_hooks");
8
+ const node = require("@surrealdb/node");
9
+ const surrealdb = require("surrealdb");
7
10
  const api = require("@opentelemetry/api");
11
+ const os = require("node:os");
8
12
  const arctic = require("arctic");
9
13
  const jose = require("jose");
14
+ const zlib = require("node:zlib");
10
15
  const Ajv = require("ajv");
11
16
  const addFormats = require("ajv-formats");
12
17
  const classTransformer = require("class-transformer");
13
18
  const classValidator = require("class-validator");
14
- const openapiAnalyzer = require("./openapi-analyzer-BN0wFCML.cjs");
19
+ const openapiAnalyzer = require("./openapi-analyzer-D9YB3IkV.cjs");
15
20
  const crypto = require("crypto");
16
21
  const events = require("events");
17
22
  function _interopNamespaceDefault(e) {
@@ -30,7 +35,9 @@ function _interopNamespaceDefault(e) {
30
35
  n.default = e;
31
36
  return Object.freeze(n);
32
37
  }
38
+ const os__namespace = /* @__PURE__ */ _interopNamespaceDefault(os);
33
39
  const jose__namespace = /* @__PURE__ */ _interopNamespaceDefault(jose);
40
+ const zlib__namespace = /* @__PURE__ */ _interopNamespaceDefault(zlib);
34
41
  class ShokupanResponse {
35
42
  _headers = null;
36
43
  _status = 200;
@@ -95,10 +102,12 @@ class ShokupanResponse {
95
102
  }
96
103
  }
97
104
  class ShokupanContext {
98
- constructor(request, server, state, app, enableMiddlewareTracking = false) {
105
+ // Raw body for compression optimization
106
+ constructor(request, server, state, app, signal, enableMiddlewareTracking = false) {
99
107
  this.request = request;
100
108
  this.server = server;
101
109
  this.app = app;
110
+ this.signal = signal;
102
111
  this.state = state || {};
103
112
  if (enableMiddlewareTracking) {
104
113
  const self = this;
@@ -122,7 +131,9 @@ class ShokupanContext {
122
131
  state;
123
132
  handlerStack = [];
124
133
  response;
134
+ _debug;
125
135
  _finalResponse;
136
+ _rawBody;
126
137
  get url() {
127
138
  if (!this._url) {
128
139
  const urlString = this.request.url || "http://localhost/";
@@ -171,7 +182,17 @@ class ShokupanContext {
171
182
  * Request query params
172
183
  */
173
184
  get query() {
174
- return Object.fromEntries(this.url.searchParams);
185
+ const q = {};
186
+ for (const [key, value] of this.url.searchParams) {
187
+ if (q[key] === void 0) {
188
+ q[key] = value;
189
+ } else if (Array.isArray(q[key])) {
190
+ q[key].push(value);
191
+ } else {
192
+ q[key] = [q[key], value];
193
+ }
194
+ }
195
+ return q;
175
196
  }
176
197
  /**
177
198
  * Client IP address
@@ -246,17 +267,36 @@ class ShokupanContext {
246
267
  setCookie(name, value, options = {}) {
247
268
  let cookie = `${encodeURIComponent(name)}=${encodeURIComponent(value)}`;
248
269
  if (options.maxAge) cookie += `; Max-Age=${Math.floor(options.maxAge)}`;
270
+ if (options.domain) cookie += `; Domain=${options.domain}`;
271
+ if (options.path) cookie += `; Path=${options.path || "/"}`;
249
272
  if (options.expires) cookie += `; Expires=${options.expires.toUTCString()}`;
250
273
  if (options.httpOnly) cookie += `; HttpOnly`;
251
274
  if (options.secure) cookie += `; Secure`;
252
- if (options.domain) cookie += `; Domain=${options.domain}`;
253
- if (options.path) cookie += `; Path=${options.path || "/"}`;
254
- if (options.sameSite) {
255
- typeof options.sameSite === "string" ? options.sameSite.toLowerCase() : options.sameSite ? "strict" : "lax";
256
- cookie += `; SameSite=${typeof options.sameSite === "boolean" ? "Strict" : options.sameSite.charAt(0).toUpperCase() + options.sameSite.slice(1)}`;
275
+ let sameSite = options.sameSite;
276
+ if (sameSite === true) sameSite = "Strict";
277
+ if (sameSite === void 0 || sameSite === false) ;
278
+ else {
279
+ const stringSameSite = typeof sameSite === "string" ? sameSite.toLowerCase() : sameSite;
280
+ switch (stringSameSite) {
281
+ case "lax":
282
+ cookie += "; SameSite=Lax";
283
+ break;
284
+ case "strict":
285
+ cookie += "; SameSite=Strict";
286
+ break;
287
+ case "none":
288
+ cookie += "; SameSite=None";
289
+ break;
290
+ default:
291
+ cookie += "; SameSite=Lax";
292
+ break;
293
+ }
257
294
  }
258
295
  if (options.priority) {
259
- cookie += `; Priority=${options.priority.charAt(0).toUpperCase() + options.priority.slice(1)}`;
296
+ const p = options.priority.toLowerCase();
297
+ if (p === "low") cookie += "; Priority=Low";
298
+ else if (p === "medium") cookie += "; Priority=Medium";
299
+ else if (p === "high") cookie += "; Priority=High";
260
300
  }
261
301
  this.response.append("Set-Cookie", cookie);
262
302
  return this;
@@ -293,6 +333,9 @@ class ShokupanContext {
293
333
  send(body, options) {
294
334
  const headers = this.mergeHeaders(options?.headers);
295
335
  const status = options?.status ?? this.response.status;
336
+ if (typeof body === "string" || body instanceof ArrayBuffer || body instanceof Uint8Array) {
337
+ this._rawBody = body;
338
+ }
296
339
  this._finalResponse = new Response(body, { status, headers });
297
340
  return this._finalResponse;
298
341
  }
@@ -300,11 +343,11 @@ class ShokupanContext {
300
343
  * Read request body
301
344
  */
302
345
  async body() {
303
- const contentType = this.request.headers.get("content-type");
304
- if (contentType?.includes("application/json")) {
346
+ const contentType = this.request.headers.get("content-type") || "";
347
+ if (contentType.includes("application/json") || contentType.includes("+json")) {
305
348
  return this.request.json();
306
349
  }
307
- if (contentType?.includes("multipart/form-data") || contentType?.includes("application/x-www-form-urlencoded")) {
350
+ if (contentType.includes("multipart/form-data") || contentType.includes("application/x-www-form-urlencoded")) {
308
351
  return this.request.formData();
309
352
  }
310
353
  return this.request.text();
@@ -315,6 +358,7 @@ class ShokupanContext {
315
358
  json(data, status, headers) {
316
359
  const finalStatus = status ?? this.response.status;
317
360
  const jsonString = JSON.stringify(data);
361
+ this._rawBody = jsonString;
318
362
  if (!headers && !this.response.hasPopulatedHeaders) {
319
363
  this._finalResponse = new Response(jsonString, {
320
364
  status: finalStatus,
@@ -332,15 +376,16 @@ class ShokupanContext {
332
376
  */
333
377
  text(data, status, headers) {
334
378
  const finalStatus = status ?? this.response.status;
379
+ this._rawBody = data;
335
380
  if (!headers && !this.response.hasPopulatedHeaders) {
336
381
  this._finalResponse = new Response(data, {
337
382
  status: finalStatus,
338
- headers: { "content-type": "text/plain" }
383
+ headers: { "content-type": "text/plain; charset=utf-8" }
339
384
  });
340
385
  return this._finalResponse;
341
386
  }
342
387
  const finalHeaders = this.mergeHeaders(headers);
343
- finalHeaders.set("content-type", "text/plain");
388
+ finalHeaders.set("content-type", "text/plain; charset=utf-8");
344
389
  this._finalResponse = new Response(data, { status: finalStatus, headers: finalHeaders });
345
390
  return this._finalResponse;
346
391
  }
@@ -348,9 +393,10 @@ class ShokupanContext {
348
393
  * Respond with HTML content
349
394
  */
350
395
  html(html, status, headers) {
351
- const finalHeaders = this.mergeHeaders(headers);
352
- finalHeaders.set("content-type", "text/html");
353
396
  const finalStatus = status ?? this.response.status;
397
+ const finalHeaders = this.mergeHeaders(headers);
398
+ finalHeaders.set("content-type", "text/html; charset=utf-8");
399
+ this._rawBody = html;
354
400
  this._finalResponse = new Response(html, { status: finalStatus, headers: finalHeaders });
355
401
  return this._finalResponse;
356
402
  }
@@ -375,11 +421,20 @@ class ShokupanContext {
375
421
  /**
376
422
  * Respond with a file
377
423
  */
378
- file(path2, fileOptions, responseOptions) {
424
+ async file(path2, fileOptions, responseOptions) {
379
425
  const headers = this.mergeHeaders(responseOptions?.headers);
380
426
  const status = responseOptions?.status ?? this.response.status;
381
- this._finalResponse = new Response(Bun.file(path2, fileOptions), { status, headers });
382
- return this._finalResponse;
427
+ if (typeof Bun !== "undefined") {
428
+ this._finalResponse = new Response(Bun.file(path2, fileOptions), { status, headers });
429
+ return this._finalResponse;
430
+ } else {
431
+ const fileBuffer = await promises.readFile(path2);
432
+ if (fileOptions?.type) {
433
+ headers.set("content-type", fileOptions.type);
434
+ }
435
+ this._finalResponse = new Response(fileBuffer, { status, headers });
436
+ return this._finalResponse;
437
+ }
383
438
  }
384
439
  /**
385
440
  * JSX Rendering Function
@@ -399,6 +454,74 @@ class ShokupanContext {
399
454
  return this.html(html, status, headers);
400
455
  }
401
456
  }
457
+ function RateLimitMiddleware(options = {}) {
458
+ const windowMs = options.windowMs || 60 * 1e3;
459
+ const max = options.limit || options.max || 5;
460
+ const message = options.message || "Too many requests, please try again later.";
461
+ const statusCode = options.statusCode || 429;
462
+ const headers = options.headers !== false;
463
+ const mode = options.mode || "user";
464
+ const keyGenerator = options.keyGenerator || ((ctx) => {
465
+ if (mode === "absolute") {
466
+ return "global";
467
+ }
468
+ return ctx.headers.get("x-forwarded-for") || ctx.request.headers.get("x-forwarded-for") || ctx.server?.requestIP?.(ctx.request)?.address || "unknown";
469
+ });
470
+ const skip = options.skip || (() => false);
471
+ const hits = /* @__PURE__ */ new Map();
472
+ const interval = setInterval(() => {
473
+ const now = Date.now();
474
+ for (const [key, record] of hits.entries()) {
475
+ if (record.resetTime <= now) {
476
+ hits.delete(key);
477
+ }
478
+ }
479
+ }, windowMs);
480
+ if (interval.unref) interval.unref();
481
+ const rateLimitMiddleware = async function RateLimitMiddleware2(ctx, next) {
482
+ if (skip(ctx)) return next();
483
+ const key = keyGenerator(ctx);
484
+ const now = Date.now();
485
+ let record = hits.get(key);
486
+ if (!record || record.resetTime <= now) {
487
+ record = {
488
+ hits: 0,
489
+ resetTime: now + windowMs
490
+ };
491
+ hits.set(key, record);
492
+ }
493
+ record.hits++;
494
+ const remaining = Math.max(0, max - record.hits);
495
+ const resetTime = Math.ceil(record.resetTime / 1e3);
496
+ const retryAfter = Math.ceil((record.resetTime - now) / 1e3);
497
+ const setHeaders = (res) => {
498
+ if (!headers || !res || !res.headers) return;
499
+ try {
500
+ res.headers.set("X-RateLimit-Limit", String(max));
501
+ res.headers.set("X-RateLimit-Remaining", String(remaining));
502
+ res.headers.set("X-RateLimit-Reset", String(resetTime));
503
+ } catch (e) {
504
+ }
505
+ };
506
+ if (record.hits > max) {
507
+ typeof message === "object" ? JSON.stringify(message) : String(message);
508
+ const res = typeof message === "object" ? ctx.json(message, statusCode) : ctx.text(String(message), statusCode);
509
+ if (headers) {
510
+ setHeaders(res);
511
+ res.headers.set("Retry-After", String(retryAfter));
512
+ }
513
+ return res;
514
+ }
515
+ const response = await next();
516
+ if (response instanceof Response && headers) {
517
+ setHeaders(response);
518
+ }
519
+ return response;
520
+ };
521
+ rateLimitMiddleware.isBuiltin = true;
522
+ rateLimitMiddleware.pluginName = "RateLimit";
523
+ return rateLimitMiddleware;
524
+ }
402
525
  const $isApplication = /* @__PURE__ */ Symbol.for("Shokupan.app");
403
526
  const $appRoot = /* @__PURE__ */ Symbol.for("Shokupan.app-root");
404
527
  const $isMounted = /* @__PURE__ */ Symbol("Shokupan.isMounted");
@@ -495,6 +618,9 @@ const Patch = createMethodDecorator("PATCH");
495
618
  const Options = createMethodDecorator("OPTIONS");
496
619
  const Head = createMethodDecorator("HEAD");
497
620
  const All = createMethodDecorator("ALL");
621
+ function RateLimit(options) {
622
+ return Use(RateLimitMiddleware(options));
623
+ }
498
624
  class Container {
499
625
  static services = /* @__PURE__ */ new Map();
500
626
  static register(target, instance) {
@@ -536,17 +662,31 @@ const compose = (middleware) => {
536
662
  }
537
663
  return function dispatch(context, next) {
538
664
  let index = -1;
539
- function runner(i) {
665
+ async function runner(i) {
540
666
  if (i <= index) return Promise.reject(new Error("next() called multiple times"));
541
667
  index = i;
542
668
  if (i >= middleware.length) {
543
669
  return next ? next() : Promise.resolve();
544
670
  }
545
671
  const fn = middleware[i];
672
+ if (!context._debug) {
673
+ return fn(context, () => runner(i + 1));
674
+ }
675
+ const debug = context._debug;
676
+ const debugId = fn._debugId || fn.name || "anonymous";
677
+ const previousNode = debug.getCurrentNode();
678
+ debug.trackEdge(previousNode, debugId);
679
+ debug.setNode(debugId);
680
+ const start = performance.now();
546
681
  try {
547
- return Promise.resolve(fn(context, () => runner(i + 1)));
682
+ const res = await Promise.resolve(fn(context, () => runner(i + 1)));
683
+ debug.trackStep(debugId, "middleware", performance.now() - start, "success");
684
+ return res;
548
685
  } catch (err) {
686
+ debug.trackStep(debugId, "middleware", performance.now() - start, "error", err);
549
687
  return Promise.reject(err);
688
+ } finally {
689
+ if (previousNode) debug.setNode(previousNode);
550
690
  }
551
691
  }
552
692
  return runner(0);
@@ -608,6 +748,15 @@ function deepMerge(target, ...sources) {
608
748
  }
609
749
  return deepMerge(target, ...sources);
610
750
  }
751
+ const REGEX_QUERY_INT = /parseInt\(ctx\.query\.(\w+)\)/g;
752
+ const REGEX_QUERY_FLOAT = /parseFloat\(ctx\.query\.(\w+)\)/g;
753
+ const REGEX_QUERY_NUMBER = /Number\(ctx\.query\.(\w+)\)/g;
754
+ const REGEX_QUERY_BOOL = /(?:Boolean\(ctx\.query\.(\w+)\)|!+ctx\.query\.(\w+))/g;
755
+ const REGEX_QUERY_GENERIC = /ctx\.query\.(\w+)/g;
756
+ const REGEX_PARAM_INT = /parseInt\(ctx\.params\.(\w+)\)/g;
757
+ const REGEX_PARAM_FLOAT = /parseFloat\(ctx\.params\.(\w+)\)/g;
758
+ const REGEX_HEADER_GET = /ctx\.get\(['"](\w+)['"]\)/g;
759
+ const REGEX_ERROR_STATUS = /ctx\.(?:json|text|html)\([^)]+,\s*(\d{3,})\)/g;
611
760
  function analyzeHandler(handler) {
612
761
  const handlerSource = handler.toString();
613
762
  const inferredSpec = {};
@@ -617,46 +766,28 @@ function analyzeHandler(handler) {
617
766
  };
618
767
  }
619
768
  const queryParams = /* @__PURE__ */ new Map();
620
- const queryIntMatch = handlerSource.match(/parseInt\(ctx\.query\.(\w+)\)/g);
621
- if (queryIntMatch) {
622
- queryIntMatch.forEach((match) => {
623
- const paramName = match.match(/ctx\.query\.(\w+)/)?.[1];
624
- if (paramName) queryParams.set(paramName, { type: "integer", format: "int32" });
625
- });
769
+ for (const match of handlerSource.matchAll(REGEX_QUERY_INT)) {
770
+ if (match[1]) queryParams.set(match[1], { type: "integer", format: "int32" });
626
771
  }
627
- const queryFloatMatch = handlerSource.match(/parseFloat\(ctx\.query\.(\w+)\)/g);
628
- if (queryFloatMatch) {
629
- queryFloatMatch.forEach((match) => {
630
- const paramName = match.match(/ctx\.query\.(\w+)/)?.[1];
631
- if (paramName) queryParams.set(paramName, { type: "number", format: "float" });
632
- });
772
+ for (const match of handlerSource.matchAll(REGEX_QUERY_FLOAT)) {
773
+ if (match[1]) queryParams.set(match[1], { type: "number", format: "float" });
633
774
  }
634
- const queryNumberMatch = handlerSource.match(/Number\(ctx\.query\.(\w+)\)/g);
635
- if (queryNumberMatch) {
636
- queryNumberMatch.forEach((match) => {
637
- const paramName = match.match(/ctx\.query\.(\w+)/)?.[1];
638
- if (paramName && !queryParams.has(paramName)) {
639
- queryParams.set(paramName, { type: "number" });
640
- }
641
- });
775
+ for (const match of handlerSource.matchAll(REGEX_QUERY_NUMBER)) {
776
+ if (match[1] && !queryParams.has(match[1])) {
777
+ queryParams.set(match[1], { type: "number" });
778
+ }
642
779
  }
643
- const queryBoolMatch = handlerSource.match(/(?:Boolean\(ctx\.query\.(\w+)\)|!+ctx\.query\.(\w+))/g);
644
- if (queryBoolMatch) {
645
- queryBoolMatch.forEach((match) => {
646
- const paramName = match.match(/ctx\.query\.(\w+)/)?.[1];
647
- if (paramName && !queryParams.has(paramName)) {
648
- queryParams.set(paramName, { type: "boolean" });
649
- }
650
- });
780
+ for (const match of handlerSource.matchAll(REGEX_QUERY_BOOL)) {
781
+ const name = match[1] || match[2];
782
+ if (name && !queryParams.has(name)) {
783
+ queryParams.set(name, { type: "boolean" });
784
+ }
651
785
  }
652
- const queryMatch = handlerSource.match(/ctx\.query\.(\w+)/g);
653
- if (queryMatch) {
654
- queryMatch.forEach((match) => {
655
- const paramName = match.split(".")[2];
656
- if (paramName && !queryParams.has(paramName)) {
657
- queryParams.set(paramName, { type: "string" });
658
- }
659
- });
786
+ for (const match of handlerSource.matchAll(REGEX_QUERY_GENERIC)) {
787
+ const name = match[1];
788
+ if (name && !queryParams.has(name)) {
789
+ queryParams.set(name, { type: "string" });
790
+ }
660
791
  }
661
792
  if (queryParams.size > 0) {
662
793
  if (!inferredSpec.parameters) inferredSpec.parameters = [];
@@ -669,19 +800,11 @@ function analyzeHandler(handler) {
669
800
  });
670
801
  }
671
802
  const pathParams = /* @__PURE__ */ new Map();
672
- const paramIntMatch = handlerSource.match(/parseInt\(ctx\.params\.(\w+)\)/g);
673
- if (paramIntMatch) {
674
- paramIntMatch.forEach((match) => {
675
- const paramName = match.match(/ctx\.params\.(\w+)/)?.[1];
676
- if (paramName) pathParams.set(paramName, { type: "integer", format: "int32" });
677
- });
803
+ for (const match of handlerSource.matchAll(REGEX_PARAM_INT)) {
804
+ if (match[1]) pathParams.set(match[1], { type: "integer", format: "int32" });
678
805
  }
679
- const paramFloatMatch = handlerSource.match(/parseFloat\(ctx\.params\.(\w+)\)/g);
680
- if (paramFloatMatch) {
681
- paramFloatMatch.forEach((match) => {
682
- const paramName = match.match(/ctx\.params\.(\w+)/)?.[1];
683
- if (paramName) pathParams.set(paramName, { type: "number", format: "float" });
684
- });
806
+ for (const match of handlerSource.matchAll(REGEX_PARAM_FLOAT)) {
807
+ if (match[1]) pathParams.set(match[1], { type: "number", format: "float" });
685
808
  }
686
809
  if (pathParams.size > 0) {
687
810
  if (!inferredSpec.parameters) inferredSpec.parameters = [];
@@ -694,76 +817,55 @@ function analyzeHandler(handler) {
694
817
  });
695
818
  });
696
819
  }
697
- const headerMatch = handlerSource.match(/ctx\.get\(['"](\w+)['"]\)/g);
698
- if (headerMatch) {
699
- if (!inferredSpec.parameters) inferredSpec.parameters = [];
700
- headerMatch.forEach((match) => {
701
- const headerName = match.match(/['"](\w+)['"]/)?.[1];
702
- if (headerName) {
703
- inferredSpec.parameters.push({
704
- name: headerName,
705
- in: "header",
706
- schema: { type: "string" }
707
- });
708
- }
709
- });
820
+ for (const match of handlerSource.matchAll(REGEX_HEADER_GET)) {
821
+ if (match[1]) {
822
+ if (!inferredSpec.parameters) inferredSpec.parameters = [];
823
+ inferredSpec.parameters.push({
824
+ name: match[1],
825
+ in: "header",
826
+ schema: { type: "string" }
827
+ });
828
+ }
710
829
  }
711
830
  const responses = {};
712
831
  if (handlerSource.includes("ctx.json(")) {
713
832
  responses["200"] = {
714
833
  description: "Successful response",
715
- content: {
716
- "application/json": { schema: { type: "object" } }
717
- }
834
+ content: { "application/json": { schema: { type: "object" } } }
718
835
  };
719
836
  }
720
837
  if (handlerSource.includes("ctx.html(")) {
721
838
  responses["200"] = {
722
839
  description: "Successful response",
723
- content: {
724
- "text/html": { schema: { type: "string" } }
725
- }
840
+ content: { "text/html": { schema: { type: "string" } } }
726
841
  };
727
842
  }
728
843
  if (handlerSource.includes("ctx.text(")) {
729
844
  responses["200"] = {
730
845
  description: "Successful response",
731
- content: {
732
- "text/plain": { schema: { type: "string" } }
733
- }
846
+ content: { "text/plain": { schema: { type: "string" } } }
734
847
  };
735
848
  }
736
849
  if (handlerSource.includes("ctx.file(")) {
737
850
  responses["200"] = {
738
851
  description: "File download",
739
- content: {
740
- "application/octet-stream": { schema: { type: "string", format: "binary" } }
741
- }
852
+ content: { "application/octet-stream": { schema: { type: "string", format: "binary" } } }
742
853
  };
743
854
  }
744
855
  if (handlerSource.includes("ctx.redirect(")) {
745
- responses["302"] = {
746
- description: "Redirect"
747
- };
856
+ responses["302"] = { description: "Redirect" };
748
857
  }
749
858
  if (!responses["200"] && /return\s+\{/.test(handlerSource)) {
750
859
  responses["200"] = {
751
860
  description: "Successful response",
752
- content: {
753
- "application/json": { schema: { type: "object" } }
754
- }
861
+ content: { "application/json": { schema: { type: "object" } } }
755
862
  };
756
863
  }
757
- const errorStatusMatch = handlerSource.match(/ctx\.(?:json|text|html)\([^)]+,\s*(\d{3,})\)/g);
758
- if (errorStatusMatch) {
759
- errorStatusMatch.forEach((match) => {
760
- const statusCode = match.match(/,\s*(\d{3,})\)/)?.[1];
761
- if (statusCode && statusCode !== "200") {
762
- responses[statusCode] = {
763
- description: `Error response (${statusCode})`
764
- };
765
- }
766
- });
864
+ for (const match of handlerSource.matchAll(REGEX_ERROR_STATUS)) {
865
+ const statusCode = match[1];
866
+ if (statusCode && statusCode !== "200") {
867
+ responses[statusCode] = { description: `Error response (${statusCode})` };
868
+ }
767
869
  }
768
870
  if (Object.keys(responses).length > 0) {
769
871
  inferredSpec.responses = responses;
@@ -777,7 +879,7 @@ async function generateOpenApi(rootRouter, options = {}) {
777
879
  const defaultTagName = options.defaultTag || "Application";
778
880
  let astRoutes = [];
779
881
  try {
780
- const { OpenAPIAnalyzer } = await Promise.resolve().then(() => require("./openapi-analyzer-BN0wFCML.cjs"));
882
+ const { OpenAPIAnalyzer } = await Promise.resolve().then(() => require("./openapi-analyzer-D9YB3IkV.cjs"));
781
883
  const analyzer = new OpenAPIAnalyzer(process.cwd());
782
884
  const { applications } = await analyzer.analyze();
783
885
  const appMap = /* @__PURE__ */ new Map();
@@ -1047,10 +1149,10 @@ async function generateOpenApi(rootRouter, options = {}) {
1047
1149
  };
1048
1150
  }
1049
1151
  const eta$1 = new eta$2.Eta();
1050
- function serveStatic(ctx, config, prefix) {
1152
+ function serveStatic(config, prefix) {
1051
1153
  const rootPath = path.resolve(config.root || ".");
1052
1154
  const normalizedPrefix = prefix.endsWith("/") && prefix !== "/" ? prefix.slice(0, -1) : prefix;
1053
- return async () => {
1155
+ const serveStaticMiddleware = async (ctx) => {
1054
1156
  let relative = ctx.path.slice(normalizedPrefix.length);
1055
1157
  if (!relative.startsWith("/") && relative.length > 0) relative = "/" + relative;
1056
1158
  if (relative.length === 0) relative = "/";
@@ -1083,13 +1185,13 @@ function serveStatic(ctx, config, prefix) {
1083
1185
  let finalPath = requestPath;
1084
1186
  let stats;
1085
1187
  try {
1086
- stats = await promises.stat(requestPath);
1188
+ stats = await promises$1.stat(requestPath);
1087
1189
  } catch (e) {
1088
1190
  if (config.extensions) {
1089
1191
  for (const ext of config.extensions) {
1090
1192
  const p = requestPath + (ext.startsWith(".") ? ext : "." + ext);
1091
1193
  try {
1092
- const s = await promises.stat(p);
1194
+ const s = await promises$1.stat(p);
1093
1195
  if (s.isFile()) {
1094
1196
  finalPath = p;
1095
1197
  stats = s;
@@ -1118,7 +1220,7 @@ function serveStatic(ctx, config, prefix) {
1118
1220
  for (const idx of indexes) {
1119
1221
  const idxPath = path.join(finalPath, idx);
1120
1222
  try {
1121
- const idxStats = await promises.stat(idxPath);
1223
+ const idxStats = await promises$1.stat(idxPath);
1122
1224
  if (idxStats.isFile()) {
1123
1225
  finalPath = idxPath;
1124
1226
  foundIndex = true;
@@ -1130,7 +1232,7 @@ function serveStatic(ctx, config, prefix) {
1130
1232
  if (!foundIndex) {
1131
1233
  if (config.listDirectory) {
1132
1234
  try {
1133
- const files = await promises.readdir(requestPath);
1235
+ const files = await promises$1.readdir(requestPath);
1134
1236
  const listing = eta$1.renderString(`
1135
1237
  <!DOCTYPE html>
1136
1238
  <html>
@@ -1167,16 +1269,158 @@ function serveStatic(ctx, config, prefix) {
1167
1269
  }
1168
1270
  }
1169
1271
  }
1170
- const file = Bun.file(finalPath);
1171
- let response = new Response(file);
1272
+ let response;
1273
+ if (typeof Bun !== "undefined") {
1274
+ response = new Response(Bun.file(finalPath));
1275
+ } else {
1276
+ const fileBuffer = await promises$1.readFile(finalPath);
1277
+ response = new Response(fileBuffer);
1278
+ }
1172
1279
  if (config.hooks?.onResponse) {
1173
1280
  const hooked = await config.hooks.onResponse(ctx, response);
1174
1281
  if (hooked) response = hooked;
1175
1282
  }
1176
1283
  return response;
1177
1284
  };
1285
+ serveStaticMiddleware.isBuiltin = true;
1286
+ serveStaticMiddleware.pluginName = "ServeStatic";
1287
+ return serveStaticMiddleware;
1288
+ }
1289
+ class RouterTrie {
1290
+ root;
1291
+ constructor() {
1292
+ this.root = this.createNode();
1293
+ }
1294
+ createNode() {
1295
+ return {
1296
+ children: {}
1297
+ };
1298
+ }
1299
+ insert(method, path2, handler) {
1300
+ let node2 = this.root;
1301
+ const segments = this.splitPath(path2);
1302
+ for (const segment of segments) {
1303
+ if (segment === "**") {
1304
+ if (!node2.recursiveChild) {
1305
+ node2.recursiveChild = this.createNode();
1306
+ }
1307
+ node2 = node2.recursiveChild;
1308
+ } else if (segment === "*") {
1309
+ if (!node2.wildcardChild) {
1310
+ node2.wildcardChild = this.createNode();
1311
+ }
1312
+ node2 = node2.wildcardChild;
1313
+ } else if (segment.startsWith(":")) {
1314
+ const paramName = segment.slice(1);
1315
+ if (!node2.paramChild) {
1316
+ node2.paramChild = this.createNode();
1317
+ node2.paramChild.paramName = paramName;
1318
+ }
1319
+ node2 = node2.paramChild;
1320
+ node2.paramName = paramName;
1321
+ } else {
1322
+ if (!node2.children[segment]) {
1323
+ node2.children[segment] = this.createNode();
1324
+ }
1325
+ node2 = node2.children[segment];
1326
+ }
1327
+ }
1328
+ if (!node2.handlers) {
1329
+ node2.handlers = {};
1330
+ }
1331
+ node2.handlers[method] = handler;
1332
+ }
1333
+ search(method, path2) {
1334
+ const segments = this.splitPath(path2);
1335
+ const params = {};
1336
+ const match = this.findNode(this.root, segments, 0, params);
1337
+ if (match && match.handlers) {
1338
+ const handler = match.handlers[method] || match.handlers["ALL"];
1339
+ if (handler) {
1340
+ return { handler, params };
1341
+ }
1342
+ if (method === "HEAD" && match.handlers["GET"]) {
1343
+ return { handler: match.handlers["GET"], params };
1344
+ }
1345
+ }
1346
+ return null;
1347
+ }
1348
+ findNode(node2, segments, index, params) {
1349
+ if (index === segments.length) {
1350
+ if (node2.handlers) return node2;
1351
+ if (node2.recursiveChild && node2.recursiveChild.handlers) {
1352
+ return node2.recursiveChild;
1353
+ }
1354
+ return null;
1355
+ }
1356
+ const segment = segments[index];
1357
+ const child = node2.children[segment];
1358
+ if (child) {
1359
+ const result = this.findNode(child, segments, index + 1, params);
1360
+ if (result) return result;
1361
+ }
1362
+ if (node2.paramChild) {
1363
+ params[node2.paramChild.paramName] = segment;
1364
+ const result = this.findNode(node2.paramChild, segments, index + 1, params);
1365
+ if (result) return result;
1366
+ delete params[node2.paramChild.paramName];
1367
+ }
1368
+ if (node2.wildcardChild) {
1369
+ const result = this.findNode(node2.wildcardChild, segments, index + 1, params);
1370
+ if (result) return result;
1371
+ }
1372
+ if (node2.recursiveChild) {
1373
+ const remaining = segments.length - index;
1374
+ for (let k = 0; k <= remaining; k++) {
1375
+ const result = this.findNode(node2.recursiveChild, segments, index + k, params);
1376
+ if (result) return result;
1377
+ }
1378
+ }
1379
+ return null;
1380
+ }
1381
+ splitPath(path2) {
1382
+ if (path2 === "/" || path2 === "") return [];
1383
+ const s = path2.startsWith("/") ? path2.slice(1) : path2;
1384
+ if (s === "") return [];
1385
+ return s.split("/");
1386
+ }
1178
1387
  }
1179
1388
  const asyncContext = new node_async_hooks.AsyncLocalStorage();
1389
+ const engine = process.env["SHOKUPAN_DB_ENGINE"] === "memory" ? "mem://" : "rocksdb://database";
1390
+ const db = new surrealdb.Surreal({
1391
+ engines: node.createNodeEngines()
1392
+ });
1393
+ const ready = db.connect(engine, { namespace: "vendor", database: "shokupan" }).then(() => {
1394
+ return db.query(`
1395
+ DEFINE TABLE OVERWRITE failed_requests SCHEMALESS COMMENT "Created by Shokupan";
1396
+ DEFINE TABLE OVERWRITE sessions SCHEMALESS COMMENT "Created by Shokupan";
1397
+ DEFINE TABLE OVERWRITE users SCHEMALESS COMMENT "Created by Shokupan";
1398
+ DEFINE TABLE OVERWRITE idempotency_keys SCHEMALESS COMMENT "Created by Shokupan";
1399
+ DEFINE TABLE OVERWRITE middleware_tracking SCHEMALESS COMMENT "Created by Shokupan";
1400
+ DEFINE TABLE OVERWRITE requests SCHEMALESS COMMENT "Created by Shokupan";
1401
+ `);
1402
+ });
1403
+ const datastore = {
1404
+ get(store, key) {
1405
+ return db.select(new surrealdb.RecordId(store, key));
1406
+ },
1407
+ set(store, key, value) {
1408
+ return db.create(new surrealdb.RecordId(store, key)).content(value);
1409
+ },
1410
+ async query(query, vars) {
1411
+ try {
1412
+ const r = await db.query(query, vars).collect();
1413
+ return r;
1414
+ } catch (e) {
1415
+ console.error("DS ERROR:", e);
1416
+ throw e;
1417
+ }
1418
+ },
1419
+ ready
1420
+ };
1421
+ process.on("exit", async () => {
1422
+ await db.close();
1423
+ });
1180
1424
  const tracer = api.trace.getTracer("shokupan.middleware");
1181
1425
  function traceHandler(fn, name) {
1182
1426
  return async function(...args) {
@@ -1200,6 +1444,35 @@ function traceHandler(fn, name) {
1200
1444
  });
1201
1445
  };
1202
1446
  }
1447
+ function getCallerInfo(skipFrames = 1) {
1448
+ let file = "unknown";
1449
+ let line = 0;
1450
+ try {
1451
+ const err = new Error();
1452
+ const stack = err.stack?.split("\n") || [];
1453
+ let found = 0;
1454
+ for (let i = 1; i < stack.length; i++) {
1455
+ const l = stack[i];
1456
+ if (!l.includes(":")) continue;
1457
+ if (l.includes("node_modules")) continue;
1458
+ if (l.includes("bun:main")) continue;
1459
+ if (l.includes("src/util/stack.ts")) continue;
1460
+ if (l.includes("src/router.ts")) continue;
1461
+ if (l.includes("src/shokupan.ts")) continue;
1462
+ found++;
1463
+ if (found >= skipFrames) {
1464
+ const match = l.match(/\((.*):(\d+):(\d+)\)/) || l.match(/at (.*):(\d+):(\d+)/);
1465
+ if (match) {
1466
+ file = match[1];
1467
+ line = parseInt(match[2], 10);
1468
+ return { file, line };
1469
+ }
1470
+ }
1471
+ }
1472
+ } catch (e) {
1473
+ }
1474
+ return { file, line };
1475
+ }
1203
1476
  const RouterRegistry = /* @__PURE__ */ new Map();
1204
1477
  const ShokupanApplicationTree = {};
1205
1478
  class ShokupanRouter {
@@ -1219,6 +1492,7 @@ class ShokupanRouter {
1219
1492
  [$parent] = null;
1220
1493
  [$childRouters] = [];
1221
1494
  [$childControllers] = [];
1495
+ middleware = [];
1222
1496
  get rootConfig() {
1223
1497
  return this[$appRoot]?.applicationConfig;
1224
1498
  }
@@ -1227,7 +1501,66 @@ class ShokupanRouter {
1227
1501
  }
1228
1502
  [$routes] = [];
1229
1503
  // Public via Symbol for OpenAPI generator
1504
+ trie = new RouterTrie();
1505
+ metadata;
1506
+ // Metadata for the router itself
1230
1507
  currentGuards = [];
1508
+ // Registry Accessor
1509
+ getComponentRegistry() {
1510
+ const controllerRoutesMap = /* @__PURE__ */ new Map();
1511
+ const localRoutes = [];
1512
+ for (const r of this[$routes]) {
1513
+ const entry = {
1514
+ type: "route",
1515
+ path: r.path,
1516
+ method: r.method,
1517
+ metadata: r.metadata,
1518
+ handlerName: r.handler.name,
1519
+ tags: r.handlerSpec?.tags,
1520
+ order: r.order,
1521
+ _fn: r.handler
1522
+ };
1523
+ if (r.controller) {
1524
+ if (!controllerRoutesMap.has(r.controller)) {
1525
+ controllerRoutesMap.set(r.controller, []);
1526
+ }
1527
+ controllerRoutesMap.get(r.controller).push(entry);
1528
+ } else {
1529
+ localRoutes.push(entry);
1530
+ }
1531
+ }
1532
+ const mw = this.middleware;
1533
+ const middleware = mw ? mw.map((m) => ({
1534
+ name: m.name || "middleware",
1535
+ metadata: m.metadata,
1536
+ order: m.order,
1537
+ _fn: m
1538
+ // Expose function for debugging instrumentation
1539
+ })) : [];
1540
+ const routers = this[$childRouters].map((r) => ({
1541
+ type: "router",
1542
+ path: r[$mountPath],
1543
+ metadata: r.metadata,
1544
+ children: r.getComponentRegistry()
1545
+ }));
1546
+ const controllers = this[$childControllers].map((c) => {
1547
+ const routes = controllerRoutesMap.get(c) || [];
1548
+ return {
1549
+ type: "controller",
1550
+ path: c[$mountPath] || "/",
1551
+ name: c.constructor.name,
1552
+ metadata: c.metadata,
1553
+ children: { routes }
1554
+ };
1555
+ });
1556
+ return {
1557
+ metadata: this.metadata,
1558
+ middleware,
1559
+ routes: localRoutes,
1560
+ routers,
1561
+ controllers
1562
+ };
1563
+ }
1231
1564
  isRouterInstance(target) {
1232
1565
  return typeof target === "object" && target !== null && $isRouter in target;
1233
1566
  }
@@ -1253,6 +1586,14 @@ class ShokupanRouter {
1253
1586
  throw new Error("Router is already mounted");
1254
1587
  }
1255
1588
  controller[$mountPath] = prefix;
1589
+ if (!controller.metadata) {
1590
+ const info = getCallerInfo();
1591
+ controller.metadata = {
1592
+ file: info.file,
1593
+ line: info.line,
1594
+ name: "MountedRouter"
1595
+ };
1596
+ }
1256
1597
  this[$childRouters].push(controller);
1257
1598
  controller[$parent] = this;
1258
1599
  const setRouterContext = (router) => {
@@ -1285,6 +1626,12 @@ class ShokupanRouter {
1285
1626
  }
1286
1627
  }
1287
1628
  instance[$mountPath] = prefix;
1629
+ const info = getCallerInfo();
1630
+ instance.metadata = {
1631
+ file: info.file,
1632
+ line: info.line,
1633
+ name: instance.constructor.name
1634
+ };
1288
1635
  this[$childControllers].push(instance);
1289
1636
  const controllerMiddleware = (typeof controller === "function" ? controller[$middleware] : instance[$middleware]) || [];
1290
1637
  const proto = Object.getPrototypeOf(instance);
@@ -1299,7 +1646,7 @@ class ShokupanRouter {
1299
1646
  const decoratedArgs = instance[$routeArgs] || proto && proto[$routeArgs];
1300
1647
  const methodMiddlewareMap = instance[$middleware] || proto && proto[$middleware];
1301
1648
  let routesAttached = 0;
1302
- for (const name of methods) {
1649
+ for (const name of Array.from(methods)) {
1303
1650
  if (name === "constructor") continue;
1304
1651
  if (["arguments", "caller", "callee"].includes(name)) continue;
1305
1652
  const originalHandler = instance[name];
@@ -1368,14 +1715,39 @@ class ShokupanRouter {
1368
1715
  for (const arg of sortedArgs) {
1369
1716
  switch (arg.type) {
1370
1717
  case RouteParamType.BODY:
1371
- args[arg.index] = await ctx.req.json().catch(() => ({}));
1718
+ try {
1719
+ if (ctx.req.headers.get("content-type")?.includes("application/json")) {
1720
+ args[arg.index] = await ctx.req.json();
1721
+ } else {
1722
+ const text = await ctx.req.text();
1723
+ if (!text) {
1724
+ args[arg.index] = {};
1725
+ } else {
1726
+ args[arg.index] = JSON.parse(text);
1727
+ }
1728
+ }
1729
+ } catch (e) {
1730
+ const err = new Error("Invalid JSON body");
1731
+ err.status = 400;
1732
+ throw err;
1733
+ }
1372
1734
  break;
1373
1735
  case RouteParamType.PARAM:
1374
1736
  args[arg.index] = arg.name ? ctx.params[arg.name] : ctx.params;
1375
1737
  break;
1376
1738
  case RouteParamType.QUERY: {
1377
1739
  const url = new URL(ctx.req.url);
1378
- args[arg.index] = arg.name ? url.searchParams.get(arg.name) : Object.fromEntries(url.searchParams);
1740
+ if (arg.name) {
1741
+ const vals = url.searchParams.getAll(arg.name);
1742
+ args[arg.index] = vals.length > 1 ? vals : vals[0];
1743
+ } else {
1744
+ const query = {};
1745
+ for (const key of url.searchParams.keys()) {
1746
+ const vals = url.searchParams.getAll(key);
1747
+ query[key] = vals.length > 1 ? vals : vals[0];
1748
+ }
1749
+ args[arg.index] = query;
1750
+ }
1379
1751
  break;
1380
1752
  }
1381
1753
  case RouteParamType.HEADER:
@@ -1408,7 +1780,7 @@ class ShokupanRouter {
1408
1780
  const decoratedSpecs = instance[$routeSpec] || proto && proto[$routeSpec];
1409
1781
  const userSpec = decoratedSpecs && decoratedSpecs.get(name);
1410
1782
  const spec = { tags: [tagName], ...userSpec };
1411
- this.add({ method, path: normalizedPath, handler: finalHandler, spec });
1783
+ this.add({ method, path: normalizedPath, handler: finalHandler, spec, controller: instance });
1412
1784
  }
1413
1785
  }
1414
1786
  if (routesAttached === 0) {
@@ -1523,30 +1895,59 @@ class ShokupanRouter {
1523
1895
  data: result
1524
1896
  };
1525
1897
  }
1526
- applyHooks(match) {
1898
+ applyRouterHooks(match) {
1527
1899
  if (!this.config?.hooks) return match;
1528
1900
  const hooks = this.config.hooks;
1529
- if (!hooks.onRequestStart && !hooks.onRequestEnd && !hooks.onError) return match;
1530
- const originalHandler = match.handler;
1531
- match.handler = async (ctx) => {
1532
- if (hooks.onRequestStart) await hooks.onRequestStart(ctx);
1901
+ return {
1902
+ ...match,
1903
+ handler: this.wrapWithHooks(match.handler, hooks)
1904
+ };
1905
+ }
1906
+ wrapWithHooks(handler, hooks) {
1907
+ const hookList = Array.isArray(hooks) ? hooks : [hooks];
1908
+ const hasStart = hookList.some((h) => !!h.onRequestStart);
1909
+ const hasEnd = hookList.some((h) => !!h.onRequestEnd);
1910
+ const hasError = hookList.some((h) => !!h.onError);
1911
+ if (!hasStart && !hasEnd && !hasError) return handler;
1912
+ const originalHandler = handler;
1913
+ const wrapped = async (ctx) => {
1914
+ if (hasStart) {
1915
+ for (let i = 0; i < hookList.length; i++) {
1916
+ const h = hookList[i];
1917
+ if (typeof h.onRequestStart === "function") await h.onRequestStart(ctx);
1918
+ }
1919
+ }
1920
+ const debug = ctx._debug;
1921
+ let debugId;
1922
+ let previousNode;
1923
+ if (debug) {
1924
+ debugId = originalHandler._debugId || originalHandler.name || "handler";
1925
+ previousNode = debug.getCurrentNode();
1926
+ debug.trackEdge(previousNode, debugId);
1927
+ debug.setNode(debugId);
1928
+ }
1929
+ const start = performance.now();
1533
1930
  try {
1534
- const result = await originalHandler(ctx);
1535
- if (hooks.onRequestEnd) await hooks.onRequestEnd(ctx);
1536
- return result;
1931
+ const res = await originalHandler(ctx);
1932
+ debug?.trackStep(debugId, "handler", performance.now() - start, "success");
1933
+ for (let i = 0; i < hookList.length; i++) {
1934
+ const h = hookList[i];
1935
+ if (typeof h.onRequestEnd === "function") await h.onRequestEnd(ctx);
1936
+ }
1937
+ return res;
1537
1938
  } catch (err) {
1538
- if (hooks.onError) {
1539
- try {
1540
- await hooks.onError(err, ctx);
1541
- } catch (e) {
1542
- console.error("Error in router onError hook:", e);
1543
- }
1939
+ debug?.trackStep(debugId, "handler", performance.now() - start, "error", err);
1940
+ for (let i = 0; i < hookList.length; i++) {
1941
+ const h = hookList[i];
1942
+ if (typeof h.onError === "function") await h.onError(err, ctx);
1544
1943
  }
1545
1944
  throw err;
1945
+ } finally {
1946
+ if (debug && previousNode) debug.setNode(previousNode);
1546
1947
  }
1547
1948
  };
1548
- match.handler.originalHandler = originalHandler.originalHandler || originalHandler;
1549
- return match;
1949
+ wrapped.originalHandler = originalHandler.originalHandler ?? originalHandler;
1950
+ return wrapped;
1550
1951
  }
1551
1952
  /**
1552
1953
  * Find a route matching the given method and path.
@@ -1555,24 +1956,10 @@ class ShokupanRouter {
1555
1956
  * @returns Route handler and parameters if found, otherwise null
1556
1957
  */
1557
1958
  find(method, path2) {
1558
- const findInRoutes = (routes, m) => {
1559
- for (const route of routes) {
1560
- if (route.method !== "ALL" && route.method !== m) continue;
1561
- const match = route.regex.exec(path2);
1562
- if (match) {
1563
- const params = {};
1564
- route.keys.forEach((key, index) => {
1565
- params[key] = match[index + 1];
1566
- });
1567
- return this.applyHooks({ handler: route.handler, params });
1568
- }
1569
- }
1570
- return null;
1571
- };
1572
- let result = findInRoutes(this[$routes], method);
1959
+ let result = this.trie.search(method, path2);
1573
1960
  if (result) return result;
1574
1961
  if (method === "HEAD") {
1575
- result = findInRoutes(this[$routes], "GET");
1962
+ result = this.trie.search("GET", path2);
1576
1963
  if (result) return result;
1577
1964
  }
1578
1965
  for (const child of this[$childRouters]) {
@@ -1580,13 +1967,13 @@ class ShokupanRouter {
1580
1967
  if (path2 === prefix || path2.startsWith(prefix + "/")) {
1581
1968
  const subPath = path2.slice(prefix.length) || "/";
1582
1969
  const match = child.find(method, subPath);
1583
- if (match) return this.applyHooks(match);
1970
+ if (match) return this.applyRouterHooks(match);
1584
1971
  }
1585
1972
  if (prefix.endsWith("/")) {
1586
1973
  if (path2.startsWith(prefix)) {
1587
1974
  const subPath = path2.slice(prefix.length) || "/";
1588
1975
  const match = child.find(method, subPath);
1589
- if (match) return this.applyHooks(match);
1976
+ if (match) return this.applyRouterHooks(match);
1590
1977
  }
1591
1978
  }
1592
1979
  }
@@ -1597,7 +1984,7 @@ class ShokupanRouter {
1597
1984
  const pattern = path2.replace(/:([a-zA-Z0-9_]+)/g, (_, key) => {
1598
1985
  keys.push(key);
1599
1986
  return "([^/]+)";
1600
- }).replace(/\*/g, ".*");
1987
+ }).replace(/\*\*/g, ".*").replace(/\*/g, "[^/]+");
1601
1988
  return {
1602
1989
  regex: new RegExp(`^${pattern}$`),
1603
1990
  keys
@@ -1614,8 +2001,23 @@ class ShokupanRouter {
1614
2001
  * @param handler - Route handler function
1615
2002
  * @param requestTimeout - Timeout for this route in milliseconds
1616
2003
  */
1617
- add({ method, path: path2, spec, handler, regex: customRegex, group, requestTimeout, renderer }) {
2004
+ add({ method, path: path2, spec, handler, regex: customRegex, group, requestTimeout, renderer, controller }) {
1618
2005
  const { regex, keys } = customRegex ? { regex: customRegex, keys: [] } : this.parsePath(path2);
2006
+ if (this.currentGuards.length > 0) {
2007
+ spec = spec || {};
2008
+ for (const guard of this.currentGuards) {
2009
+ if (guard.spec) {
2010
+ if (guard.spec.responses) {
2011
+ spec.responses = spec.responses || {};
2012
+ Object.assign(spec.responses, guard.spec.responses);
2013
+ }
2014
+ if (guard.spec.security) {
2015
+ spec.security = spec.security || [];
2016
+ spec.security.push(...guard.spec.security);
2017
+ }
2018
+ }
2019
+ }
2020
+ }
1619
2021
  let wrappedHandler = handler;
1620
2022
  const routeGuards = [...this.currentGuards];
1621
2023
  const effectiveTimeout = requestTimeout ?? this.requestTimeout ?? this.rootConfig?.requestTimeout;
@@ -1666,47 +2068,85 @@ class ShokupanRouter {
1666
2068
  return innerHandler(ctx);
1667
2069
  };
1668
2070
  }
1669
- let file = "unknown";
1670
- let line = 0;
1671
- try {
1672
- const err = new Error();
1673
- const stack = err.stack?.split("\n") || [];
1674
- const callerLine = stack.find(
1675
- (l) => l.includes(":") && !l.includes("router.ts") && !l.includes("shokupan.ts") && !l.includes("node_modules") && !l.includes("bun:main")
1676
- );
1677
- if (callerLine) {
1678
- const match = callerLine.match(/\((.*):(\d+):(\d+)\)/) || callerLine.match(/at (.*):(\d+):(\d+)/);
1679
- if (match) {
1680
- file = match[1];
1681
- line = parseInt(match[2], 10);
1682
- }
1683
- }
1684
- } catch (e) {
1685
- }
1686
- const trackedHandler = wrappedHandler;
2071
+ const { file, line } = getCallerInfo();
2072
+ const trackingHandler = wrappedHandler;
1687
2073
  wrappedHandler = async (ctx) => {
1688
- if (ctx.app?.applicationConfig.enableMiddlewareTracking) {
1689
- ctx.handlerStack.push({
1690
- name: handler.name || "anonymous",
1691
- file,
1692
- line
1693
- });
2074
+ if (!ctx.app?.applicationConfig.enableMiddlewareTracking) {
2075
+ return trackingHandler(ctx);
2076
+ }
2077
+ const startTime = performance.now();
2078
+ let error = void 0;
2079
+ try {
2080
+ if (ctx.app?.applicationConfig.enableMiddlewareTracking) {
2081
+ ctx.handlerStack.push({
2082
+ name: handler.name || "anonymous",
2083
+ file,
2084
+ line
2085
+ });
2086
+ }
2087
+ return await trackingHandler(ctx);
2088
+ } catch (e) {
2089
+ error = e;
2090
+ throw e;
2091
+ } finally {
2092
+ if (ctx.app?.applicationConfig.enableMiddlewareTracking) {
2093
+ const duration = performance.now() - startTime;
2094
+ const config = ctx.app.applicationConfig;
2095
+ try {
2096
+ const timestamp = Date.now();
2097
+ const key = `${timestamp}-${handler.name || "anonymous"}-${Math.random().toString(36).substring(7)}`;
2098
+ await datastore.set("middleware_tracking", key, {
2099
+ name: handler.name || "anonymous",
2100
+ path: ctx.path,
2101
+ timestamp,
2102
+ duration,
2103
+ file,
2104
+ line,
2105
+ error: error ? String(error) : void 0,
2106
+ metadata: {
2107
+ isBuiltin: handler.isBuiltin,
2108
+ pluginName: handler.pluginName
2109
+ }
2110
+ });
2111
+ const ttl = config.middlewareTrackingTTL ?? 864e5;
2112
+ const maxCapacity = config.middlewareTrackingMaxCapacity ?? 1e4;
2113
+ const cutoff = Date.now() - ttl;
2114
+ await datastore.query(`DELETE middleware_tracking WHERE timestamp < ${cutoff}`);
2115
+ const results = await datastore.query("SELECT count() FROM middleware_tracking GROUP ALL");
2116
+ if (results && results[0] && results[0].count > maxCapacity) {
2117
+ const toDelete = results[0].count - maxCapacity;
2118
+ await datastore.query(`DELETE middleware_tracking ORDER BY timestamp ASC LIMIT ${toDelete}`);
2119
+ }
2120
+ } catch (datastoreError) {
2121
+ console.error("Failed to store middleware tracking:", datastoreError);
2122
+ }
2123
+ }
1694
2124
  }
1695
- return trackedHandler(ctx);
1696
2125
  };
1697
- wrappedHandler.originalHandler = trackedHandler.originalHandler || trackedHandler;
2126
+ wrappedHandler.originalHandler = trackingHandler.originalHandler || trackingHandler;
2127
+ let bakedHandler = wrappedHandler;
2128
+ if (this.config?.hooks) {
2129
+ bakedHandler = this.wrapWithHooks(wrappedHandler, this.config.hooks);
2130
+ }
1698
2131
  this[$routes].push({
1699
2132
  method,
1700
2133
  path: path2,
1701
- regex,
1702
- keys,
1703
- handler: wrappedHandler,
2134
+ regex: regex ?? new RegExp(""),
2135
+ keys: keys ?? [],
2136
+ handler,
2137
+ bakedHandler,
1704
2138
  handlerSpec: spec,
1705
2139
  group,
1706
- guards: routeGuards.length > 0 ? routeGuards : void 0,
1707
- requestTimeout: effectiveTimeout
1708
- // Save for inspection? Or just relying on closure
2140
+ hooks: this.config?.hooks,
2141
+ requestTimeout,
2142
+ renderer,
2143
+ metadata: {
2144
+ file,
2145
+ line
2146
+ },
2147
+ controller
1709
2148
  });
2149
+ this.trie.insert(method, path2, bakedHandler);
1710
2150
  return this;
1711
2151
  }
1712
2152
  get(path2, ...args) {
@@ -1780,10 +2220,10 @@ class ShokupanRouter {
1780
2220
  const config = typeof options === "string" ? { root: options } : options;
1781
2221
  const prefix = uriPath.startsWith("/") ? uriPath : "/" + uriPath;
1782
2222
  const normalizedPrefix = prefix.endsWith("/") && prefix !== "/" ? prefix.slice(0, -1) : prefix;
1783
- serveStatic(null, config, prefix);
2223
+ const handlerMiddleware = serveStatic(config, prefix);
1784
2224
  const routeHandler = async (ctx) => {
1785
- const runner = serveStatic(ctx, config, prefix);
1786
- return runner();
2225
+ return handlerMiddleware(ctx, async () => {
2226
+ });
1787
2227
  };
1788
2228
  let groupName = "Static";
1789
2229
  const segments = normalizedPrefix.split("/").filter(Boolean);
@@ -1844,6 +2284,49 @@ class ShokupanRouter {
1844
2284
  return generateOpenApi(this, options);
1845
2285
  }
1846
2286
  }
2287
+ class SystemCpuMonitor {
2288
+ constructor(intervalMs = 1e3) {
2289
+ this.intervalMs = intervalMs;
2290
+ }
2291
+ interval = null;
2292
+ lastCpus = [];
2293
+ currentUsage = 0;
2294
+ start() {
2295
+ if (this.interval) return;
2296
+ this.lastCpus = os__namespace.cpus();
2297
+ this.interval = setInterval(() => this.update(), this.intervalMs);
2298
+ }
2299
+ stop() {
2300
+ if (this.interval) {
2301
+ clearInterval(this.interval);
2302
+ this.interval = null;
2303
+ }
2304
+ }
2305
+ getUsage() {
2306
+ return this.currentUsage;
2307
+ }
2308
+ update() {
2309
+ const cpus = os__namespace.cpus();
2310
+ let idle = 0;
2311
+ let total = 0;
2312
+ for (let i = 0; i < cpus.length; i++) {
2313
+ const cpu = cpus[i];
2314
+ const prev = this.lastCpus[i];
2315
+ let type;
2316
+ for (type in cpu.times) {
2317
+ const ticks = cpu.times[type];
2318
+ const prevTicks = prev.times[type];
2319
+ const diff = ticks - prevTicks;
2320
+ total += diff;
2321
+ if (type === "idle") {
2322
+ idle += diff;
2323
+ }
2324
+ }
2325
+ }
2326
+ this.lastCpus = cpus;
2327
+ this.currentUsage = total === 0 ? 0 : (1 - idle / total) * 100;
2328
+ }
2329
+ }
1847
2330
  const defaults = {
1848
2331
  port: 3e3,
1849
2332
  hostname: "localhost",
@@ -1855,51 +2338,67 @@ api.trace.getTracer("shokupan.application");
1855
2338
  class Shokupan extends ShokupanRouter {
1856
2339
  applicationConfig = {};
1857
2340
  openApiSpec;
1858
- middleware = [];
1859
2341
  composedMiddleware;
2342
+ cpuMonitor;
2343
+ hookCache = /* @__PURE__ */ new Map();
2344
+ hooksInitialized = false;
1860
2345
  get logger() {
1861
2346
  return this.applicationConfig.logger;
1862
2347
  }
1863
2348
  constructor(applicationConfig = {}) {
1864
- super();
2349
+ const config = Object.assign({}, defaults, applicationConfig);
2350
+ const { hooks, ...routerConfig } = config;
2351
+ super(routerConfig);
1865
2352
  this[$isApplication] = true;
1866
2353
  this[$appRoot] = this;
1867
- Object.assign(this.applicationConfig, defaults, applicationConfig);
2354
+ this.applicationConfig = config;
2355
+ const { file, line } = getCallerInfo();
2356
+ this.metadata = {
2357
+ file,
2358
+ line,
2359
+ name: "ShokupanApplication"
2360
+ };
1868
2361
  }
1869
2362
  /**
1870
2363
  * Adds middleware to the application.
1871
2364
  */
1872
2365
  use(middleware) {
1873
2366
  let trackedMiddleware = middleware;
1874
- let file = "unknown";
1875
- let line = 0;
1876
- try {
1877
- const err = new Error();
1878
- const stack = err.stack?.split("\n") || [];
1879
- const callerLine = stack.find(
1880
- (l) => l.includes(":") && !l.includes("shokupan.ts") && !l.includes("router.ts") && // In case called from router?
1881
- !l.includes("node_modules") && !l.includes("bun:main")
1882
- );
1883
- if (callerLine) {
1884
- const match = callerLine.match(/\((.*):(\d+):(\d+)\)/) || callerLine.match(/at (.*):(\d+):(\d+)/);
1885
- if (match) {
1886
- file = match[1];
1887
- line = parseInt(match[2], 10);
1888
- }
1889
- }
1890
- } catch (e) {
2367
+ const { file, line } = getCallerInfo();
2368
+ if (!middleware.metadata) {
2369
+ middleware.metadata = {
2370
+ file,
2371
+ line,
2372
+ name: middleware.name || "middleware",
2373
+ isBuiltin: middleware.isBuiltin,
2374
+ pluginName: middleware.pluginName
2375
+ };
1891
2376
  }
1892
2377
  trackedMiddleware = async (ctx, next) => {
1893
2378
  const c = ctx;
1894
2379
  if (c.handlerStack && c.app?.applicationConfig.enableMiddlewareTracking) {
1895
- c.handlerStack.push({
1896
- name: middleware.name || "middleware",
1897
- file,
1898
- line
1899
- });
2380
+ const metadata = middleware.metadata || {};
2381
+ const start = performance.now();
2382
+ const item = {
2383
+ name: metadata.pluginName ? `${metadata.pluginName} (${metadata.name})` : metadata.name || middleware.name || "middleware",
2384
+ file: metadata.file || file,
2385
+ line: metadata.line || line,
2386
+ isBuiltin: metadata.isBuiltin,
2387
+ startTime: start,
2388
+ duration: -1
2389
+ };
2390
+ c.handlerStack.push(item);
2391
+ try {
2392
+ return await middleware(ctx, next);
2393
+ } finally {
2394
+ item.duration = performance.now() - start;
2395
+ }
1900
2396
  }
1901
2397
  return middleware(ctx, next);
1902
2398
  };
2399
+ trackedMiddleware.metadata = middleware.metadata;
2400
+ Object.defineProperty(trackedMiddleware, "name", { value: middleware.name || "middleware" });
2401
+ trackedMiddleware.order = this.middleware.length;
1903
2402
  this.middleware.push(trackedMiddleware);
1904
2403
  return this;
1905
2404
  }
@@ -1911,6 +2410,15 @@ class Shokupan extends ShokupanRouter {
1911
2410
  this.startupHooks.push(callback);
1912
2411
  return this;
1913
2412
  }
2413
+ specAvailableHooks = [];
2414
+ /**
2415
+ * Registers a callback to be executed when the OpenAPI spec is available.
2416
+ * This happens after generateOpenApi() but before the server starts listening (or at least before it finishes startup if async).
2417
+ */
2418
+ onSpecAvailable(callback) {
2419
+ this.specAvailableHooks.push(callback);
2420
+ return this;
2421
+ }
1914
2422
  /**
1915
2423
  * Starts the application server.
1916
2424
  *
@@ -1927,8 +2435,15 @@ class Shokupan extends ShokupanRouter {
1927
2435
  }
1928
2436
  if (this.applicationConfig.enableOpenApiGen) {
1929
2437
  this.openApiSpec = await generateOpenApi(this);
2438
+ for (const hook of this.specAvailableHooks) {
2439
+ await hook(this.openApiSpec);
2440
+ }
1930
2441
  }
1931
2442
  if (port === 0 && process.platform === "linux") ;
2443
+ if (this.applicationConfig.autoBackpressureFeedback) {
2444
+ this.cpuMonitor = new SystemCpuMonitor();
2445
+ this.cpuMonitor.start();
2446
+ }
1932
2447
  const serveOptions = {
1933
2448
  port: finalPort,
1934
2449
  hostname: this.applicationConfig.hostname,
@@ -1953,7 +2468,7 @@ class Shokupan extends ShokupanRouter {
1953
2468
  };
1954
2469
  let factory = this.applicationConfig.serverFactory;
1955
2470
  if (!factory && typeof Bun === "undefined") {
1956
- const { createHttpServer } = await Promise.resolve().then(() => require("./server-adapter-BD6oKEto.cjs"));
2471
+ const { createHttpServer } = await Promise.resolve().then(() => require("./server-adapter-fVKP60e0.cjs"));
1957
2472
  factory = createHttpServer();
1958
2473
  }
1959
2474
  const server = factory ? await factory(serveOptions) : Bun.serve(serveOptions);
@@ -2038,11 +2553,18 @@ class Shokupan extends ShokupanRouter {
2038
2553
  }
2039
2554
  async handleRequest(req, server) {
2040
2555
  const request = req;
2041
- const ctx = new ShokupanContext(request, server, void 0, this, this.applicationConfig.enableMiddlewareTracking);
2556
+ const controller = new AbortController();
2557
+ const ctx = new ShokupanContext(request, server, void 0, this, controller.signal, this.applicationConfig.enableMiddlewareTracking);
2042
2558
  const handle = async () => {
2559
+ if (this.cpuMonitor && this.cpuMonitor.getUsage() > (this.applicationConfig.autoBackpressureLevel ?? 60)) {
2560
+ const msg = "Too Many Requests (CPU Backpressure)";
2561
+ const res = ctx.text(msg, 429);
2562
+ await this.executeHook("onResponseEnd", ctx, res);
2563
+ return res;
2564
+ }
2043
2565
  try {
2044
- if (this.applicationConfig.hooks?.onRequestStart) {
2045
- await this.applicationConfig.hooks.onRequestStart(ctx);
2566
+ if (this.hasHook("onRequestStart")) {
2567
+ await this.executeHook("onRequestStart", ctx);
2046
2568
  }
2047
2569
  const fn = this.composedMiddleware ??= compose(this.middleware);
2048
2570
  const result = await fn(ctx, async () => {
@@ -2058,23 +2580,24 @@ class Shokupan extends ShokupanRouter {
2058
2580
  response = result;
2059
2581
  } else if ((result === null || result === void 0) && ctx._finalResponse instanceof Response) {
2060
2582
  response = ctx._finalResponse;
2061
- } else if ((result === null || result === void 0) && ctx.response.status === 404) {
2062
- const span = asyncContext.getStore()?.get("span");
2063
- if (span) span.setAttribute("http.status_code", 404);
2064
- response = ctx.text("Not Found", 404);
2065
2583
  } else if (result === null || result === void 0) {
2066
- if (ctx._finalResponse) response = ctx._finalResponse;
2067
- else response = ctx.text("Not Found", 404);
2584
+ if (ctx._finalResponse instanceof Response) {
2585
+ response = ctx._finalResponse;
2586
+ } else if (ctx.response.status !== 200 || ctx.response.hasPopulatedHeaders) {
2587
+ response = ctx.send(null, { status: ctx.response.status, headers: ctx.response.headers });
2588
+ } else {
2589
+ response = ctx.text("Not Found", 404);
2590
+ }
2068
2591
  } else if (typeof result === "object") {
2069
2592
  response = ctx.json(result);
2070
2593
  } else {
2071
2594
  response = ctx.text(String(result));
2072
2595
  }
2073
- if (this.applicationConfig.hooks?.onRequestEnd) {
2074
- await this.applicationConfig.hooks.onRequestEnd(ctx);
2596
+ if (this.hasHook("onRequestEnd")) {
2597
+ await this.executeHook("onRequestEnd", ctx);
2075
2598
  }
2076
- if (this.applicationConfig.hooks?.onResponseStart) {
2077
- await this.applicationConfig.hooks.onResponseStart(ctx, response);
2599
+ if (this.hasHook("onResponseStart")) {
2600
+ await this.executeHook("onResponseStart", ctx, response);
2078
2601
  }
2079
2602
  return response;
2080
2603
  } catch (err) {
@@ -2084,28 +2607,21 @@ class Shokupan extends ShokupanRouter {
2084
2607
  const status = err.status || err.statusCode || 500;
2085
2608
  const body = { error: err.message || "Internal Server Error" };
2086
2609
  if (err.errors) body.errors = err.errors;
2087
- if (this.applicationConfig.hooks?.onError) {
2088
- try {
2089
- await this.applicationConfig.hooks.onError(err, ctx);
2090
- } catch (hookErr) {
2091
- console.error("Error in onError hook:", hookErr);
2092
- }
2610
+ if (this.hasHook("onError")) {
2611
+ await this.executeHook("onError", err, ctx);
2093
2612
  }
2094
2613
  return ctx.json(body, status);
2095
2614
  }
2096
2615
  };
2097
2616
  let executionPromise = handle();
2098
2617
  const timeoutMs = this.applicationConfig.requestTimeout;
2099
- if (timeoutMs && timeoutMs > 0 && this.applicationConfig.hooks?.onRequestTimeout) {
2618
+ if (timeoutMs && timeoutMs > 0) {
2100
2619
  let timeoutId;
2101
2620
  const timeoutPromise = new Promise((_, reject) => {
2102
2621
  timeoutId = setTimeout(async () => {
2103
- try {
2104
- if (this.applicationConfig.hooks?.onRequestTimeout) {
2105
- await this.applicationConfig.hooks.onRequestTimeout(ctx);
2106
- }
2107
- } catch (e) {
2108
- console.error("Error in onRequestTimeout hook:", e);
2622
+ controller.abort();
2623
+ if (this.hasHook("onRequestTimeout")) {
2624
+ await this.executeHook("onRequestTimeout", ctx);
2109
2625
  }
2110
2626
  reject(new Error("Request Timeout"));
2111
2627
  }, timeoutMs);
@@ -2119,12 +2635,56 @@ class Shokupan extends ShokupanRouter {
2119
2635
  console.error("Unexpected error in request execution:", err);
2120
2636
  return ctx.text("Internal Server Error", 500);
2121
2637
  }).then(async (res) => {
2122
- if (this.applicationConfig.hooks?.onResponseEnd) {
2123
- await this.applicationConfig.hooks.onResponseEnd(ctx, res);
2638
+ if (this.hasHook("onResponseEnd")) {
2639
+ await this.executeHook("onResponseEnd", ctx, res);
2124
2640
  }
2125
2641
  return res;
2126
2642
  });
2127
2643
  }
2644
+ ensureHooksInitialized() {
2645
+ const hooks = this.applicationConfig.hooks;
2646
+ if (hooks) {
2647
+ const hookList = Array.isArray(hooks) ? hooks : [hooks];
2648
+ const hookTypes = [
2649
+ "onRequestStart",
2650
+ "onRequestEnd",
2651
+ "onResponseStart",
2652
+ "onResponseEnd",
2653
+ "onError",
2654
+ "beforeValidate",
2655
+ "afterValidate",
2656
+ "onRequestTimeout",
2657
+ "onReadTimeout",
2658
+ "onWriteTimeout"
2659
+ ];
2660
+ for (const type of hookTypes) {
2661
+ const fns = [];
2662
+ for (const h of hookList) {
2663
+ if (h[type]) fns.push(h[type]);
2664
+ }
2665
+ if (fns.length > 0) {
2666
+ this.hookCache.set(type, fns);
2667
+ }
2668
+ }
2669
+ }
2670
+ this.hooksInitialized = true;
2671
+ }
2672
+ async executeHook(name, ...args) {
2673
+ if (!this.hooksInitialized) {
2674
+ this.ensureHooksInitialized();
2675
+ }
2676
+ const fns = this.hookCache.get(name);
2677
+ if (!fns) return;
2678
+ for (const fn of fns) {
2679
+ await fn(...args);
2680
+ }
2681
+ }
2682
+ hasHook(name) {
2683
+ if (!this.hooksInitialized) {
2684
+ this.ensureHooksInitialized();
2685
+ }
2686
+ return this.hookCache.has(name);
2687
+ }
2128
2688
  }
2129
2689
  class AuthPlugin extends ShokupanRouter {
2130
2690
  constructor(authConfig) {
@@ -2329,7 +2889,7 @@ class AuthPlugin extends ShokupanRouter {
2329
2889
  /**
2330
2890
  * Middleware to verify JWT
2331
2891
  */
2332
- middleware() {
2892
+ getMiddleware() {
2333
2893
  return async (ctx, next) => {
2334
2894
  const authHeader = ctx.req.headers.get("Authorization");
2335
2895
  let token = authHeader?.startsWith("Bearer ") ? authHeader.substring(7) : null;
@@ -2350,12 +2910,16 @@ class AuthPlugin extends ShokupanRouter {
2350
2910
  }
2351
2911
  function Compression(options = {}) {
2352
2912
  const threshold = options.threshold ?? 512;
2353
- return async (ctx, next) => {
2913
+ const compressionMiddleware = async function CompressionMiddleware(ctx, next) {
2354
2914
  const acceptEncoding = ctx.headers.get("accept-encoding") || "";
2355
2915
  let method = null;
2356
2916
  if (acceptEncoding.includes("br")) method = "br";
2357
- else if (acceptEncoding.includes("zstd")) method = "zstd";
2358
- else if (acceptEncoding.includes("gzip")) method = "gzip";
2917
+ else if (acceptEncoding.includes("zstd")) {
2918
+ if (typeof Bun === "undefined") {
2919
+ throw new Error("zstd compression is only available in Bun runtime. Client requested zstd but server is running on Node.js.");
2920
+ }
2921
+ method = "zstd";
2922
+ } else if (acceptEncoding.includes("gzip")) method = "gzip";
2359
2923
  else if (acceptEncoding.includes("deflate")) method = "deflate";
2360
2924
  if (!method) return next();
2361
2925
  let response = await next();
@@ -2364,21 +2928,37 @@ function Compression(options = {}) {
2364
2928
  }
2365
2929
  if (response instanceof Response) {
2366
2930
  if (response.headers.has("Content-Encoding")) return response;
2367
- const body = await response.arrayBuffer();
2368
- if (body.byteLength < threshold) {
2931
+ let body;
2932
+ let bodySize;
2933
+ if (ctx._rawBody !== void 0) {
2934
+ if (typeof ctx._rawBody === "string") {
2935
+ const encoded = new TextEncoder().encode(ctx._rawBody);
2936
+ body = encoded;
2937
+ bodySize = encoded.byteLength;
2938
+ } else if (ctx._rawBody instanceof Uint8Array) {
2939
+ body = ctx._rawBody;
2940
+ bodySize = ctx._rawBody.byteLength;
2941
+ } else {
2942
+ body = ctx._rawBody;
2943
+ bodySize = body.byteLength;
2944
+ }
2945
+ } else {
2946
+ body = await response.arrayBuffer();
2947
+ bodySize = body.byteLength;
2948
+ }
2949
+ if (bodySize < threshold) {
2369
2950
  return new Response(body, {
2370
2951
  status: response.status,
2371
2952
  statusText: response.statusText,
2372
- headers: response.headers
2953
+ headers: new Headers(response.headers)
2373
2954
  });
2374
2955
  }
2375
2956
  let compressed;
2376
2957
  switch (method) {
2377
2958
  case "br":
2378
- const zlib = require("node:zlib");
2379
- compressed = await new Promise((res, rej) => zlib.brotliCompress(body, {
2959
+ compressed = await new Promise((res, rej) => zlib__namespace.brotliCompress(body, {
2380
2960
  params: {
2381
- [zlib.constants.BROTLI_PARAM_QUALITY]: 4
2961
+ [zlib__namespace.constants.BROTLI_PARAM_QUALITY]: 4
2382
2962
  }
2383
2963
  }, (err, data) => {
2384
2964
  if (err) return rej(err);
@@ -2386,13 +2966,19 @@ function Compression(options = {}) {
2386
2966
  }));
2387
2967
  break;
2388
2968
  case "gzip":
2389
- compressed = Bun.gzipSync(body);
2969
+ compressed = await new Promise((res, rej) => zlib__namespace.gzip(body, (err, data) => {
2970
+ if (err) return rej(err);
2971
+ res(data);
2972
+ }));
2390
2973
  break;
2391
2974
  case "zstd":
2392
2975
  compressed = await Bun.zstdCompress(body);
2393
2976
  break;
2394
2977
  default:
2395
- compressed = Bun.deflateSync(body);
2978
+ compressed = await new Promise((res, rej) => zlib__namespace.deflate(body, (err, data) => {
2979
+ if (err) return rej(err);
2980
+ res(data);
2981
+ }));
2396
2982
  break;
2397
2983
  }
2398
2984
  const headers = new Headers(response.headers);
@@ -2406,6 +2992,9 @@ function Compression(options = {}) {
2406
2992
  }
2407
2993
  return response;
2408
2994
  };
2995
+ compressionMiddleware.isBuiltin = true;
2996
+ compressionMiddleware.pluginName = "Compression";
2997
+ return compressionMiddleware;
2409
2998
  }
2410
2999
  function Cors(options = {}) {
2411
3000
  const defaults2 = {
@@ -2415,7 +3004,7 @@ function Cors(options = {}) {
2415
3004
  optionsSuccessStatus: 204
2416
3005
  };
2417
3006
  const opts = { ...defaults2, ...options };
2418
- return async (ctx, next) => {
3007
+ const corsMiddleware = async function CorsMiddleware(ctx, next) {
2419
3008
  const headers = new Headers();
2420
3009
  const origin = ctx.headers.get("origin");
2421
3010
  const set = (k, v) => headers.set(k, v);
@@ -2477,6 +3066,9 @@ function Cors(options = {}) {
2477
3066
  }
2478
3067
  return response;
2479
3068
  };
3069
+ corsMiddleware.isBuiltin = true;
3070
+ corsMiddleware.pluginName = "Cors";
3071
+ return corsMiddleware;
2480
3072
  }
2481
3073
  function useExpress(expressMiddleware) {
2482
3074
  return async (ctx, next) => {
@@ -2644,7 +3236,33 @@ const safelyGetBody = async (ctx) => {
2644
3236
  return {};
2645
3237
  }
2646
3238
  };
3239
+ function getValidator(schema) {
3240
+ if (isZod(schema)) {
3241
+ return (data) => validateZod(schema, data);
3242
+ }
3243
+ if (isTypeBox(schema)) {
3244
+ return (data) => validateTypeBox(schema, data);
3245
+ }
3246
+ if (isAjv(schema)) {
3247
+ return (data) => validateAjv(schema, data);
3248
+ }
3249
+ if (isValibotWrapper(schema)) {
3250
+ return (data) => validateValibotWrapper(schema, data);
3251
+ }
3252
+ if (isClass(schema)) {
3253
+ return (data) => validateClassValidator(schema, data);
3254
+ }
3255
+ if (typeof schema === "function") {
3256
+ return schema;
3257
+ }
3258
+ throw new Error("Unknown validator type provided. Please use a supported library (Zod, Ajv, TypeBox) or a custom function.");
3259
+ }
2647
3260
  function validate(config) {
3261
+ const validators = {};
3262
+ if (config.params) validators.params = getValidator(config.params);
3263
+ if (config.query) validators.query = getValidator(config.query);
3264
+ if (config.headers) validators.headers = getValidator(config.headers);
3265
+ if (config.body) validators.body = getValidator(config.body);
2648
3266
  return async (ctx, next) => {
2649
3267
  const dataToValidate = {};
2650
3268
  if (config.params) dataToValidate.params = ctx.params;
@@ -2663,21 +3281,21 @@ function validate(config) {
2663
3281
  if (ctx.app?.applicationConfig.hooks?.beforeValidate) {
2664
3282
  await ctx.app.applicationConfig.hooks.beforeValidate(ctx, dataToValidate);
2665
3283
  }
2666
- if (config.params) {
2667
- ctx.params = await runValidation(config.params, ctx.params);
3284
+ if (validators.params) {
3285
+ ctx.params = await validators.params(ctx.params);
2668
3286
  }
2669
3287
  let validQuery;
2670
- if (config.query && queryObj) {
2671
- validQuery = await runValidation(config.query, queryObj);
3288
+ if (validators.query && queryObj) {
3289
+ validQuery = await validators.query(queryObj);
2672
3290
  }
2673
- if (config.headers) {
3291
+ if (validators.headers) {
2674
3292
  const headersObj = Object.fromEntries(ctx.req.headers.entries());
2675
- await runValidation(config.headers, headersObj);
3293
+ await validators.headers(headersObj);
2676
3294
  }
2677
3295
  let validBody;
2678
- if (config.body) {
3296
+ if (validators.body) {
2679
3297
  const b = body ?? await safelyGetBody(ctx);
2680
- validBody = await runValidation(config.body, b);
3298
+ validBody = await validators.body(b);
2681
3299
  const req = ctx.req;
2682
3300
  req._bodyValue = validBody;
2683
3301
  Object.defineProperty(req, "json", {
@@ -2696,36 +3314,6 @@ function validate(config) {
2696
3314
  return next();
2697
3315
  };
2698
3316
  }
2699
- async function runValidation(schema, data) {
2700
- if (isZod(schema)) {
2701
- return validateZod(schema, data);
2702
- }
2703
- if (isTypeBox(schema)) {
2704
- return validateTypeBox(schema, data);
2705
- }
2706
- if (isAjv(schema)) {
2707
- return validateAjv(schema, data);
2708
- }
2709
- if (isValibotWrapper(schema)) {
2710
- return validateValibotWrapper(schema, data);
2711
- }
2712
- if (isClass(schema)) {
2713
- return validateClassValidator(schema, data);
2714
- }
2715
- if (isTypeBox(schema)) {
2716
- return validateTypeBox(schema, data);
2717
- }
2718
- if (isAjv(schema)) {
2719
- return validateAjv(schema, data);
2720
- }
2721
- if (isValibotWrapper(schema)) {
2722
- return validateValibotWrapper(schema, data);
2723
- }
2724
- if (typeof schema === "function") {
2725
- return schema(data);
2726
- }
2727
- throw new Error("Unknown validator type provided. Please use a supported library (Zod, Ajv, TypeBox) or a custom function.");
2728
- }
2729
3317
  const ajv = new Ajv({ coerceTypes: true, allErrors: true });
2730
3318
  addFormats(ajv);
2731
3319
  const compiledValidators = /* @__PURE__ */ new WeakMap();
@@ -2740,17 +3328,18 @@ function openApiValidator() {
2740
3328
  cache = compileValidators(app.openApiSpec);
2741
3329
  compiledValidators.set(app, cache);
2742
3330
  }
2743
- const method = ctx.req.method.toLowerCase();
2744
3331
  let matchPath;
2745
- if (cache.has(ctx.path)) {
3332
+ let matchParams = {};
3333
+ if (cache.validators.has(ctx.path)) {
2746
3334
  matchPath = ctx.path;
2747
3335
  } else {
2748
- for (const specPath of cache.keys()) {
2749
- const regexStr = "^" + specPath.replace(/{([^}]+)}/g, "([^/]+)") + "$";
2750
- const regex = new RegExp(regexStr);
3336
+ for (const [path2, { regex, paramNames }] of cache.paths) {
2751
3337
  const match = regex.exec(ctx.path);
2752
3338
  if (match) {
2753
- matchPath = specPath;
3339
+ matchPath = path2;
3340
+ paramNames.forEach((name, i) => {
3341
+ matchParams[name] = match[i + 1];
3342
+ });
2754
3343
  break;
2755
3344
  }
2756
3345
  }
@@ -2758,7 +3347,8 @@ function openApiValidator() {
2758
3347
  if (!matchPath) {
2759
3348
  return next();
2760
3349
  }
2761
- const validators = cache.get(matchPath)?.[method];
3350
+ const method = ctx.req.method.toLowerCase();
3351
+ const validators = cache.validators.get(matchPath)?.[method];
2762
3352
  if (!validators) {
2763
3353
  return next();
2764
3354
  }
@@ -2783,21 +3373,7 @@ function openApiValidator() {
2783
3373
  }
2784
3374
  }
2785
3375
  if (validators.params) {
2786
- let params = ctx.params;
2787
- if (Object.keys(params).length === 0 && matchPath) {
2788
- const paramNames = (matchPath.match(/{([^}]+)}/g) || []).map((s) => s.slice(1, -1));
2789
- if (paramNames.length > 0) {
2790
- const regexStr = "^" + matchPath.replace(/{([^}]+)}/g, "([^/]+)") + "$";
2791
- const regex = new RegExp(regexStr);
2792
- const match = regex.exec(ctx.path);
2793
- if (match) {
2794
- params = {};
2795
- paramNames.forEach((name, i) => {
2796
- params[name] = match[i + 1];
2797
- });
2798
- }
2799
- }
2800
- }
3376
+ const params = { ...matchParams, ...ctx.params };
2801
3377
  const valid = validators.params(params);
2802
3378
  if (!valid && validators.params.errors) {
2803
3379
  errors.push(...validators.params.errors.map((e) => ({ ...e, location: "path" })));
@@ -2817,15 +3393,27 @@ function openApiValidator() {
2817
3393
  };
2818
3394
  }
2819
3395
  function compileValidators(spec) {
2820
- const cache = /* @__PURE__ */ new Map();
3396
+ const validators = /* @__PURE__ */ new Map();
3397
+ const paths = /* @__PURE__ */ new Map();
2821
3398
  for (const [path2, pathItem] of Object.entries(spec.paths || {})) {
3399
+ if (path2.includes("{")) {
3400
+ const paramNames = [];
3401
+ const regexStr = "^" + path2.replace(/{([^}]+)}/g, (_, name) => {
3402
+ paramNames.push(name);
3403
+ return "([^/]+)";
3404
+ }) + "$";
3405
+ paths.set(path2, {
3406
+ regex: new RegExp(regexStr),
3407
+ paramNames
3408
+ });
3409
+ }
2822
3410
  const pathValidators = {};
2823
3411
  for (const [method, operation] of Object.entries(pathItem)) {
2824
3412
  if (method === "parameters" || method === "summary" || method === "description") continue;
2825
3413
  const oper = operation;
2826
- const validators = {};
3414
+ const opValidators = {};
2827
3415
  if (oper.requestBody?.content?.["application/json"]?.schema) {
2828
- validators.body = ajv.compile(oper.requestBody.content["application/json"].schema);
3416
+ opValidators.body = ajv.compile(oper.requestBody.content["application/json"].schema);
2829
3417
  }
2830
3418
  const parameters = [...oper.parameters || [], ...pathItem.parameters || []];
2831
3419
  const queryProps = {};
@@ -2847,85 +3435,41 @@ function compileValidators(spec) {
2847
3435
  }
2848
3436
  }
2849
3437
  if (Object.keys(queryProps).length > 0) {
2850
- validators.query = ajv.compile({
3438
+ opValidators.query = ajv.compile({
2851
3439
  type: "object",
2852
3440
  properties: queryProps,
2853
3441
  required: queryRequired.length > 0 ? queryRequired : void 0
2854
3442
  });
2855
3443
  }
2856
3444
  if (Object.keys(pathProps).length > 0) {
2857
- validators.params = ajv.compile({
3445
+ opValidators.params = ajv.compile({
2858
3446
  type: "object",
2859
3447
  properties: pathProps,
2860
3448
  required: pathRequired.length > 0 ? pathRequired : void 0
2861
3449
  });
2862
3450
  }
2863
3451
  if (Object.keys(headerProps).length > 0) {
2864
- validators.headers = ajv.compile({
3452
+ opValidators.headers = ajv.compile({
2865
3453
  type: "object",
2866
3454
  properties: headerProps,
2867
3455
  required: headerRequired.length > 0 ? headerRequired : void 0
2868
3456
  });
2869
3457
  }
2870
- pathValidators[method] = validators;
3458
+ pathValidators[method] = opValidators;
2871
3459
  }
2872
- cache.set(path2, pathValidators);
3460
+ validators.set(path2, pathValidators);
2873
3461
  }
2874
- return cache;
3462
+ return { paths, validators };
2875
3463
  }
2876
- function RateLimit(options = {}) {
2877
- const windowMs = options.windowMs || 60 * 1e3;
2878
- const max = options.max || 5;
2879
- const message = options.message || "Too many requests, please try again later.";
2880
- const statusCode = options.statusCode || 429;
2881
- const headers = options.headers !== false;
2882
- const keyGenerator = options.keyGenerator || ((ctx) => {
2883
- return ctx.headers.get("x-forwarded-for") || ctx.url.hostname || "unknown";
3464
+ function precompileValidators(app, spec) {
3465
+ const cache = compileValidators(spec);
3466
+ compiledValidators.set(app, cache);
3467
+ }
3468
+ function enableOpenApiValidation(app) {
3469
+ app.use(openApiValidator());
3470
+ app.onSpecAvailable((spec) => {
3471
+ precompileValidators(app, spec);
2884
3472
  });
2885
- const skip = options.skip || (() => false);
2886
- const hits = /* @__PURE__ */ new Map();
2887
- const interval = setInterval(() => {
2888
- const now = Date.now();
2889
- for (const [key, record] of hits.entries()) {
2890
- if (record.resetTime <= now) {
2891
- hits.delete(key);
2892
- }
2893
- }
2894
- }, windowMs);
2895
- interval.unref?.();
2896
- return async (ctx, next) => {
2897
- if (skip(ctx)) return next();
2898
- const key = keyGenerator(ctx);
2899
- const now = Date.now();
2900
- let record = hits.get(key);
2901
- if (!record || record.resetTime <= now) {
2902
- record = {
2903
- hits: 0,
2904
- resetTime: now + windowMs
2905
- };
2906
- hits.set(key, record);
2907
- }
2908
- record.hits++;
2909
- const remaining = Math.max(0, max - record.hits);
2910
- const resetTime = Math.ceil(record.resetTime / 1e3);
2911
- if (record.hits > max) {
2912
- if (headers) {
2913
- const res = typeof message === "object" ? ctx.json(message, statusCode) : ctx.text(String(message), statusCode);
2914
- res.headers.set("X-RateLimit-Limit", String(max));
2915
- res.headers.set("X-RateLimit-Remaining", "0");
2916
- res.headers.set("X-RateLimit-Reset", String(resetTime));
2917
- return res;
2918
- }
2919
- return typeof message === "object" ? ctx.json(message, statusCode) : ctx.text(String(message), statusCode);
2920
- }
2921
- const response = await next();
2922
- if (response instanceof Response && headers) {
2923
- response.headers.set("X-RateLimit-Limit", String(max));
2924
- response.headers.set("X-RateLimit-Remaining", String(remaining));
2925
- response.headers.set("X-RateLimit-Reset", String(resetTime));
2926
- }
2927
- return response;
2928
- };
2929
3473
  }
2930
3474
  const eta = new eta$2.Eta();
2931
3475
  class ScalarPlugin extends ShokupanRouter {
@@ -3002,7 +3546,7 @@ class ScalarPlugin extends ShokupanRouter {
3002
3546
  }
3003
3547
  }
3004
3548
  function SecurityHeaders(options = {}) {
3005
- return async (ctx, next) => {
3549
+ const securityHeadersMiddleware = async function SecurityHeadersMiddleware(ctx, next) {
3006
3550
  const headers = {};
3007
3551
  const set = (k, v) => headers[k] = v;
3008
3552
  if (options.dnsPrefetchControl !== false) {
@@ -3056,6 +3600,9 @@ function SecurityHeaders(options = {}) {
3056
3600
  }
3057
3601
  return response;
3058
3602
  };
3603
+ securityHeadersMiddleware.isBuiltin = true;
3604
+ securityHeadersMiddleware.pluginName = "SecurityHeaders";
3605
+ return securityHeadersMiddleware;
3059
3606
  }
3060
3607
  class Cookie {
3061
3608
  maxAge;
@@ -3169,7 +3716,7 @@ function Session(options) {
3169
3716
  const resave = options.resave === void 0 ? true : options.resave;
3170
3717
  const saveUninitialized = options.saveUninitialized === void 0 ? true : options.saveUninitialized;
3171
3718
  const rolling = options.rolling || false;
3172
- return async (ctx, next) => {
3719
+ const sessionMiddleware = async function SessionMiddleware(ctx, next) {
3173
3720
  let reqSessionId = null;
3174
3721
  const cookieHeader = ctx.req.headers.get("cookie");
3175
3722
  const cookies = {};
@@ -3305,6 +3852,9 @@ function Session(options) {
3305
3852
  }
3306
3853
  return result;
3307
3854
  };
3855
+ sessionMiddleware.isBuiltin = true;
3856
+ sessionMiddleware.pluginName = "Session";
3857
+ return sessionMiddleware;
3308
3858
  }
3309
3859
  exports.$appRoot = $appRoot;
3310
3860
  exports.$childControllers = $childControllers;
@@ -3344,6 +3894,7 @@ exports.Post = Post;
3344
3894
  exports.Put = Put;
3345
3895
  exports.Query = Query;
3346
3896
  exports.RateLimit = RateLimit;
3897
+ exports.RateLimitMiddleware = RateLimitMiddleware;
3347
3898
  exports.Req = Req;
3348
3899
  exports.RouteParamType = RouteParamType;
3349
3900
  exports.RouterRegistry = RouterRegistry;
@@ -3359,8 +3910,11 @@ exports.ShokupanRouter = ShokupanRouter;
3359
3910
  exports.Spec = Spec;
3360
3911
  exports.Use = Use;
3361
3912
  exports.ValidationError = ValidationError;
3913
+ exports.compileValidators = compileValidators;
3362
3914
  exports.compose = compose;
3915
+ exports.enableOpenApiValidation = enableOpenApiValidation;
3363
3916
  exports.openApiValidator = openApiValidator;
3917
+ exports.precompileValidators = precompileValidators;
3364
3918
  exports.useExpress = useExpress;
3365
3919
  exports.valibot = valibot;
3366
3920
  exports.validate = validate;