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.js CHANGED
@@ -1,15 +1,20 @@
1
+ import { readFile } from "node:fs/promises";
1
2
  import { Eta } from "eta";
2
- import { stat, readdir } from "fs/promises";
3
+ import { stat, readdir, readFile as readFile$1 } from "fs/promises";
3
4
  import { resolve, join, basename } from "path";
4
5
  import { AsyncLocalStorage } from "node:async_hooks";
6
+ import { createNodeEngines } from "@surrealdb/node";
7
+ import { Surreal, RecordId } from "surrealdb";
5
8
  import { trace, SpanKind, SpanStatusCode, context } from "@opentelemetry/api";
9
+ import * as os from "node:os";
6
10
  import { OAuth2Client, Okta, Auth0, Apple, MicrosoftEntraId, Google, GitHub, generateState, generateCodeVerifier } from "arctic";
7
11
  import * as jose from "jose";
12
+ import * as zlib from "node:zlib";
8
13
  import Ajv from "ajv";
9
14
  import addFormats from "ajv-formats";
10
15
  import { plainToInstance } from "class-transformer";
11
16
  import { validateOrReject } from "class-validator";
12
- import { OpenAPIAnalyzer } from "./openapi-analyzer-BTExMLX4.js";
17
+ import { OpenAPIAnalyzer } from "./openapi-analyzer-BtIaHIfe.js";
13
18
  import { randomUUID, createHmac } from "crypto";
14
19
  import { EventEmitter } from "events";
15
20
  class ShokupanResponse {
@@ -76,10 +81,12 @@ class ShokupanResponse {
76
81
  }
77
82
  }
