shokupan 0.1.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 (61) hide show
  1. package/README.md +1 -0
  2. package/dist/benchmarking/advanced-cases/elysia.d.ts +1 -0
  3. package/dist/benchmarking/advanced-cases/express.d.ts +1 -0
  4. package/dist/benchmarking/advanced-cases/fastify.d.ts +1 -0
  5. package/dist/benchmarking/advanced-cases/hapi.d.ts +1 -0
  6. package/dist/benchmarking/advanced-cases/hono.d.ts +1 -0
  7. package/dist/benchmarking/advanced-cases/koa.d.ts +1 -0
  8. package/dist/benchmarking/advanced-cases/nest.d.ts +1 -0
  9. package/dist/benchmarking/advanced-cases/shokupan.d.ts +1 -0
  10. package/dist/benchmarking/advanced-data.d.ts +33 -0
  11. package/dist/benchmarking/advanced-runner.d.ts +1 -0
  12. package/dist/benchmarking/advanced-worker.d.ts +0 -0
  13. package/dist/benchmarking/cases/elysia.d.ts +1 -0
  14. package/dist/benchmarking/cases/express.d.ts +1 -0
  15. package/dist/benchmarking/cases/fastify.d.ts +1 -0
  16. package/dist/benchmarking/cases/hapi.d.ts +1 -0
  17. package/dist/benchmarking/cases/hono.d.ts +1 -0
  18. package/dist/benchmarking/cases/koa.d.ts +1 -0
  19. package/dist/benchmarking/cases/nest.d.ts +1 -0
  20. package/dist/benchmarking/cases/shokupan.d.ts +1 -0
  21. package/dist/benchmarking/data.d.ts +15 -0
  22. package/dist/benchmarking/quick_bench.d.ts +1 -0
  23. package/dist/benchmarking/runner.d.ts +1 -0
  24. package/dist/benchmarking/worker.d.ts +0 -0
  25. package/dist/buntest.d.ts +1 -0
  26. package/dist/cli.cjs +1 -1
  27. package/dist/cli.js +1 -1
  28. package/dist/context.d.ts +25 -8
  29. package/dist/decorators.d.ts +47 -0
  30. package/dist/index.cjs +1538 -655
  31. package/dist/index.cjs.map +1 -1
  32. package/dist/index.d.ts +1 -0
  33. package/dist/index.js +1532 -651
  34. package/dist/index.js.map +1 -1
  35. package/dist/middleware.d.ts +2 -0
  36. package/dist/{openapi-analyzer-cjdGeQ5a.js → openapi-analyzer-BtIaHIfe.js} +14 -6
  37. package/dist/openapi-analyzer-BtIaHIfe.js.map +1 -0
  38. package/dist/{openapi-analyzer-CFqgSLNK.cjs → openapi-analyzer-D9YB3IkV.cjs} +14 -6
  39. package/dist/openapi-analyzer-D9YB3IkV.cjs.map +1 -0
  40. package/dist/plugins/auth.d.ts +1 -1
  41. package/dist/plugins/debugview/plugin.d.ts +28 -0
  42. package/dist/plugins/failed-request-recorder.d.ts +14 -0
  43. package/dist/plugins/idempotency/plugin.d.ts +14 -0
  44. package/dist/plugins/openapi-validator.d.ts +30 -0
  45. package/dist/plugins/proxy.d.ts +9 -0
  46. package/dist/plugins/rate-limit.d.ts +3 -1
  47. package/dist/plugins/serve-static.d.ts +2 -3
  48. package/dist/response.d.ts +4 -0
  49. package/dist/router/trie.d.ts +14 -0
  50. package/dist/router.d.ts +50 -3
  51. package/dist/server-adapter-BWrEJbKL.js +64 -0
  52. package/dist/server-adapter-BWrEJbKL.js.map +1 -0
  53. package/dist/server-adapter-fVKP60e0.cjs +81 -0
  54. package/dist/server-adapter-fVKP60e0.cjs.map +1 -0
  55. package/dist/shokupan.d.ts +16 -3
  56. package/dist/types.d.ts +108 -4
  57. package/dist/util/cpu-monitor.d.ts +11 -0
  58. package/dist/util/stack.d.ts +8 -0
  59. package/package.json +8 -3
  60. package/dist/openapi-analyzer-CFqgSLNK.cjs.map +0 -1
  61. package/dist/openapi-analyzer-cjdGeQ5a.js.map +0 -1
package/dist/index.js CHANGED
@@ -1,22 +1,30 @@
1
- import { trace, SpanKind, SpanStatusCode, context } from "@opentelemetry/api";
1
+ import { readFile } from "node:fs/promises";
2
2
  import { Eta } from "eta";
3
- import { stat, readdir } from "fs/promises";
3
+ import { stat, readdir, readFile as readFile$1 } from "fs/promises";
4
4
  import { resolve, join, basename } from "path";
5
5
  import { AsyncLocalStorage } from "node:async_hooks";
6
+ import { createNodeEngines } from "@surrealdb/node";
7
+ import { Surreal, RecordId } from "surrealdb";
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";
8
- import { OpenAPIAnalyzer } from "./openapi-analyzer-cjdGeQ5a.js";
9
- import { randomUUID, createHmac } from "crypto";
10
- import { EventEmitter } from "events";
12
+ import * as zlib from "node:zlib";
13
+ import Ajv from "ajv";
14
+ import addFormats from "ajv-formats";
11
15
  import { plainToInstance } from "class-transformer";
12
16
  import { validateOrReject } from "class-validator";
