shokupan 0.2.0 → 0.3.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 (58) hide show
  1. package/dist/benchmarking/advanced-cases/elysia.d.ts +1 -0
  2. package/dist/benchmarking/advanced-cases/express.d.ts +1 -0
  3. package/dist/benchmarking/advanced-cases/fastify.d.ts +1 -0
  4. package/dist/benchmarking/advanced-cases/hapi.d.ts +1 -0
  5. package/dist/benchmarking/advanced-cases/hono.d.ts +1 -0
  6. package/dist/benchmarking/advanced-cases/koa.d.ts +1 -0
  7. package/dist/benchmarking/advanced-cases/nest.d.ts +1 -0
  8. package/dist/benchmarking/advanced-cases/shokupan.d.ts +1 -0
  9. package/dist/benchmarking/advanced-data.d.ts +33 -0
  10. package/dist/benchmarking/advanced-runner.d.ts +1 -0
  11. package/dist/benchmarking/advanced-worker.d.ts +0 -0
  12. package/dist/benchmarking/cases/elysia.d.ts +1 -0
  13. package/dist/benchmarking/cases/express.d.ts +1 -0
  14. package/dist/benchmarking/cases/fastify.d.ts +1 -0
  15. package/dist/benchmarking/cases/hapi.d.ts +1 -0
  16. package/dist/benchmarking/cases/hono.d.ts +1 -0
  17. package/dist/benchmarking/cases/koa.d.ts +1 -0
  18. package/dist/benchmarking/cases/nest.d.ts +1 -0
  19. package/dist/benchmarking/cases/shokupan.d.ts +1 -0
  20. package/dist/benchmarking/data.d.ts +15 -0
  21. package/dist/benchmarking/quick_bench.d.ts +1 -0
  22. package/dist/benchmarking/runner.d.ts +1 -0
  23. package/dist/benchmarking/worker.d.ts +0 -0
  24. package/dist/buntest.d.ts +1 -0
  25. package/dist/cli.cjs +1 -1
  26. package/dist/cli.js +1 -1
  27. package/dist/context.d.ts +14 -6
  28. package/dist/decorators.d.ts +47 -0
  29. package/dist/index.cjs +895 -379
  30. package/dist/index.cjs.map +1 -1
  31. package/dist/index.js +887 -373
  32. package/dist/index.js.map +1 -1
  33. package/dist/{openapi-analyzer-BTExMLX4.js → openapi-analyzer-BtIaHIfe.js} +11 -6
  34. package/dist/openapi-analyzer-BtIaHIfe.js.map +1 -0
  35. package/dist/{openapi-analyzer-BN0wFCML.cjs → openapi-analyzer-D9YB3IkV.cjs} +11 -6
  36. package/dist/openapi-analyzer-D9YB3IkV.cjs.map +1 -0
  37. package/dist/plugins/auth.d.ts +1 -1
  38. package/dist/plugins/debugview/plugin.d.ts +6 -12
  39. package/dist/plugins/failed-request-recorder.d.ts +14 -0
  40. package/dist/plugins/idempotency/plugin.d.ts +14 -0
  41. package/dist/plugins/openapi-validator.d.ts +28 -0
  42. package/dist/plugins/rate-limit.d.ts +3 -1
  43. package/dist/plugins/serve-static.d.ts +2 -3
  44. package/dist/router/trie.d.ts +14 -0
  45. package/dist/router.d.ts +48 -3
  46. package/dist/{server-adapter-CnQFr4P7.js → server-adapter-BWrEJbKL.js} +5 -5
  47. package/dist/server-adapter-BWrEJbKL.js.map +1 -0
  48. package/dist/{server-adapter-BD6oKEto.cjs → server-adapter-fVKP60e0.cjs} +5 -5
  49. package/dist/server-adapter-fVKP60e0.cjs.map +1 -0
  50. package/dist/shokupan.d.ts +14 -3
  51. package/dist/types.d.ts +96 -4
  52. package/dist/util/cpu-monitor.d.ts +11 -0
  53. package/dist/util/stack.d.ts +8 -0
  54. package/package.json +4 -2
  55. package/dist/openapi-analyzer-BN0wFCML.cjs.map +0 -1
  56. package/dist/openapi-analyzer-BTExMLX4.js.map +0 -1
  57. package/dist/server-adapter-BD6oKEto.cjs.map +0 -1
  58. 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,157 @@ 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