78
83
  class ShokupanContext {
79
- constructor(request, server, state, app, enableMiddlewareTracking = false) {
84
+ // Raw body for compression optimization
85
+ constructor(request, server, state, app, signal, enableMiddlewareTracking = false) {
80
86
  this.request = request;
81
87
  this.server = server;
82
88
  this.app = app;
89
+ this.signal = signal;
83
90
  this.state = state || {};
84
91
  if (enableMiddlewareTracking) {
85
92
  const self = this;
@@ -103,7 +110,9 @@ class ShokupanContext {
103
110
  state;
104
111
  handlerStack = [];
105
112
  response;
113
+ _debug;
106
114
  _finalResponse;
115
+ _rawBody;
107
116
  get url() {
108
117
  if (!this._url) {
109
118
  const urlString = this.request.url || "http://localhost/";
@@ -152,7 +161,17 @@ class ShokupanContext {
152
161
  * Request query params
153
162
  */
154
163
  get query() {
155
- return Object.fromEntries(this.url.searchParams);
164
+ const q = {};
165
+ for (const [key, value] of this.url.searchParams) {
166
+ if (q[key] === void 0) {
167
+ q[key] = value;
168
+ } else if (Array.isArray(q[key])) {
169
+ q[key].push(value);
170
+ } else {
171
+ q[key] = [q[key], value];
172
+ }
173
+ }
174
+ return q;
156
175
  }
157
176
  /**
158
177
  * Client IP address
@@ -227,17 +246,36 @@ class ShokupanContext {
227
246
  setCookie(name, value, options = {}) {
228
247
  let cookie = `${encodeURIComponent(name)}=${encodeURIComponent(value)}`;
229
248
  if (options.maxAge) cookie += `; Max-Age=${Math.floor(options.maxAge)}`;
249
+ if (options.domain) cookie += `; Domain=${options.domain}`;
250
+ if (options.path) cookie += `; Path=${options.path || "/"}`;
230
251
  if (options.expires) cookie += `; Expires=${options.expires.toUTCString()}`;
231
252
  if (options.httpOnly) cookie += `; HttpOnly`;
232
253
  if (options.secure) cookie += `; Secure`;
233
- if (options.domain) cookie += `; Domain=${options.domain}`;
234
- if (options.path) cookie += `; Path=${options.path || "/"}`;
235
- if (options.sameSite) {
236
- typeof options.sameSite === "string" ? options.sameSite.toLowerCase() : options.sameSite ? "strict" : "lax";
237
- cookie += `; SameSite=${typeof options.sameSite === "boolean" ? "Strict" : options.sameSite.charAt(0).toUpperCase() + options.sameSite.slice(1)}`;
254
+ let sameSite = options.sameSite;
255
+ if (sameSite === true) sameSite = "Strict";
256
+ if (sameSite === void 0 || sameSite === false) ;
257
+ else {
258
+ const stringSameSite = typeof sameSite === "string" ? sameSite.toLowerCase() : sameSite;
259
+ switch (stringSameSite) {
260
+ case "lax":
261
+ cookie += "; SameSite=Lax";
262
+ break;
263
+ case "strict":
264
+ cookie += "; SameSite=Strict";
265
+ break;
266
+ case "none":
267
+ cookie += "; SameSite=None";
268
+ break;
269
+ default:
270
+ cookie += "; SameSite=Lax";
271
+ break;
272
+ }
238
273
  }
239
274
  if (options.priority) {
240
- cookie += `; Priority=${options.priority.charAt(0).toUpperCase() + options.priority.slice(1)}`;
275
+ const p = options.priority.toLowerCase();
276
+ if (p === "low") cookie += "; Priority=Low";
277
+ else if (p === "medium") cookie += "; Priority=Medium";
278
+ else if (p === "high") cookie += "; Priority=High";
241
279
  }
242
280
  this.response.append("Set-Cookie", cookie);
243
281
  return this;
@@ -274,6 +312,9 @@ class ShokupanContext {
274
312
  send(body, options) {
275
313
  const headers = this.mergeHeaders(options?.headers);
276
314
  const status = options?.status ?? this.response.status;
315
+ if (typeof body === "string" || body instanceof ArrayBuffer || body instanceof Uint8Array) {
316
+ this._rawBody = body;
317
+ }
277
318
  this._finalResponse = new Response(body, { status, headers });
278
319
  return this._finalResponse;
279
320
  }
@@ -281,11 +322,11 @@ class ShokupanContext {
281
322
  * Read request body
282
323
  */
283
324
  async body() {
284
- const contentType = this.request.headers.get("content-type");
285
- if (contentType?.includes("application/json")) {
325
+ const contentType = this.request.headers.get("content-type") || "";
326
+ if (contentType.includes("application/json") || contentType.includes("+json")) {
286
327
  return this.request.json();
287
328
  }
288
- if (contentType?.includes("multipart/form-data") || contentType?.includes("application/x-www-form-urlencoded")) {
329
+ if (contentType.includes("multipart/form-data") || contentType.includes("application/x-www-form-urlencoded")) {
289
330
  return this.request.formData();
290
331
  }
291
332
  return this.request.text();
@@ -296,6 +337,7 @@ class ShokupanContext {
296
337
  json(data, status, headers) {
297
338
  const finalStatus = status ?? this.response.status;
298
339
  const jsonString = JSON.stringify(data);
340
+ this._rawBody = jsonString;
299
341
  if (!headers && !this.response.hasPopulatedHeaders) {
300
342
  this._finalResponse = new Response(jsonString, {
301
343
  status: finalStatus,
@@ -313,15 +355,16 @@ class ShokupanContext {
313
355
  */
314
356
  text(data, status, headers) {
315
357
  const finalStatus = status ?? this.response.status;
358
+ this._rawBody = data;
316
359
  if (!headers && !this.response.hasPopulatedHeaders) {
317
360
  this._finalResponse = new Response(data, {
318
361
  status: finalStatus,
319
- headers: { "content-type": "text/plain" }
362
+ headers: { "content-type": "text/plain; charset=utf-8" }
320
363
  });
321
364
  return this._finalResponse;
322
365
  }
323
366
  const finalHeaders = this.mergeHeaders(headers);
324
- finalHeaders.set("content-type", "text/plain");
367
+ finalHeaders.set("content-type", "text/plain; charset=utf-8");
325
368
  this._finalResponse = new Response(data, { status: finalStatus, headers: finalHeaders });
326
369
  return this._finalResponse;
327
370
  }
@@ -329,9 +372,10 @@ class ShokupanContext {
329
372
  * Respond with HTML content
330
373
  */
331
374
  html(html, status, headers) {
332
- const finalHeaders = this.mergeHeaders(headers);
333
- finalHeaders.set("content-type", "text/html");
334
375
  const finalStatus = status ?? this.response.status;
376
+ const finalHeaders = this.mergeHeaders(headers);
377
+ finalHeaders.set("content-type", "text/html; charset=utf-8");
378
+ this._rawBody = html;
335
379
  this._finalResponse = new Response(html, { status: finalStatus, headers: finalHeaders });
336
380
  return this._finalResponse;
337
381
  }
@@ -356,11 +400,20 @@ class ShokupanContext {
356
400
  /**
357
401
  * Respond with a file
358
402
  */
359
- file(path, fileOptions, responseOptions) {
403
+ async file(path, fileOptions, responseOptions) {
360
404
  const headers = this.mergeHeaders(responseOptions?.headers);
361
405
  const status = responseOptions?.status ?? this.response.status;
362
- this._finalResponse = new Response(Bun.file(path, fileOptions), { status, headers });
363
- return this._finalResponse;
406
+ if (typeof Bun !== "undefined") {
407
+ this._finalResponse = new Response(Bun.file(path, fileOptions), { status, headers });
408
+ return this._finalResponse;
409
+ } else {
410
+ const fileBuffer = await readFile(path);
411
+ if (fileOptions?.type) {
412
+ headers.set("content-type", fileOptions.type);
413
+ }
414
+ this._finalResponse = new Response(fileBuffer, { status, headers });
415
+ return this._finalResponse;
416
+ }
364
417
  }
365
418
  /**
366
419
  * JSX Rendering Function
@@ -380,6 +433,74 @@ class ShokupanContext {
380
433
  return this.html(html, status, headers);
381
434
  }
382
435
  }
436
+ function RateLimitMiddleware(options = {}) {
437
+ const windowMs = options.windowMs || 60 * 1e3;
438
+ const max = options.limit || options.max || 5;
439
+ const message = options.message || "Too many requests, please try again later.";
440
+ const statusCode = options.statusCode || 429;
441
+ const headers = options.headers !== false;
442
+ const mode = options.mode || "user";
443
+ const keyGenerator = options.keyGenerator || ((ctx) => {
444
+ if (mode === "absolute") {
445
+ return "global";
446
+ }
447
+ return ctx.headers.get("x-forwarded-for") || ctx.request.headers.get("x-forwarded-for") || ctx.server?.requestIP?.(ctx.request)?.address || "unknown";
448
+ });
449
+ const skip = options.skip || (() => false);
450
+ const hits = /* @__PURE__ */ new Map();
451
+ const interval = setInterval(() => {
452
+ const now = Date.now();
453
+ for (const [key, record] of hits.entries()) {
454
+ if (record.resetTime <= now) {
455
+ hits.delete(key);
456
+ }
457
+ }
458
+ }, windowMs);
459
+ if (interval.unref) interval.unref();
460
+ const rateLimitMiddleware = async function RateLimitMiddleware2(ctx, next) {
461
+ if (skip(ctx)) return next();
462
+ const key = keyGenerator(ctx);
463
+ const now = Date.now();
464
+ let record = hits.get(key);
465
+ if (!record || record.resetTime <= now) {
466
+ record = {
467
+ hits: 0,
468
+ resetTime: now + windowMs
469
+ };
470
+ hits.set(key, record);
471
+ }
472
+ record.hits++;
473
+ const remaining = Math.max(0, max - record.hits);
474
+ const resetTime = Math.ceil(record.resetTime / 1e3);
475
+ const retryAfter = Math.ceil((record.resetTime - now) / 1e3);
476
+ const setHeaders = (res) => {
477
+ if (!headers || !res || !res.headers) return;
478
+ try {
479
+ res.headers.set("X-RateLimit-Limit", String(max));
480
+ res.headers.set("X-RateLimit-Remaining", String(remaining));
481
+ res.headers.set("X-RateLimit-Reset", String(resetTime));
482
+ } catch (e) {
483
+ }
484
+ };
485
+ if (record.hits > max) {
486
+ typeof message === "object" ? JSON.stringify(message) : String(message);
487
+ const res = typeof message === "object" ? ctx.json(message, statusCode) : ctx.text(String(message), statusCode);
488
+ if (headers) {
489
+ setHeaders(res);
490
+ res.headers.set("Retry-After", String(retryAfter));
491
+ }
492
+ return res;
493
+ }
494
+ const response = await next();
495
+ if (response instanceof Response && headers) {
496
+ setHeaders(response);
497
+ }
498
+ return response;
499
+ };
500
+ rateLimitMiddleware.isBuiltin = true;
501
+ rateLimitMiddleware.pluginName = "RateLimit";
502
+ return rateLimitMiddleware;
503
+ }
383
504
  const $isApplication = /* @__PURE__ */ Symbol.for("Shokupan.app");
384
505
  const $appRoot = /* @__PURE__ */ Symbol.for("Shokupan.app-root");
385
506
  const $isMounted = /* @__PURE__ */ Symbol("Shokupan.isMounted");
@@ -476,6 +597,9 @@ const Patch = createMethodDecorator("PATCH");
476
597
  const Options = createMethodDecorator("OPTIONS");
477
598
  const Head = createMethodDecorator("HEAD");
478
599
  const All = createMethodDecorator("ALL");
600
+ function RateLimit(options) {
601
+ return Use(RateLimitMiddleware(options));
602
+ }
479
603
  class Container {
480
604
  static services = /* @__PURE__ */ new Map();
481
605
  static register(target, instance) {
@@ -517,17 +641,31 @@ const compose = (middleware) => {
517
641
  }
518
642
  return function dispatch(context2, next) {
519
643
  let index = -1;
520
- function runner(i) {
644
+ async function runner(i) {
521
645
  if (i <= index) return Promise.reject(new Error("next() called multiple times"));
522
646
  index = i;
523
647
  if (i >= middleware.length) {
524
648
  return next ? next() : Promise.resolve();
525
649
  }
526
650
  const fn = middleware[i];
651
+ if (!context2._debug) {
652
+ return fn(context2, () => runner(i + 1));
653
+ }
654
+ const debug = context2._debug;
655
+ const debugId = fn._debugId || fn.name || "anonymous";
656
+ const previousNode = debug.getCurrentNode();
657
+ debug.trackEdge(previousNode, debugId);
658
+ debug.setNode(debugId);
659
+ const start = performance.now();
527
660
  try {
528
- return Promise.resolve(fn(context2, () => runner(i + 1)));
661
+ const res = await Promise.resolve(fn(context2, () => runner(i + 1)));
662
+ debug.trackStep(debugId, "middleware", performance.now() - start, "success");
663
+ return res;
529
664
  } catch (err) {
665
+ debug.trackStep(debugId, "middleware", performance.now() - start, "error", err);
530
666
  return Promise.reject(err);
667
+ } finally {
668
+ if (previousNode) debug.setNode(previousNode);
531
669
  }
532
670
  }
533
671
  return runner(0);
@@ -589,6 +727,15 @@ function deepMerge(target, ...sources) {
589
727
  }
590
728
  return deepMerge(target, ...sources);
591
729
  }
730
+ const REGEX_QUERY_INT = /parseInt\(ctx\.query\.(\w+)\)/g;
731
+ const REGEX_QUERY_FLOAT = /parseFloat\(ctx\.query\.(\w+)\)/g;
732
+ const REGEX_QUERY_NUMBER = /Number\(ctx\.query\.(\w+)\)/g;
733
+ const REGEX_QUERY_BOOL = /(?:Boolean\(ctx\.query\.(\w+)\)|!+ctx\.query\.(\w+))/g;
734
+ const REGEX_QUERY_GENERIC = /ctx\.query\.(\w+)/g;
735
+ const REGEX_PARAM_INT = /parseInt\(ctx\.params\.(\w+)\)/g;
736
+ const REGEX_PARAM_FLOAT = /parseFloat\(ctx\.params\.(\w+)\)/g;
737
+ const REGEX_HEADER_GET = /ctx\.get\(['"](\w+)['"]\)/g;
738
+ const REGEX_ERROR_STATUS = /ctx\.(?:json|text|html)\([^)]+,\s*(\d{3,})\)/g;
592
739
  function analyzeHandler(handler) {
593
740
  const handlerSource = handler.toString();
594
741
  const inferredSpec = {};
@@ -598,46 +745,28 @@ function analyzeHandler(handler) {
598
745
  };
599
746
  }
600
747
  const queryParams = /* @__PURE__ */ new Map();
601
- const queryIntMatch = handlerSource.match(/parseInt\(ctx\.query\.(\w+)\)/g);
602
- if (queryIntMatch) {
603
- queryIntMatch.forEach((match) => {
604
- const paramName = match.match(/ctx\.query\.(\w+)/)?.[1];
605
- if (paramName) queryParams.set(paramName, { type: "integer", format: "int32" });
606
- });
748
+ for (const match of handlerSource.matchAll(REGEX_QUERY_INT)) {
749
+ if (match[1]) queryParams.set(match[1], { type: "integer", format: "int32" });
607
750
  }
608
- const queryFloatMatch = handlerSource.match(/parseFloat\(ctx\.query\.(\w+)\)/g);
609
- if (queryFloatMatch) {
610
- queryFloatMatch.forEach((match) => {
611
- const paramName = match.match(/ctx\.query\.(\w+)/)?.[1];
612
- if (paramName) queryParams.set(paramName, { type: "number", format: "float" });
613
- });
751
+ for (const match of handlerSource.matchAll(REGEX_QUERY_FLOAT)) {
752
+ if (match[1]) queryParams.set(match[1], { type: "number", format: "float" });
614
753
  }
615
- const queryNumberMatch = handlerSource.match(/Number\(ctx\.query\.(\w+)\)/g);
616
- if (queryNumberMatch) {
617
- queryNumberMatch.forEach((match) => {
618
- const paramName = match.match(/ctx\.query\.(\w+)/)?.[1];
619
- if (paramName && !queryParams.has(paramName)) {
620
- queryParams.set(paramName, { type: "number" });
621
- }
622
- });
754
+ for (const match of handlerSource.matchAll(REGEX_QUERY_NUMBER)) {
755
+ if (match[1] && !queryParams.has(match[1])) {
756
+ queryParams.set(match[1], { type: "number" });
757
+ }
623
758
  }
624
- const queryBoolMatch = handlerSource.match(/(?:Boolean\(ctx\.query\.(\w+)\)|!+ctx\.query\.(\w+))/g);
625
- if (queryBoolMatch) {
626
- queryBoolMatch.forEach((match) => {
627
- const paramName = match.match(/ctx\.query\.(\w+)/)?.[1];
628
- if (paramName && !queryParams.has(paramName)) {
629
- queryParams.set(paramName, { type: "boolean" });
630
- }
631
- });
759
+ for (const match of handlerSource.matchAll(REGEX_QUERY_BOOL)) {
760
+ const name = match[1] || match[2];
761
+ if (name && !queryParams.has(name)) {
762
+ queryParams.set(name, { type: "boolean" });
763
+ }
632
764
  }
633
- const queryMatch = handlerSource.match(/ctx\.query\.(\w+)/g);
634
- if (queryMatch) {
635
- queryMatch.forEach((match) => {
636
- const paramName = match.split(".")[2];
637
- if (paramName && !queryParams.has(paramName)) {
638
- queryParams.set(paramName, { type: "string" });
639
- }
640
- });
765
+ for (const match of handlerSource.matchAll(REGEX_QUERY_GENERIC)) {
766
+ const name = match[1];
767
+ if (name && !queryParams.has(name)) {
768
+ queryParams.set(name, { type: "string" });
769
+ }
641
770
  }
642
771
  if (queryParams.size > 0) {
643
772
  if (!inferredSpec.parameters) inferredSpec.parameters = [];
@@ -650,19 +779,11 @@ function analyzeHandler(handler) {
650
779
  });
651
780
  }
652
781
  const pathParams = /* @__PURE__ */ new Map();
653
- const paramIntMatch = handlerSource.match(/parseInt\(ctx\.params\.(\w+)\)/g);
654
- if (paramIntMatch) {
655
- paramIntMatch.forEach((match) => {
656
- const paramName = match.match(/ctx\.params\.(\w+)/)?.[1];
657
- if (paramName) pathParams.set(paramName, { type: "integer", format: "int32" });
658
- });
782
+ for (const match of handlerSource.matchAll(REGEX_PARAM_INT)) {
783
+ if (match[1]) pathParams.set(match[1], { type: "integer", format: "int32" });
659
784
  }
660
- const paramFloatMatch = handlerSource.match(/parseFloat\(ctx\.params\.(\w+)\)/g);
661
- if (paramFloatMatch) {
662
- paramFloatMatch.forEach((match) => {
663
- const paramName = match.match(/ctx\.params\.(\w+)/)?.[1];
664
- if (paramName) pathParams.set(paramName, { type: "number", format: "float" });
665
- });
785
+ for (const match of handlerSource.matchAll(REGEX_PARAM_FLOAT)) {
786
+ if (match[1]) pathParams.set(match[1], { type: "number", format: "float" });
666
787
  }
667
788
  if (pathParams.size > 0) {
668
789
  if (!inferredSpec.parameters) inferredSpec.parameters = [];
@@ -675,76 +796,55 @@ function analyzeHandler(handler) {
675
796
  });
676
797
  });
677
798
  }
678
- const headerMatch = handlerSource.match(/ctx\.get\(['"](\w+)['"]\)/g);
679
- if (headerMatch) {
680
- if (!inferredSpec.parameters) inferredSpec.parameters = [];
681
- headerMatch.forEach((match) => {
682
- const headerName = match.match(/['"](\w+)['"]/)?.[1];
683
- if (headerName) {
684
- inferredSpec.parameters.push({
685
- name: headerName,
686
- in: "header",
687
- schema: { type: "string" }
688
- });
689
- }
690
- });
799
+ for (const match of handlerSource.matchAll(REGEX_HEADER_GET)) {
800
+ if (match[1]) {
801
+ if (!inferredSpec.parameters) inferredSpec.parameters = [];
802
+ inferredSpec.parameters.push({
803
+ name: match[1],
804
+ in: "header",
805
+ schema: { type: "string" }
806
+ });
807
+ }
691
808
  }
692
809
  const responses = {};
693
810
  if (handlerSource.includes("ctx.json(")) {
694
811
  responses["200"] = {
695
812
  description: "Successful response",
696
- content: {
697
- "application/json": { schema: { type: "object" } }
698
- }
813
+ content: { "application/json": { schema: { type: "object" } } }
699
814
  };
700
815
  }
701
816
  if (handlerSource.includes("ctx.html(")) {
702
817
  responses["200"] = {
703
818
  description: "Successful response",
704
- content: {
705
- "text/html": { schema: { type: "string" } }
706
- }
819
+ content: { "text/html": { schema: { type: "string" } } }
707
820
  };
708
821
  }
709
822
  if (handlerSource.includes("ctx.text(")) {
710
823
  responses["200"] = {
711
824
  description: "Successful response",
712
- content: {
713
- "text/plain": { schema: { type: "string" } }
714
- }
825
+ content: { "text/plain": { schema: { type: "string" } } }
715
826
  };
716
827
  }
717
828
  if (handlerSource.includes("ctx.file(")) {
718
829
  responses["200"] = {
719
830
  description: "File download",
720
- content: {
721
- "application/octet-stream": { schema: { type: "string", format: "binary" } }
722
- }
831
+ content: { "application/octet-stream": { schema: { type: "string", format: "binary" } } }
723
832
  };
724
833
  }
725
834
  if (handlerSource.includes("ctx.redirect(")) {
726
- responses["302"] = {
727
- description: "Redirect"
728
- };
835
+ responses["302"] = { description: "Redirect" };
729
836
  }
730
837
  if (!responses["200"] && /return\s+\{/.test(handlerSource)) {
731
838
  responses["200"] = {
732
839
  description: "Successful response",
733
- content: {
734
- "application/json": { schema: { type: "object" } }
735
- }
840
+ content: { "application/json": { schema: { type: "object" } } }
736
841
  };
737
842
  }
738
- const errorStatusMatch = handlerSource.match(/ctx\.(?:json|text|html)\([^)]+,\s*(\d{3,})\)/g);
739
- if (errorStatusMatch) {
740
- errorStatusMatch.forEach((match) => {
741
- const statusCode = match.match(/,\s*(\d{3,})\)/)?.[1];
742
- if (statusCode && statusCode !== "200") {
743
- responses[statusCode] = {
744
- description: `Error response (${statusCode})`
745
- };
746
- }
747
- });
843
+ for (const match of handlerSource.matchAll(REGEX_ERROR_STATUS)) {
844
+ const statusCode = match[1];
845
+ if (statusCode && statusCode !== "200") {
846
+ responses[statusCode] = { description: `Error response (${statusCode})` };
847
+ }
748
848
  }
749
849
  if (Object.keys(responses).length > 0) {
750
850
  inferredSpec.responses = responses;
@@ -758,7 +858,7 @@ async function generateOpenApi(rootRouter, options = {}) {
758
858
  const defaultTagName = options.defaultTag || "Application";
759
859
  let astRoutes = [];
760
860
  try {
761
- const { OpenAPIAnalyzer: OpenAPIAnalyzer2 } = await import("./openapi-analyzer-BTExMLX4.js");
861
+ const { OpenAPIAnalyzer: OpenAPIAnalyzer2 } = await import("./openapi-analyzer-BtIaHIfe.js");
762
862
  const analyzer = new OpenAPIAnalyzer2(process.cwd());
763
863
  const { applications } = await analyzer.analyze();
764
864
  const appMap = /* @__PURE__ */ new Map();
@@ -1028,10 +1128,10 @@ async function generateOpenApi(rootRouter, options = {}) {
1028
1128
  };
1029
1129
  }
1030
1130
  const eta$1 = new Eta();
1031
- function serveStatic(ctx, config, prefix) {
1131
+ function serveStatic(config, prefix) {
1032
1132
  const rootPath = resolve(config.root || ".");
1033
1133
  const normalizedPrefix = prefix.endsWith("/") && prefix !== "/" ? prefix.slice(0, -1) : prefix;
1034
- return async () => {
1134
+ const serveStaticMiddleware = async (ctx) => {
1035
1135
  let relative = ctx.path.slice(normalizedPrefix.length);
1036
1136
  if (!relative.startsWith("/") && relative.length > 0) relative = "/" + relative;
1037
1137
  if (relative.length === 0) relative = "/";
@@ -1148,16 +1248,157 @@ function serveStatic(ctx, config, prefix) {
1148
1248
  }
1149
1249
  }
1150
1250
  }
1151
- const file = Bun.file(finalPath);
1152
- let response = new Response(file);
1251
+ let response;
1252
+ if (typeof Bun !== "undefined") {
1253
+ response = new Response(Bun.file(finalPath));
1254
+ } else {
1255
+ const fileBuffer = await readFile$1(finalPath);
1256
+ response = new Response(fileBuffer);
1257
+ }
1153
1258
  if (config.hooks?.onResponse) {
1154
1259
  const hooked = await config.hooks.onResponse(ctx, response);
1155
1260
  if (hooked) response = hooked;
1156
1261
  }
1157
1262
  return response;
1158
1263
  };
1264
+ serveStaticMiddleware.isBuiltin = true;
1265
+ serveStaticMiddleware.pluginName = "ServeStatic";
1266
+ return serveStaticMiddleware;
1267
+ }
1268
+ class RouterTrie {
1269
+ root;
1270
+ constructor() {
1271
+ this.root = this.createNode();
1272
+ }
1273
+ createNode() {
1274
+ return {
1275
+ children: {}
1276
+ };
1277
+ }
1278
+ insert(method, path, handler) {
1279
+ let node = this.root;
1280
+ const segments = this.splitPath(path);
1281
+ for (const segment of segments) {
1282
+ if (segment === "**") {
1283
+ if (!node.recursiveChild) {
1284
+ node.recursiveChild = this.createNode();
1285
+ }
1286
+ node = node.recursiveChild;
1287
+ } else if (segment === "*") {
1288
+ if (!node.wildcardChild) {
1289
+ node.wildcardChild = this.createNode();
1290
+ }
1291
+ node = node.wildcardChild;
1292
+ } else if (segment.startsWith(":")) {
1293
+ const paramName = segment.slice(1);
1294
+ if (!node.paramChild) {
1295
+ node.paramChild = this.createNode();
1296
+ node.paramChild.paramName = paramName;
1297
+ }
1298
+ node = node.paramChild;
1299
+ node.paramName = paramName;
1300
+ } else {
1301
+ if (!node.children[segment]) {
1302
+ node.children[segment] = this.createNode();
1303
+ }
1304
+ node = node.children[segment];
1305
+ }
1306
+ }
1307
+ if (!node.handlers) {
1308
+ node.handlers = {};
1309
+ }
1310
+ node.handlers[method] = handler;
1311
+ }
1312
+ search(method, path) {
1313
+ const segments = this.splitPath(path);
1314
+ const params = {};
1315
+ const match = this.findNode(this.root, segments, 0, params);
1316
+ if (match && match.handlers) {
1317
+ const handler = match.handlers[method] || match.handlers["ALL"];
1318
+ if (handler) {
1319
+ return { handler, params };
1320
+ }
1321
+ if (method === "HEAD" && match.handlers["GET"]) {
1322
+ return { handler: match.handlers["GET"], params };
1323
+ }
1324
+ }
1325
+ return null;
1326
+ }
1327
+ findNode(node, segments, index, params) {
1328
+ if (index === segments.length) {
1329
+ if (node.handlers) return node;
1330
+ if (node.recursiveChild && node.recursiveChild.handlers) {
1331
+ return node.recursiveChild;
1332
+ }
1333
+ return null;
1334
+ }
1335
+ const segment = segments[index];
1336
+ const child = node.children[segment];
1337
+ if (child) {
1338
+ const result = this.findNode(child, segments, index + 1, params);
1339
+ if (result) return result;
1340
+ }
1341
+ if (node.paramChild) {
1342
+ params[node.paramChild.paramName] = segment;
1343
+ const result = this.findNode(node.paramChild, segments, index + 1, params);
1344
+ if (result) return result;
1345
+ delete params[node.paramChild.paramName];
1346
+ }
1347
+ if (node.wildcardChild) {
1348
+ const result = this.findNode(node.wildcardChild, segments, index + 1, params);
1349
+ if (result) return result;
1350
+ }
1351
+ if (node.recursiveChild) {
1352
+ const remaining = segments.length - index;
1353
+ for (let k = 0; k <= remaining; k++) {
1354
+ const result = this.findNode(node.recursiveChild, segments, index + k, params);
1355
+ if (result) return result;
1356
+ }
1357
+ }
1358
+ return null;
1359
+ }
1360
+ splitPath(path) {
1361
+ if (path === "/" || path === "") return [];
1362
+ const s = path.startsWith("/") ? path.slice(1) : path;
1363
+ if (s === "") return [];
1364
+ return s.split("/");
1365
+ }
1159
1366
  }
1160
1367
  const asyncContext = new AsyncLocalStorage();
1368
+ const engine = process.env["SHOKUPAN_DB_ENGINE"] === "memory" ? "mem://" : "rocksdb://database";
1369
+ const db = new Surreal({
1370
+ engines: createNodeEngines()
1371
+ });
1372
+ const ready = db.connect(engine, { namespace: "vendor", database: "shokupan" }).then(() => {
1373
+ return db.query(`
1374
+ DEFINE TABLE OVERWRITE failed_requests SCHEMALESS COMMENT "Created by Shokupan";
1375
+ DEFINE TABLE OVERWRITE sessions SCHEMALESS COMMENT "Created by Shokupan";
1376
+ DEFINE TABLE OVERWRITE users SCHEMALESS COMMENT "Created by Shokupan";
1377
+ DEFINE TABLE OVERWRITE idempotency_keys SCHEMALESS COMMENT "Created by Shokupan";
1378
+ DEFINE TABLE OVERWRITE middleware_tracking SCHEMALESS COMMENT "Created by Shokupan";
1379
+ `);
1380
+ });
1381
+ const datastore = {
1382
+ get(store, key) {
1383
+ return db.select(new RecordId(store, key));
1384
+ },
1385
+ set(store, key, value) {
1386
+ return db.create(new RecordId(store, key)).content(value);
1387
+ },
1388
+ async query(query, vars) {
1389
+ try {
1390
+ const r = await db.query(query, vars).collect();
1391
+ return r;
1392
+ } catch (e) {
1393
+ console.error("DS ERROR:", e);
1394
+ throw e;
1395
+ }
1396
+ },
1397
+ ready
1398
+ };
1399
+ process.on("exit", async () => {
1400
+ await db.close();
1401
+ });
1161
1402
  const tracer = trace.getTracer("shokupan.middleware");
1162
1403
  function traceHandler(fn, name) {
1163
1404
  return async function(...args) {
@@ -1181,6 +1422,35 @@ function traceHandler(fn, name) {
1181
1422
  });
1182
1423
  };
1183
1424
  }
1425
+ function getCallerInfo(skipFrames = 1) {
1426
+ let file = "unknown";
1427
+ let line = 0;
1428
+ try {
1429
+ const err = new Error();
1430
+ const stack = err.stack?.split("\n") || [];
1431
+ let found = 0;
1432
+ for (let i = 1; i < stack.length; i++) {
1433
+ const l = stack[i];
1434
+ if (!l.includes(":")) continue;
1435
+ if (l.includes("node_modules")) continue;
1436
+ if (l.includes("bun:main")) continue;
1437
+ if (l.includes("src/util/stack.ts")) continue;
1438
+ if (l.includes("src/router.ts")) continue;
1439
+ if (l.includes("src/shokupan.ts")) continue;
1440
+ found++;
1441
+ if (found >= skipFrames) {
1442
+ const match = l.match(/\((.*):(\d+):(\d+)\)/) || l.match(/at (.*):(\d+):(\d+)/);
1443
+ if (match) {
1444
+ file = match[1];
1445
+ line = parseInt(match[2], 10);
1446
+ return { file, line };
1447
+ }
1448
+ }
1449
+ }
1450
+ } catch (e) {
1451
+ }
1452
+ return { file, line };
1453
+ }
1184
1454
  const RouterRegistry = /* @__PURE__ */ new Map();
1185
1455
  const ShokupanApplicationTree = {};
1186
1456
  class ShokupanRouter {
@@ -1200,6 +1470,7 @@ class ShokupanRouter {
1200
1470
  [$parent] = null;
1201
1471
  [$childRouters] = [];
1202
1472
  [$childControllers] = [];
1473
+ middleware = [];
1203
1474
  get rootConfig() {
1204
1475
  return this[$appRoot]?.applicationConfig;
1205
1476
  }
@@ -1208,7 +1479,54 @@ class ShokupanRouter {
1208
1479
  }
1209
1480
  [$routes] = [];
1210
1481
  // Public via Symbol for OpenAPI generator
1482
+ trie = new RouterTrie();
1483
+ metadata;
1484
+ // Metadata for the router itself
1211
1485
  currentGuards = [];
1486
+ // Registry Accessor
1487
+ getComponentRegistry() {
1488
+ const routes = this[$routes].map((r) => ({
1489
+ type: "route",
1490
+ path: r.path,
1491
+ method: r.method,
1492
+ metadata: r.metadata,
1493
+ handlerName: r.handler.name,
1494
+ tags: r.handlerSpec?.tags,
1495
+ order: r.order,
1496
+ _fn: r.handler
1497
+ // Expose handler for debugging instrumentation
1498
+ }));
1499
+ const mw = this.middleware;
1500
+ const middleware = mw ? mw.map((m) => ({
1501
+ name: m.name || "middleware",
1502
+ metadata: m.metadata,
1503
+ order: m.order,
1504
+ _fn: m
1505
+ // Expose function for debugging instrumentation
1506
+ })) : [];
1507
+ const routers = this[$childRouters].map((r) => ({
1508
+ type: "router",
1509
+ path: r[$mountPath],
1510
+ metadata: r.metadata,
1511
+ children: r.getComponentRegistry()
1512
+ }));
1513
+ const controllers = this[$childControllers].map((c) => {
1514
+ return {
1515
+ type: "controller",
1516
+ path: c[$mountPath] || "/",
1517
+ name: c.constructor.name,
1518
+ metadata: c.metadata
1519
+ // Check if we can store this
1520
+ };
1521
+ });
1522
+ return {
1523
+ metadata: this.metadata,
1524
+ middleware,
1525
+ routes,
1526
+ routers,
1527
+ controllers
1528
+ };
1529
+ }
1212
1530
  isRouterInstance(target) {
1213
1531
  return typeof target === "object" && target !== null && $isRouter in target;
1214
1532
  }
@@ -1234,6 +1552,14 @@ class ShokupanRouter {
1234
1552
  throw new Error("Router is already mounted");
1235
1553
  }
1236
1554
  controller[$mountPath] = prefix;
1555
+ if (!controller.metadata) {
1556
+ const info = getCallerInfo();
1557
+ controller.metadata = {
1558
+ file: info.file,
1559
+ line: info.line,
1560
+ name: "MountedRouter"
1561
+ };
1562
+ }
1237
1563
  this[$childRouters].push(controller);
1238
1564
  controller[$parent] = this;
1239
1565
  const setRouterContext = (router) => {
@@ -1266,6 +1592,12 @@ class ShokupanRouter {
1266
1592
  }
1267
1593
  }
1268
1594
  instance[$mountPath] = prefix;
1595
+ const info = getCallerInfo();
1596
+ instance.metadata = {
1597
+ file: info.file,
1598
+ line: info.line,
1599
+ name: instance.constructor.name
1600
+ };
1269
1601
  this[$childControllers].push(instance);
1270
1602
  const controllerMiddleware = (typeof controller === "function" ? controller[$middleware] : instance[$middleware]) || [];
1271
1603
  const proto = Object.getPrototypeOf(instance);
@@ -1349,14 +1681,39 @@ class ShokupanRouter {
1349
1681
  for (const arg of sortedArgs) {
1350
1682
  switch (arg.type) {
1351
1683
  case RouteParamType.BODY:
1352
- args[arg.index] = await ctx.req.json().catch(() => ({}));
1684
+ try {
1685
+ if (ctx.req.headers.get("content-type")?.includes("application/json")) {
1686
+ args[arg.index] = await ctx.req.json();
1687
+ } else {
1688
+ const text = await ctx.req.text();
1689
+ if (!text) {
1690
+ args[arg.index] = {};
1691
+ } else {
1692
+ args[arg.index] = JSON.parse(text);
1693
+ }
1694
+ }
1695
+ } catch (e) {
1696
+ const err = new Error("Invalid JSON body");
1697
+ err.status = 400;
1698
+ throw err;
1699
+ }
1353
1700
  break;
1354
1701
  case RouteParamType.PARAM:
1355
1702
  args[arg.index] = arg.name ? ctx.params[arg.name] : ctx.params;
1356
1703
  break;
1357
1704
  case RouteParamType.QUERY: {
1358
1705
  const url = new URL(ctx.req.url);
1359
- args[arg.index] = arg.name ? url.searchParams.get(arg.name) : Object.fromEntries(url.searchParams);
1706
+ if (arg.name) {
1707
+ const vals = url.searchParams.getAll(arg.name);
1708
+ args[arg.index] = vals.length > 1 ? vals : vals[0];
1709
+ } else {
1710
+ const query = {};
1711
+ for (const key of url.searchParams.keys()) {
1712
+ const vals = url.searchParams.getAll(key);
1713
+ query[key] = vals.length > 1 ? vals : vals[0];
1714
+ }
1715
+ args[arg.index] = query;
1716
+ }
1360
1717
  break;
1361
1718
  }
1362
1719
  case RouteParamType.HEADER:
@@ -1504,30 +1861,59 @@ class ShokupanRouter {
1504
1861
  data: result
1505
1862
  };
1506
1863
  }
1507
- applyHooks(match) {
1864
+ applyRouterHooks(match) {
1508
1865
  if (!this.config?.hooks) return match;
1509
1866
  const hooks = this.config.hooks;
1510
- if (!hooks.onRequestStart && !hooks.onRequestEnd && !hooks.onError) return match;
1511
- const originalHandler = match.handler;
1512
- match.handler = async (ctx) => {
1513
- if (hooks.onRequestStart) await hooks.onRequestStart(ctx);
1867
+ return {
1868
+ ...match,
1869
+ handler: this.wrapWithHooks(match.handler, hooks)
1870
+ };
1871
+ }
1872
+ wrapWithHooks(handler, hooks) {
1873
+ const hookList = Array.isArray(hooks) ? hooks : [hooks];
1874
+ const hasStart = hookList.some((h) => !!h.onRequestStart);
1875
+ const hasEnd = hookList.some((h) => !!h.onRequestEnd);
1876
+ const hasError = hookList.some((h) => !!h.onError);
1877
+ if (!hasStart && !hasEnd && !hasError) return handler;
1878
+ const originalHandler = handler;
1879
+ const wrapped = async (ctx) => {
1880
+ if (hasStart) {
1881
+ for (let i = 0; i < hookList.length; i++) {
1882
+ const h = hookList[i];
1883
+ if (typeof h.onRequestStart === "function") await h.onRequestStart(ctx);
1884
+ }
1885
+ }
1886
+ const debug = ctx._debug;
1887
+ let debugId;
1888
+ let previousNode;
1889
+ if (debug) {
1890
+ debugId = originalHandler._debugId || originalHandler.name || "handler";
1891
+ previousNode = debug.getCurrentNode();
1892
+ debug.trackEdge(previousNode, debugId);
1893
+ debug.setNode(debugId);
1894
+ }
1895
+ const start = performance.now();
1514
1896
  try {
1515
- const result = await originalHandler(ctx);
1516
- if (hooks.onRequestEnd) await hooks.onRequestEnd(ctx);
1517
- return result;
1897
+ const res = await originalHandler(ctx);
1898
+ debug?.trackStep(debugId, "handler", performance.now() - start, "success");
1899
+ for (let i = 0; i < hookList.length; i++) {
1900
+ const h = hookList[i];
1901
+ if (typeof h.onRequestEnd === "function") await h.onRequestEnd(ctx);
1902
+ }
1903
+ return res;
1518
1904
  } catch (err) {
1519
- if (hooks.onError) {
1520
- try {
1521
- await hooks.onError(err, ctx);
1522
- } catch (e) {
1523
- console.error("Error in router onError hook:", e);
1524
- }
1905
+ debug?.trackStep(debugId, "handler", performance.now() - start, "error", err);
1906
+ for (let i = 0; i < hookList.length; i++) {
1907
+ const h = hookList[i];
1908
+ if (typeof h.onError === "function") await h.onError(err, ctx);
1525
1909
  }
1526
1910
  throw err;
1911
+ } finally {
1912
+ if (debug && previousNode) debug.setNode(previousNode);
1527
1913
  }
1528
1914
  };
1529
- match.handler.originalHandler = originalHandler.originalHandler || originalHandler;
1530
- return match;
1915
+ wrapped.originalHandler = originalHandler.originalHandler ?? originalHandler;
1916
+ return wrapped;
1531
1917
  }
1532
1918
  /**
1533
1919
  * Find a route matching the given method and path.
@@ -1536,24 +1922,10 @@ class ShokupanRouter {
1536
1922
  * @returns Route handler and parameters if found, otherwise null
1537
1923
  */
1538
1924
  find(method, path) {
1539
- const findInRoutes = (routes, m) => {
1540
- for (const route of routes) {
1541
- if (route.method !== "ALL" && route.method !== m) continue;
1542
- const match = route.regex.exec(path);
1543
- if (match) {
1544
- const params = {};
1545
- route.keys.forEach((key, index) => {
1546
- params[key] = match[index + 1];
1547
- });
1548
- return this.applyHooks({ handler: route.handler, params });
1549
- }
1550
- }
1551
- return null;
1552
- };
1553
- let result = findInRoutes(this[$routes], method);
1925
+ let result = this.trie.search(method, path);
1554
1926
  if (result) return result;
1555
1927
  if (method === "HEAD") {
1556
- result = findInRoutes(this[$routes], "GET");
1928
+ result = this.trie.search("GET", path);
1557
1929
  if (result) return result;
1558
1930
  }
1559
1931
  for (const child of this[$childRouters]) {
@@ -1561,13 +1933,13 @@ class ShokupanRouter {
1561
1933
  if (path === prefix || path.startsWith(prefix + "/")) {
1562
1934
  const subPath = path.slice(prefix.length) || "/";
1563
1935
  const match = child.find(method, subPath);
1564
- if (match) return this.applyHooks(match);
1936
+ if (match) return this.applyRouterHooks(match);
1565
1937
  }
1566
1938
  if (prefix.endsWith("/")) {
1567
1939
  if (path.startsWith(prefix)) {
1568
1940
  const subPath = path.slice(prefix.length) || "/";
1569
1941
  const match = child.find(method, subPath);
1570
- if (match) return this.applyHooks(match);
1942
+ if (match) return this.applyRouterHooks(match);
1571
1943
  }
1572
1944
  }
1573
1945
  }
@@ -1578,7 +1950,7 @@ class ShokupanRouter {
1578
1950
  const pattern = path.replace(/:([a-zA-Z0-9_]+)/g, (_, key) => {
1579
1951
  keys.push(key);
1580
1952
  return "([^/]+)";
1581
- }).replace(/\*/g, ".*");
1953
+ }).replace(/\*\*/g, ".*").replace(/\*/g, "[^/]+");
1582
1954
  return {
1583
1955
  regex: new RegExp(`^${pattern}$`),
1584
1956
  keys
@@ -1647,47 +2019,84 @@ class ShokupanRouter {
1647
2019
  return innerHandler(ctx);
1648
2020
  };
1649
2021
  }
1650
- let file = "unknown";
1651
- let line = 0;
1652
- try {
1653
- const err = new Error();
1654
- const stack = err.stack?.split("\n") || [];
1655
- const callerLine = stack.find(
1656
- (l) => l.includes(":") && !l.includes("router.ts") && !l.includes("shokupan.ts") && !l.includes("node_modules") && !l.includes("bun:main")
1657
- );
1658
- if (callerLine) {
1659
- const match = callerLine.match(/\((.*):(\d+):(\d+)\)/) || callerLine.match(/at (.*):(\d+):(\d+)/);
1660
- if (match) {
1661
- file = match[1];
1662
- line = parseInt(match[2], 10);
1663
- }
1664
- }
1665
- } catch (e) {
1666
- }
1667
- const trackedHandler = wrappedHandler;
2022
+ const { file, line } = getCallerInfo();
2023
+ const trackingHandler = wrappedHandler;
1668
2024
  wrappedHandler = async (ctx) => {
1669
- if (ctx.app?.applicationConfig.enableMiddlewareTracking) {
1670
- ctx.handlerStack.push({
1671
- name: handler.name || "anonymous",
1672
- file,
1673
- line
1674
- });
2025
+ if (!ctx.app?.applicationConfig.enableMiddlewareTracking) {
2026
+ return trackingHandler(ctx);
2027
+ }
2028
+ const startTime = performance.now();
2029
+ let error = void 0;
2030
+ try {
2031
+ if (ctx.app?.applicationConfig.enableMiddlewareTracking) {
2032
+ ctx.handlerStack.push({
2033
+ name: handler.name || "anonymous",
2034
+ file,
2035
+ line
2036
+ });
2037
+ }
2038
+ return await trackingHandler(ctx);
2039
+ } catch (e) {
2040
+ error = e;
2041
+ throw e;
2042
+ } finally {
2043
+ if (ctx.app?.applicationConfig.enableMiddlewareTracking) {
2044
+ const duration = performance.now() - startTime;
2045
+ const config = ctx.app.applicationConfig;
2046
+ try {
2047
+ const timestamp = Date.now();
2048
+ const key = `${timestamp}-${handler.name || "anonymous"}-${Math.random().toString(36).substring(7)}`;
2049
+ await datastore.set("middleware_tracking", key, {
2050
+ name: handler.name || "anonymous",
2051
+ path: ctx.path,
2052
+ timestamp,
2053
+ duration,
2054
+ file,
2055
+ line,
2056
+ error: error ? String(error) : void 0,
2057
+ metadata: {
2058
+ isBuiltin: handler.isBuiltin,
2059
+ pluginName: handler.pluginName
2060
+ }
2061
+ });
2062
+ const ttl = config.middlewareTrackingTTL ?? 864e5;
2063
+ const maxCapacity = config.middlewareTrackingMaxCapacity ?? 1e4;
2064
+ const cutoff = Date.now() - ttl;
2065
+ await datastore.query(`DELETE middleware_tracking WHERE timestamp < ${cutoff}`);
2066
+ const results = await datastore.query("SELECT count() FROM middleware_tracking GROUP ALL");
2067
+ if (results && results[0] && results[0].count > maxCapacity) {
2068
+ const toDelete = results[0].count - maxCapacity;
2069
+ await datastore.query(`DELETE middleware_tracking ORDER BY timestamp ASC LIMIT ${toDelete}`);
2070
+ }
2071
+ } catch (datastoreError) {
2072
+ console.error("Failed to store middleware tracking:", datastoreError);
2073
+ }
2074
+ }
1675
2075
  }
1676
- return trackedHandler(ctx);
1677
2076
  };
1678
- wrappedHandler.originalHandler = trackedHandler.originalHandler || trackedHandler;
2077
+ wrappedHandler.originalHandler = trackingHandler.originalHandler || trackingHandler;
2078
+ let bakedHandler = wrappedHandler;
2079
+ if (this.config?.hooks) {
2080
+ bakedHandler = this.wrapWithHooks(wrappedHandler, this.config.hooks);
2081
+ }
1679
2082
  this[$routes].push({
1680
2083
  method,
1681
2084
  path,
1682
- regex,
1683
- keys,
1684
- handler: wrappedHandler,
2085
+ regex: regex ?? new RegExp(""),
2086
+ keys: keys ?? [],
2087
+ handler,
2088
+ bakedHandler,
1685
2089
  handlerSpec: spec,
1686
2090
  group,
1687
- guards: routeGuards.length > 0 ? routeGuards : void 0,
1688
- requestTimeout: effectiveTimeout
1689
- // Save for inspection? Or just relying on closure
2091
+ hooks: this.config?.hooks,
2092
+ requestTimeout,
2093
+ renderer,
2094
+ metadata: {
2095
+ file,
2096
+ line
2097
+ }
1690
2098
  });
2099
+ this.trie.insert(method, path, bakedHandler);
1691
2100
  return this;
1692
2101
  }
1693
2102
  get(path, ...args) {
@@ -1761,10 +2170,10 @@ class ShokupanRouter {
1761
2170
  const config = typeof options === "string" ? { root: options } : options;
1762
2171
  const prefix = uriPath.startsWith("/") ? uriPath : "/" + uriPath;
1763
2172
  const normalizedPrefix = prefix.endsWith("/") && prefix !== "/" ? prefix.slice(0, -1) : prefix;
1764
- serveStatic(null, config, prefix);
2173
+ const handlerMiddleware = serveStatic(config, prefix);
1765
2174
  const routeHandler = async (ctx) => {
1766
- const runner = serveStatic(ctx, config, prefix);
1767
- return runner();
2175
+ return handlerMiddleware(ctx, async () => {
2176
+ });
1768
2177
  };
1769
2178
  let groupName = "Static";
1770
2179
  const segments = normalizedPrefix.split("/").filter(Boolean);
@@ -1825,6 +2234,49 @@ class ShokupanRouter {
1825
2234
  return generateOpenApi(this, options);
1826
2235
  }
1827
2236
  }
2237
+ class SystemCpuMonitor {
2238
+ constructor(intervalMs = 1e3) {
2239
+ this.intervalMs = intervalMs;
2240
+ }
2241
+ interval = null;
2242
+ lastCpus = [];
2243
+ currentUsage = 0;
2244
+ start() {
2245
+ if (this.interval) return;
2246
+ this.lastCpus = os.cpus();
2247
+ this.interval = setInterval(() => this.update(), this.intervalMs);
2248
+ }
2249
+ stop() {
2250
+ if (this.interval) {
2251
+ clearInterval(this.interval);
2252
+ this.interval = null;
2253
+ }
2254
+ }
2255
+ getUsage() {
2256
+ return this.currentUsage;
2257
+ }
2258
+ update() {
2259
+ const cpus = os.cpus();
2260
+ let idle = 0;
2261
+ let total = 0;
2262
+ for (let i = 0; i < cpus.length; i++) {
2263
+ const cpu = cpus[i];
2264
+ const prev = this.lastCpus[i];
2265
+ let type;
2266
+ for (type in cpu.times) {
2267
+ const ticks = cpu.times[type];
2268
+ const prevTicks = prev.times[type];
2269
+ const diff = ticks - prevTicks;
2270
+ total += diff;
2271
+ if (type === "idle") {
2272
+ idle += diff;
2273
+ }
2274
+ }
2275
+ }
2276
+ this.lastCpus = cpus;
2277
+ this.currentUsage = total === 0 ? 0 : (1 - idle / total) * 100;
2278
+ }
2279
+ }
1828
2280
  const defaults = {
1829
2281
  port: 3e3,
1830
2282
  hostname: "localhost",
@@ -1836,51 +2288,58 @@ trace.getTracer("shokupan.application");
1836
2288
  class Shokupan extends ShokupanRouter {
1837
2289
  applicationConfig = {};
1838
2290
  openApiSpec;
1839
- middleware = [];
1840
2291
  composedMiddleware;
2292
+ cpuMonitor;
2293
+ hookCache = /* @__PURE__ */ new Map();
2294
+ hooksInitialized = false;
1841
2295
  get logger() {
1842
2296
  return this.applicationConfig.logger;
1843
2297
  }
1844
2298
  constructor(applicationConfig = {}) {
1845
- super();
2299
+ const config = Object.assign({}, defaults, applicationConfig);
2300
+ const { hooks, ...routerConfig } = config;
2301
+ super(routerConfig);
1846
2302
  this[$isApplication] = true;
1847
2303
  this[$appRoot] = this;
1848
- Object.assign(this.applicationConfig, defaults, applicationConfig);
2304
+ this.applicationConfig = config;
2305
+ const { file, line } = getCallerInfo();
2306
+ this.metadata = {
2307
+ file,
2308
+ line,
2309
+ name: "ShokupanApplication"
2310
+ };
1849
2311
  }
1850
2312
  /**
1851
2313
  * Adds middleware to the application.
1852
2314
  */
1853
2315
  use(middleware) {
1854
2316
  let trackedMiddleware = middleware;
1855
- let file = "unknown";
1856
- let line = 0;
1857
- try {
1858
- const err = new Error();
1859
- const stack = err.stack?.split("\n") || [];
1860
- const callerLine = stack.find(
1861
- (l) => l.includes(":") && !l.includes("shokupan.ts") && !l.includes("router.ts") && // In case called from router?
1862
- !l.includes("node_modules") && !l.includes("bun:main")
1863
- );
1864
- if (callerLine) {
1865
- const match = callerLine.match(/\((.*):(\d+):(\d+)\)/) || callerLine.match(/at (.*):(\d+):(\d+)/);
1866
- if (match) {
1867
- file = match[1];
1868
- line = parseInt(match[2], 10);
1869
- }
1870
- }
1871
- } catch (e) {
2317
+ const { file, line } = getCallerInfo();
2318
+ if (!middleware.metadata) {
2319
+ middleware.metadata = {
2320
+ file,
2321
+ line,
2322
+ name: middleware.name || "middleware",
2323
+ isBuiltin: middleware.isBuiltin,
2324
+ pluginName: middleware.pluginName
2325
+ };
1872
2326
  }
1873
2327
  trackedMiddleware = async (ctx, next) => {
1874
2328
  const c = ctx;
1875
2329
  if (c.handlerStack && c.app?.applicationConfig.enableMiddlewareTracking) {
2330
+ const metadata = middleware.metadata || {};
1876
2331
  c.handlerStack.push({
1877
- name: middleware.name || "middleware",
1878
- file,
1879
- line
2332
+ name: metadata.pluginName ? `${metadata.pluginName} (${metadata.name})` : metadata.name || middleware.name || "middleware",
2333
+ file: metadata.file || file,
2334
+ line: metadata.line || line,
2335
+ isBuiltin: metadata.isBuiltin
1880
2336
  });
1881
2337
  }
1882
2338
  return middleware(ctx, next);
1883
2339
  };
2340
+ trackedMiddleware.metadata = middleware.metadata;
2341
+ Object.defineProperty(trackedMiddleware, "name", { value: middleware.name || "middleware" });
2342
+ trackedMiddleware.order = this.middleware.length;
1884
2343
  this.middleware.push(trackedMiddleware);
1885
2344
  return this;
1886
2345
  }
@@ -1892,6 +2351,15 @@ class Shokupan extends ShokupanRouter {
1892
2351
  this.startupHooks.push(callback);
1893
2352
  return this;
1894
2353
  }
2354
+ specAvailableHooks = [];
2355
+ /**
2356
+ * Registers a callback to be executed when the OpenAPI spec is available.
2357
+ * This happens after generateOpenApi() but before the server starts listening (or at least before it finishes startup if async).
2358
+ */
2359
+ onSpecAvailable(callback) {
2360
+ this.specAvailableHooks.push(callback);
2361
+ return this;
2362
+ }
1895
2363
  /**
1896
2364
  * Starts the application server.
1897
2365
  *
@@ -1908,8 +2376,15 @@ class Shokupan extends ShokupanRouter {
1908
2376
  }
1909
2377
  if (this.applicationConfig.enableOpenApiGen) {
1910
2378
  this.openApiSpec = await generateOpenApi(this);
2379
+ for (const hook of this.specAvailableHooks) {
2380
+ await hook(this.openApiSpec);
2381
+ }
1911
2382
  }
1912
2383
  if (port === 0 && process.platform === "linux") ;
2384
+ if (this.applicationConfig.autoBackpressureFeedback) {
2385
+ this.cpuMonitor = new SystemCpuMonitor();
2386
+ this.cpuMonitor.start();
2387
+ }
1913
2388
  const serveOptions = {
1914
2389
  port: finalPort,
1915
2390
  hostname: this.applicationConfig.hostname,
@@ -1934,7 +2409,7 @@ class Shokupan extends ShokupanRouter {
1934
2409
  };
1935
2410
  let factory = this.applicationConfig.serverFactory;
1936
2411
  if (!factory && typeof Bun === "undefined") {
1937
- const { createHttpServer } = await import("./server-adapter-CnQFr4P7.js");
2412
+ const { createHttpServer } = await import("./server-adapter-BWrEJbKL.js");
1938
2413
  factory = createHttpServer();
1939
2414
  }
1940
2415
  const server = factory ? await factory(serveOptions) : Bun.serve(serveOptions);
@@ -2019,11 +2494,18 @@ class Shokupan extends ShokupanRouter {
2019
2494
  }
2020
2495
  async handleRequest(req, server) {
2021
2496
  const request = req;
2022
- const ctx = new ShokupanContext(request, server, void 0, this, this.applicationConfig.enableMiddlewareTracking);
2497
+ const controller = new AbortController();
2498
+ const ctx = new ShokupanContext(request, server, void 0, this, controller.signal, this.applicationConfig.enableMiddlewareTracking);
2023
2499
  const handle = async () => {
2500
+ if (this.cpuMonitor && this.cpuMonitor.getUsage() > (this.applicationConfig.autoBackpressureLevel ?? 60)) {
2501
+ const msg = "Too Many Requests (CPU Backpressure)";
2502
+ const res = ctx.text(msg, 429);
2503
+ await this.executeHook("onResponseEnd", ctx, res);
2504
+ return res;
2505
+ }
2024
2506
  try {
2025
- if (this.applicationConfig.hooks?.onRequestStart) {
2026
- await this.applicationConfig.hooks.onRequestStart(ctx);
2507
+ if (this.hasHook("onRequestStart")) {
2508
+ await this.executeHook("onRequestStart", ctx);
2027
2509
  }
2028
2510
  const fn = this.composedMiddleware ??= compose(this.middleware);
2029
2511
  const result = await fn(ctx, async () => {
@@ -2039,23 +2521,24 @@ class Shokupan extends ShokupanRouter {
2039
2521
  response = result;
2040
2522
  } else if ((result === null || result === void 0) && ctx._finalResponse instanceof Response) {
2041
2523
  response = ctx._finalResponse;
2042
- } else if ((result === null || result === void 0) && ctx.response.status === 404) {
2043
- const span = asyncContext.getStore()?.get("span");
2044
- if (span) span.setAttribute("http.status_code", 404);
2045
- response = ctx.text("Not Found", 404);
2046
2524
  } else if (result === null || result === void 0) {
2047
- if (ctx._finalResponse) response = ctx._finalResponse;
2048
- else response = ctx.text("Not Found", 404);
2525
+ if (ctx._finalResponse instanceof Response) {
2526
+ response = ctx._finalResponse;
2527
+ } else if (ctx.response.status !== 200 || ctx.response.hasPopulatedHeaders) {
2528
+ response = ctx.send(null, { status: ctx.response.status, headers: ctx.response.headers });
2529
+ } else {
2530
+ response = ctx.text("Not Found", 404);
2531
+ }
2049
2532
  } else if (typeof result === "object") {
2050
2533
  response = ctx.json(result);
2051
2534
  } else {
2052
2535
  response = ctx.text(String(result));
2053
2536
  }
2054
- if (this.applicationConfig.hooks?.onRequestEnd) {
2055
- await this.applicationConfig.hooks.onRequestEnd(ctx);
2537
+ if (this.hasHook("onRequestEnd")) {
2538
+ await this.executeHook("onRequestEnd", ctx);
2056
2539
  }
2057
- if (this.applicationConfig.hooks?.onResponseStart) {
2058
- await this.applicationConfig.hooks.onResponseStart(ctx, response);
2540
+ if (this.hasHook("onResponseStart")) {
2541
+ await this.executeHook("onResponseStart", ctx, response);
2059
2542
  }
2060
2543
  return response;
2061
2544
  } catch (err) {
@@ -2065,28 +2548,21 @@ class Shokupan extends ShokupanRouter {
2065
2548
  const status = err.status || err.statusCode || 500;
2066
2549
  const body = { error: err.message || "Internal Server Error" };
2067
2550
  if (err.errors) body.errors = err.errors;
2068
- if (this.applicationConfig.hooks?.onError) {
2069
- try {
2070
- await this.applicationConfig.hooks.onError(err, ctx);
2071
- } catch (hookErr) {
2072
- console.error("Error in onError hook:", hookErr);
2073
- }
2551
+ if (this.hasHook("onError")) {
2552
+ await this.executeHook("onError", err, ctx);
2074
2553
  }
2075
2554
  return ctx.json(body, status);
2076
2555
  }
2077
2556
  };
2078
2557
  let executionPromise = handle();
2079
2558
  const timeoutMs = this.applicationConfig.requestTimeout;
2080
- if (timeoutMs && timeoutMs > 0 && this.applicationConfig.hooks?.onRequestTimeout) {
2559
+ if (timeoutMs && timeoutMs > 0) {
2081
2560
  let timeoutId;
2082
2561
  const timeoutPromise = new Promise((_, reject) => {
2083
2562
  timeoutId = setTimeout(async () => {
2084
- try {
2085
- if (this.applicationConfig.hooks?.onRequestTimeout) {
2086
- await this.applicationConfig.hooks.onRequestTimeout(ctx);
2087
- }
2088
- } catch (e) {
2089
- console.error("Error in onRequestTimeout hook:", e);
2563
+ controller.abort();
2564
+ if (this.hasHook("onRequestTimeout")) {
2565
+ await this.executeHook("onRequestTimeout", ctx);
2090
2566
  }
2091
2567
  reject(new Error("Request Timeout"));
2092
2568
  }, timeoutMs);
@@ -2100,12 +2576,56 @@ class Shokupan extends ShokupanRouter {
2100
2576
  console.error("Unexpected error in request execution:", err);
2101
2577
  return ctx.text("Internal Server Error", 500);
2102
2578
  }).then(async (res) => {
2103
- if (this.applicationConfig.hooks?.onResponseEnd) {
2104
- await this.applicationConfig.hooks.onResponseEnd(ctx, res);
2579
+ if (this.hasHook("onResponseEnd")) {
2580
+ await this.executeHook("onResponseEnd", ctx, res);
2105
2581
  }
2106
2582
  return res;
2107
2583
  });
2108
2584
  }
2585
+ ensureHooksInitialized() {
2586
+ const hooks = this.applicationConfig.hooks;
2587
+ if (hooks) {
2588
+ const hookList = Array.isArray(hooks) ? hooks : [hooks];
2589
+ const hookTypes = [
2590
+ "onRequestStart",
2591
+ "onRequestEnd",
2592
+ "onResponseStart",
2593
+ "onResponseEnd",
2594
+ "onError",
2595
+ "beforeValidate",
2596
+ "afterValidate",
2597
+ "onRequestTimeout",
2598
+ "onReadTimeout",
2599
+ "onWriteTimeout"
2600
+ ];
2601
+ for (const type of hookTypes) {
2602
+ const fns = [];
2603
+ for (const h of hookList) {
2604
+ if (h[type]) fns.push(h[type]);
2605
+ }
2606
+ if (fns.length > 0) {
2607
+ this.hookCache.set(type, fns);
2608
+ }
2609
+ }
2610
+ }
2611
+ this.hooksInitialized = true;
2612
+ }
2613
+ async executeHook(name, ...args) {
2614
+ if (!this.hooksInitialized) {
2615
+ this.ensureHooksInitialized();
2616
+ }
2617
+ const fns = this.hookCache.get(name);
2618
+ if (!fns) return;
2619
+ for (const fn of fns) {
2620
+ await fn(...args);
2621
+ }
2622
+ }
2623
+ hasHook(name) {
2624
+ if (!this.hooksInitialized) {
2625
+ this.ensureHooksInitialized();
2626
+ }
2627
+ return this.hookCache.has(name);
2628
+ }
2109
2629
  }
2110
2630
  class AuthPlugin extends ShokupanRouter {
2111
2631
  constructor(authConfig) {
@@ -2310,7 +2830,7 @@ class AuthPlugin extends ShokupanRouter {
2310
2830
  /**
2311
2831
  * Middleware to verify JWT
2312
2832
  */
2313
- middleware() {
2833
+ getMiddleware() {
2314
2834
  return async (ctx, next) => {
2315
2835
  const authHeader = ctx.req.headers.get("Authorization");
2316
2836
  let token = authHeader?.startsWith("Bearer ") ? authHeader.substring(7) : null;
@@ -2331,12 +2851,16 @@ class AuthPlugin extends ShokupanRouter {
2331
2851
  }
2332
2852
  function Compression(options = {}) {
2333
2853
  const threshold = options.threshold ?? 512;
2334
- return async (ctx, next) => {
2854
+ const compressionMiddleware = async function CompressionMiddleware(ctx, next) {
2335
2855
  const acceptEncoding = ctx.headers.get("accept-encoding") || "";
2336
2856
  let method = null;
2337
2857
  if (acceptEncoding.includes("br")) method = "br";
2338
- else if (acceptEncoding.includes("zstd")) method = "zstd";
2339
- else if (acceptEncoding.includes("gzip")) method = "gzip";
2858
+ else if (acceptEncoding.includes("zstd")) {
2859
+ if (typeof Bun === "undefined") {
2860
+ throw new Error("zstd compression is only available in Bun runtime. Client requested zstd but server is running on Node.js.");
2861
+ }
2862
+ method = "zstd";
2863
+ } else if (acceptEncoding.includes("gzip")) method = "gzip";
2340
2864
  else if (acceptEncoding.includes("deflate")) method = "deflate";
2341
2865
  if (!method) return next();
2342
2866
  let response = await next();
@@ -2345,8 +2869,25 @@ function Compression(options = {}) {
2345
2869
  }
2346
2870
  if (response instanceof Response) {
2347
2871
  if (response.headers.has("Content-Encoding")) return response;
2348
- const body = await response.arrayBuffer();
2349
- if (body.byteLength < threshold) {
2872
+ let body;
2873
+ let bodySize;
2874
+ if (ctx._rawBody !== void 0) {
2875
+ if (typeof ctx._rawBody === "string") {
2876
+ const encoded = new TextEncoder().encode(ctx._rawBody);
2877
+ body = encoded.buffer;
2878
+ bodySize = encoded.byteLength;
2879
+ } else if (ctx._rawBody instanceof Uint8Array) {
2880
+ body = ctx._rawBody.buffer;
2881
+ bodySize = ctx._rawBody.byteLength;
2882
+ } else {
2883
+ body = ctx._rawBody;
2884
+ bodySize = ctx._rawBody.byteLength;
2885
+ }
2886
+ } else {
2887
+ body = await response.arrayBuffer();
2888
+ bodySize = body.byteLength;
2889
+ }
2890
+ if (bodySize < threshold) {
2350
2891
  return new Response(body, {
2351
2892
  status: response.status,
2352
2893
  statusText: response.statusText,
@@ -2356,7 +2897,6 @@ function Compression(options = {}) {
2356
2897
  let compressed;
2357
2898
  switch (method) {
2358
2899
  case "br":
2359
- const zlib = require("node:zlib");
2360
2900
  compressed = await new Promise((res, rej) => zlib.brotliCompress(body, {
2361
2901
  params: {
2362
2902
  [zlib.constants.BROTLI_PARAM_QUALITY]: 4
@@ -2367,13 +2907,19 @@ function Compression(options = {}) {
2367
2907
  }));
2368
2908
  break;
2369
2909
  case "gzip":
2370
- compressed = Bun.gzipSync(body);
2910
+ compressed = await new Promise((res, rej) => zlib.gzip(body, (err, data) => {
2911
+ if (err) return rej(err);
2912
+ res(data);
2913
+ }));
2371
2914
  break;
2372
2915
  case "zstd":
2373
2916
  compressed = await Bun.zstdCompress(body);
2374
2917
  break;
2375
2918
  default:
2376
- compressed = Bun.deflateSync(body);
2919
+ compressed = await new Promise((res, rej) => zlib.deflate(body, (err, data) => {
2920
+ if (err) return rej(err);
2921
+ res(data);
2922
+ }));
2377
2923
  break;
2378
2924
  }
2379
2925
  const headers = new Headers(response.headers);
@@ -2387,6 +2933,9 @@ function Compression(options = {}) {
2387
2933
  }
2388
2934
  return response;
2389
2935
  };
2936
+ compressionMiddleware.isBuiltin = true;
2937
+ compressionMiddleware.pluginName = "Compression";
2938
+ return compressionMiddleware;
2390
2939
  }
2391
2940
  function Cors(options = {}) {
2392
2941
  const defaults2 = {
@@ -2396,7 +2945,7 @@ function Cors(options = {}) {
2396
2945
  optionsSuccessStatus: 204
2397
2946
  };
2398
2947
  const opts = { ...defaults2, ...options };
2399
- return async (ctx, next) => {
2948
+ const corsMiddleware = async function CorsMiddleware(ctx, next) {
2400
2949
  const headers = new Headers();
2401
2950
  const origin = ctx.headers.get("origin");
2402
2951
  const set = (k, v) => headers.set(k, v);
@@ -2458,6 +3007,9 @@ function Cors(options = {}) {
2458
3007
  }
2459
3008
  return response;
2460
3009
  };
3010
+ corsMiddleware.isBuiltin = true;
3011
+ corsMiddleware.pluginName = "Cors";
3012
+ return corsMiddleware;
2461
3013
  }
2462
3014
  function useExpress(expressMiddleware) {
2463
3015
  return async (ctx, next) => {
@@ -2625,7 +3177,33 @@ const safelyGetBody = async (ctx) => {
2625
3177
  return {};
2626
3178
  }
2627
3179
  };
3180
+ function getValidator(schema) {
3181
+ if (isZod(schema)) {
3182
+ return (data) => validateZod(schema, data);
3183
+ }
3184
+ if (isTypeBox(schema)) {
3185
+ return (data) => validateTypeBox(schema, data);
3186
+ }
3187
+ if (isAjv(schema)) {
3188
+ return (data) => validateAjv(schema, data);
3189
+ }
3190
+ if (isValibotWrapper(schema)) {
3191
+ return (data) => validateValibotWrapper(schema, data);
3192
+ }
3193
+ if (isClass(schema)) {
3194
+ return (data) => validateClassValidator(schema, data);
3195
+ }
3196
+ if (typeof schema === "function") {
3197
+ return schema;
3198
+ }
3199
+ throw new Error("Unknown validator type provided. Please use a supported library (Zod, Ajv, TypeBox) or a custom function.");
3200
+ }
2628
3201
  function validate(config) {
3202
+ const validators = {};
3203
+ if (config.params) validators.params = getValidator(config.params);
3204
+ if (config.query) validators.query = getValidator(config.query);
3205
+ if (config.headers) validators.headers = getValidator(config.headers);
3206
+ if (config.body) validators.body = getValidator(config.body);
2629
3207
  return async (ctx, next) => {
2630
3208
  const dataToValidate = {};
2631
3209
  if (config.params) dataToValidate.params = ctx.params;
@@ -2644,21 +3222,21 @@ function validate(config) {
2644
3222
  if (ctx.app?.applicationConfig.hooks?.beforeValidate) {
2645
3223
  await ctx.app.applicationConfig.hooks.beforeValidate(ctx, dataToValidate);
2646
3224
  }
2647
- if (config.params) {
2648
- ctx.params = await runValidation(config.params, ctx.params);
3225
+ if (validators.params) {
3226
+ ctx.params = await validators.params(ctx.params);
2649
3227
  }
2650
3228
  let validQuery;
2651
- if (config.query && queryObj) {
2652
- validQuery = await runValidation(config.query, queryObj);
3229
+ if (validators.query && queryObj) {
3230
+ validQuery = await validators.query(queryObj);
2653
3231
  }
2654
- if (config.headers) {
3232
+ if (validators.headers) {
2655
3233
  const headersObj = Object.fromEntries(ctx.req.headers.entries());
2656
- await runValidation(config.headers, headersObj);
3234
+ await validators.headers(headersObj);
2657
3235
  }
2658
3236
  let validBody;
2659
- if (config.body) {
3237
+ if (validators.body) {
2660
3238
  const b = body ?? await safelyGetBody(ctx);
2661
- validBody = await runValidation(config.body, b);
3239
+ validBody = await validators.body(b);
2662
3240
  const req = ctx.req;
2663
3241
  req._bodyValue = validBody;
2664
3242
  Object.defineProperty(req, "json", {
@@ -2677,36 +3255,6 @@ function validate(config) {
2677
3255
  return next();
2678
3256
  };
2679
3257
  }
2680
- async function runValidation(schema, data) {
2681
- if (isZod(schema)) {
2682
- return validateZod(schema, data);
2683
- }
2684
- if (isTypeBox(schema)) {
2685
- return validateTypeBox(schema, data);
2686
- }
2687
- if (isAjv(schema)) {
2688
- return validateAjv(schema, data);
2689
- }
2690
- if (isValibotWrapper(schema)) {
2691
- return validateValibotWrapper(schema, data);
2692
- }
2693
- if (isClass(schema)) {
2694
- return validateClassValidator(schema, data);
2695
- }
2696
- if (isTypeBox(schema)) {
2697
- return validateTypeBox(schema, data);
2698
- }
2699
- if (isAjv(schema)) {
2700
- return validateAjv(schema, data);
2701
- }
2702
- if (isValibotWrapper(schema)) {
2703
- return validateValibotWrapper(schema, data);
2704
- }
2705
- if (typeof schema === "function") {
2706
- return schema(data);
2707
- }
2708
- throw new Error("Unknown validator type provided. Please use a supported library (Zod, Ajv, TypeBox) or a custom function.");
2709
- }
2710
3258
  const ajv = new Ajv({ coerceTypes: true, allErrors: true });
2711
3259
  addFormats(ajv);
2712
3260
  const compiledValidators = /* @__PURE__ */ new WeakMap();
@@ -2721,17 +3269,18 @@ function openApiValidator() {
2721
3269
  cache = compileValidators(app.openApiSpec);
2722
3270
  compiledValidators.set(app, cache);
2723
3271
  }
2724
- const method = ctx.req.method.toLowerCase();
2725
3272
  let matchPath;
2726
- if (cache.has(ctx.path)) {
3273
+ let matchParams = {};
3274
+ if (cache.validators.has(ctx.path)) {
2727
3275
  matchPath = ctx.path;
2728
3276
  } else {
2729
- for (const specPath of cache.keys()) {
2730
- const regexStr = "^" + specPath.replace(/{([^}]+)}/g, "([^/]+)") + "$";
2731
- const regex = new RegExp(regexStr);
3277
+ for (const [path, { regex, paramNames }] of cache.paths) {
2732
3278
  const match = regex.exec(ctx.path);
2733
3279
  if (match) {
2734
- matchPath = specPath;
3280
+ matchPath = path;
3281
+ paramNames.forEach((name, i) => {
3282
+ matchParams[name] = match[i + 1];
3283
+ });
2735
3284
  break;
2736
3285
  }
2737
3286
  }
@@ -2739,7 +3288,8 @@ function openApiValidator() {
2739
3288
  if (!matchPath) {
2740
3289
  return next();
2741
3290
  }
2742
- const validators = cache.get(matchPath)?.[method];
3291
+ const method = ctx.req.method.toLowerCase();
3292
+ const validators = cache.validators.get(matchPath)?.[method];
2743
3293
  if (!validators) {
2744
3294
  return next();
2745
3295
  }
@@ -2764,21 +3314,7 @@ function openApiValidator() {
2764
3314
  }
2765
3315
  }
2766
3316
  if (validators.params) {
2767
- let params = ctx.params;
2768
- if (Object.keys(params).length === 0 && matchPath) {
2769
- const paramNames = (matchPath.match(/{([^}]+)}/g) || []).map((s) => s.slice(1, -1));
2770
- if (paramNames.length > 0) {
2771
- const regexStr = "^" + matchPath.replace(/{([^}]+)}/g, "([^/]+)") + "$";
2772
- const regex = new RegExp(regexStr);
2773
- const match = regex.exec(ctx.path);
2774
- if (match) {
2775
- params = {};
2776
- paramNames.forEach((name, i) => {
2777
- params[name] = match[i + 1];
2778
- });
2779
- }
2780
- }
2781
- }
3317
+ const params = { ...matchParams, ...ctx.params };
2782
3318
  const valid = validators.params(params);
2783
3319
  if (!valid && validators.params.errors) {
2784
3320
  errors.push(...validators.params.errors.map((e) => ({ ...e, location: "path" })));
@@ -2798,15 +3334,27 @@ function openApiValidator() {
2798
3334
  };
2799
3335
  }
2800
3336
  function compileValidators(spec) {
2801
- const cache = /* @__PURE__ */ new Map();
3337
+ const validators = /* @__PURE__ */ new Map();
3338
+ const paths = /* @__PURE__ */ new Map();
2802
3339
  for (const [path, pathItem] of Object.entries(spec.paths || {})) {
3340
+ if (path.includes("{")) {
3341
+ const paramNames = [];
3342
+ const regexStr = "^" + path.replace(/{([^}]+)}/g, (_, name) => {
3343
+ paramNames.push(name);
3344
+ return "([^/]+)";
3345
+ }) + "$";
3346
+ paths.set(path, {
3347
+ regex: new RegExp(regexStr),
3348
+ paramNames
3349
+ });
3350
+ }
2803
3351
  const pathValidators = {};
2804
3352
  for (const [method, operation] of Object.entries(pathItem)) {
2805
3353
  if (method === "parameters" || method === "summary" || method === "description") continue;
2806
3354
  const oper = operation;
2807
- const validators = {};
3355
+ const opValidators = {};
2808
3356
  if (oper.requestBody?.content?.["application/json"]?.schema) {
2809
- validators.body = ajv.compile(oper.requestBody.content["application/json"].schema);
3357
+ opValidators.body = ajv.compile(oper.requestBody.content["application/json"].schema);
2810
3358
  }
2811
3359
  const parameters = [...oper.parameters || [], ...pathItem.parameters || []];
2812
3360
  const queryProps = {};
@@ -2828,85 +3376,41 @@ function compileValidators(spec) {
2828
3376
  }
2829
3377
  }
2830
3378
  if (Object.keys(queryProps).length > 0) {
2831
- validators.query = ajv.compile({
3379
+ opValidators.query = ajv.compile({
2832
3380
  type: "object",
2833
3381
  properties: queryProps,
2834
3382
  required: queryRequired.length > 0 ? queryRequired : void 0
2835
3383
  });
2836
3384
  }
2837
3385
  if (Object.keys(pathProps).length > 0) {
2838
- validators.params = ajv.compile({
3386
+ opValidators.params = ajv.compile({
2839
3387
  type: "object",
2840
3388
  properties: pathProps,
2841
3389
  required: pathRequired.length > 0 ? pathRequired : void 0
2842
3390
  });
2843
3391
  }
2844
3392
  if (Object.keys(headerProps).length > 0) {
2845
- validators.headers = ajv.compile({
3393
+ opValidators.headers = ajv.compile({
2846
3394
  type: "object",
2847
3395
  properties: headerProps,
2848
3396
  required: headerRequired.length > 0 ? headerRequired : void 0
2849
3397
  });
2850
3398
  }
2851
- pathValidators[method] = validators;
3399
+ pathValidators[method] = opValidators;
2852
3400
  }
2853
- cache.set(path, pathValidators);
3401
+ validators.set(path, pathValidators);
2854
3402
  }
2855
- return cache;
3403
+ return { paths, validators };
2856
3404
  }
2857
- function RateLimit(options = {}) {
2858
- const windowMs = options.windowMs || 60 * 1e3;
2859
- const max = options.max || 5;
2860
- const message = options.message || "Too many requests, please try again later.";
2861
- const statusCode = options.statusCode || 429;
2862
- const headers = options.headers !== false;
2863
- const keyGenerator = options.keyGenerator || ((ctx) => {
2864
- return ctx.headers.get("x-forwarded-for") || ctx.url.hostname || "unknown";
3405
+ function precompileValidators(app, spec) {
3406
+ const cache = compileValidators(spec);
3407
+ compiledValidators.set(app, cache);
3408
+ }
3409
+ function enableOpenApiValidation(app) {
3410
+ app.use(openApiValidator());
3411
+ app.onSpecAvailable((spec) => {
3412
+ precompileValidators(app, spec);
2865
3413
  });
2866
- const skip = options.skip || (() => false);
2867
- const hits = /* @__PURE__ */ new Map();
2868
- const interval = setInterval(() => {
2869
- const now = Date.now();
2870
- for (const [key, record] of hits.entries()) {
2871
- if (record.resetTime <= now) {
2872
- hits.delete(key);
2873
- }
2874
- }
2875
- }, windowMs);
2876
- interval.unref?.();
2877
- return async (ctx, next) => {
2878
- if (skip(ctx)) return next();
2879
- const key = keyGenerator(ctx);
2880
- const now = Date.now();
2881
- let record = hits.get(key);
2882
- if (!record || record.resetTime <= now) {
2883
- record = {
2884
- hits: 0,
2885
- resetTime: now + windowMs
2886
- };
2887
- hits.set(key, record);
2888
- }
2889
- record.hits++;
2890
- const remaining = Math.max(0, max - record.hits);
2891
- const resetTime = Math.ceil(record.resetTime / 1e3);
2892
- if (record.hits > max) {
2893
- if (headers) {
2894
- const res = typeof message === "object" ? ctx.json(message, statusCode) : ctx.text(String(message), statusCode);
2895
- res.headers.set("X-RateLimit-Limit", String(max));
2896
- res.headers.set("X-RateLimit-Remaining", "0");
2897
- res.headers.set("X-RateLimit-Reset", String(resetTime));
2898
- return res;
2899
- }
2900
- return typeof message === "object" ? ctx.json(message, statusCode) : ctx.text(String(message), statusCode);
2901
- }
2902
- const response = await next();
2903
- if (response instanceof Response && headers) {
2904
- response.headers.set("X-RateLimit-Limit", String(max));
2905
- response.headers.set("X-RateLimit-Remaining", String(remaining));
2906
- response.headers.set("X-RateLimit-Reset", String(resetTime));
2907
- }
2908
- return response;
2909
- };
2910
3414
  }
2911
3415
  const eta = new Eta();
2912
3416
  class ScalarPlugin extends ShokupanRouter {
@@ -2983,7 +3487,7 @@ class ScalarPlugin extends ShokupanRouter {
2983
3487
  }
2984
3488
  }
2985
3489
  function SecurityHeaders(options = {}) {
2986
- return async (ctx, next) => {
3490
+ const securityHeadersMiddleware = async function SecurityHeadersMiddleware(ctx, next) {
2987
3491
  const headers = {};
2988
3492
  const set = (k, v) => headers[k] = v;
2989
3493
  if (options.dnsPrefetchControl !== false) {
@@ -3037,6 +3541,9 @@ function SecurityHeaders(options = {}) {
3037
3541
  }
3038
3542
  return response;
3039
3543
  };
3544
+ securityHeadersMiddleware.isBuiltin = true;
3545
+ securityHeadersMiddleware.pluginName = "SecurityHeaders";
3546
+ return securityHeadersMiddleware;
3040
3547
  }
3041
3548
  class Cookie {
3042
3549
  maxAge;
@@ -3150,7 +3657,7 @@ function Session(options) {
3150
3657
  const resave = options.resave === void 0 ? true : options.resave;
3151
3658
  const saveUninitialized = options.saveUninitialized === void 0 ? true : options.saveUninitialized;
3152
3659
  const rolling = options.rolling || false;
3153
- return async (ctx, next) => {
3660
+ const sessionMiddleware = async function SessionMiddleware(ctx, next) {
3154
3661
  let reqSessionId = null;
3155
3662
  const cookieHeader = ctx.req.headers.get("cookie");
3156
3663
  const cookies = {};
@@ -3286,6 +3793,9 @@ function Session(options) {
3286
3793
  }
3287
3794
  return result;
3288
3795
  };
3796
+ sessionMiddleware.isBuiltin = true;
3797
+ sessionMiddleware.pluginName = "Session";
3798
+ return sessionMiddleware;
3289
3799
  }
3290
3800
  export {
3291
3801
  $appRoot,
@@ -3326,6 +3836,7 @@ export {
3326
3836
  Put,
3327
3837
  Query,
3328
3838
  RateLimit,
3839
+ RateLimitMiddleware,
3329
3840
  Req,
3330
3841
  RouteParamType,
3331
3842
  RouterRegistry,
@@ -3341,8 +3852,11 @@ export {
3341
3852
  Spec,
3342
3853
  Use,
3343
3854
  ValidationError,
3855
+ compileValidators,
3344
3856
  compose,
3857
+ enableOpenApiValidation,
3345
3858
  openApiValidator,
3859
+ precompileValidators,
3346
3860
  useExpress,
3347
3861
  valibot,
3348
3862
  validate