17
+ import { OpenAPIAnalyzer } from "./openapi-analyzer-BtIaHIfe.js";
18
+ import { randomUUID, createHmac } from "crypto";
19
+ import { EventEmitter } from "events";
13
20
  class ShokupanResponse {
14
- _headers = new Headers();
21
+ _headers = null;
15
22
  _status = 200;
16
23
  /**
17
24
  * Get the current headers
18
25
  */
19
26
  get headers() {
27
+ if (!this._headers) this._headers = new Headers();
20
28
  return this._headers;
21
29
  }
22
30
  /**
@@ -37,6 +45,7 @@ class ShokupanResponse {
37
45
  * @param value Header value
38
46
  */
39
47
  set(key, value) {
48
+ if (!this._headers) this._headers = new Headers();
40
49
  this._headers.set(key, value);
41
50
  return this;
42
51
  }
@@ -46,6 +55,7 @@ class ShokupanResponse {
46
55
  * @param value Header value
47
56
  */
48
57
  append(key, value) {
58
+ if (!this._headers) this._headers = new Headers();
49
59
  this._headers.append(key, value);
50
60
  return this;
51
61
  }
@@ -54,29 +64,62 @@ class ShokupanResponse {
54
64
  * @param key Header name
55
65
  */
56
66
  get(key) {
57
- return this._headers.get(key);
67
+ return this._headers?.get(key) || null;
58
68
  }
59
69
  /**
60
70
  * Check if a header exists
61
71
  * @param key Header name
62
72
  */
63
73
  has(key) {
64
- return this._headers.has(key);
74
+ return this._headers?.has(key) || false;
75
+ }
76
+ /**
77
+ * Internal: check if headers have been initialized/modified
78
+ */
79
+ get hasPopulatedHeaders() {
80
+ return this._headers !== null;
65
81
  }
66
82
  }
67
83
  class ShokupanContext {
68
- constructor(request, server, state, app) {
84
+ // Raw body for compression optimization
85
+ constructor(request, server, state, app, signal, enableMiddlewareTracking = false) {
69
86
  this.request = request;
70
87
  this.server = server;
71
88
  this.app = app;
72
- this.url = new URL(request.url);
89
+ this.signal = signal;
73
90
  this.state = state || {};
91
+ if (enableMiddlewareTracking) {
92
+ const self = this;
93
+ this.state = new Proxy(this.state, {
94
+ set(target, p, newValue, receiver) {
95
+ const result = Reflect.set(target, p, newValue, receiver);
96
+ const currentHandler = self.handlerStack[self.handlerStack.length - 1];
97
+ if (currentHandler) {
98
+ if (!currentHandler.stateChanges) currentHandler.stateChanges = {};
99
+ currentHandler.stateChanges[p] = newValue;
100
+ }
101
+ return result;
102
+ }
103
+ });
104
+ }
74
105
  this.response = new ShokupanResponse();
75
106
  }
76
- url;
107
+ _url;
77
108
  params = {};
109
+ // Router assigns this, but default to empty object
78
110
  state;
111
+ handlerStack = [];
79
112
  response;
113
+ _debug;
114
+ _finalResponse;
115
+ _rawBody;
116
+ get url() {
117
+ if (!this._url) {
118
+ const urlString = this.request.url || "http://localhost/";
119
+ this._url = new URL(urlString);
120
+ }
121
+ return this._url;
122
+ }
80
123
  /**
81
124
  * Base request
82
125
  */
@@ -93,13 +136,42 @@ class ShokupanContext {
93
136
  * Request path
94
137
  */
95
138
  get path() {
96
- return this.url.pathname;
139
+ if (this._url) return this._url.pathname;
140
+ const url = this.request.url;
141
+ let queryIndex = url.indexOf("?");
142
+ const end = queryIndex === -1 ? url.length : queryIndex;
143
+ let start = 0;
144
+ const protocolIndex = url.indexOf("://");
145
+ if (protocolIndex !== -1) {
146
+ const hostStart = protocolIndex + 3;
147
+ const pathStart = url.indexOf("/", hostStart);
148
+ if (pathStart !== -1 && pathStart < end) {
149
+ start = pathStart;
150
+ } else {
151
+ return "/";
152
+ }
153
+ } else {
154
+ if (url.charCodeAt(0) === 47) {
155
+ start = 0;
156
+ }
157
+ }
158
+ return url.substring(start, end);
97
159
  }
98
160
  /**
99
161
  * Request query params
100
162
  */
101
163
  get query() {
102
- 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;
103
175
  }
104
176
  /**
105
177
  * Client IP address
@@ -174,25 +246,60 @@ class ShokupanContext {
174
246
  setCookie(name, value, options = {}) {
175
247
  let cookie = `${encodeURIComponent(name)}=${encodeURIComponent(value)}`;
176
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 || "/"}`;
177
251
  if (options.expires) cookie += `; Expires=${options.expires.toUTCString()}`;
178
252
  if (options.httpOnly) cookie += `; HttpOnly`;
179
253
  if (options.secure) cookie += `; Secure`;
180
- if (options.domain) cookie += `; Domain=${options.domain}`;
181
- if (options.path) cookie += `; Path=${options.path || "/"}`;
182
- if (options.sameSite) {
183
- typeof options.sameSite === "string" ? options.sameSite.toLowerCase() : options.sameSite ? "strict" : "lax";
184
- 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
+ }
185
273
  }
186
274
  if (options.priority) {
187
- 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";
188
279
  }
189
280
  this.response.append("Set-Cookie", cookie);
190
281
  return this;
191
282
  }
192
283
  mergeHeaders(headers) {
193
- const h = new Headers(this.response.headers);
284
+ let h;
285
+ if (this.response.hasPopulatedHeaders) {
286
+ h = new Headers(this.response.headers);
287
+ } else {
288
+ h = new Headers();
289
+ }
194
290
  if (headers) {
195
- new Headers(headers).forEach((v, k) => h.set(k, v));
291
+ if (headers instanceof Headers) {
292
+ headers.forEach((v, k) => h.set(k, v));
293
+ } else if (Array.isArray(headers)) {
294
+ headers.forEach(([k, v]) => h.set(k, v));
295
+ } else {
296
+ const keys = Object.keys(headers);
297
+ for (let i = 0; i < keys.length; i++) {
298
+ const key = keys[i];
299
+ const val = headers[key];
300
+ h.set(key, val);
301
+ }
302
+ }
196
303
  }
197
304
  return h;
198
305
  }
@@ -205,17 +312,21 @@ class ShokupanContext {
205
312
  send(body, options) {
206
313
  const headers = this.mergeHeaders(options?.headers);
207
314
  const status = options?.status ?? this.response.status;
208
- return new Response(body, { status, headers });
315
+ if (typeof body === "string" || body instanceof ArrayBuffer || body instanceof Uint8Array) {
316
+ this._rawBody = body;
317
+ }
318
+ this._finalResponse = new Response(body, { status, headers });
319
+ return this._finalResponse;
209
320
  }
210
321
  /**
211
322
  * Read request body
212
323
  */
213
324
  async body() {
214
- const contentType = this.request.headers.get("content-type");
215
- if (contentType?.includes("application/json")) {
325
+ const contentType = this.request.headers.get("content-type") || "";
326
+ if (contentType.includes("application/json") || contentType.includes("+json")) {
216
327
  return this.request.json();
217
328
  }
218
- 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")) {
219
330
  return this.request.formData();
220
331
  }
221
332
  return this.request.text();
@@ -224,28 +335,49 @@ class ShokupanContext {
224
335
  * Respond with a JSON object
225
336
  */
226
337
  json(data, status, headers) {
338
+ const finalStatus = status ?? this.response.status;
339
+ const jsonString = JSON.stringify(data);
340
+ this._rawBody = jsonString;
341
+ if (!headers && !this.response.hasPopulatedHeaders) {
342
+ this._finalResponse = new Response(jsonString, {
343
+ status: finalStatus,
344
+ headers: { "content-type": "application/json" }
345
+ });
346
+ return this._finalResponse;
347
+ }
227
348
  const finalHeaders = this.mergeHeaders(headers);
228
349
  finalHeaders.set("content-type", "application/json");
229
- const finalStatus = status ?? this.response.status;
230
- return new Response(JSON.stringify(data), { status: finalStatus, headers: finalHeaders });
350
+ this._finalResponse = new Response(jsonString, { status: finalStatus, headers: finalHeaders });
351
+ return this._finalResponse;
231
352
  }
232
353
  /**
233
354
  * Respond with a text string
234
355
  */
235
356
  text(data, status, headers) {
236
- const finalHeaders = this.mergeHeaders(headers);
237
- finalHeaders.set("content-type", "text/plain");
238
357
  const finalStatus = status ?? this.response.status;
239
- return new Response(data, { status: finalStatus, headers: finalHeaders });
358
+ this._rawBody = data;
359
+ if (!headers && !this.response.hasPopulatedHeaders) {
360
+ this._finalResponse = new Response(data, {
361
+ status: finalStatus,
362
+ headers: { "content-type": "text/plain; charset=utf-8" }
363
+ });
364
+ return this._finalResponse;
365
+ }
366
+ const finalHeaders = this.mergeHeaders(headers);
367
+ finalHeaders.set("content-type", "text/plain; charset=utf-8");
368
+ this._finalResponse = new Response(data, { status: finalStatus, headers: finalHeaders });
369
+ return this._finalResponse;
240
370
  }
241
371
  /**
242
372
  * Respond with HTML content
243
373
  */
244
374
  html(html, status, headers) {
245
- const finalHeaders = this.mergeHeaders(headers);
246
- finalHeaders.set("content-type", "text/html");
247
375
  const finalStatus = status ?? this.response.status;
248
- return new Response(html, { status: finalStatus, headers: finalHeaders });
376
+ const finalHeaders = this.mergeHeaders(headers);
377
+ finalHeaders.set("content-type", "text/html; charset=utf-8");
378
+ this._rawBody = html;
379
+ this._finalResponse = new Response(html, { status: finalStatus, headers: finalHeaders });
380
+ return this._finalResponse;
249
381
  }
250
382
  /**
251
383
  * Respond with a redirect
@@ -253,7 +385,8 @@ class ShokupanContext {
253
385
  redirect(url, status = 302) {
254
386
  const headers = this.mergeHeaders();
255
387
  headers.set("Location", url);
256
- return new Response(null, { status, headers });
388
+ this._finalResponse = new Response(null, { status, headers });
389
+ return this._finalResponse;
257
390
  }
258
391
  /**
259
392
  * Respond with a status code
@@ -261,15 +394,26 @@ class ShokupanContext {
261
394
  */
262
395
  status(status) {
263
396
  const headers = this.mergeHeaders();
264
- return new Response(null, { status, headers });
397
+ this._finalResponse = new Response(null, { status, headers });
398
+ return this._finalResponse;
265
399
  }
266
400
  /**
267
401
  * Respond with a file
268
402
  */
269
- file(path, fileOptions, responseOptions) {
403
+ async file(path, fileOptions, responseOptions) {
270
404
  const headers = this.mergeHeaders(responseOptions?.headers);
271
405
  const status = responseOptions?.status ?? this.response.status;
272
- return new Response(Bun.file(path, fileOptions), { status, headers });
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
+ }
273
417
  }
274
418
  /**
275
419
  * JSX Rendering Function
@@ -289,6 +433,74 @@ class ShokupanContext {
289
433
  return this.html(html, status, headers);
290
434
  }
291
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
+ }
292
504
  const $isApplication = /* @__PURE__ */ Symbol.for("Shokupan.app");
293
505
  const $appRoot = /* @__PURE__ */ Symbol.for("Shokupan.app-root");
294
506
  const $isMounted = /* @__PURE__ */ Symbol("Shokupan.isMounted");
@@ -385,6 +597,9 @@ const Patch = createMethodDecorator("PATCH");
385
597
  const Options = createMethodDecorator("OPTIONS");
386
598
  const Head = createMethodDecorator("HEAD");
387
599
  const All = createMethodDecorator("ALL");
600
+ function RateLimit(options) {
601
+ return Use(RateLimitMiddleware(options));
602
+ }
388
603
  class Container {
389
604
  static services = /* @__PURE__ */ new Map();
390
605
  static register(target, instance) {
@@ -418,69 +633,43 @@ function Inject(token) {
418
633
  });
419
634
  };
420
635
  }
421
- const tracer = trace.getTracer("shokupan.middleware");
422
- function traceMiddleware(fn, name) {
423
- const middlewareName = fn.name || "anonymous middleware";
424
- return async (ctx, next) => {
425
- return tracer.startActiveSpan(`middleware - ${middlewareName}`, {
426
- kind: SpanKind.INTERNAL,
427
- attributes: {
428
- "code.function": middlewareName,
429
- "component": "shokupan.middleware"
430
- }
431
- }, async (span) => {
432
- try {
433
- const result = await fn(ctx, next);
434
- return result;
435
- } catch (err) {
436
- span.recordException(err);
437
- span.setStatus({ code: SpanStatusCode.ERROR, message: err.message });
438
- throw err;
439
- } finally {
440
- span.end();
441
- }
442
- });
443
- };
444
- }
445
- function traceHandler(fn, name) {
446
- return async function(...args) {
447
- return tracer.startActiveSpan(`route handler - ${name}`, {
448
- kind: SpanKind.INTERNAL,
449
- attributes: {
450
- "http.route": name,
451
- "component": "shokupan.route"
452
- }
453
- }, async (span) => {
636
+ const compose = (middleware) => {
637
+ if (!middleware.length) {
638
+ return (context2, next) => {
639
+ return next ? next() : Promise.resolve();
640
+ };
641
+ }
642
+ return function dispatch(context2, next) {
643
+ let index = -1;
644
+ async function runner(i) {
645
+ if (i <= index) return Promise.reject(new Error("next() called multiple times"));
646
+ index = i;
647
+ if (i >= middleware.length) {
648
+ return next ? next() : Promise.resolve();
649
+ }
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();
454
660
  try {
455
- const result = await fn.apply(this, args);
456
- return result;
661
+ const res = await Promise.resolve(fn(context2, () => runner(i + 1)));
662
+ debug.trackStep(debugId, "middleware", performance.now() - start, "success");
663
+ return res;
457
664
  } catch (err) {
458
- span.recordException(err);
459
- span.setStatus({ code: SpanStatusCode.ERROR, message: err.message });
460
- throw err;
665
+ debug.trackStep(debugId, "middleware", performance.now() - start, "error", err);
666
+ return Promise.reject(err);
461
667
  } finally {
462
- span.end();
668
+ if (previousNode) debug.setNode(previousNode);
463
669
  }
464
- });
465
- };
466
- }
467
- const compose = (middleware) => {
468
- function fn(context2, next) {
469
- let runner = next || (async () => {
470
- });
471
- for (let i = middleware.length - 1; i >= 0; i--) {
472
- const fn2 = traceMiddleware(middleware[i]);
473
- const nextStep = runner;
474
- let called = false;
475
- runner = async () => {
476
- if (called) throw new Error("next() called multiple times");
477
- called = true;
478
- return fn2(context2, nextStep);
479
- };
480
670
  }
481
- return runner();
482
- }
483
- return fn;
671
+ return runner(0);
672
+ };
484
673
  };
485
674
  class ShokupanRequestBase {
486
675
  method;
@@ -538,6 +727,15 @@ function deepMerge(target, ...sources) {
538
727
  }
539
728
  return deepMerge(target, ...sources);
540
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;
541
739
  function analyzeHandler(handler) {
542
740
  const handlerSource = handler.toString();
543
741
  const inferredSpec = {};
@@ -547,46 +745,28 @@ function analyzeHandler(handler) {
547
745
  };
548
746
  }
549
747
  const queryParams = /* @__PURE__ */ new Map();
550
- const queryIntMatch = handlerSource.match(/parseInt\(ctx\.query\.(\w+)\)/g);
551
- if (queryIntMatch) {
552
- queryIntMatch.forEach((match) => {
553
- const paramName = match.match(/ctx\.query\.(\w+)/)?.[1];
554
- if (paramName) queryParams.set(paramName, { type: "integer", format: "int32" });
555
- });
748
+ for (const match of handlerSource.matchAll(REGEX_QUERY_INT)) {
749
+ if (match[1]) queryParams.set(match[1], { type: "integer", format: "int32" });
556
750
  }
557
- const queryFloatMatch = handlerSource.match(/parseFloat\(ctx\.query\.(\w+)\)/g);
558
- if (queryFloatMatch) {
559
- queryFloatMatch.forEach((match) => {
560
- const paramName = match.match(/ctx\.query\.(\w+)/)?.[1];
561
- if (paramName) queryParams.set(paramName, { type: "number", format: "float" });
562
- });
751
+ for (const match of handlerSource.matchAll(REGEX_QUERY_FLOAT)) {
752
+ if (match[1]) queryParams.set(match[1], { type: "number", format: "float" });
563
753
  }
564
- const queryNumberMatch = handlerSource.match(/Number\(ctx\.query\.(\w+)\)/g);
565
- if (queryNumberMatch) {
566
- queryNumberMatch.forEach((match) => {
567
- const paramName = match.match(/ctx\.query\.(\w+)/)?.[1];
568
- if (paramName && !queryParams.has(paramName)) {
569
- queryParams.set(paramName, { type: "number" });
570
- }
571
- });
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
+ }
572
758
  }
573
- const queryBoolMatch = handlerSource.match(/(?:Boolean\(ctx\.query\.(\w+)\)|!+ctx\.query\.(\w+))/g);
574
- if (queryBoolMatch) {
575
- queryBoolMatch.forEach((match) => {
576
- const paramName = match.match(/ctx\.query\.(\w+)/)?.[1];
577
- if (paramName && !queryParams.has(paramName)) {
578
- queryParams.set(paramName, { type: "boolean" });
579
- }
580
- });
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
+ }
581
764
  }
582
- const queryMatch = handlerSource.match(/ctx\.query\.(\w+)/g);
583
- if (queryMatch) {
584
- queryMatch.forEach((match) => {
585
- const paramName = match.split(".")[2];
586
- if (paramName && !queryParams.has(paramName)) {
587
- queryParams.set(paramName, { type: "string" });
588
- }
589
- });
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
+ }
590
770
  }
591
771
  if (queryParams.size > 0) {
592
772
  if (!inferredSpec.parameters) inferredSpec.parameters = [];
@@ -599,19 +779,11 @@ function analyzeHandler(handler) {
599
779
  });
600
780
  }
601
781
  const pathParams = /* @__PURE__ */ new Map();
602
- const paramIntMatch = handlerSource.match(/parseInt\(ctx\.params\.(\w+)\)/g);
603
- if (paramIntMatch) {
604
- paramIntMatch.forEach((match) => {
605
- const paramName = match.match(/ctx\.params\.(\w+)/)?.[1];
606
- if (paramName) pathParams.set(paramName, { type: "integer", format: "int32" });
607
- });
782
+ for (const match of handlerSource.matchAll(REGEX_PARAM_INT)) {
783
+ if (match[1]) pathParams.set(match[1], { type: "integer", format: "int32" });
608
784
  }
609
- const paramFloatMatch = handlerSource.match(/parseFloat\(ctx\.params\.(\w+)\)/g);
610
- if (paramFloatMatch) {
611
- paramFloatMatch.forEach((match) => {
612
- const paramName = match.match(/ctx\.params\.(\w+)/)?.[1];
613
- if (paramName) pathParams.set(paramName, { type: "number", format: "float" });
614
- });
785
+ for (const match of handlerSource.matchAll(REGEX_PARAM_FLOAT)) {
786
+ if (match[1]) pathParams.set(match[1], { type: "number", format: "float" });
615
787
  }
616
788
  if (pathParams.size > 0) {
617
789
  if (!inferredSpec.parameters) inferredSpec.parameters = [];
@@ -624,76 +796,55 @@ function analyzeHandler(handler) {
624
796
  });
625
797
  });
626
798
  }
627
- const headerMatch = handlerSource.match(/ctx\.get\(['"](\w+)['"]\)/g);
628
- if (headerMatch) {
629
- if (!inferredSpec.parameters) inferredSpec.parameters = [];
630
- headerMatch.forEach((match) => {
631
- const headerName = match.match(/['"](\w+)['"]/)?.[1];
632
- if (headerName) {
633
- inferredSpec.parameters.push({
634
- name: headerName,
635
- in: "header",
636
- schema: { type: "string" }
637
- });
638
- }
639
- });
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
+ }
640
808
  }
641
809
  const responses = {};
642
810
  if (handlerSource.includes("ctx.json(")) {
643
811
  responses["200"] = {
644
812
  description: "Successful response",
645
- content: {
646
- "application/json": { schema: { type: "object" } }
647
- }
813
+ content: { "application/json": { schema: { type: "object" } } }
648
814
  };
649
815
  }
650
816
  if (handlerSource.includes("ctx.html(")) {
651
817
  responses["200"] = {
652
818
  description: "Successful response",
653
- content: {
654
- "text/html": { schema: { type: "string" } }
655
- }
819
+ content: { "text/html": { schema: { type: "string" } } }
656
820
  };
657
821
  }
658
822
  if (handlerSource.includes("ctx.text(")) {
659
823
  responses["200"] = {
660
824
  description: "Successful response",
661
- content: {
662
- "text/plain": { schema: { type: "string" } }
663
- }
825
+ content: { "text/plain": { schema: { type: "string" } } }
664
826
  };
665
827
  }
666
828
  if (handlerSource.includes("ctx.file(")) {
667
829
  responses["200"] = {
668
830
  description: "File download",
669
- content: {
670
- "application/octet-stream": { schema: { type: "string", format: "binary" } }
671
- }
831
+ content: { "application/octet-stream": { schema: { type: "string", format: "binary" } } }
672
832
  };
673
833
  }
674
834
  if (handlerSource.includes("ctx.redirect(")) {
675
- responses["302"] = {
676
- description: "Redirect"
677
- };
835
+ responses["302"] = { description: "Redirect" };
678
836
  }
679
837
  if (!responses["200"] && /return\s+\{/.test(handlerSource)) {
680
838
  responses["200"] = {
681
839
  description: "Successful response",
682
- content: {
683
- "application/json": { schema: { type: "object" } }
684
- }
840
+ content: { "application/json": { schema: { type: "object" } } }
685
841
  };
686
842
  }
687
- const errorStatusMatch = handlerSource.match(/ctx\.(?:json|text|html)\([^)]+,\s*(\d{3,})\)/g);
688
- if (errorStatusMatch) {
689
- errorStatusMatch.forEach((match) => {
690
- const statusCode = match.match(/,\s*(\d{3,})\)/)?.[1];
691
- if (statusCode && statusCode !== "200") {
692
- responses[statusCode] = {
693
- description: `Error response (${statusCode})`
694
- };
695
- }
696
- });
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
+ }
697
848
  }
698
849
  if (Object.keys(responses).length > 0) {
699
850
  inferredSpec.responses = responses;
@@ -707,7 +858,7 @@ async function generateOpenApi(rootRouter, options = {}) {
707
858
  const defaultTagName = options.defaultTag || "Application";
708
859
  let astRoutes = [];
709
860
  try {
710
- const { OpenAPIAnalyzer: OpenAPIAnalyzer2 } = await import("./openapi-analyzer-cjdGeQ5a.js");
861
+ const { OpenAPIAnalyzer: OpenAPIAnalyzer2 } = await import("./openapi-analyzer-BtIaHIfe.js");
711
862
  const analyzer = new OpenAPIAnalyzer2(process.cwd());
712
863
  const { applications } = await analyzer.analyze();
713
864
  const appMap = /* @__PURE__ */ new Map();
@@ -977,10 +1128,10 @@ async function generateOpenApi(rootRouter, options = {}) {
977
1128
  };
978
1129
  }
979
1130
  const eta$1 = new Eta();
980
- function serveStatic(ctx, config, prefix) {
1131
+ function serveStatic(config, prefix) {
981
1132
  const rootPath = resolve(config.root || ".");
982
1133
  const normalizedPrefix = prefix.endsWith("/") && prefix !== "/" ? prefix.slice(0, -1) : prefix;
983
- return async () => {
1134
+ const serveStaticMiddleware = async (ctx) => {
984
1135
  let relative = ctx.path.slice(normalizedPrefix.length);
985
1136
  if (!relative.startsWith("/") && relative.length > 0) relative = "/" + relative;
986
1137
  if (relative.length === 0) relative = "/";
@@ -1097,16 +1248,209 @@ function serveStatic(ctx, config, prefix) {
1097
1248
  }
1098
1249
  }
1099
1250
  }
1100
- const file = Bun.file(finalPath);
1101
- 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
+ }
1102
1258
  if (config.hooks?.onResponse) {
1103
1259
  const hooked = await config.hooks.onResponse(ctx, response);
1104
1260
  if (hooked) response = hooked;
1105
1261
  }
1106
1262
  return response;
1107
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
+ }
1108
1366
  }
1109
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
+ });
1402
+ const tracer = trace.getTracer("shokupan.middleware");
1403
+ function traceHandler(fn, name) {
1404
+ return async function(...args) {
1405
+ return tracer.startActiveSpan(`route handler - ${name}`, {
1406
+ kind: SpanKind.INTERNAL,
1407
+ attributes: {
1408
+ "http.route": name,
1409
+ "component": "shokupan.route"
1410
+ }
1411
+ }, async (span) => {
1412
+ try {
1413
+ const result = await fn.apply(this, args);
1414
+ return result;
1415
+ } catch (err) {
1416
+ span.recordException(err);
1417
+ span.setStatus({ code: SpanStatusCode.ERROR, message: err.message });
1418
+ throw err;
1419
+ } finally {
1420
+ span.end();
1421
+ }
1422
+ });
1423
+ };
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
+ }
1110
1454
  const RouterRegistry = /* @__PURE__ */ new Map();
1111
1455
  const ShokupanApplicationTree = {};
1112
1456
  class ShokupanRouter {
@@ -1126,6 +1470,7 @@ class ShokupanRouter {
1126
1470
  [$parent] = null;
1127
1471
  [$childRouters] = [];
1128
1472
  [$childControllers] = [];
1473
+ middleware = [];
1129
1474
  get rootConfig() {
1130
1475
  return this[$appRoot]?.applicationConfig;
1131
1476
  }
@@ -1134,7 +1479,54 @@ class ShokupanRouter {
1134
1479
  }
1135
1480
  [$routes] = [];
1136
1481
  // Public via Symbol for OpenAPI generator
1482
+ trie = new RouterTrie();
1483
+ metadata;
1484
+ // Metadata for the router itself
1137
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
+ }
1138
1530
  isRouterInstance(target) {
1139
1531
  return typeof target === "object" && target !== null && $isRouter in target;
1140
1532
  }
@@ -1160,6 +1552,14 @@ class ShokupanRouter {
1160
1552
  throw new Error("Router is already mounted");
1161
1553
  }
1162
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
+ }
1163
1563
  this[$childRouters].push(controller);
1164
1564
  controller[$parent] = this;
1165
1565
  const setRouterContext = (router) => {
@@ -1192,6 +1592,12 @@ class ShokupanRouter {
1192
1592
  }
1193
1593
  }
1194
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
+ };
1195
1601
  this[$childControllers].push(instance);
1196
1602
  const controllerMiddleware = (typeof controller === "function" ? controller[$middleware] : instance[$middleware]) || [];
1197
1603
  const proto = Object.getPrototypeOf(instance);
@@ -1275,14 +1681,39 @@ class ShokupanRouter {
1275
1681
  for (const arg of sortedArgs) {
1276
1682
  switch (arg.type) {
1277
1683
  case RouteParamType.BODY:
1278
- 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
+ }
1279
1700
  break;
1280
1701
  case RouteParamType.PARAM:
1281
1702
  args[arg.index] = arg.name ? ctx.params[arg.name] : ctx.params;
1282
1703
  break;
1283
1704
  case RouteParamType.QUERY: {
1284
1705
  const url = new URL(ctx.req.url);
1285
- 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
+ }
1286
1717
  break;
1287
1718
  }
1288
1719
  case RouteParamType.HEADER:
@@ -1297,7 +1728,7 @@ class ShokupanRouter {
1297
1728
  }
1298
1729
  }
1299
1730
  }
1300
- const tracedOriginalHandler = traceHandler(originalHandler, normalizedPath);
1731
+ const tracedOriginalHandler = ctx.app?.applicationConfig.enableTracing ? traceHandler(originalHandler, normalizedPath) : originalHandler;
1301
1732
  return tracedOriginalHandler.apply(instance, args);
1302
1733
  };
1303
1734
  let finalHandler = wrappedHandler;
@@ -1430,29 +1861,59 @@ class ShokupanRouter {
1430
1861
  data: result
1431
1862
  };
1432
1863
  }
1433
- applyHooks(match) {
1864
+ applyRouterHooks(match) {
1434
1865
  if (!this.config?.hooks) return match;
1435
1866
  const hooks = this.config.hooks;
1436
- const originalHandler = match.handler;
1437
- match.handler = async (ctx) => {
1438
- if (hooks.onRequestStart) await hooks.onRequestStart(ctx);
1439
- try {
1440
- const result = await originalHandler(ctx);
1441
- if (hooks.onRequestEnd) await hooks.onRequestEnd(ctx);
1442
- return result;
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();
1896
+ try {
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;
1443
1904
  } catch (err) {
1444
- if (hooks.onError) {
1445
- try {
1446
- await hooks.onError(err, ctx);
1447
- } catch (e) {
1448
- console.error("Error in router onError hook:", e);
1449
- }
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);
1450
1909
  }
1451
1910
  throw err;
1911
+ } finally {
1912
+ if (debug && previousNode) debug.setNode(previousNode);
1452
1913
  }
1453
1914
  };
1454
- match.handler.originalHandler = originalHandler.originalHandler || originalHandler;
1455
- return match;
1915
+ wrapped.originalHandler = originalHandler.originalHandler ?? originalHandler;
1916
+ return wrapped;
1456
1917
  }
1457
1918
  /**
1458
1919
  * Find a route matching the given method and path.
@@ -1461,29 +1922,24 @@ class ShokupanRouter {
1461
1922
  * @returns Route handler and parameters if found, otherwise null
1462
1923
  */
1463
1924
  find(method, path) {
1464
- for (const route of this[$routes]) {
1465
- if (route.method !== "ALL" && route.method !== method) continue;
1466
- const match = route.regex.exec(path);
1467
- if (match) {
1468
- const params = {};
1469
- route.keys.forEach((key, index) => {
1470
- params[key] = match[index + 1];
1471
- });
1472
- return this.applyHooks({ handler: route.handler, params });
1473
- }
1925
+ let result = this.trie.search(method, path);
1926
+ if (result) return result;
1927
+ if (method === "HEAD") {
1928
+ result = this.trie.search("GET", path);
1929
+ if (result) return result;
1474
1930
  }
1475
1931
  for (const child of this[$childRouters]) {
1476
1932
  const prefix = child[$mountPath];
1477
1933
  if (path === prefix || path.startsWith(prefix + "/")) {
1478
1934
  const subPath = path.slice(prefix.length) || "/";
1479
1935
  const match = child.find(method, subPath);
1480
- if (match) return this.applyHooks(match);
1936
+ if (match) return this.applyRouterHooks(match);
1481
1937
  }
1482
1938
  if (prefix.endsWith("/")) {
1483
1939
  if (path.startsWith(prefix)) {
1484
1940
  const subPath = path.slice(prefix.length) || "/";
1485
1941
  const match = child.find(method, subPath);
1486
- if (match) return this.applyHooks(match);
1942
+ if (match) return this.applyRouterHooks(match);
1487
1943
  }
1488
1944
  }
1489
1945
  }
@@ -1494,7 +1950,7 @@ class ShokupanRouter {
1494
1950
  const pattern = path.replace(/:([a-zA-Z0-9_]+)/g, (_, key) => {
1495
1951
  keys.push(key);
1496
1952
  return "([^/]+)";
1497
- }).replace(/\*/g, ".*");
1953
+ }).replace(/\*\*/g, ".*").replace(/\*/g, "[^/]+");
1498
1954
  return {
1499
1955
  regex: new RegExp(`^${pattern}$`),
1500
1956
  keys
@@ -1563,18 +2019,84 @@ class ShokupanRouter {
1563
2019
  return innerHandler(ctx);
1564
2020
  };
1565
2021
  }
2022
+ const { file, line } = getCallerInfo();
2023
+ const trackingHandler = wrappedHandler;
2024
+ wrappedHandler = async (ctx) => {
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
+ }
2075
+ }
2076
+ };
2077
+ wrappedHandler.originalHandler = trackingHandler.originalHandler || trackingHandler;
2078
+ let bakedHandler = wrappedHandler;
2079
+ if (this.config?.hooks) {
2080
+ bakedHandler = this.wrapWithHooks(wrappedHandler, this.config.hooks);
2081
+ }
1566
2082
  this[$routes].push({
1567
2083
  method,
1568
2084
  path,
1569
- regex,
1570
- keys,
1571
- handler: wrappedHandler,
2085
+ regex: regex ?? new RegExp(""),
2086
+ keys: keys ?? [],
2087
+ handler,
2088
+ bakedHandler,
1572
2089
  handlerSpec: spec,
1573
2090
  group,
1574
- guards: routeGuards.length > 0 ? routeGuards : void 0,
1575
- requestTimeout: effectiveTimeout
1576
- // Save for inspection? Or just relying on closure
2091
+ hooks: this.config?.hooks,
2092
+ requestTimeout,
2093
+ renderer,
2094
+ metadata: {
2095
+ file,
2096
+ line
2097
+ }
1577
2098
  });
2099
+ this.trie.insert(method, path, bakedHandler);
1578
2100
  return this;
1579
2101
  }
1580
2102
  get(path, ...args) {
@@ -1608,7 +2130,35 @@ class ShokupanRouter {
1608
2130
  guard(specOrHandler, handler) {
1609
2131
  const spec = typeof specOrHandler === "function" ? void 0 : specOrHandler;
1610
2132
  const guardHandler = typeof specOrHandler === "function" ? specOrHandler : handler;
1611
- this.currentGuards.push({ handler: guardHandler, spec });
2133
+ let file = "unknown";
2134
+ let line = 0;
2135
+ try {
2136
+ const err = new Error();
2137
+ const stack = err.stack?.split("\n") || [];
2138
+ const callerLine = stack.find(
2139
+ (l) => l.includes(":") && !l.includes("router.ts") && !l.includes("shokupan.ts") && !l.includes("node_modules") && !l.includes("bun:main")
2140
+ );
2141
+ if (callerLine) {
2142
+ const match = callerLine.match(/\((.*):(\d+):(\d+)\)/) || callerLine.match(/at (.*):(\d+):(\d+)/);
2143
+ if (match) {
2144
+ file = match[1];
2145
+ line = parseInt(match[2], 10);
2146
+ }
2147
+ }
2148
+ } catch (e) {
2149
+ }
2150
+ const trackedGuard = async (ctx, next) => {
2151
+ if (ctx.app?.applicationConfig.enableMiddlewareTracking) {
2152
+ ctx.handlerStack.push({
2153
+ name: guardHandler.name || "guard",
2154
+ file,
2155
+ line
2156
+ });
2157
+ }
2158
+ return guardHandler(ctx, next);
2159
+ };
2160
+ trackedGuard.originalHandler = guardHandler.originalHandler || guardHandler;
2161
+ this.currentGuards.push({ handler: trackedGuard, spec });
1612
2162
  return this;
1613
2163
  }
1614
2164
  /**
@@ -1620,10 +2170,10 @@ class ShokupanRouter {
1620
2170
  const config = typeof options === "string" ? { root: options } : options;
1621
2171
  const prefix = uriPath.startsWith("/") ? uriPath : "/" + uriPath;
1622
2172
  const normalizedPrefix = prefix.endsWith("/") && prefix !== "/" ? prefix.slice(0, -1) : prefix;
1623
- serveStatic(null, config, prefix);
2173
+ const handlerMiddleware = serveStatic(config, prefix);
1624
2174
  const routeHandler = async (ctx) => {
1625
- const runner = serveStatic(ctx, config, prefix);
1626
- return runner();
2175
+ return handlerMiddleware(ctx, async () => {
2176
+ });
1627
2177
  };
1628
2178
  let groupName = "Static";
1629
2179
  const segments = normalizedPrefix.split("/").filter(Boolean);
@@ -1684,6 +2234,49 @@ class ShokupanRouter {
1684
2234
  return generateOpenApi(this, options);
1685
2235
  }
1686
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
+ }
1687
2280
  const defaults = {
1688
2281
  port: 3e3,
1689
2282
  hostname: "localhost",
@@ -1695,21 +2288,59 @@ trace.getTracer("shokupan.application");
1695
2288
  class Shokupan extends ShokupanRouter {
1696
2289
  applicationConfig = {};
1697
2290
  openApiSpec;
1698
- middleware = [];
2291
+ composedMiddleware;
2292
+ cpuMonitor;
2293
+ hookCache = /* @__PURE__ */ new Map();
2294
+ hooksInitialized = false;
1699
2295
  get logger() {
1700
2296
  return this.applicationConfig.logger;
1701
2297
  }
1702
2298
  constructor(applicationConfig = {}) {
1703
- super();
2299
+ const config = Object.assign({}, defaults, applicationConfig);
2300
+ const { hooks, ...routerConfig } = config;
2301
+ super(routerConfig);
1704
2302
  this[$isApplication] = true;
1705
2303
  this[$appRoot] = this;
1706
- 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
+ };
1707
2311
  }
1708
2312
  /**
1709
2313
  * Adds middleware to the application.
1710
2314
  */
1711
2315
  use(middleware) {
1712
- this.middleware.push(middleware);
2316
+ let trackedMiddleware = middleware;
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
+ };
2326
+ }
2327
+ trackedMiddleware = async (ctx, next) => {
2328
+ const c = ctx;
2329
+ if (c.handlerStack && c.app?.applicationConfig.enableMiddlewareTracking) {
2330
+ const metadata = middleware.metadata || {};
2331
+ c.handlerStack.push({
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
2336
+ });
2337
+ }
2338
+ return middleware(ctx, next);
2339
+ };
2340
+ trackedMiddleware.metadata = middleware.metadata;
2341
+ Object.defineProperty(trackedMiddleware, "name", { value: middleware.name || "middleware" });
2342
+ trackedMiddleware.order = this.middleware.length;
2343
+ this.middleware.push(trackedMiddleware);
1713
2344
  return this;
1714
2345
  }
1715
2346
  startupHooks = [];
@@ -1720,6 +2351,15 @@ class Shokupan extends ShokupanRouter {
1720
2351
  this.startupHooks.push(callback);
1721
2352
  return this;
1722
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
+ }
1723
2363
  /**
1724
2364
  * Starts the application server.
1725
2365
  *
@@ -1736,17 +2376,43 @@ class Shokupan extends ShokupanRouter {
1736
2376
  }
1737
2377
  if (this.applicationConfig.enableOpenApiGen) {
1738
2378
  this.openApiSpec = await generateOpenApi(this);
2379
+ for (const hook of this.specAvailableHooks) {
2380
+ await hook(this.openApiSpec);
2381
+ }
1739
2382
  }
1740
2383
  if (port === 0 && process.platform === "linux") ;
2384
+ if (this.applicationConfig.autoBackpressureFeedback) {
2385
+ this.cpuMonitor = new SystemCpuMonitor();
2386
+ this.cpuMonitor.start();
2387
+ }
1741
2388
  const serveOptions = {
1742
2389
  port: finalPort,
1743
2390
  hostname: this.applicationConfig.hostname,
1744
2391
  development: this.applicationConfig.development,
1745
2392
  fetch: this.fetch.bind(this),
1746
2393
  reusePort: this.applicationConfig.reusePort,
1747
- idleTimeout: this.applicationConfig.readTimeout ? this.applicationConfig.readTimeout / 1e3 : void 0
2394
+ idleTimeout: this.applicationConfig.readTimeout ? this.applicationConfig.readTimeout / 1e3 : void 0,
2395
+ websocket: {
2396
+ open(ws) {
2397
+ ws.data?.handler?.open?.(ws);
2398
+ },
2399
+ message(ws, message) {
2400
+ ws.data?.handler?.message?.(ws, message);
2401
+ },
2402
+ drain(ws) {
2403
+ ws.data?.handler?.drain?.(ws);
2404
+ },
2405
+ close(ws, code, reason) {
2406
+ ws.data?.handler?.close?.(ws, code, reason);
2407
+ }
2408
+ }
1748
2409
  };
1749
- const server = this.applicationConfig.serverFactory ? await this.applicationConfig.serverFactory(serveOptions) : Bun.serve(serveOptions);
2410
+ let factory = this.applicationConfig.serverFactory;
2411
+ if (!factory && typeof Bun === "undefined") {
2412
+ const { createHttpServer } = await import("./server-adapter-BWrEJbKL.js");
2413
+ factory = createHttpServer();
2414
+ }
2415
+ const server = factory ? await factory(serveOptions) : Bun.serve(serveOptions);
1750
2416
  console.log(`Shokupan server listening on http://${server.hostname}:${server.port}`);
1751
2417
  return server;
1752
2418
  }
@@ -1801,110 +2467,165 @@ class Shokupan extends ShokupanRouter {
1801
2467
  * @returns The response to send.
1802
2468
  */
1803
2469
  async fetch(req, server) {
1804
- const tracer2 = trace.getTracer("shokupan.application");
1805
- const store = asyncContext.getStore();
1806
- const attrs = {
1807
- attributes: {
1808
- "http.url": req.url,
1809
- "http.method": req.method
1810
- }
1811
- };
1812
- const parent = store?.get("span");
1813
- const ctx = parent ? trace.setSpan(context.active(), parent) : void 0;
1814
- return tracer2.startActiveSpan(`${req.method} ${new URL(req.url).pathname}`, attrs, ctx, (span) => {
2470
+ if (this.applicationConfig.enableTracing) {
2471
+ const tracer2 = trace.getTracer("shokupan.application");
2472
+ const store = asyncContext.getStore();
2473
+ const attrs = {
2474
+ attributes: {
2475
+ "http.url": req.url,
2476
+ "http.method": req.method
2477
+ }
2478
+ };
2479
+ const parent = store?.get("span");
2480
+ const ctx = parent ? trace.setSpan(context.active(), parent) : void 0;
2481
+ return tracer2.startActiveSpan(`${req.method} ${new URL(req.url).pathname}`, attrs, ctx, (span) => {
2482
+ const ctxMap = /* @__PURE__ */ new Map();
2483
+ ctxMap.set("span", span);
2484
+ ctxMap.set("request", req);
2485
+ return asyncContext.run(ctxMap, () => this.handleRequest(req, server).finally(() => span.end()));
2486
+ });
2487
+ }
2488
+ if (this.applicationConfig.enableAsyncLocalStorage) {
1815
2489
  const ctxMap = /* @__PURE__ */ new Map();
1816
- ctxMap.set("span", span);
1817
2490
  ctxMap.set("request", req);
1818
- const runCallback = () => {
1819
- const request = req;
1820
- const ctx2 = new ShokupanContext(request, server, void 0, this);
1821
- const handle = async () => {
1822
- try {
1823
- if (this.applicationConfig.hooks?.onRequestStart) {
1824
- await this.applicationConfig.hooks.onRequestStart(ctx2);
1825
- }
1826
- const fn = compose(this.middleware);
1827
- const result = await fn(ctx2, async () => {
1828
- const match = this.find(req.method, ctx2.path);
1829
- if (match) {
1830
- ctx2.params = match.params;
1831
- return match.handler(ctx2);
1832
- }
1833
- return null;
1834
- });
1835
- let response;
1836
- if (result instanceof Response) {
1837
- response = result;
1838
- } else if (result === null || result === void 0) {
1839
- span.setAttribute("http.status_code", 404);
1840
- response = ctx2.text("Not Found", 404);
1841
- } else if (typeof result === "object") {
1842
- response = ctx2.json(result);
1843
- } else {
1844
- response = ctx2.text(String(result));
1845
- }
1846
- if (this.applicationConfig.hooks?.onRequestEnd) {
1847
- await this.applicationConfig.hooks.onRequestEnd(ctx2);
1848
- }
1849
- if (this.applicationConfig.hooks?.onResponseStart) {
1850
- await this.applicationConfig.hooks.onResponseStart(ctx2, response);
1851
- }
1852
- return response;
1853
- } catch (err) {
1854
- console.error(err);
1855
- span.recordException(err);
1856
- span.setStatus({ code: 2 });
1857
- const status = err.status || err.statusCode || 500;
1858
- const body = { error: err.message || "Internal Server Error" };
1859
- if (err.errors) body.errors = err.errors;
1860
- if (this.applicationConfig.hooks?.onError) {
1861
- try {
1862
- await this.applicationConfig.hooks.onError(err, ctx2);
1863
- } catch (hookErr) {
1864
- console.error("Error in onError hook:", hookErr);
1865
- }
1866
- }
1867
- return ctx2.json(body, status);
1868
- }
1869
- };
1870
- let executionPromise = handle();
1871
- const timeoutMs = this.applicationConfig.requestTimeout;
1872
- if (timeoutMs && timeoutMs > 0 && this.applicationConfig.hooks?.onRequestTimeout) {
1873
- let timeoutId;
1874
- const timeoutPromise = new Promise((_, reject) => {
1875
- timeoutId = setTimeout(async () => {
1876
- try {
1877
- if (this.applicationConfig.hooks?.onRequestTimeout) {
1878
- await this.applicationConfig.hooks.onRequestTimeout(ctx2);
1879
- }
1880
- } catch (e) {
1881
- console.error("Error in onRequestTimeout hook:", e);
1882
- }
1883
- reject(new Error("Request Timeout"));
1884
- }, timeoutMs);
1885
- });
1886
- executionPromise = Promise.race([executionPromise, timeoutPromise]).finally(() => clearTimeout(timeoutId));
2491
+ return asyncContext.run(ctxMap, () => this.handleRequest(req, server));
2492
+ }
2493
+ return this.handleRequest(req, server);
2494
+ }
2495
+ async handleRequest(req, server) {
2496
+ const request = req;
2497
+ const controller = new AbortController();
2498
+ const ctx = new ShokupanContext(request, server, void 0, this, controller.signal, this.applicationConfig.enableMiddlewareTracking);
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
+ }
2506
+ try {
2507
+ if (this.hasHook("onRequestStart")) {
2508
+ await this.executeHook("onRequestStart", ctx);
1887
2509
  }
1888
- return executionPromise.catch((err) => {
1889
- if (err.message === "Request Timeout") {
1890
- return ctx2.text("Request Timeout", 408);
2510
+ const fn = this.composedMiddleware ??= compose(this.middleware);
2511
+ const result = await fn(ctx, async () => {
2512
+ const match = this.find(req.method, ctx.path);
2513
+ if (match) {
2514
+ ctx.params = match.params;
2515
+ return match.handler(ctx);
1891
2516
  }
1892
- console.error("Unexpected error in request execution:", err);
1893
- return ctx2.text("Internal Server Error", 500);
1894
- }).then(async (res) => {
1895
- if (this.applicationConfig.hooks?.onResponseEnd) {
1896
- await this.applicationConfig.hooks.onResponseEnd(ctx2, res);
2517
+ return null;
2518
+ });
2519
+ let response;
2520
+ if (result instanceof Response) {
2521
+ response = result;
2522
+ } else if ((result === null || result === void 0) && ctx._finalResponse instanceof Response) {
2523
+ response = ctx._finalResponse;
2524
+ } else if (result === null || result === void 0) {
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);
1897
2531
  }
1898
- return res;
1899
- }).finally(() => span.end());
1900
- };
1901
- if (this.applicationConfig.enableAsyncLocalStorage) {
1902
- return asyncContext.run(ctxMap, runCallback);
1903
- } else {
1904
- return runCallback();
2532
+ } else if (typeof result === "object") {
2533
+ response = ctx.json(result);
2534
+ } else {
2535
+ response = ctx.text(String(result));
2536
+ }
2537
+ if (this.hasHook("onRequestEnd")) {
2538
+ await this.executeHook("onRequestEnd", ctx);
2539
+ }
2540
+ if (this.hasHook("onResponseStart")) {
2541
+ await this.executeHook("onResponseStart", ctx, response);
2542
+ }
2543
+ return response;
2544
+ } catch (err) {
2545
+ console.error(err);
2546
+ const span = asyncContext.getStore()?.get("span");
2547
+ if (span) span.setStatus({ code: 2 });
2548
+ const status = err.status || err.statusCode || 500;
2549
+ const body = { error: err.message || "Internal Server Error" };
2550
+ if (err.errors) body.errors = err.errors;
2551
+ if (this.hasHook("onError")) {
2552
+ await this.executeHook("onError", err, ctx);
2553
+ }
2554
+ return ctx.json(body, status);
1905
2555
  }
2556
+ };
2557
+ let executionPromise = handle();
2558
+ const timeoutMs = this.applicationConfig.requestTimeout;
2559
+ if (timeoutMs && timeoutMs > 0) {
2560
+ let timeoutId;
2561
+ const timeoutPromise = new Promise((_, reject) => {
2562
+ timeoutId = setTimeout(async () => {
2563
+ controller.abort();
2564
+ if (this.hasHook("onRequestTimeout")) {
2565
+ await this.executeHook("onRequestTimeout", ctx);
2566
+ }
2567
+ reject(new Error("Request Timeout"));
2568
+ }, timeoutMs);
2569
+ });
2570
+ executionPromise = Promise.race([executionPromise, timeoutPromise]).finally(() => clearTimeout(timeoutId));
2571
+ }
2572
+ return executionPromise.catch((err) => {
2573
+ if (err.message === "Request Timeout") {
2574
+ return ctx.text("Request Timeout", 408);
2575
+ }
2576
+ console.error("Unexpected error in request execution:", err);
2577
+ return ctx.text("Internal Server Error", 500);
2578
+ }).then(async (res) => {
2579
+ if (this.hasHook("onResponseEnd")) {
2580
+ await this.executeHook("onResponseEnd", ctx, res);
2581
+ }
2582
+ return res;
1906
2583
  });
1907
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
+ }
1908
2629
  }
1909
2630
  class AuthPlugin extends ShokupanRouter {
1910
2631
  constructor(authConfig) {
@@ -2109,7 +2830,7 @@ class AuthPlugin extends ShokupanRouter {
2109
2830
  /**
2110
2831
  * Middleware to verify JWT
2111
2832
  */
2112
- middleware() {
2833
+ getMiddleware() {
2113
2834
  return async (ctx, next) => {
2114
2835
  const authHeader = ctx.req.headers.get("Authorization");
2115
2836
  let token = authHeader?.startsWith("Bearer ") ? authHeader.substring(7) : null;
@@ -2129,19 +2850,44 @@ class AuthPlugin extends ShokupanRouter {
2129
2850
  }
2130
2851
  }
2131
2852
  function Compression(options = {}) {
2132
- const threshold = options.threshold ?? 1024;
2133
- return async (ctx, next) => {
2853
+ const threshold = options.threshold ?? 512;
2854
+ const compressionMiddleware = async function CompressionMiddleware(ctx, next) {
2134
2855
  const acceptEncoding = ctx.headers.get("accept-encoding") || "";
2135
2856
  let method = null;
2136
2857
  if (acceptEncoding.includes("br")) method = "br";
2137
- 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";
2138
2864
  else if (acceptEncoding.includes("deflate")) method = "deflate";
2139
2865
  if (!method) return next();
2140
- const response = await next();
2866
+ let response = await next();
2867
+ if (!(response instanceof Response) && ctx._finalResponse instanceof Response) {
2868
+ response = ctx._finalResponse;
2869
+ }
2141
2870
  if (response instanceof Response) {
2142
2871
  if (response.headers.has("Content-Encoding")) return response;
2143
- const body = await response.arrayBuffer();
2144
- 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) {
2145
2891
  return new Response(body, {
2146
2892
  status: response.status,
2147
2893
  statusText: response.statusText,
@@ -2149,17 +2895,36 @@ function Compression(options = {}) {
2149
2895
  });
2150
2896
  }
2151
2897
  let compressed;
2152
- if (method === "br") {
2153
- compressed = require("node:zlib").brotliCompressSync(body);
2154
- } else if (method === "gzip") {
2155
- compressed = Bun.gzipSync(body);
2156
- } else {
2157
- compressed = Bun.deflateSync(body);
2898
+ switch (method) {
2899
+ case "br":
2900
+ compressed = await new Promise((res, rej) => zlib.brotliCompress(body, {
2901
+ params: {
2902
+ [zlib.constants.BROTLI_PARAM_QUALITY]: 4
2903
+ }
2904
+ }, (err, data) => {
2905
+ if (err) return rej(err);
2906
+ res(data);
2907
+ }));
2908
+ break;
2909
+ case "gzip":
2910
+ compressed = await new Promise((res, rej) => zlib.gzip(body, (err, data) => {
2911
+ if (err) return rej(err);
2912
+ res(data);
2913
+ }));
2914
+ break;
2915
+ case "zstd":
2916
+ compressed = await Bun.zstdCompress(body);
2917
+ break;
2918
+ default:
2919
+ compressed = await new Promise((res, rej) => zlib.deflate(body, (err, data) => {
2920
+ if (err) return rej(err);
2921
+ res(data);
2922
+ }));
2923
+ break;
2158
2924
  }
2159
2925
  const headers = new Headers(response.headers);
2160
2926
  headers.set("Content-Encoding", method);
2161
2927
  headers.set("Content-Length", String(compressed.length));
2162
- headers.delete("Content-Length");
2163
2928
  return new Response(compressed, {
2164
2929
  status: response.status,
2165
2930
  statusText: response.statusText,
@@ -2168,6 +2933,9 @@ function Compression(options = {}) {
2168
2933
  }
2169
2934
  return response;
2170
2935
  };
2936
+ compressionMiddleware.isBuiltin = true;
2937
+ compressionMiddleware.pluginName = "Compression";
2938
+ return compressionMiddleware;
2171
2939
  }
2172
2940
  function Cors(options = {}) {
2173
2941
  const defaults2 = {
@@ -2177,7 +2945,7 @@ function Cors(options = {}) {
2177
2945
  optionsSuccessStatus: 204
2178
2946
  };
2179
2947
  const opts = { ...defaults2, ...options };
2180
- return async (ctx, next) => {
2948
+ const corsMiddleware = async function CorsMiddleware(ctx, next) {
2181
2949
  const headers = new Headers();
2182
2950
  const origin = ctx.headers.get("origin");
2183
2951
  const set = (k, v) => headers.set(k, v);
@@ -2239,6 +3007,9 @@ function Cors(options = {}) {
2239
3007
  }
2240
3008
  return response;
2241
3009
  };
3010
+ corsMiddleware.isBuiltin = true;
3011
+ corsMiddleware.pluginName = "Cors";
3012
+ return corsMiddleware;
2242
3013
  }
2243
3014
  function useExpress(expressMiddleware) {
2244
3015
  return async (ctx, next) => {
@@ -2300,122 +3071,409 @@ function useExpress(expressMiddleware) {
2300
3071
  });
2301
3072
  };
2302
3073
  }
2303
- function RateLimit(options = {}) {
2304
- const windowMs = options.windowMs || 60 * 1e3;
2305
- const max = options.max || 5;
2306
- const message = options.message || "Too many requests, please try again later.";
2307
- const statusCode = options.statusCode || 429;
2308
- const headers = options.headers !== false;
2309
- const keyGenerator = options.keyGenerator || ((ctx) => {
2310
- return ctx.headers.get("x-forwarded-for") || ctx.url.hostname || "unknown";
2311
- });
2312
- const skip = options.skip || (() => false);
2313
- const hits = /* @__PURE__ */ new Map();
2314
- const interval = setInterval(() => {
2315
- const now = Date.now();
2316
- for (const [key, record] of hits.entries()) {
2317
- if (record.resetTime <= now) {
2318
- hits.delete(key);
2319
- }
2320
- }
2321
- }, windowMs);
2322
- if (interval.unref) interval.unref();
2323
- return async (ctx, next) => {
2324
- if (skip(ctx)) return next();
2325
- const key = keyGenerator(ctx);
2326
- const now = Date.now();
2327
- let record = hits.get(key);
2328
- if (!record || record.resetTime <= now) {
2329
- record = {
2330
- hits: 0,
2331
- resetTime: now + windowMs
2332
- };
2333
- hits.set(key, record);
2334
- }
2335
- record.hits++;
2336
- const remaining = Math.max(0, max - record.hits);
2337
- const resetTime = Math.ceil(record.resetTime / 1e3);
2338
- if (record.hits > max) {
2339
- if (headers) {
2340
- const res = typeof message === "object" ? ctx.json(message, statusCode) : ctx.text(String(message), statusCode);
2341
- res.headers.set("X-RateLimit-Limit", String(max));
2342
- res.headers.set("X-RateLimit-Remaining", "0");
2343
- res.headers.set("X-RateLimit-Reset", String(resetTime));
2344
- return res;
2345
- }
2346
- return typeof message === "object" ? ctx.json(message, statusCode) : ctx.text(String(message), statusCode);
2347
- }
2348
- const response = await next();
2349
- if (response instanceof Response && headers) {
2350
- response.headers.set("X-RateLimit-Limit", String(max));
2351
- response.headers.set("X-RateLimit-Remaining", String(remaining));
2352
- response.headers.set("X-RateLimit-Reset", String(resetTime));
2353
- }
2354
- return response;
2355
- };
3074
+ class ValidationError extends Error {
3075
+ constructor(errors) {
3076
+ super("Validation Error");
3077
+ this.errors = errors;
3078
+ }
3079
+ status = 400;
2356
3080
  }
2357
- const eta = new Eta();
2358
- class ScalarPlugin extends ShokupanRouter {
2359
- constructor(pluginOptions) {
2360
- super();
2361
- this.pluginOptions = pluginOptions;
2362
- this.init();
3081
+ function isZod(schema) {
3082
+ return typeof schema?.safeParse === "function";
3083
+ }
3084
+ async function validateZod(schema, data) {
3085
+ const result = await schema.safeParseAsync(data);
3086
+ if (!result.success) {
3087
+ throw new ValidationError(result.error.errors);
2363
3088
  }
2364
- init() {
2365
- this.get("/", (ctx) => {
2366
- let path = ctx.url.toString();
2367
- if (!path.endsWith("/")) path += "/";
2368
- return ctx.html(eta.renderString(`<!doctype html>
2369
- <html>
2370
- <head>
2371
- <title>API Reference</title>
2372
- <meta charset = "utf-8" />
2373
- <meta name="viewport" content = "width=device-width, initial-scale=1" />
2374
- </head>
2375
-
2376
- <body>
2377
- <div id="app"></div>
2378
-
2379
- <script src="<%= it.path %>scalar.js"><\/script>
2380
- <script>
2381
- Scalar.createApiReference('#app', [{ ...<%~ JSON.stringify(it.config.baseDocument) %>,
2382
- url: "<%= it.path %>openapi.json",
2383
- }
2384
- ])
2385
- <\/script>
2386
- </body>
2387
-
2388
- </html>`, { path, config: this.pluginOptions }));
2389
- });
2390
- this.get("/scalar.js", (ctx) => {
2391
- return ctx.file(__dirname + "/../../node_modules/@scalar/api-reference/dist/browser/standalone.js");
2392
- });
2393
- this.get("/openapi.json", async (ctx) => {
2394
- let spec;
2395
- if (this.root.openApiSpec) {
2396
- try {
2397
- spec = structuredClone(this.root.openApiSpec);
2398
- } catch (e) {
2399
- spec = Object.assign({}, this.root.openApiSpec);
2400
- }
2401
- } else {
2402
- spec = await (this.root || this).generateApiSpec();
2403
- }
2404
- if (this.pluginOptions.baseDocument) {
2405
- deepMerge(spec, this.pluginOptions.baseDocument);
2406
- }
2407
- return ctx.json(spec);
2408
- });
3089
+ return result.data;
3090
+ }
3091
+ function isTypeBox(schema) {
3092
+ return typeof schema?.Check === "function" && typeof schema?.Errors === "function";
3093
+ }
3094
+ function validateTypeBox(schema, data) {
3095
+ if (!schema.Check(data)) {
3096
+ throw new ValidationError([...schema.Errors(data)]);
2409
3097
  }
2410
- // New lifecycle method to be called by router.mount
2411
- onMount(parent) {
2412
- if (parent.onStart) {
2413
- parent.onStart(async () => {
2414
- if (this.pluginOptions.enableStaticAnalysis) {
2415
- try {
2416
- const entrypoint = process.argv[1];
2417
- console.log(`[ScalarPlugin] Running eager static analysis on entrypoint: ${entrypoint}`);
2418
- const analyzer = new OpenAPIAnalyzer(process.cwd(), entrypoint);
3098
+ return data;
3099
+ }
3100
+ function isAjv(schema) {
3101
+ return typeof schema === "function" && "errors" in schema;
3102
+ }
3103
+ function validateAjv(schema, data) {
3104
+ const valid = schema(data);
3105
+ if (!valid) {
3106
+ throw new ValidationError(schema.errors);
3107
+ }
3108
+ return data;
3109
+ }
3110
+ const valibot = (schema, parser) => {
3111
+ return {
3112
+ _valibot: true,
3113
+ schema,
3114
+ parser
3115
+ };
3116
+ };
3117
+ function isValibotWrapper(schema) {
3118
+ return schema?._valibot === true;
3119
+ }
3120
+ async function validateValibotWrapper(wrapper, data) {
3121
+ const result = await wrapper.parser(wrapper.schema, data);
3122
+ if (!result.success) {
3123
+ throw new ValidationError(result.issues);
3124
+ }
3125
+ return result.output;
3126
+ }
3127
+ function isClass(schema) {
3128
+ try {
3129
+ if (typeof schema === "function" && /^\s*class\s+/.test(schema.toString())) {
3130
+ return true;
3131
+ }
3132
+ return typeof schema === "function" && schema.prototype && schema.name;
3133
+ } catch {
3134
+ return false;
3135
+ }
3136
+ }
3137
+ async function validateClassValidator(schema, data) {
3138
+ const object = plainToInstance(schema, data);
3139
+ try {
3140
+ await validateOrReject(object);
3141
+ return object;
3142
+ } catch (errors) {
3143
+ const formattedErrors = Array.isArray(errors) ? errors.map((err) => ({
3144
+ property: err.property,
3145
+ constraints: err.constraints,
3146
+ children: err.children
3147
+ })) : errors;
3148
+ throw new ValidationError(formattedErrors);
3149
+ }
3150
+ }
3151
+ const safelyGetBody = async (ctx) => {
3152
+ const req = ctx.req;
3153
+ if (req._bodyParsed) {
3154
+ return req._bodyValue;
3155
+ }
3156
+ try {
3157
+ let data;
3158
+ if (typeof req.json === "function") {
3159
+ data = await req.json();
3160
+ } else {
3161
+ data = req.body;
3162
+ if (typeof data === "string") {
3163
+ try {
3164
+ data = JSON.parse(data);
3165
+ } catch {
3166
+ }
3167
+ }
3168
+ }
3169
+ req._bodyParsed = true;
3170
+ req._bodyValue = data;
3171
+ Object.defineProperty(req, "json", {
3172
+ value: async () => req._bodyValue,
3173
+ configurable: true
3174
+ });
3175
+ return data;
3176
+ } catch (e) {
3177
+ return {};
3178
+ }
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
+ }
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);
3207
+ return async (ctx, next) => {
3208
+ const dataToValidate = {};
3209
+ if (config.params) dataToValidate.params = ctx.params;
3210
+ let queryObj;
3211
+ if (config.query) {
3212
+ const url = new URL(ctx.req.url);
3213
+ queryObj = Object.fromEntries(url.searchParams.entries());
3214
+ dataToValidate.query = queryObj;
3215
+ }
3216
+ if (config.headers) dataToValidate.headers = Object.fromEntries(ctx.req.headers.entries());
3217
+ let body;
3218
+ if (config.body) {
3219
+ body = await safelyGetBody(ctx);
3220
+ dataToValidate.body = body;
3221
+ }
3222
+ if (ctx.app?.applicationConfig.hooks?.beforeValidate) {
3223
+ await ctx.app.applicationConfig.hooks.beforeValidate(ctx, dataToValidate);
3224
+ }
3225
+ if (validators.params) {
3226
+ ctx.params = await validators.params(ctx.params);
3227
+ }
3228
+ let validQuery;
3229
+ if (validators.query && queryObj) {
3230
+ validQuery = await validators.query(queryObj);
3231
+ }
3232
+ if (validators.headers) {
3233
+ const headersObj = Object.fromEntries(ctx.req.headers.entries());
3234
+ await validators.headers(headersObj);
3235
+ }
3236
+ let validBody;
3237
+ if (validators.body) {
3238
+ const b = body ?? await safelyGetBody(ctx);
3239
+ validBody = await validators.body(b);
3240
+ const req = ctx.req;
3241
+ req._bodyValue = validBody;
3242
+ Object.defineProperty(req, "json", {
3243
+ value: async () => validBody,
3244
+ configurable: true
3245
+ });
3246
+ ctx.body = validBody;
3247
+ }
3248
+ if (ctx.app?.applicationConfig.hooks?.afterValidate) {
3249
+ const validatedData = { ...dataToValidate };
3250
+ if (config.params) validatedData.params = ctx.params;
3251
+ if (config.query) validatedData.query = validQuery;
3252
+ if (config.body) validatedData.body = validBody;
3253
+ await ctx.app.applicationConfig.hooks.afterValidate(ctx, validatedData);
3254
+ }
3255
+ return next();
3256
+ };
3257
+ }
3258
+ const ajv = new Ajv({ coerceTypes: true, allErrors: true });
3259
+ addFormats(ajv);
3260
+ const compiledValidators = /* @__PURE__ */ new WeakMap();
3261
+ function openApiValidator() {
3262
+ return async (ctx, next) => {
3263
+ const app = ctx.app;
3264
+ if (!app || !app.openApiSpec) {
3265
+ return next();
3266
+ }
3267
+ let cache = compiledValidators.get(app);
3268
+ if (!cache) {
3269
+ cache = compileValidators(app.openApiSpec);
3270
+ compiledValidators.set(app, cache);
3271
+ }
3272
+ let matchPath;
3273
+ let matchParams = {};
3274
+ if (cache.validators.has(ctx.path)) {
3275
+ matchPath = ctx.path;
3276
+ } else {
3277
+ for (const [path, { regex, paramNames }] of cache.paths) {
3278
+ const match = regex.exec(ctx.path);
3279
+ if (match) {
3280
+ matchPath = path;
3281
+ paramNames.forEach((name, i) => {
3282
+ matchParams[name] = match[i + 1];
3283
+ });
3284
+ break;
3285
+ }
3286
+ }
3287
+ }
3288
+ if (!matchPath) {
3289
+ return next();
3290
+ }
3291
+ const method = ctx.req.method.toLowerCase();
3292
+ const validators = cache.validators.get(matchPath)?.[method];
3293
+ if (!validators) {
3294
+ return next();
3295
+ }
3296
+ const errors = [];
3297
+ if (validators.body) {
3298
+ let body;
3299
+ try {
3300
+ body = await ctx.req.json().catch(() => ({}));
3301
+ } catch {
3302
+ body = {};
3303
+ }
3304
+ const valid = validators.body(body);
3305
+ if (!valid && validators.body.errors) {
3306
+ errors.push(...validators.body.errors.map((e) => ({ ...e, location: "body" })));
3307
+ }
3308
+ }
3309
+ if (validators.query) {
3310
+ const query = Object.fromEntries(new URL(ctx.req.url).searchParams.entries());
3311
+ const valid = validators.query(query);
3312
+ if (!valid && validators.query.errors) {
3313
+ errors.push(...validators.query.errors.map((e) => ({ ...e, location: "query" })));
3314
+ }
3315
+ }
3316
+ if (validators.params) {
3317
+ const params = { ...matchParams, ...ctx.params };
3318
+ const valid = validators.params(params);
3319
+ if (!valid && validators.params.errors) {
3320
+ errors.push(...validators.params.errors.map((e) => ({ ...e, location: "path" })));
3321
+ }
3322
+ }
3323
+ if (validators.headers) {
3324
+ const headers = Object.fromEntries(ctx.req.headers.entries());
3325
+ const valid = validators.headers(headers);
3326
+ if (!valid && validators.headers.errors) {
3327
+ errors.push(...validators.headers.errors.map((e) => ({ ...e, location: "header" })));
3328
+ }
3329
+ }
3330
+ if (errors.length > 0) {
3331
+ throw new ValidationError(errors);
3332
+ }
3333
+ return next();
3334
+ };
3335
+ }
3336
+ function compileValidators(spec) {
3337
+ const validators = /* @__PURE__ */ new Map();
3338
+ const paths = /* @__PURE__ */ new Map();
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
+ }
3351
+ const pathValidators = {};
3352
+ for (const [method, operation] of Object.entries(pathItem)) {
3353
+ if (method === "parameters" || method === "summary" || method === "description") continue;
3354
+ const oper = operation;
3355
+ const opValidators = {};
3356
+ if (oper.requestBody?.content?.["application/json"]?.schema) {
3357
+ opValidators.body = ajv.compile(oper.requestBody.content["application/json"].schema);
3358
+ }
3359
+ const parameters = [...oper.parameters || [], ...pathItem.parameters || []];
3360
+ const queryProps = {};
3361
+ const pathProps = {};
3362
+ const headerProps = {};
3363
+ const queryRequired = [];
3364
+ const pathRequired = [];
3365
+ const headerRequired = [];
3366
+ for (const param of parameters) {
3367
+ if (param.in === "query") {
3368
+ queryProps[param.name] = param.schema || {};
3369
+ if (param.required) queryRequired.push(param.name);
3370
+ } else if (param.in === "path") {
3371
+ pathProps[param.name] = param.schema || {};
3372
+ pathRequired.push(param.name);
3373
+ } else if (param.in === "header") {
3374
+ headerProps[param.name] = param.schema || {};
3375
+ if (param.required) headerRequired.push(param.name);
3376
+ }
3377
+ }
3378
+ if (Object.keys(queryProps).length > 0) {
3379
+ opValidators.query = ajv.compile({
3380
+ type: "object",
3381
+ properties: queryProps,
3382
+ required: queryRequired.length > 0 ? queryRequired : void 0
3383
+ });
3384
+ }
3385
+ if (Object.keys(pathProps).length > 0) {
3386
+ opValidators.params = ajv.compile({
3387
+ type: "object",
3388
+ properties: pathProps,
3389
+ required: pathRequired.length > 0 ? pathRequired : void 0
3390
+ });
3391
+ }
3392
+ if (Object.keys(headerProps).length > 0) {
3393
+ opValidators.headers = ajv.compile({
3394
+ type: "object",
3395
+ properties: headerProps,
3396
+ required: headerRequired.length > 0 ? headerRequired : void 0
3397
+ });
3398
+ }
3399
+ pathValidators[method] = opValidators;
3400
+ }
3401
+ validators.set(path, pathValidators);
3402
+ }
3403
+ return { paths, validators };
3404
+ }
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);
3413
+ });
3414
+ }
3415
+ const eta = new Eta();
3416
+ class ScalarPlugin extends ShokupanRouter {
3417
+ constructor(pluginOptions) {
3418
+ super();
3419
+ this.pluginOptions = pluginOptions;
3420
+ this.init();
3421
+ }
3422
+ init() {
3423
+ this.get("/", (ctx) => {
3424
+ let path = ctx.url.toString();
3425
+ if (!path.endsWith("/")) path += "/";
3426
+ return ctx.html(eta.renderString(`<!doctype html>
3427
+ <html>
3428
+ <head>
3429
+ <title>API Reference</title>
3430
+ <meta charset = "utf-8" />
3431
+ <meta name="viewport" content = "width=device-width, initial-scale=1" />
3432
+ </head>
3433
+
3434
+ <body>
3435
+ <div id="app"></div>
3436
+
3437
+ <script src="<%= it.path %>scalar.js"><\/script>
3438
+ <script>
3439
+ Scalar.createApiReference('#app', [{ ...<%~ JSON.stringify(it.config.baseDocument) %>,
3440
+ url: "<%= it.path %>openapi.json",
3441
+ }
3442
+ ])
3443
+ <\/script>
3444
+ </body>
3445
+
3446
+ </html>`, { path, config: this.pluginOptions }));
3447
+ });
3448
+ this.get("/scalar.js", (ctx) => {
3449
+ return ctx.file(__dirname + "/../../node_modules/@scalar/api-reference/dist/browser/standalone.js");
3450
+ });
3451
+ this.get("/openapi.json", async (ctx) => {
3452
+ let spec;
3453
+ if (this.root.openApiSpec) {
3454
+ try {
3455
+ spec = structuredClone(this.root.openApiSpec);
3456
+ } catch (e) {
3457
+ spec = Object.assign({}, this.root.openApiSpec);
3458
+ }
3459
+ } else {
3460
+ spec = await (this.root || this).generateApiSpec();
3461
+ }
3462
+ if (this.pluginOptions.baseDocument) {
3463
+ deepMerge(spec, this.pluginOptions.baseDocument);
3464
+ }
3465
+ return ctx.json(spec);
3466
+ });
3467
+ }
3468
+ // New lifecycle method to be called by router.mount
3469
+ onMount(parent) {
3470
+ if (parent.onStart) {
3471
+ parent.onStart(async () => {
3472
+ if (this.pluginOptions.enableStaticAnalysis) {
3473
+ try {
3474
+ const entrypoint = process.argv[1];
3475
+ console.log(`[ScalarPlugin] Running eager static analysis on entrypoint: ${entrypoint}`);
3476
+ const analyzer = new OpenAPIAnalyzer(process.cwd(), entrypoint);
2419
3477
  let staticSpec = await analyzer.analyze();
2420
3478
  if (!this.pluginOptions.baseDocument) this.pluginOptions.baseDocument = {};
2421
3479
  deepMerge(this.pluginOptions.baseDocument, staticSpec);
@@ -2429,7 +3487,7 @@ class ScalarPlugin extends ShokupanRouter {
2429
3487
  }
2430
3488
  }
2431
3489
  function SecurityHeaders(options = {}) {
2432
- return async (ctx, next) => {
3490
+ const securityHeadersMiddleware = async function SecurityHeadersMiddleware(ctx, next) {
2433
3491
  const headers = {};
2434
3492
  const set = (k, v) => headers[k] = v;
2435
3493
  if (options.dnsPrefetchControl !== false) {
@@ -2483,6 +3541,9 @@ function SecurityHeaders(options = {}) {
2483
3541
  }
2484
3542
  return response;
2485
3543
  };
3544
+ securityHeadersMiddleware.isBuiltin = true;
3545
+ securityHeadersMiddleware.pluginName = "SecurityHeaders";
3546
+ return securityHeadersMiddleware;
2486
3547
  }
2487
3548
  class Cookie {
2488
3549
  maxAge;
@@ -2596,7 +3657,7 @@ function Session(options) {
2596
3657
  const resave = options.resave === void 0 ? true : options.resave;
2597
3658
  const saveUninitialized = options.saveUninitialized === void 0 ? true : options.saveUninitialized;
2598
3659
  const rolling = options.rolling || false;
2599
- return async (ctx, next) => {
3660
+ const sessionMiddleware = async function SessionMiddleware(ctx, next) {
2600
3661
  let reqSessionId = null;
2601
3662
  const cookieHeader = ctx.req.headers.get("cookie");
2602
3663
  const cookies = {};
@@ -2732,194 +3793,9 @@ function Session(options) {
2732
3793
  }
2733
3794
  return result;
2734
3795
  };
2735
- }
2736
- class ValidationError extends Error {
2737
- constructor(errors) {
2738
- super("Validation Error");
2739
- this.errors = errors;
2740
- }
2741
- status = 400;
2742
- }
2743
- function isZod(schema) {
2744
- return typeof schema?.safeParse === "function";
2745
- }
2746
- async function validateZod(schema, data) {
2747
- const result = await schema.safeParseAsync(data);
2748
- if (!result.success) {
2749
- throw new ValidationError(result.error.errors);
2750
- }
2751
- return result.data;
2752
- }
2753
- function isTypeBox(schema) {
2754
- return typeof schema?.Check === "function" && typeof schema?.Errors === "function";
2755
- }
2756
- function validateTypeBox(schema, data) {
2757
- if (!schema.Check(data)) {
2758
- throw new ValidationError([...schema.Errors(data)]);
2759
- }
2760
- return data;
2761
- }
2762
- function isAjv(schema) {
2763
- return typeof schema === "function" && "errors" in schema;
2764
- }
2765
- function validateAjv(schema, data) {
2766
- const valid = schema(data);
2767
- if (!valid) {
2768
- throw new ValidationError(schema.errors);
2769
- }
2770
- return data;
2771
- }
2772
- const valibot = (schema, parser) => {
2773
- return {
2774
- _valibot: true,
2775
- schema,
2776
- parser
2777
- };
2778
- };
2779
- function isValibotWrapper(schema) {
2780
- return schema?._valibot === true;
2781
- }
2782
- async function validateValibotWrapper(wrapper, data) {
2783
- const result = await wrapper.parser(wrapper.schema, data);
2784
- if (!result.success) {
2785
- throw new ValidationError(result.issues);
2786
- }
2787
- return result.output;
2788
- }
2789
- function isClass(schema) {
2790
- try {
2791
- if (typeof schema === "function" && /^\s*class\s+/.test(schema.toString())) {
2792
- return true;
2793
- }
2794
- return typeof schema === "function" && schema.prototype && schema.name;
2795
- } catch {
2796
- return false;
2797
- }
2798
- }
2799
- async function validateClassValidator(schema, data) {
2800
- const object = plainToInstance(schema, data);
2801
- try {
2802
- await validateOrReject(object);
2803
- return object;
2804
- } catch (errors) {
2805
- const formattedErrors = Array.isArray(errors) ? errors.map((err) => ({
2806
- property: err.property,
2807
- constraints: err.constraints,
2808
- children: err.children
2809
- })) : errors;
2810
- throw new ValidationError(formattedErrors);
2811
- }
2812
- }
2813
- const safelyGetBody = async (ctx) => {
2814
- const req = ctx.req;
2815
- if (req._bodyParsed) {
2816
- return req._bodyValue;
2817
- }
2818
- try {
2819
- let data;
2820
- if (typeof req.json === "function") {
2821
- data = await req.json();
2822
- } else {
2823
- data = req.body;
2824
- if (typeof data === "string") {
2825
- try {
2826
- data = JSON.parse(data);
2827
- } catch {
2828
- }
2829
- }
2830
- }
2831
- req._bodyParsed = true;
2832
- req._bodyValue = data;
2833
- Object.defineProperty(req, "json", {
2834
- value: async () => req._bodyValue,
2835
- configurable: true
2836
- });
2837
- return data;
2838
- } catch (e) {
2839
- return {};
2840
- }
2841
- };
2842
- function validate(config) {
2843
- return async (ctx, next) => {
2844
- const dataToValidate = {};
2845
- if (config.params) dataToValidate.params = ctx.params;
2846
- let queryObj;
2847
- if (config.query) {
2848
- const url = new URL(ctx.req.url);
2849
- queryObj = Object.fromEntries(url.searchParams.entries());
2850
- dataToValidate.query = queryObj;
2851
- }
2852
- if (config.headers) dataToValidate.headers = Object.fromEntries(ctx.req.headers.entries());
2853
- let body;
2854
- if (config.body) {
2855
- body = await safelyGetBody(ctx);
2856
- dataToValidate.body = body;
2857
- }
2858
- if (ctx.app?.applicationConfig.hooks?.beforeValidate) {
2859
- await ctx.app.applicationConfig.hooks.beforeValidate(ctx, dataToValidate);
2860
- }
2861
- if (config.params) {
2862
- ctx.params = await runValidation(config.params, ctx.params);
2863
- }
2864
- let validQuery;
2865
- if (config.query && queryObj) {
2866
- validQuery = await runValidation(config.query, queryObj);
2867
- }
2868
- if (config.headers) {
2869
- const headersObj = Object.fromEntries(ctx.req.headers.entries());
2870
- await runValidation(config.headers, headersObj);
2871
- }
2872
- let validBody;
2873
- if (config.body) {
2874
- const b = body ?? await safelyGetBody(ctx);
2875
- validBody = await runValidation(config.body, b);
2876
- const req = ctx.req;
2877
- req._bodyValue = validBody;
2878
- Object.defineProperty(req, "json", {
2879
- value: async () => validBody,
2880
- configurable: true
2881
- });
2882
- ctx.body = validBody;
2883
- }
2884
- if (ctx.app?.applicationConfig.hooks?.afterValidate) {
2885
- const validatedData = { ...dataToValidate };
2886
- if (config.params) validatedData.params = ctx.params;
2887
- if (config.query) validatedData.query = validQuery;
2888
- if (config.body) validatedData.body = validBody;
2889
- await ctx.app.applicationConfig.hooks.afterValidate(ctx, validatedData);
2890
- }
2891
- return next();
2892
- };
2893
- }
2894
- async function runValidation(schema, data) {
2895
- if (isZod(schema)) {
2896
- return validateZod(schema, data);
2897
- }
2898
- if (isTypeBox(schema)) {
2899
- return validateTypeBox(schema, data);
2900
- }
2901
- if (isAjv(schema)) {
2902
- return validateAjv(schema, data);
2903
- }
2904
- if (isValibotWrapper(schema)) {
2905
- return validateValibotWrapper(schema, data);
2906
- }
2907
- if (isClass(schema)) {
2908
- return validateClassValidator(schema, data);
2909
- }
2910
- if (isTypeBox(schema)) {
2911
- return validateTypeBox(schema, data);
2912
- }
2913
- if (isAjv(schema)) {
2914
- return validateAjv(schema, data);
2915
- }
2916
- if (isValibotWrapper(schema)) {
2917
- return validateValibotWrapper(schema, data);
2918
- }
2919
- if (typeof schema === "function") {
2920
- return schema(data);
2921
- }
2922
- throw new Error("Unknown validator type provided. Please use a supported library (Zod, Ajv, TypeBox) or a custom function.");
3796
+ sessionMiddleware.isBuiltin = true;
3797
+ sessionMiddleware.pluginName = "Session";
3798
+ return sessionMiddleware;
2923
3799
  }
2924
3800
  export {
2925
3801
  $appRoot,
@@ -2960,6 +3836,7 @@ export {
2960
3836
  Put,
2961
3837
  Query,
2962
3838
  RateLimit,
3839
+ RateLimitMiddleware,
2963
3840
  Req,
2964
3841
  RouteParamType,
2965
3842
  RouterRegistry,
@@ -2975,7 +3852,11 @@ export {
2975
3852
  Spec,
2976
3853
  Use,
2977
3854
  ValidationError,
3855
+ compileValidators,
2978
3856
  compose,
3857
+ enableOpenApiValidation,
3858
+ openApiValidator,
3859
+ precompileValidators,
2979
3860
  useExpress,
2980
3861
  valibot,
2981
3862
  validate