+ `);
1401
+ });
1402
+ const datastore = {
1403
+ get(store, key) {
1404
+ return db.select(new surrealdb.RecordId(store, key));
1405
+ },
1406
+ set(store, key, value) {
1407
+ return db.create(new surrealdb.RecordId(store, key)).content(value);
1408
+ },
1409
+ async query(query, vars) {
1410
+ try {
1411
+ const r = await db.query(query, vars).collect();
1412
+ return r;
1413
+ } catch (e) {
1414
+ console.error("DS ERROR:", e);
1415
+ throw e;
1416
+ }
1417
+ },
1418
+ ready
1419
+ };
1420
+ process.on("exit", async () => {
1421
+ await db.close();
1422
+ });
1180
1423
  const tracer = api.trace.getTracer("shokupan.middleware");
1181
1424
  function traceHandler(fn, name) {
1182
1425
  return async function(...args) {
@@ -1200,6 +1443,35 @@ function traceHandler(fn, name) {
1200
1443
  });
1201
1444
  };
1202
1445
  }
1446
+ function getCallerInfo(skipFrames = 1) {
1447
+ let file = "unknown";
1448
+ let line = 0;
1449
+ try {
1450
+ const err = new Error();
1451
+ const stack = err.stack?.split("\n") || [];
1452
+ let found = 0;
1453
+ for (let i = 1; i < stack.length; i++) {
1454
+ const l = stack[i];
1455
+ if (!l.includes(":")) continue;
1456
+ if (l.includes("node_modules")) continue;
1457
+ if (l.includes("bun:main")) continue;
1458
+ if (l.includes("src/util/stack.ts")) continue;
1459
+ if (l.includes("src/router.ts")) continue;
1460
+ if (l.includes("src/shokupan.ts")) continue;
1461
+ found++;
1462
+ if (found >= skipFrames) {
1463
+ const match = l.match(/\((.*):(\d+):(\d+)\)/) || l.match(/at (.*):(\d+):(\d+)/);
1464
+ if (match) {
1465
+ file = match[1];
1466
+ line = parseInt(match[2], 10);
1467
+ return { file, line };
1468
+ }
1469
+ }
1470
+ }
1471
+ } catch (e) {
1472
+ }
1473
+ return { file, line };
1474
+ }
1203
1475
  const RouterRegistry = /* @__PURE__ */ new Map();
1204
1476
  const ShokupanApplicationTree = {};
1205
1477
  class ShokupanRouter {
@@ -1219,6 +1491,7 @@ class ShokupanRouter {
1219
1491
  [$parent] = null;
1220
1492
  [$childRouters] = [];
1221
1493
  [$childControllers] = [];
1494
+ middleware = [];
1222
1495
  get rootConfig() {
1223
1496
  return this[$appRoot]?.applicationConfig;
1224
1497
  }
@@ -1227,7 +1500,54 @@ class ShokupanRouter {
1227
1500
  }
1228
1501
  [$routes] = [];
1229
1502
  // Public via Symbol for OpenAPI generator
1503
+ trie = new RouterTrie();
1504
+ metadata;
1505
+ // Metadata for the router itself
1230
1506
  currentGuards = [];
1507
+ // Registry Accessor
1508
+ getComponentRegistry() {
1509
+ const routes = this[$routes].map((r) => ({
1510
+ type: "route",
1511
+ path: r.path,
1512
+ method: r.method,
1513
+ metadata: r.metadata,
1514
+ handlerName: r.handler.name,
1515
+ tags: r.handlerSpec?.tags,
1516
+ order: r.order,
1517
+ _fn: r.handler
1518
+ // Expose handler for debugging instrumentation
1519
+ }));
1520
+ const mw = this.middleware;
1521
+ const middleware = mw ? mw.map((m) => ({
1522
+ name: m.name || "middleware",
1523
+ metadata: m.metadata,
1524
+ order: m.order,
1525
+ _fn: m
1526
+ // Expose function for debugging instrumentation
1527
+ })) : [];
1528
+ const routers = this[$childRouters].map((r) => ({
1529
+ type: "router",
1530
+ path: r[$mountPath],
1531
+ metadata: r.metadata,
1532
+ children: r.getComponentRegistry()
1533
+ }));
1534
+ const controllers = this[$childControllers].map((c) => {
1535
+ return {
1536
+ type: "controller",
1537
+ path: c[$mountPath] || "/",
1538
+ name: c.constructor.name,
1539
+ metadata: c.metadata
1540
+ // Check if we can store this
1541
+ };
1542
+ });
1543
+ return {
1544
+ metadata: this.metadata,
1545
+ middleware,
1546
+ routes,
1547
+ routers,
1548
+ controllers
1549
+ };
1550
+ }
1231
1551
  isRouterInstance(target) {
1232
1552
  return typeof target === "object" && target !== null && $isRouter in target;
1233
1553
  }
@@ -1253,6 +1573,14 @@ class ShokupanRouter {
1253
1573
  throw new Error("Router is already mounted");
1254
1574
  }
1255
1575
  controller[$mountPath] = prefix;
1576
+ if (!controller.metadata) {
1577
+ const info = getCallerInfo();
1578
+ controller.metadata = {
1579
+ file: info.file,
1580
+ line: info.line,
1581
+ name: "MountedRouter"
1582
+ };
1583
+ }
1256
1584
  this[$childRouters].push(controller);
1257
1585
  controller[$parent] = this;
1258
1586
  const setRouterContext = (router) => {
@@ -1285,6 +1613,12 @@ class ShokupanRouter {
1285
1613
  }
1286
1614
  }
1287
1615
  instance[$mountPath] = prefix;
1616
+ const info = getCallerInfo();
1617
+ instance.metadata = {
1618
+ file: info.file,
1619
+ line: info.line,
1620
+ name: instance.constructor.name
1621
+ };
1288
1622
  this[$childControllers].push(instance);
1289
1623
  const controllerMiddleware = (typeof controller === "function" ? controller[$middleware] : instance[$middleware]) || [];
1290
1624
  const proto = Object.getPrototypeOf(instance);
@@ -1368,14 +1702,39 @@ class ShokupanRouter {
1368
1702
  for (const arg of sortedArgs) {
1369
1703
  switch (arg.type) {
1370
1704
  case RouteParamType.BODY:
1371
- args[arg.index] = await ctx.req.json().catch(() => ({}));
1705
+ try {
1706
+ if (ctx.req.headers.get("content-type")?.includes("application/json")) {
1707
+ args[arg.index] = await ctx.req.json();
1708
+ } else {
1709
+ const text = await ctx.req.text();
1710
+ if (!text) {
1711
+ args[arg.index] = {};
1712
+ } else {
1713
+ args[arg.index] = JSON.parse(text);
1714
+ }
1715
+ }
1716
+ } catch (e) {
1717
+ const err = new Error("Invalid JSON body");
1718
+ err.status = 400;
1719
+ throw err;
1720
+ }
1372
1721
  break;
1373
1722
  case RouteParamType.PARAM:
1374
1723
  args[arg.index] = arg.name ? ctx.params[arg.name] : ctx.params;
1375
1724
  break;
1376
1725
  case RouteParamType.QUERY: {
1377
1726
  const url = new URL(ctx.req.url);
1378
- args[arg.index] = arg.name ? url.searchParams.get(arg.name) : Object.fromEntries(url.searchParams);
1727
+ if (arg.name) {
1728
+ const vals = url.searchParams.getAll(arg.name);
1729
+ args[arg.index] = vals.length > 1 ? vals : vals[0];
1730
+ } else {
1731
+ const query = {};
1732
+ for (const key of url.searchParams.keys()) {
1733
+ const vals = url.searchParams.getAll(key);
1734
+ query[key] = vals.length > 1 ? vals : vals[0];
1735
+ }
1736
+ args[arg.index] = query;
1737
+ }
1379
1738
  break;
1380
1739
  }
1381
1740
  case RouteParamType.HEADER:
@@ -1523,30 +1882,59 @@ class ShokupanRouter {
1523
1882
  data: result
1524
1883
  };
1525
1884
  }
1526
- applyHooks(match) {
1885
+ applyRouterHooks(match) {
1527
1886
  if (!this.config?.hooks) return match;
1528
1887
  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);
1888
+ return {
1889
+ ...match,
1890
+ handler: this.wrapWithHooks(match.handler, hooks)
1891
+ };
1892
+ }
1893
+ wrapWithHooks(handler, hooks) {
1894
+ const hookList = Array.isArray(hooks) ? hooks : [hooks];
1895
+ const hasStart = hookList.some((h) => !!h.onRequestStart);
1896
+ const hasEnd = hookList.some((h) => !!h.onRequestEnd);
1897
+ const hasError = hookList.some((h) => !!h.onError);
1898
+ if (!hasStart && !hasEnd && !hasError) return handler;
1899
+ const originalHandler = handler;
1900
+ const wrapped = async (ctx) => {
1901
+ if (hasStart) {
1902
+ for (let i = 0; i < hookList.length; i++) {
1903
+ const h = hookList[i];
1904
+ if (typeof h.onRequestStart === "function") await h.onRequestStart(ctx);
1905
+ }
1906
+ }
1907
+ const debug = ctx._debug;
1908
+ let debugId;
1909
+ let previousNode;
1910
+ if (debug) {
1911
+ debugId = originalHandler._debugId || originalHandler.name || "handler";
1912
+ previousNode = debug.getCurrentNode();
1913
+ debug.trackEdge(previousNode, debugId);
1914
+ debug.setNode(debugId);
1915
+ }
1916
+ const start = performance.now();
1533
1917
  try {
1534
- const result = await originalHandler(ctx);
1535
- if (hooks.onRequestEnd) await hooks.onRequestEnd(ctx);
1536
- return result;
1918
+ const res = await originalHandler(ctx);
1919
+ debug?.trackStep(debugId, "handler", performance.now() - start, "success");
1920
+ for (let i = 0; i < hookList.length; i++) {
1921
+ const h = hookList[i];
1922
+ if (typeof h.onRequestEnd === "function") await h.onRequestEnd(ctx);
1923
+ }
1924
+ return res;
1537
1925
  } 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
- }
1926
+ debug?.trackStep(debugId, "handler", performance.now() - start, "error", err);
1927
+ for (let i = 0; i < hookList.length; i++) {
1928
+ const h = hookList[i];
1929
+ if (typeof h.onError === "function") await h.onError(err, ctx);
1544
1930
  }
1545
1931
  throw err;
1932
+ } finally {
1933
+ if (debug && previousNode) debug.setNode(previousNode);
1546
1934
  }
1547
1935
  };
1548
- match.handler.originalHandler = originalHandler.originalHandler || originalHandler;
1549
- return match;
1936
+ wrapped.originalHandler = originalHandler.originalHandler ?? originalHandler;
1937
+ return wrapped;
1550
1938
  }
1551
1939
  /**
1552
1940
  * Find a route matching the given method and path.
@@ -1555,24 +1943,10 @@ class ShokupanRouter {
1555
1943
  * @returns Route handler and parameters if found, otherwise null
1556
1944
  */
1557
1945
  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);
1946
+ let result = this.trie.search(method, path2);
1573
1947
  if (result) return result;
1574
1948
  if (method === "HEAD") {
1575
- result = findInRoutes(this[$routes], "GET");
1949
+ result = this.trie.search("GET", path2);
1576
1950
  if (result) return result;
1577
1951
  }
1578
1952
  for (const child of this[$childRouters]) {
@@ -1580,13 +1954,13 @@ class ShokupanRouter {
1580
1954
  if (path2 === prefix || path2.startsWith(prefix + "/")) {
1581
1955
  const subPath = path2.slice(prefix.length) || "/";
1582
1956
  const match = child.find(method, subPath);
1583
- if (match) return this.applyHooks(match);
1957
+ if (match) return this.applyRouterHooks(match);
1584
1958
  }
1585
1959
  if (prefix.endsWith("/")) {
1586
1960
  if (path2.startsWith(prefix)) {
1587
1961
  const subPath = path2.slice(prefix.length) || "/";
1588
1962
  const match = child.find(method, subPath);
1589
- if (match) return this.applyHooks(match);
1963
+ if (match) return this.applyRouterHooks(match);
1590
1964
  }
1591
1965
  }
1592
1966
  }
@@ -1597,7 +1971,7 @@ class ShokupanRouter {
1597
1971
  const pattern = path2.replace(/:([a-zA-Z0-9_]+)/g, (_, key) => {
1598
1972
  keys.push(key);
1599
1973
  return "([^/]+)";
1600
- }).replace(/\*/g, ".*");
1974
+ }).replace(/\*\*/g, ".*").replace(/\*/g, "[^/]+");
1601
1975
  return {
1602
1976
  regex: new RegExp(`^${pattern}$`),
1603
1977
  keys
@@ -1666,47 +2040,84 @@ class ShokupanRouter {
1666
2040
  return innerHandler(ctx);
1667
2041
  };
1668
2042
  }
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;
2043
+ const { file, line } = getCallerInfo();
2044
+ const trackingHandler = wrappedHandler;
1687
2045
  wrappedHandler = async (ctx) => {
1688
- if (ctx.app?.applicationConfig.enableMiddlewareTracking) {
1689
- ctx.handlerStack.push({
1690
- name: handler.name || "anonymous",
1691
- file,
1692
- line
1693
- });
2046
+ if (!ctx.app?.applicationConfig.enableMiddlewareTracking) {
2047
+ return trackingHandler(ctx);
2048
+ }
2049
+ const startTime = performance.now();
2050
+ let error = void 0;
2051
+ try {
2052
+ if (ctx.app?.applicationConfig.enableMiddlewareTracking) {
2053
+ ctx.handlerStack.push({
2054
+ name: handler.name || "anonymous",
2055
+ file,
2056
+ line
2057
+ });
2058
+ }
2059
+ return await trackingHandler(ctx);
2060
+ } catch (e) {
2061
+ error = e;
2062
+ throw e;
2063
+ } finally {
2064
+ if (ctx.app?.applicationConfig.enableMiddlewareTracking) {
2065
+ const duration = performance.now() - startTime;
2066
+ const config = ctx.app.applicationConfig;
2067
+ try {
2068
+ const timestamp = Date.now();
2069
+ const key = `${timestamp}-${handler.name || "anonymous"}-${Math.random().toString(36).substring(7)}`;
2070
+ await datastore.set("middleware_tracking", key, {
2071
+ name: handler.name || "anonymous",
2072
+ path: ctx.path,
2073
+ timestamp,
2074
+ duration,
2075
+ file,
2076
+ line,
2077
+ error: error ? String(error) : void 0,
2078
+ metadata: {
2079
+ isBuiltin: handler.isBuiltin,
2080
+ pluginName: handler.pluginName
2081
+ }
2082
+ });
2083
+ const ttl = config.middlewareTrackingTTL ?? 864e5;
2084
+ const maxCapacity = config.middlewareTrackingMaxCapacity ?? 1e4;
2085
+ const cutoff = Date.now() - ttl;
2086
+ await datastore.query(`DELETE middleware_tracking WHERE timestamp < ${cutoff}`);
2087
+ const results = await datastore.query("SELECT count() FROM middleware_tracking GROUP ALL");
2088
+ if (results && results[0] && results[0].count > maxCapacity) {
2089
+ const toDelete = results[0].count - maxCapacity;
2090
+ await datastore.query(`DELETE middleware_tracking ORDER BY timestamp ASC LIMIT ${toDelete}`);
2091
+ }
2092
+ } catch (datastoreError) {
2093
+ console.error("Failed to store middleware tracking:", datastoreError);
2094
+ }
2095
+ }
1694
2096
  }
1695
- return trackedHandler(ctx);
1696
2097
  };
1697
- wrappedHandler.originalHandler = trackedHandler.originalHandler || trackedHandler;
2098
+ wrappedHandler.originalHandler = trackingHandler.originalHandler || trackingHandler;
2099
+ let bakedHandler = wrappedHandler;
2100
+ if (this.config?.hooks) {
2101
+ bakedHandler = this.wrapWithHooks(wrappedHandler, this.config.hooks);
2102
+ }
1698
2103
  this[$routes].push({
1699
2104
  method,
1700
2105
  path: path2,
1701
- regex,
1702
- keys,
1703
- handler: wrappedHandler,
2106
+ regex: regex ?? new RegExp(""),
2107
+ keys: keys ?? [],
2108
+ handler,
2109
+ bakedHandler,
1704
2110
  handlerSpec: spec,
1705
2111
  group,
1706
- guards: routeGuards.length > 0 ? routeGuards : void 0,
1707
- requestTimeout: effectiveTimeout
1708
- // Save for inspection? Or just relying on closure
2112
+ hooks: this.config?.hooks,
2113
+ requestTimeout,
2114
+ renderer,
2115
+ metadata: {
2116
+ file,
2117
+ line
2118
+ }
1709
2119
  });
2120
+ this.trie.insert(method, path2, bakedHandler);
1710
2121
  return this;
1711
2122
  }
1712
2123
  get(path2, ...args) {
@@ -1780,10 +2191,10 @@ class ShokupanRouter {
1780
2191
  const config = typeof options === "string" ? { root: options } : options;
1781
2192
  const prefix = uriPath.startsWith("/") ? uriPath : "/" + uriPath;
1782
2193
  const normalizedPrefix = prefix.endsWith("/") && prefix !== "/" ? prefix.slice(0, -1) : prefix;
1783
- serveStatic(null, config, prefix);
2194
+ const handlerMiddleware = serveStatic(config, prefix);
1784
2195
  const routeHandler = async (ctx) => {
1785
- const runner = serveStatic(ctx, config, prefix);
1786
- return runner();
2196
+ return handlerMiddleware(ctx, async () => {
2197
+ });
1787
2198
  };
1788
2199
  let groupName = "Static";
1789
2200
  const segments = normalizedPrefix.split("/").filter(Boolean);
@@ -1844,6 +2255,49 @@ class ShokupanRouter {
1844
2255
  return generateOpenApi(this, options);
1845
2256
  }
1846
2257
  }
2258
+ class SystemCpuMonitor {
2259
+ constructor(intervalMs = 1e3) {
2260
+ this.intervalMs = intervalMs;
2261
+ }
2262
+ interval = null;
2263
+ lastCpus = [];
2264
+ currentUsage = 0;
2265
+ start() {
2266
+ if (this.interval) return;
2267
+ this.lastCpus = os__namespace.cpus();
2268
+ this.interval = setInterval(() => this.update(), this.intervalMs);
2269
+ }
2270
+ stop() {
2271
+ if (this.interval) {
2272
+ clearInterval(this.interval);
2273
+ this.interval = null;
2274
+ }
2275
+ }
2276
+ getUsage() {
2277
+ return this.currentUsage;
2278
+ }
2279
+ update() {
2280
+ const cpus = os__namespace.cpus();
2281
+ let idle = 0;
2282
+ let total = 0;
2283
+ for (let i = 0; i < cpus.length; i++) {
2284
+ const cpu = cpus[i];
2285
+ const prev = this.lastCpus[i];
2286
+ let type;
2287
+ for (type in cpu.times) {
2288
+ const ticks = cpu.times[type];
2289
+ const prevTicks = prev.times[type];
2290
+ const diff = ticks - prevTicks;
2291
+ total += diff;
2292
+ if (type === "idle") {
2293
+ idle += diff;
2294
+ }
2295
+ }
2296
+ }
2297
+ this.lastCpus = cpus;
2298
+ this.currentUsage = total === 0 ? 0 : (1 - idle / total) * 100;
2299
+ }
2300
+ }
1847
2301
  const defaults = {
1848
2302
  port: 3e3,
1849
2303
  hostname: "localhost",
@@ -1855,51 +2309,58 @@ api.trace.getTracer("shokupan.application");
1855
2309
  class Shokupan extends ShokupanRouter {
1856
2310
  applicationConfig = {};
1857
2311
  openApiSpec;
1858
- middleware = [];
1859
2312
  composedMiddleware;
2313
+ cpuMonitor;
2314
+ hookCache = /* @__PURE__ */ new Map();
2315
+ hooksInitialized = false;
1860
2316
  get logger() {
1861
2317
  return this.applicationConfig.logger;
1862
2318
  }
1863
2319
  constructor(applicationConfig = {}) {
1864
- super();
2320
+ const config = Object.assign({}, defaults, applicationConfig);
2321
+ const { hooks, ...routerConfig } = config;
2322
+ super(routerConfig);
1865
2323
  this[$isApplication] = true;
1866
2324
  this[$appRoot] = this;
1867
- Object.assign(this.applicationConfig, defaults, applicationConfig);
2325
+ this.applicationConfig = config;
2326
+ const { file, line } = getCallerInfo();
2327
+ this.metadata = {
2328
+ file,
2329
+ line,
2330
+ name: "ShokupanApplication"
2331
+ };
1868
2332
  }
1869
2333
  /**
1870
2334
  * Adds middleware to the application.
1871
2335
  */
1872
2336
  use(middleware) {
1873
2337
  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) {
2338
+ const { file, line } = getCallerInfo();
2339
+ if (!middleware.metadata) {
2340
+ middleware.metadata = {
2341
+ file,
2342
+ line,
2343
+ name: middleware.name || "middleware",
2344
+ isBuiltin: middleware.isBuiltin,
2345
+ pluginName: middleware.pluginName
2346
+ };
1891
2347
  }
1892
2348
  trackedMiddleware = async (ctx, next) => {
1893
2349
  const c = ctx;
1894
2350
  if (c.handlerStack && c.app?.applicationConfig.enableMiddlewareTracking) {
2351
+ const metadata = middleware.metadata || {};
1895
2352
  c.handlerStack.push({
1896
- name: middleware.name || "middleware",
1897
- file,
1898
- line
2353
+ name: metadata.pluginName ? `${metadata.pluginName} (${metadata.name})` : metadata.name || middleware.name || "middleware",
2354
+ file: metadata.file || file,
2355
+ line: metadata.line || line,
2356
+ isBuiltin: metadata.isBuiltin
1899
2357
  });
1900
2358
  }
1901
2359
  return middleware(ctx, next);
1902
2360
  };
2361
+ trackedMiddleware.metadata = middleware.metadata;
2362
+ Object.defineProperty(trackedMiddleware, "name", { value: middleware.name || "middleware" });
2363
+ trackedMiddleware.order = this.middleware.length;
1903
2364
  this.middleware.push(trackedMiddleware);
1904
2365
  return this;
1905
2366
  }
@@ -1911,6 +2372,15 @@ class Shokupan extends ShokupanRouter {
1911
2372
  this.startupHooks.push(callback);
1912
2373
  return this;
1913
2374
  }
2375
+ specAvailableHooks = [];
2376
+ /**
2377
+ * Registers a callback to be executed when the OpenAPI spec is available.
2378
+ * This happens after generateOpenApi() but before the server starts listening (or at least before it finishes startup if async).
2379
+ */
2380
+ onSpecAvailable(callback) {
2381
+ this.specAvailableHooks.push(callback);
2382
+ return this;
2383
+ }
1914
2384
  /**
1915
2385
  * Starts the application server.
1916
2386
  *
@@ -1927,8 +2397,15 @@ class Shokupan extends ShokupanRouter {
1927
2397
  }
1928
2398
  if (this.applicationConfig.enableOpenApiGen) {
1929
2399
  this.openApiSpec = await generateOpenApi(this);
2400
+ for (const hook of this.specAvailableHooks) {
2401
+ await hook(this.openApiSpec);
2402
+ }
1930
2403
  }
1931
2404
  if (port === 0 && process.platform === "linux") ;
2405
+ if (this.applicationConfig.autoBackpressureFeedback) {
2406
+ this.cpuMonitor = new SystemCpuMonitor();
2407
+ this.cpuMonitor.start();
2408
+ }
1932
2409
  const serveOptions = {
1933
2410
  port: finalPort,
1934
2411
  hostname: this.applicationConfig.hostname,
@@ -1953,7 +2430,7 @@ class Shokupan extends ShokupanRouter {
1953
2430
  };
1954
2431
  let factory = this.applicationConfig.serverFactory;
1955
2432
  if (!factory && typeof Bun === "undefined") {
1956
- const { createHttpServer } = await Promise.resolve().then(() => require("./server-adapter-BD6oKEto.cjs"));
2433
+ const { createHttpServer } = await Promise.resolve().then(() => require("./server-adapter-fVKP60e0.cjs"));
1957
2434
  factory = createHttpServer();
1958
2435
  }
1959
2436
  const server = factory ? await factory(serveOptions) : Bun.serve(serveOptions);
@@ -2038,11 +2515,18 @@ class Shokupan extends ShokupanRouter {
2038
2515
  }
2039
2516
  async handleRequest(req, server) {
2040
2517
  const request = req;
2041
- const ctx = new ShokupanContext(request, server, void 0, this, this.applicationConfig.enableMiddlewareTracking);
2518
+ const controller = new AbortController();
2519
+ const ctx = new ShokupanContext(request, server, void 0, this, controller.signal, this.applicationConfig.enableMiddlewareTracking);
2042
2520
  const handle = async () => {
2521
+ if (this.cpuMonitor && this.cpuMonitor.getUsage() > (this.applicationConfig.autoBackpressureLevel ?? 60)) {
2522
+ const msg = "Too Many Requests (CPU Backpressure)";
2523
+ const res = ctx.text(msg, 429);
2524
+ await this.executeHook("onResponseEnd", ctx, res);
2525
+ return res;
2526
+ }
2043
2527
  try {
2044
- if (this.applicationConfig.hooks?.onRequestStart) {
2045
- await this.applicationConfig.hooks.onRequestStart(ctx);
2528
+ if (this.hasHook("onRequestStart")) {
2529
+ await this.executeHook("onRequestStart", ctx);
2046
2530
  }
2047
2531
  const fn = this.composedMiddleware ??= compose(this.middleware);
2048
2532
  const result = await fn(ctx, async () => {
@@ -2058,23 +2542,24 @@ class Shokupan extends ShokupanRouter {
2058
2542
  response = result;
2059
2543
  } else if ((result === null || result === void 0) && ctx._finalResponse instanceof Response) {
2060
2544
  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
2545
  } else if (result === null || result === void 0) {
2066
- if (ctx._finalResponse) response = ctx._finalResponse;
2067
- else response = ctx.text("Not Found", 404);
2546
+ if (ctx._finalResponse instanceof Response) {
2547
+ response = ctx._finalResponse;
2548
+ } else if (ctx.response.status !== 200 || ctx.response.hasPopulatedHeaders) {
2549
+ response = ctx.send(null, { status: ctx.response.status, headers: ctx.response.headers });
2550
+ } else {
2551
+ response = ctx.text("Not Found", 404);
2552
+ }
2068
2553
  } else if (typeof result === "object") {
2069
2554
  response = ctx.json(result);
2070
2555
  } else {
2071
2556
  response = ctx.text(String(result));
2072
2557
  }
2073
- if (this.applicationConfig.hooks?.onRequestEnd) {
2074
- await this.applicationConfig.hooks.onRequestEnd(ctx);
2558
+ if (this.hasHook("onRequestEnd")) {
2559
+ await this.executeHook("onRequestEnd", ctx);
2075
2560
  }
2076
- if (this.applicationConfig.hooks?.onResponseStart) {
2077
- await this.applicationConfig.hooks.onResponseStart(ctx, response);
2561
+ if (this.hasHook("onResponseStart")) {
2562
+ await this.executeHook("onResponseStart", ctx, response);
2078
2563
  }
2079
2564
  return response;
2080
2565
  } catch (err) {
@@ -2084,28 +2569,21 @@ class Shokupan extends ShokupanRouter {
2084
2569
  const status = err.status || err.statusCode || 500;
2085
2570
  const body = { error: err.message || "Internal Server Error" };
2086
2571
  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
- }
2572
+ if (this.hasHook("onError")) {
2573
+ await this.executeHook("onError", err, ctx);
2093
2574
  }
2094
2575
  return ctx.json(body, status);
2095
2576
  }
2096
2577
  };
2097
2578
  let executionPromise = handle();
2098
2579
  const timeoutMs = this.applicationConfig.requestTimeout;
2099
- if (timeoutMs && timeoutMs > 0 && this.applicationConfig.hooks?.onRequestTimeout) {
2580
+ if (timeoutMs && timeoutMs > 0) {
2100
2581
  let timeoutId;
2101
2582
  const timeoutPromise = new Promise((_, reject) => {
2102
2583
  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);
2584
+ controller.abort();
2585
+ if (this.hasHook("onRequestTimeout")) {
2586
+ await this.executeHook("onRequestTimeout", ctx);
2109
2587
  }
2110
2588
  reject(new Error("Request Timeout"));
2111
2589
  }, timeoutMs);
@@ -2119,12 +2597,56 @@ class Shokupan extends ShokupanRouter {
2119
2597
  console.error("Unexpected error in request execution:", err);
2120
2598
  return ctx.text("Internal Server Error", 500);
2121
2599
  }).then(async (res) => {
2122
- if (this.applicationConfig.hooks?.onResponseEnd) {
2123
- await this.applicationConfig.hooks.onResponseEnd(ctx, res);
2600
+ if (this.hasHook("onResponseEnd")) {
2601
+ await this.executeHook("onResponseEnd", ctx, res);
2124
2602
  }
2125
2603
  return res;
2126
2604
  });
2127
2605
  }
2606
+ ensureHooksInitialized() {
2607
+ const hooks = this.applicationConfig.hooks;
2608
+ if (hooks) {
2609
+ const hookList = Array.isArray(hooks) ? hooks : [hooks];
2610
+ const hookTypes = [
2611
+ "onRequestStart",
2612
+ "onRequestEnd",
2613
+ "onResponseStart",
2614
+ "onResponseEnd",
2615
+ "onError",
2616
+ "beforeValidate",
2617
+ "afterValidate",
2618
+ "onRequestTimeout",
2619
+ "onReadTimeout",
2620
+ "onWriteTimeout"
2621
+ ];
2622
+ for (const type of hookTypes) {
2623
+ const fns = [];
2624
+ for (const h of hookList) {
2625
+ if (h[type]) fns.push(h[type]);
2626
+ }
2627
+ if (fns.length > 0) {
2628
+ this.hookCache.set(type, fns);
2629
+ }
2630
+ }
2631
+ }
2632
+ this.hooksInitialized = true;
2633
+ }
2634
+ async executeHook(name, ...args) {
2635
+ if (!this.hooksInitialized) {
2636
+ this.ensureHooksInitialized();
2637
+ }
2638
+ const fns = this.hookCache.get(name);
2639
+ if (!fns) return;
2640
+ for (const fn of fns) {
2641
+ await fn(...args);
2642
+ }
2643
+ }
2644
+ hasHook(name) {
2645
+ if (!this.hooksInitialized) {
2646
+ this.ensureHooksInitialized();
2647
+ }
2648
+ return this.hookCache.has(name);
2649
+ }
2128
2650
  }
2129
2651
  class AuthPlugin extends ShokupanRouter {
2130
2652
  constructor(authConfig) {
@@ -2329,7 +2851,7 @@ class AuthPlugin extends ShokupanRouter {
2329
2851
  /**
2330
2852
  * Middleware to verify JWT
2331
2853
  */
2332
- middleware() {
2854
+ getMiddleware() {
2333
2855
  return async (ctx, next) => {
2334
2856
  const authHeader = ctx.req.headers.get("Authorization");
2335
2857
  let token = authHeader?.startsWith("Bearer ") ? authHeader.substring(7) : null;
@@ -2350,12 +2872,16 @@ class AuthPlugin extends ShokupanRouter {
2350
2872
  }
2351
2873
  function Compression(options = {}) {
2352
2874
  const threshold = options.threshold ?? 512;
2353
- return async (ctx, next) => {
2875
+ const compressionMiddleware = async function CompressionMiddleware(ctx, next) {
2354
2876
  const acceptEncoding = ctx.headers.get("accept-encoding") || "";
2355
2877
  let method = null;
2356
2878
  if (acceptEncoding.includes("br")) method = "br";
2357
- else if (acceptEncoding.includes("zstd")) method = "zstd";
2358
- else if (acceptEncoding.includes("gzip")) method = "gzip";
2879
+ else if (acceptEncoding.includes("zstd")) {
2880
+ if (typeof Bun === "undefined") {
2881
+ throw new Error("zstd compression is only available in Bun runtime. Client requested zstd but server is running on Node.js.");
2882
+ }
2883
+ method = "zstd";
2884
+ } else if (acceptEncoding.includes("gzip")) method = "gzip";
2359
2885
  else if (acceptEncoding.includes("deflate")) method = "deflate";
2360
2886
  if (!method) return next();
2361
2887
  let response = await next();
@@ -2364,8 +2890,25 @@ function Compression(options = {}) {
2364
2890
  }
2365
2891
  if (response instanceof Response) {
2366
2892
  if (response.headers.has("Content-Encoding")) return response;
2367
- const body = await response.arrayBuffer();
2368
- if (body.byteLength < threshold) {
2893
+ let body;
2894
+ let bodySize;
2895
+ if (ctx._rawBody !== void 0) {
2896
+ if (typeof ctx._rawBody === "string") {
2897
+ const encoded = new TextEncoder().encode(ctx._rawBody);
2898
+ body = encoded.buffer;
2899
+ bodySize = encoded.byteLength;
2900
+ } else if (ctx._rawBody instanceof Uint8Array) {
2901
+ body = ctx._rawBody.buffer;
2902
+ bodySize = ctx._rawBody.byteLength;
2903
+ } else {
2904
+ body = ctx._rawBody;
2905
+ bodySize = ctx._rawBody.byteLength;
2906
+ }
2907
+ } else {
2908
+ body = await response.arrayBuffer();
2909
+ bodySize = body.byteLength;
2910
+ }
2911
+ if (bodySize < threshold) {
2369
2912
  return new Response(body, {
2370
2913
  status: response.status,
2371
2914
  statusText: response.statusText,
@@ -2375,10 +2918,9 @@ function Compression(options = {}) {
2375
2918
  let compressed;
2376
2919
  switch (method) {
2377
2920
  case "br":
2378
- const zlib = require("node:zlib");
2379
- compressed = await new Promise((res, rej) => zlib.brotliCompress(body, {
2921
+ compressed = await new Promise((res, rej) => zlib__namespace.brotliCompress(body, {
2380
2922
  params: {
2381
- [zlib.constants.BROTLI_PARAM_QUALITY]: 4
2923
+ [zlib__namespace.constants.BROTLI_PARAM_QUALITY]: 4
2382
2924
  }
2383
2925
  }, (err, data) => {
2384
2926
  if (err) return rej(err);
@@ -2386,13 +2928,19 @@ function Compression(options = {}) {
2386
2928
  }));
2387
2929
  break;
2388
2930
  case "gzip":
2389
- compressed = Bun.gzipSync(body);
2931
+ compressed = await new Promise((res, rej) => zlib__namespace.gzip(body, (err, data) => {
2932
+ if (err) return rej(err);
2933
+ res(data);
2934
+ }));
2390
2935
  break;
2391
2936
  case "zstd":
2392
2937
  compressed = await Bun.zstdCompress(body);
2393
2938
  break;
2394
2939
  default:
2395
- compressed = Bun.deflateSync(body);
2940
+ compressed = await new Promise((res, rej) => zlib__namespace.deflate(body, (err, data) => {
2941
+ if (err) return rej(err);
2942
+ res(data);
2943
+ }));
2396
2944
  break;
2397
2945
  }
2398
2946
  const headers = new Headers(response.headers);
@@ -2406,6 +2954,9 @@ function Compression(options = {}) {
2406
2954
  }
2407
2955
  return response;
2408
2956
  };
2957
+ compressionMiddleware.isBuiltin = true;
2958
+ compressionMiddleware.pluginName = "Compression";
2959
+ return compressionMiddleware;
2409
2960
  }
2410
2961
  function Cors(options = {}) {
2411
2962
  const defaults2 = {
@@ -2415,7 +2966,7 @@ function Cors(options = {}) {
2415
2966
  optionsSuccessStatus: 204
2416
2967
  };
2417
2968
  const opts = { ...defaults2, ...options };
2418
- return async (ctx, next) => {
2969
+ const corsMiddleware = async function CorsMiddleware(ctx, next) {
2419
2970
  const headers = new Headers();
2420
2971
  const origin = ctx.headers.get("origin");
2421
2972
  const set = (k, v) => headers.set(k, v);
@@ -2477,6 +3028,9 @@ function Cors(options = {}) {
2477
3028
  }
2478
3029
  return response;
2479
3030
  };
3031
+ corsMiddleware.isBuiltin = true;
3032
+ corsMiddleware.pluginName = "Cors";
3033
+ return corsMiddleware;
2480
3034
  }
2481
3035
  function useExpress(expressMiddleware) {
2482
3036
  return async (ctx, next) => {
@@ -2644,7 +3198,33 @@ const safelyGetBody = async (ctx) => {
2644
3198
  return {};
2645
3199
  }
2646
3200
  };
3201
+ function getValidator(schema) {
3202
+ if (isZod(schema)) {
3203
+ return (data) => validateZod(schema, data);
3204
+ }
3205
+ if (isTypeBox(schema)) {
3206
+ return (data) => validateTypeBox(schema, data);
3207
+ }
3208
+ if (isAjv(schema)) {
3209
+ return (data) => validateAjv(schema, data);
3210
+ }
3211
+ if (isValibotWrapper(schema)) {
3212
+ return (data) => validateValibotWrapper(schema, data);
3213
+ }
3214
+ if (isClass(schema)) {
3215
+ return (data) => validateClassValidator(schema, data);
3216
+ }
3217
+ if (typeof schema === "function") {
3218
+ return schema;
3219
+ }
3220
+ throw new Error("Unknown validator type provided. Please use a supported library (Zod, Ajv, TypeBox) or a custom function.");
3221
+ }
2647
3222
  function validate(config) {
3223
+ const validators = {};
3224
+ if (config.params) validators.params = getValidator(config.params);
3225
+ if (config.query) validators.query = getValidator(config.query);
3226
+ if (config.headers) validators.headers = getValidator(config.headers);
3227
+ if (config.body) validators.body = getValidator(config.body);
2648
3228
  return async (ctx, next) => {
2649
3229
  const dataToValidate = {};
2650
3230
  if (config.params) dataToValidate.params = ctx.params;
@@ -2663,21 +3243,21 @@ function validate(config) {
2663
3243
  if (ctx.app?.applicationConfig.hooks?.beforeValidate) {
2664
3244
  await ctx.app.applicationConfig.hooks.beforeValidate(ctx, dataToValidate);
2665
3245
  }
2666
- if (config.params) {
2667
- ctx.params = await runValidation(config.params, ctx.params);
3246
+ if (validators.params) {
3247
+ ctx.params = await validators.params(ctx.params);
2668
3248
  }
2669
3249
  let validQuery;
2670
- if (config.query && queryObj) {
2671
- validQuery = await runValidation(config.query, queryObj);
3250
+ if (validators.query && queryObj) {
3251
+ validQuery = await validators.query(queryObj);
2672
3252
  }
2673
- if (config.headers) {
3253
+ if (validators.headers) {
2674
3254
  const headersObj = Object.fromEntries(ctx.req.headers.entries());
2675
- await runValidation(config.headers, headersObj);
3255
+ await validators.headers(headersObj);
2676
3256
  }
2677
3257
  let validBody;
2678
- if (config.body) {
3258
+ if (validators.body) {
2679
3259
  const b = body ?? await safelyGetBody(ctx);
2680
- validBody = await runValidation(config.body, b);
3260
+ validBody = await validators.body(b);
2681
3261
  const req = ctx.req;
2682
3262
  req._bodyValue = validBody;
2683
3263
  Object.defineProperty(req, "json", {
@@ -2696,36 +3276,6 @@ function validate(config) {
2696
3276
  return next();
2697
3277
  };
2698
3278
  }
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
3279
  const ajv = new Ajv({ coerceTypes: true, allErrors: true });
2730
3280
  addFormats(ajv);
2731
3281
  const compiledValidators = /* @__PURE__ */ new WeakMap();
@@ -2740,17 +3290,18 @@ function openApiValidator() {
2740
3290
  cache = compileValidators(app.openApiSpec);
2741
3291
  compiledValidators.set(app, cache);
2742
3292
  }
2743
- const method = ctx.req.method.toLowerCase();
2744
3293
  let matchPath;
2745
- if (cache.has(ctx.path)) {
3294
+ let matchParams = {};
3295
+ if (cache.validators.has(ctx.path)) {
2746
3296
  matchPath = ctx.path;
2747
3297
  } else {
2748
- for (const specPath of cache.keys()) {
2749
- const regexStr = "^" + specPath.replace(/{([^}]+)}/g, "([^/]+)") + "$";
2750
- const regex = new RegExp(regexStr);
3298
+ for (const [path2, { regex, paramNames }] of cache.paths) {
2751
3299
  const match = regex.exec(ctx.path);
2752
3300
  if (match) {
2753
- matchPath = specPath;
3301
+ matchPath = path2;
3302
+ paramNames.forEach((name, i) => {
3303
+ matchParams[name] = match[i + 1];
3304
+ });
2754
3305
  break;
2755
3306
  }
2756
3307
  }
@@ -2758,7 +3309,8 @@ function openApiValidator() {
2758
3309
  if (!matchPath) {
2759
3310
  return next();
2760
3311
  }
2761
- const validators = cache.get(matchPath)?.[method];
3312
+ const method = ctx.req.method.toLowerCase();
3313
+ const validators = cache.validators.get(matchPath)?.[method];
2762
3314
  if (!validators) {
2763
3315
  return next();
2764
3316
  }
@@ -2783,21 +3335,7 @@ function openApiValidator() {
2783
3335
  }
2784
3336
  }
2785
3337
  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
- }
3338
+ const params = { ...matchParams, ...ctx.params };
2801
3339
  const valid = validators.params(params);
2802
3340
  if (!valid && validators.params.errors) {
2803
3341
  errors.push(...validators.params.errors.map((e) => ({ ...e, location: "path" })));
@@ -2817,15 +3355,27 @@ function openApiValidator() {
2817
3355
  };
2818
3356
  }
2819
3357
  function compileValidators(spec) {
2820
- const cache = /* @__PURE__ */ new Map();
3358
+ const validators = /* @__PURE__ */ new Map();
3359
+ const paths = /* @__PURE__ */ new Map();
2821
3360
  for (const [path2, pathItem] of Object.entries(spec.paths || {})) {
3361
+ if (path2.includes("{")) {
3362
+ const paramNames = [];
3363
+ const regexStr = "^" + path2.replace(/{([^}]+)}/g, (_, name) => {
3364
+ paramNames.push(name);
3365
+ return "([^/]+)";
3366
+ }) + "$";
3367
+ paths.set(path2, {
3368
+ regex: new RegExp(regexStr),
3369
+ paramNames
3370
+ });
3371
+ }
2822
3372
  const pathValidators = {};
2823
3373
  for (const [method, operation] of Object.entries(pathItem)) {
2824
3374
  if (method === "parameters" || method === "summary" || method === "description") continue;
2825
3375
  const oper = operation;
2826
- const validators = {};
3376
+ const opValidators = {};
2827
3377
  if (oper.requestBody?.content?.["application/json"]?.schema) {
2828
- validators.body = ajv.compile(oper.requestBody.content["application/json"].schema);
3378
+ opValidators.body = ajv.compile(oper.requestBody.content["application/json"].schema);
2829
3379
  }
2830
3380
  const parameters = [...oper.parameters || [], ...pathItem.parameters || []];
2831
3381
  const queryProps = {};
@@ -2847,85 +3397,41 @@ function compileValidators(spec) {
2847
3397
  }
2848
3398
  }
2849
3399
  if (Object.keys(queryProps).length > 0) {
2850
- validators.query = ajv.compile({
3400
+ opValidators.query = ajv.compile({
2851
3401
  type: "object",
2852
3402
  properties: queryProps,
2853
3403
  required: queryRequired.length > 0 ? queryRequired : void 0
2854
3404
  });
2855
3405
  }
2856
3406
  if (Object.keys(pathProps).length > 0) {
2857
- validators.params = ajv.compile({
3407
+ opValidators.params = ajv.compile({
2858
3408
  type: "object",
2859
3409
  properties: pathProps,
2860
3410
  required: pathRequired.length > 0 ? pathRequired : void 0
2861
3411
  });
2862
3412
  }
2863
3413
  if (Object.keys(headerProps).length > 0) {
2864
- validators.headers = ajv.compile({
3414
+ opValidators.headers = ajv.compile({
2865
3415
  type: "object",
2866
3416
  properties: headerProps,
2867
3417
  required: headerRequired.length > 0 ? headerRequired : void 0
2868
3418
  });
2869
3419
  }
2870
- pathValidators[method] = validators;
3420
+ pathValidators[method] = opValidators;
2871
3421
  }
2872
- cache.set(path2, pathValidators);
3422
+ validators.set(path2, pathValidators);
2873
3423
  }
2874
- return cache;
3424
+ return { paths, validators };
2875
3425
  }
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";
3426
+ function precompileValidators(app, spec) {
3427
+ const cache = compileValidators(spec);
3428
+ compiledValidators.set(app, cache);
3429
+ }
3430
+ function enableOpenApiValidation(app) {
3431
+ app.use(openApiValidator());
3432
+ app.onSpecAvailable((spec) => {
3433
+ precompileValidators(app, spec);
2884
3434
  });
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
3435
  }
2930
3436
  const eta = new eta$2.Eta();
2931
3437
  class ScalarPlugin extends ShokupanRouter {
@@ -3002,7 +3508,7 @@ class ScalarPlugin extends ShokupanRouter {
3002
3508
  }
3003
3509
  }
3004
3510
  function SecurityHeaders(options = {}) {
3005
- return async (ctx, next) => {
3511
+ const securityHeadersMiddleware = async function SecurityHeadersMiddleware(ctx, next) {
3006
3512
  const headers = {};
3007
3513
  const set = (k, v) => headers[k] = v;
3008
3514
  if (options.dnsPrefetchControl !== false) {
@@ -3056,6 +3562,9 @@ function SecurityHeaders(options = {}) {
3056
3562
  }
3057
3563
  return response;
3058
3564
  };
3565
+ securityHeadersMiddleware.isBuiltin = true;
3566
+ securityHeadersMiddleware.pluginName = "SecurityHeaders";
3567
+ return securityHeadersMiddleware;
3059
3568
  }
3060
3569
  class Cookie {
3061
3570
  maxAge;
@@ -3169,7 +3678,7 @@ function Session(options) {
3169
3678
  const resave = options.resave === void 0 ? true : options.resave;
3170
3679
  const saveUninitialized = options.saveUninitialized === void 0 ? true : options.saveUninitialized;
3171
3680
  const rolling = options.rolling || false;
3172
- return async (ctx, next) => {
3681
+ const sessionMiddleware = async function SessionMiddleware(ctx, next) {
3173
3682
  let reqSessionId = null;
3174
3683
  const cookieHeader = ctx.req.headers.get("cookie");
3175
3684
  const cookies = {};
@@ -3305,6 +3814,9 @@ function Session(options) {
3305
3814
  }
3306
3815
  return result;
3307
3816
  };
3817
+ sessionMiddleware.isBuiltin = true;
3818
+ sessionMiddleware.pluginName = "Session";
3819
+ return sessionMiddleware;
3308
3820
  }
3309
3821
  exports.$appRoot = $appRoot;
3310
3822
  exports.$childControllers = $childControllers;
@@ -3344,6 +3856,7 @@ exports.Post = Post;
3344
3856
  exports.Put = Put;
3345
3857
  exports.Query = Query;
3346
3858
  exports.RateLimit = RateLimit;
3859
+ exports.RateLimitMiddleware = RateLimitMiddleware;
3347
3860
  exports.Req = Req;
3348
3861
  exports.RouteParamType = RouteParamType;
3349
3862
  exports.RouterRegistry = RouterRegistry;
@@ -3359,8 +3872,11 @@ exports.ShokupanRouter = ShokupanRouter;
3359
3872
  exports.Spec = Spec;
3360
3873
  exports.Use = Use;
3361
3874
  exports.ValidationError = ValidationError;
3875
+ exports.compileValidators = compileValidators;
3362
3876
  exports.compose = compose;
3877
+ exports.enableOpenApiValidation = enableOpenApiValidation;
3363
3878
  exports.openApiValidator = openApiValidator;
3879
+ exports.precompileValidators = precompileValidators;
3364
3880
  exports.useExpress = useExpress;
3365
3881
  exports.valibot = valibot;
3366
3882
  exports.validate = validate;