shokupan 0.5.0 → 0.6.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 (33) hide show
  1. package/README.md +9 -8
  2. package/dist/cli.cjs +1 -1
  3. package/dist/cli.js +1 -1
  4. package/dist/context.d.ts +27 -5
  5. package/dist/index.cjs +662 -429
  6. package/dist/index.cjs.map +1 -1
  7. package/dist/index.js +605 -394
  8. package/dist/index.js.map +1 -1
  9. package/dist/json-parser-B3dnQmCC.js +35 -0
  10. package/dist/json-parser-B3dnQmCC.js.map +1 -0
  11. package/dist/json-parser-COdZ0fqY.cjs +35 -0
  12. package/dist/json-parser-COdZ0fqY.cjs.map +1 -0
  13. package/dist/{openapi-analyzer-z-7AoFRC.cjs → openapi-analyzer-Bei1sVWp.cjs} +33 -16
  14. package/dist/openapi-analyzer-Bei1sVWp.cjs.map +1 -0
  15. package/dist/{openapi-analyzer-D7y6Qa38.js → openapi-analyzer-Ce_7JxZh.js} +33 -16
  16. package/dist/openapi-analyzer-Ce_7JxZh.js.map +1 -0
  17. package/dist/plugins/scalar.d.ts +1 -1
  18. package/dist/router.d.ts +33 -22
  19. package/dist/{server-adapter-BWrEJbKL.js → server-adapter-0xH174zz.js} +4 -2
  20. package/dist/server-adapter-0xH174zz.js.map +1 -0
  21. package/dist/{server-adapter-fVKP60e0.cjs → server-adapter-DFhwlK8e.cjs} +4 -2
  22. package/dist/server-adapter-DFhwlK8e.cjs.map +1 -0
  23. package/dist/shokupan.d.ts +2 -7
  24. package/dist/types.d.ts +30 -2
  25. package/dist/util/datastore.d.ts +6 -0
  26. package/dist/util/json-parser.d.ts +12 -0
  27. package/dist/util/plugin-deps.d.ts +25 -0
  28. package/package.json +74 -14
  29. package/dist/buntest.d.ts +0 -1
  30. package/dist/openapi-analyzer-D7y6Qa38.js.map +0 -1
  31. package/dist/openapi-analyzer-z-7AoFRC.cjs.map +0 -1
  32. package/dist/server-adapter-BWrEJbKL.js.map +0 -1
  33. package/dist/server-adapter-fVKP60e0.cjs.map +0 -1
package/dist/index.js CHANGED
@@ -3,8 +3,6 @@ import { Eta } from "eta";
3
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
6
  import { trace, SpanKind, SpanStatusCode, context } from "@opentelemetry/api";
9
7
  import * as os from "node:os";
10
8
  import { OAuth2Client, Okta, Auth0, Apple, MicrosoftEntraId, Google, GitHub, generateState, generateCodeVerifier } from "arctic";
@@ -12,9 +10,7 @@ import * as jose from "jose";
12
10
  import * as zlib from "node:zlib";
13
11
  import Ajv from "ajv";
14
12
  import addFormats from "ajv-formats";
15
- import { plainToInstance } from "class-transformer";
16
- import { validateOrReject } from "class-validator";
17
- import { OpenAPIAnalyzer } from "./openapi-analyzer-D7y6Qa38.js";
13
+ import { OpenAPIAnalyzer } from "./openapi-analyzer-Ce_7JxZh.js";
18
14
  import { randomUUID, createHmac } from "crypto";
19
15
  import { EventEmitter } from "events";
20
16
  class ShokupanResponse {
@@ -80,8 +76,73 @@ class ShokupanResponse {
80
76
  return this._headers !== null;
81
77
  }
82
78
  }
79
+ const VALID_HTTP_STATUSES = /* @__PURE__ */ new Set([
80
+ 100,
81
+ 101,
82
+ 102,
83
+ 103,
84
+ 200,
85
+ 201,
86
+ 202,
87
+ 203,
88
+ 204,
89
+ 205,
90
+ 206,
91
+ 207,
92
+ 208,
93
+ 226,
94
+ 300,
95
+ 301,
96
+ 302,
97
+ 303,
98
+ 304,
99
+ 305,
100
+ 306,
101
+ 307,
102
+ 308,
103
+ 400,
104
+ 401,
105
+ 402,
106
+ 403,
107
+ 404,
108
+ 405,
109
+ 406,
110
+ 407,
111
+ 408,
112
+ 409,
113
+ 410,
114
+ 411,
115
+ 412,
116
+ 413,
117
+ 414,
118
+ 415,
119
+ 416,
120
+ 417,
121
+ 418,
122
+ 421,
123
+ 422,
124
+ 423,
125
+ 424,
126
+ 425,
127
+ 426,
128
+ 428,
129
+ 429,
130
+ 431,
131
+ 451,
132
+ 500,
133
+ 501,
134
+ 502,
135
+ 503,
136
+ 504,
137
+ 505,
138
+ 506,
139
+ 507,
140
+ 508,
141
+ 510,
142
+ 511
143
+ ]);
144
+ const VALID_REDIRECT_STATUSES = /* @__PURE__ */ new Set([301, 302, 303, 307, 308]);
83
145
  class ShokupanContext {
84
- // Raw body for compression optimization
85
146
  constructor(request, server, state, app, signal, enableMiddlewareTracking = false) {
86
147
  this.request = request;
87
148
  this.server = server;
@@ -104,7 +165,6 @@ class ShokupanContext {
104
165
  }
105
166
  this.response = new ShokupanResponse();
106
167
  }
107
- _url;
108
168
  params = {};
109
169
  // Router assigns this, but default to empty object
110
170
  state;
@@ -113,6 +173,19 @@ class ShokupanContext {
113
173
  _debug;
114
174
  _finalResponse;
115
175
  _rawBody;
176
+ // Raw body for compression optimization
177
+ // Body caching to avoid double parsing
178
+ _url;
179
+ _cachedBody;
180
+ _bodyType;
181
+ _bodyParsed = false;
182
+ _bodyParseError;
183
+ // Cached URL properties to avoid repeated parsing
184
+ _cachedHostname;
185
+ _cachedProtocol;
186
+ _cachedHost;
187
+ _cachedOrigin;
188
+ _cachedQuery;
116
189
  get url() {
117
190
  if (!this._url) {
118
191
  const urlString = this.request.url || "http://localhost/";
@@ -161,8 +234,11 @@ class ShokupanContext {
161
234
  * Request query params
162
235
  */
163
236
  get query() {
237
+ if (this._cachedQuery) return this._cachedQuery;
164
238
  const q = {};
165
- for (const [key, value] of this.url.searchParams) {
239
+ const entries = Object.entries(this.url.searchParams);
240
+ for (let i = 0; i < entries.length; i++) {
241
+ const [key, value] = entries[i];
166
242
  if (q[key] === void 0) {
167
243
  q[key] = value;
168
244
  } else if (Array.isArray(q[key])) {
@@ -171,6 +247,7 @@ class ShokupanContext {
171
247
  q[key] = [q[key], value];
172
248
  }
173
249
  }
250
+ this._cachedQuery = q;
174
251
  return q;
175
252
  }
176
253
  /**
@@ -183,31 +260,31 @@ class ShokupanContext {
183
260
  * Request hostname (e.g. "localhost")
184
261
  */
185
262
  get hostname() {
186
- return this.url.hostname;
263
+ return this._cachedHostname ??= this.url.hostname;
187
264
  }
188
265
  /**
189
266
  * Request host (e.g. "localhost:3000")
190
267
  */
191
268
  get host() {
192
- return this.url.host;
269
+ return this._cachedHost ??= this.url.host;
193
270
  }
194
271
  /**
195
272
  * Request protocol (e.g. "http:", "https:")
196
273
  */
197
274
  get protocol() {
198
- return this.url.protocol;
275
+ return this._cachedProtocol ??= this.url.protocol;
199
276
  }
200
277
  /**
201
278
  * Whether request is secure (https)
202
279
  */
203
280
  get secure() {
204
- return this.url.protocol === "https:";
281
+ return this.protocol === "https:";
205
282
  }
206
283
  /**
207
284
  * Request origin (e.g. "http://localhost:3000")
208
285
  */
209
286
  get origin() {
210
- return this.url.origin;
287
+ return this._cachedOrigin ??= this.url.origin;
211
288
  }
212
289
  /**
213
290
  * Request headers
@@ -303,6 +380,91 @@ class ShokupanContext {
303
380
  }
304
381
  return h;
305
382
  }
383
+ /**
384
+ * Read request body with caching to avoid double parsing.
385
+ * The body is only parsed once and cached for subsequent reads.
386
+ */
387
+ async body() {
388
+ if (this._bodyParseError) {
389
+ throw this._bodyParseError;
390
+ }
391
+ if (this._bodyParsed) {
392
+ return this._cachedBody;
393
+ }
394
+ const contentType = this.request.headers.get("content-type") || "";
395
+ if (contentType.includes("application/json") || contentType.includes("+json")) {
396
+ const rawText = await this.readRawBody();
397
+ const parserType = this.app?.applicationConfig?.jsonParser || "native";
398
+ if (parserType === "native") {
399
+ this._cachedBody = JSON.parse(rawText);
400
+ } else {
401
+ const { getJSONParser } = await import("./json-parser-B3dnQmCC.js");
402
+ const parser = getJSONParser(parserType);
403
+ this._cachedBody = parser(rawText);
404
+ }
405
+ this._bodyType = "json";
406
+ } else if (contentType.includes("multipart/form-data") || contentType.includes("application/x-www-form-urlencoded")) {
407
+ this._cachedBody = await this.request.formData();
408
+ this._bodyType = "formData";
409
+ } else {
410
+ this._cachedBody = await this.readRawBody();
411
+ this._bodyType = "text";
412
+ }
413
+ this._bodyParsed = true;
414
+ return this._cachedBody;
415
+ }
416
+ /**
417
+ * Pre-parse the request body before handler execution.
418
+ * This improves performance and enables Node.js compatibility for large payloads.
419
+ * Errors are deferred until the body is actually accessed in the handler.
420
+ */
421
+ async parseBody() {
422
+ if (this._bodyParsed) {
423
+ return;
424
+ }
425
+ if (this.request.method === "GET" || this.request.method === "HEAD") {
426
+ return;
427
+ }
428
+ try {
429
+ await this.body();
430
+ } catch (error) {
431
+ this._bodyParseError = error;
432
+ }
433
+ }
434
+ /**
435
+ * Read raw body from ReadableStream efficiently.
436
+ * This is much faster than request.text() for large payloads.
437
+ * Also handles the case where body is already a string (e.g., in tests).
438
+ */
439
+ async readRawBody() {
440
+ if (typeof this.request.body === "string") {
441
+ return this.request.body;
442
+ }
443
+ const reader = this.request.body?.getReader();
444
+ if (!reader) {
445
+ return "";
446
+ }
447
+ const chunks = [];
448
+ let totalSize = 0;
449
+ try {
450
+ while (true) {
451
+ const { done, value } = await reader.read();
452
+ if (done) break;
453
+ chunks.push(value);
454
+ totalSize += value.length;
455
+ }
456
+ } finally {
457
+ reader.releaseLock();
458
+ }
459
+ const result = new Uint8Array(totalSize);
460
+ let offset = 0;
461
+ for (let i = 0; i < chunks.length; i++) {
462
+ const chunk = chunks[i];
463
+ result.set(chunk, offset);
464
+ offset += chunk.length;
465
+ }
466
+ return new TextDecoder().decode(result);
467
+ }
306
468
  /**
307
469
  * Send a response
308
470
  * @param body Response body
@@ -311,31 +473,24 @@ class ShokupanContext {
311
473
  */
312
474
  send(body, options) {
313
475
  const headers = this.mergeHeaders(options?.headers);
314
- const status = options?.status ?? this.response.status;
476
+ const status = options?.status ?? this.response.status ?? 200;
477
+ if (this.app.applicationConfig.validateStatusCodes && !VALID_HTTP_STATUSES.has(status)) {
478
+ throw new Error(`Invalid HTTP status code: ${status}`);
479
+ }
315
480
  if (typeof body === "string" || body instanceof ArrayBuffer || body instanceof Uint8Array) {
316
481
  this._rawBody = body;
317
482
  }
318
483
  this._finalResponse = new Response(body, { status, headers });
319
484
  return this._finalResponse;
320
485
  }
321
- /**
322
- * Read request body
323
- */
324
- async body() {
325
- const contentType = this.request.headers.get("content-type") || "";
326
- if (contentType.includes("application/json") || contentType.includes("+json")) {
327
- return this.request.json();
328
- }
329
- if (contentType.includes("multipart/form-data") || contentType.includes("application/x-www-form-urlencoded")) {
330
- return this.request.formData();
331
- }
332
- return this.request.text();
333
- }
334
486
  /**
335
487
  * Respond with a JSON object
336
488
  */
337
489
  json(data, status, headers) {
338
- const finalStatus = status ?? this.response.status;
490
+ const finalStatus = status ?? this.response.status ?? 200;
491
+ if (!VALID_HTTP_STATUSES.has(finalStatus)) {
492
+ throw new Error(`Invalid HTTP status code: ${finalStatus}`);
493
+ }
339
494
  const jsonString = JSON.stringify(data);
340
495
  this._rawBody = jsonString;
341
496
  if (!headers && !this.response.hasPopulatedHeaders) {
@@ -354,7 +509,10 @@ class ShokupanContext {
354
509
  * Respond with a text string
355
510
  */
356
511
  text(data, status, headers) {
357
- const finalStatus = status ?? this.response.status;
512
+ const finalStatus = status ?? this.response.status ?? 200;
513
+ if (this.app.applicationConfig.validateStatusCodes && !VALID_HTTP_STATUSES.has(finalStatus)) {
514
+ throw new Error(`Invalid HTTP status code: ${finalStatus}`);
515
+ }
358
516
  this._rawBody = data;
359
517
  if (!headers && !this.response.hasPopulatedHeaders) {
360
518
  this._finalResponse = new Response(data, {
@@ -372,7 +530,10 @@ class ShokupanContext {
372
530
  * Respond with HTML content
373
531
  */
374
532
  html(html, status, headers) {
375
- const finalStatus = status ?? this.response.status;
533
+ const finalStatus = status ?? this.response.status ?? 200;
534
+ if (this.app.applicationConfig.validateStatusCodes && !VALID_HTTP_STATUSES.has(finalStatus)) {
535
+ throw new Error(`Invalid HTTP status code: ${finalStatus}`);
536
+ }
376
537
  const finalHeaders = this.mergeHeaders(headers);
377
538
  finalHeaders.set("content-type", "text/html; charset=utf-8");
378
539
  this._rawBody = html;
@@ -383,6 +544,9 @@ class ShokupanContext {
383
544
  * Respond with a redirect
384
545
  */
385
546
  redirect(url, status = 302) {
547
+ if (this.app.applicationConfig.validateStatusCodes && !VALID_REDIRECT_STATUSES.has(status)) {
548
+ throw new Error(`Invalid redirect status code: ${status}`);
549
+ }
386
550
  const headers = this.mergeHeaders();
387
551
  headers.set("Location", url);
388
552
  this._finalResponse = new Response(null, { status, headers });
@@ -393,6 +557,9 @@ class ShokupanContext {
393
557
  * DOES NOT CHAIN!
394
558
  */
395
559
  status(status) {
560
+ if (this.app.applicationConfig.validateStatusCodes && !VALID_HTTP_STATUSES.has(status)) {
561
+ throw new Error(`Invalid HTTP status code: ${status}`);
562
+ }
396
563
  const headers = this.mergeHeaders();
397
564
  this._finalResponse = new Response(null, { status, headers });
398
565
  return this._finalResponse;
@@ -403,6 +570,9 @@ class ShokupanContext {
403
570
  async file(path, fileOptions, responseOptions) {
404
571
  const headers = this.mergeHeaders(responseOptions?.headers);
405
572
  const status = responseOptions?.status ?? this.response.status;
573
+ if (this.app.applicationConfig.validateStatusCodes && !VALID_HTTP_STATUSES.has(status)) {
574
+ throw new Error(`Invalid HTTP status code: ${status}`);
575
+ }
406
576
  if (typeof Bun !== "undefined") {
407
577
  this._finalResponse = new Response(Bun.file(path, fileOptions), { status, headers });
408
578
  return this._finalResponse;
@@ -426,6 +596,10 @@ class ShokupanContext {
426
596
  * @param headers HTTP Headers
427
597
  */
428
598
  async jsx(element, args, status, headers) {
599
+ status ??= 200;
600
+ if (this.app.applicationConfig.validateStatusCodes && !VALID_HTTP_STATUSES.has(status)) {
601
+ throw new Error(`Invalid HTTP status code: ${status}`);
602
+ }
429
603
  if (!this.renderer) {
430
604
  throw new Error("No JSX renderer configured");
431
605
  }
@@ -450,7 +624,9 @@ function RateLimitMiddleware(options = {}) {
450
624
  const hits = /* @__PURE__ */ new Map();
451
625
  const interval = setInterval(() => {
452
626
  const now = Date.now();
453
- for (const [key, record] of hits.entries()) {
627
+ const entries = Array.from(hits.entries());
628
+ for (let i = 0; i < entries.length; i++) {
629
+ const [key, record] = entries[i];
454
630
  if (record.resetTime <= now) {
455
631
  hits.delete(key);
456
632
  }
@@ -703,7 +879,9 @@ function deepMerge(target, ...sources) {
703
879
  if (!sources.length) return target;
704
880
  const source = sources.shift();
705
881
  if (isObject(target) && isObject(source)) {
706
- for (const key in source) {
882
+ const sourceKeys = Object.keys(source);
883
+ for (let i = 0; i < sourceKeys.length; i++) {
884
+ const key = sourceKeys[i];
707
885
  if (isObject(source[key])) {
708
886
  if (!target[key]) Object.assign(target, { [key]: {} });
709
887
  deepMerge(target[key], source[key]);
@@ -727,15 +905,17 @@ function deepMerge(target, ...sources) {
727
905
  }
728
906
  return deepMerge(target, ...sources);
729
907
  }
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;
908
+ const REGEX_PATTERNS = {
909
+ QUERY_INT: /parseInt\(ctx\.query\.(\w+)\)/g,
910
+ QUERY_FLOAT: /parseFloat\(ctx\.query\.(\w+)\)/g,
911
+ QUERY_NUMBER: /Number\(ctx\.query\.(\w+)\)/g,
912
+ QUERY_BOOL: /(?:Boolean\(ctx\.query\.(\w+)\)|!+ctx\.query\.(\w+))/g,
913
+ QUERY_GENERIC: /ctx\.query\.(\w+)/g,
914
+ PARAM_INT: /parseInt\(ctx\.params\.(\w+)\)/g,
915
+ PARAM_FLOAT: /parseFloat\(ctx\.params\.(\w+)\)/g,
916
+ HEADER_GET: /ctx\.get\(['"](\w+)['"]\)/g,
917
+ ERROR_STATUS: /ctx\.(?:json|text|html)\([^)]+,\s*(\d{3,})\)/g
918
+ };
739
919
  function analyzeHandler(handler) {
740
920
  const handlerSource = handler.toString();
741
921
  const inferredSpec = {};
@@ -745,29 +925,20 @@ function analyzeHandler(handler) {
745
925
  };
746
926
  }
747
927
  const queryParams = /* @__PURE__ */ new Map();
748
- for (const match of handlerSource.matchAll(REGEX_QUERY_INT)) {
749
- if (match[1]) queryParams.set(match[1], { type: "integer", format: "int32" });
750
- }
751
- for (const match of handlerSource.matchAll(REGEX_QUERY_FLOAT)) {
752
- if (match[1]) queryParams.set(match[1], { type: "number", format: "float" });
753
- }
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
- }
758
- }
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
- }
764
- }
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" });
928
+ const processMatches = (regex, type, format) => {
929
+ const matches = Array.from(handlerSource.matchAll(regex));
930
+ for (const match of matches) {
931
+ const name = match[1] || match[2];
932
+ if (name && !queryParams.has(name)) {
933
+ queryParams.set(name, { type, format });
934
+ }
769
935
  }
770
- }
936
+ };
937
+ processMatches(REGEX_PATTERNS.QUERY_INT, "integer", "int32");
938
+ processMatches(REGEX_PATTERNS.QUERY_FLOAT, "number", "float");
939
+ processMatches(REGEX_PATTERNS.QUERY_NUMBER, "number");
940
+ processMatches(REGEX_PATTERNS.QUERY_BOOL, "boolean");
941
+ processMatches(REGEX_PATTERNS.QUERY_GENERIC, "string");
771
942
  if (queryParams.size > 0) {
772
943
  if (!inferredSpec.parameters) inferredSpec.parameters = [];
773
944
  queryParams.forEach((schema, paramName) => {
@@ -779,12 +950,15 @@ function analyzeHandler(handler) {
779
950
  });
780
951
  }
781
952
  const pathParams = /* @__PURE__ */ new Map();
782
- for (const match of handlerSource.matchAll(REGEX_PARAM_INT)) {
783
- if (match[1]) pathParams.set(match[1], { type: "integer", format: "int32" });
784
- }
785
- for (const match of handlerSource.matchAll(REGEX_PARAM_FLOAT)) {
786
- if (match[1]) pathParams.set(match[1], { type: "number", format: "float" });
787
- }
953
+ const processPathMatches = (regex, type, format) => {
954
+ const matches = Array.from(handlerSource.matchAll(regex));
955
+ for (const match of matches) {
956
+ const name = match[1];
957
+ if (name) pathParams.set(name, { type, format });
958
+ }
959
+ };
960
+ processPathMatches(REGEX_PATTERNS.PARAM_INT, "integer", "int32");
961
+ processPathMatches(REGEX_PATTERNS.PARAM_FLOAT, "number", "float");
788
962
  if (pathParams.size > 0) {
789
963
  if (!inferredSpec.parameters) inferredSpec.parameters = [];
790
964
  pathParams.forEach((schema, paramName) => {
@@ -796,7 +970,8 @@ function analyzeHandler(handler) {
796
970
  });
797
971
  });
798
972
  }
799
- for (const match of handlerSource.matchAll(REGEX_HEADER_GET)) {
973
+ const headerMatches = Array.from(handlerSource.matchAll(REGEX_PATTERNS.HEADER_GET));
974
+ for (const match of headerMatches) {
800
975
  if (match[1]) {
801
976
  if (!inferredSpec.parameters) inferredSpec.parameters = [];
802
977
  inferredSpec.parameters.push({
@@ -815,13 +990,19 @@ function analyzeHandler(handler) {
815
990
  }
816
991
  if (handlerSource.includes("ctx.html(")) {
817
992
  responses["200"] = {
818
- description: "Successful response",
993
+ description: "Successful HTML response",
994
+ content: { "text/html": { schema: { type: "string" } } }
995
+ };
996
+ }
997
+ if (handlerSource.includes("ctx.jsx(")) {
998
+ responses["200"] = {
999
+ description: "Successful HTML response (Rendered JSX)",
819
1000
  content: { "text/html": { schema: { type: "string" } } }
820
1001
  };
821
1002
  }
822
1003
  if (handlerSource.includes("ctx.text(")) {
823
1004
  responses["200"] = {
824
- description: "Successful response",
1005
+ description: "Successful text response",
825
1006
  content: { "text/plain": { schema: { type: "string" } } }
826
1007
  };
827
1008
  }
@@ -832,7 +1013,18 @@ function analyzeHandler(handler) {
832
1013
  };
833
1014
  }
834
1015
  if (handlerSource.includes("ctx.redirect(")) {
835
- responses["302"] = { description: "Redirect" };
1016
+ let hasSpecificRedirect = false;
1017
+ const redirectMatches = Array.from(handlerSource.matchAll(/ctx\.redirect\([^,]+,\s*(\d{3})\)/g));
1018
+ for (const match of redirectMatches) {
1019
+ const status = match[1];
1020
+ if (/^30[12378]$/.test(status)) {
1021
+ responses[status] = { description: `Redirect (${status})` };
1022
+ hasSpecificRedirect = true;
1023
+ }
1024
+ }
1025
+ if (!hasSpecificRedirect) {
1026
+ responses["302"] = { description: "Redirect" };
1027
+ }
836
1028
  }
837
1029
  if (!responses["200"] && /return\s+\{/.test(handlerSource)) {
838
1030
  responses["200"] = {
@@ -840,7 +1032,8 @@ function analyzeHandler(handler) {
840
1032
  content: { "application/json": { schema: { type: "object" } } }
841
1033
  };
842
1034
  }
843
- for (const match of handlerSource.matchAll(REGEX_ERROR_STATUS)) {
1035
+ const errorStatusMatches = Array.from(handlerSource.matchAll(REGEX_PATTERNS.ERROR_STATUS));
1036
+ for (const match of errorStatusMatches) {
844
1037
  const statusCode = match[1];
845
1038
  if (statusCode && statusCode !== "200") {
846
1039
  responses[statusCode] = { description: `Error response (${statusCode})` };
@@ -851,6 +1044,52 @@ function analyzeHandler(handler) {
851
1044
  }
852
1045
  return { inferredSpec };
853
1046
  }
1047
+ async function getAstRoutes(applications) {
1048
+ const astRoutes = [];
1049
+ const getExpandedRoutes = (app, prefix = "", seen = /* @__PURE__ */ new Set()) => {
1050
+ if (seen.has(app.name)) return [];
1051
+ const newSeen = new Set(seen);
1052
+ newSeen.add(app.name);
1053
+ const expanded = [];
1054
+ for (const route of app.routes) {
1055
+ const cleanPrefix = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
1056
+ const cleanPath = route.path.startsWith("/") ? route.path : "/" + route.path;
1057
+ let joined = cleanPrefix + cleanPath;
1058
+ if (joined.length > 1 && joined.endsWith("/")) {
1059
+ joined = joined.slice(0, -1);
1060
+ }
1061
+ expanded.push({
1062
+ ...route,
1063
+ path: joined || "/"
1064
+ });
1065
+ }
1066
+ if (app.mounted) {
1067
+ for (const mount of app.mounted) {
1068
+ const targetApp = applications.find((a) => a.name === mount.target || a.className === mount.target);
1069
+ if (targetApp) {
1070
+ const cleanPrefix = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
1071
+ const mountPrefix = mount.prefix.startsWith("/") ? mount.prefix : "/" + mount.prefix;
1072
+ expanded.push(...getExpandedRoutes(targetApp, cleanPrefix + mountPrefix, newSeen));
1073
+ }
1074
+ }
1075
+ }
1076
+ return expanded;
1077
+ };
1078
+ applications.forEach((app) => {
1079
+ astRoutes.push(...getExpandedRoutes(app));
1080
+ });
1081
+ const dedupedRoutes = /* @__PURE__ */ new Map();
1082
+ for (const route of astRoutes) {
1083
+ const key = `${route.method.toUpperCase()}:${route.path}`;
1084
+ let score = 0;
1085
+ if (route.responseSchema) score += 10;
1086
+ if (route.handlerSource) score += 5;
1087
+ if (!dedupedRoutes.has(key) || score > dedupedRoutes.get(key).score) {
1088
+ dedupedRoutes.set(key, { route, score });
1089
+ }
1090
+ }
1091
+ return Array.from(dedupedRoutes.values()).map((v) => v.route);
1092
+ }
854
1093
  async function generateOpenApi(rootRouter, options = {}) {
855
1094
  const paths = {};
856
1095
  const tagGroups = /* @__PURE__ */ new Map();
@@ -858,61 +1097,11 @@ async function generateOpenApi(rootRouter, options = {}) {
858
1097
  const defaultTagName = options.defaultTag || "Application";
859
1098
  let astRoutes = [];
860
1099
  try {
861
- const { OpenAPIAnalyzer: OpenAPIAnalyzer2 } = await import("./openapi-analyzer-D7y6Qa38.js");
1100
+ const { OpenAPIAnalyzer: OpenAPIAnalyzer2 } = await import("./openapi-analyzer-Ce_7JxZh.js");
862
1101
  const analyzer = new OpenAPIAnalyzer2(process.cwd());
863
1102
  const { applications } = await analyzer.analyze();
864
- const appMap = /* @__PURE__ */ new Map();
865
- applications.forEach((app) => {
866
- appMap.set(app.name, app);
867
- if (app.name !== app.className) {
868
- appMap.set(app.className, app);
869
- }
870
- });
871
- const getExpandedRoutes = (app, prefix = "", seen = /* @__PURE__ */ new Set()) => {
872
- if (seen.has(app.name)) return [];
873
- const newSeen = new Set(seen);
874
- newSeen.add(app.name);
875
- const expanded = [];
876
- for (const route of app.routes) {
877
- const cleanPrefix = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
878
- const cleanPath = route.path.startsWith("/") ? route.path : "/" + route.path;
879
- let joined = cleanPrefix + cleanPath;
880
- if (joined.length > 1 && joined.endsWith("/")) {
881
- joined = joined.slice(0, -1);
882
- }
883
- expanded.push({
884
- ...route,
885
- path: joined || "/"
886
- });
887
- }
888
- if (app.mounted) {
889
- for (const mount of app.mounted) {
890
- const targetApp = appMap.get(mount.target);
891
- if (targetApp) {
892
- const cleanPrefix = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
893
- const mountPrefix = mount.prefix.startsWith("/") ? mount.prefix : "/" + mount.prefix;
894
- expanded.push(...getExpandedRoutes(targetApp, cleanPrefix + mountPrefix, newSeen));
895
- }
896
- }
897
- }
898
- return expanded;
899
- };
900
- applications.forEach((app) => {
901
- astRoutes.push(...getExpandedRoutes(app));
902
- });
903
- const dedupedRoutes = /* @__PURE__ */ new Map();
904
- for (const route of astRoutes) {
905
- const key = `${route.method.toUpperCase()}:${route.path}`;
906
- let score = 0;
907
- if (route.responseSchema) score += 10;
908
- if (route.handlerSource) score += 5;
909
- if (!dedupedRoutes.has(key) || score > dedupedRoutes.get(key).score) {
910
- dedupedRoutes.set(key, { route, score });
911
- }
912
- }
913
- astRoutes = Array.from(dedupedRoutes.values()).map((v) => v.route);
1103
+ astRoutes = await getAstRoutes(applications);
914
1104
  } catch (e) {
915
- console.warn("OpenAPI AST analysis failed or skipped:", e);
916
1105
  }
917
1106
  const collect = (router, prefix = "", currentGroup = defaultTagGroup, defaultTag = defaultTagName) => {
918
1107
  let group = currentGroup;
@@ -969,33 +1158,15 @@ async function generateOpenApi(rootRouter, options = {}) {
969
1158
  (r) => r.method.toUpperCase() === route.method.toUpperCase() && r.path === fullPath
970
1159
  );
971
1160
  if (!astMatch) {
972
- let runtimeSource = route.handler.toString();
973
- if (route.handler.originalHandler) {
974
- runtimeSource = route.handler.originalHandler.toString();
975
- }
1161
+ const runtimeSource = (route.handler.originalHandler || route.handler).toString();
976
1162
  const runtimeHandlerSrc = runtimeSource.replace(/\s+/g, " ");
977
1163
  const sameMethodRoutes = astRoutes.filter((r) => r.method.toUpperCase() === route.method.toUpperCase());
978
1164
  astMatch = sameMethodRoutes.find((r) => {
979
1165
  const astHandlerSrc = (r.handlerSource || r.handlerName || "").replace(/\s+/g, " ");
980
1166
  if (!astHandlerSrc || astHandlerSrc.length < 20) return false;
981
- const match = runtimeHandlerSrc.includes(astHandlerSrc) || astHandlerSrc.includes(runtimeHandlerSrc) || r.handlerSource && runtimeHandlerSrc.includes(r.handlerSource.substring(0, 50));
982
- return match;
1167
+ return runtimeHandlerSrc.includes(astHandlerSrc) || astHandlerSrc.includes(runtimeHandlerSrc) || r.handlerSource && runtimeHandlerSrc.includes(r.handlerSource.substring(0, 50));
983
1168
  });
984
1169
  }
985
- const potentialMatches = astRoutes.filter(
986
- (r) => r.method.toUpperCase() === route.method.toUpperCase() && r.path === fullPath
987
- );
988
- if (potentialMatches.length > 1) {
989
- const runtimeHandlerSrc = route.handler.toString().replace(/\s+/g, " ");
990
- const preciseMatch = potentialMatches.find((r) => {
991
- const astHandlerSrc = (r.handlerSource || r.handlerName || "").replace(/\s+/g, " ");
992
- const match = runtimeHandlerSrc.includes(astHandlerSrc) || astHandlerSrc.includes(runtimeHandlerSrc) || r.handlerSource && runtimeHandlerSrc.includes(r.handlerSource.substring(0, 50));
993
- return match;
994
- });
995
- if (preciseMatch) {
996
- astMatch = preciseMatch;
997
- }
998
- }
999
1170
  if (astMatch) {
1000
1171
  if (astMatch.summary) operation.summary = astMatch.summary;
1001
1172
  if (astMatch.description) operation.description = astMatch.description;
@@ -1003,25 +1174,19 @@ async function generateOpenApi(rootRouter, options = {}) {
1003
1174
  if (astMatch.operationId) operation.operationId = astMatch.operationId;
1004
1175
  if (astMatch.requestTypes?.body) {
1005
1176
  operation.requestBody = {
1006
- content: {
1007
- "application/json": { schema: astMatch.requestTypes.body }
1008
- }
1177
+ content: { "application/json": { schema: astMatch.requestTypes.body } }
1009
1178
  };
1010
1179
  }
1011
1180
  if (astMatch.responseSchema) {
1012
1181
  operation.responses["200"] = {
1013
1182
  description: "Successful response",
1014
- content: {
1015
- "application/json": { schema: astMatch.responseSchema }
1016
- }
1183
+ content: { "application/json": { schema: astMatch.responseSchema } }
1017
1184
  };
1018
1185
  } else if (astMatch.responseType) {
1019
1186
  const contentType = astMatch.responseType === "string" ? "text/plain" : "application/json";
1020
1187
  operation.responses["200"] = {
1021
1188
  description: "Successful response",
1022
- content: {
1023
- [contentType]: { schema: { type: astMatch.responseType } }
1024
- }
1189
+ content: { [contentType]: { schema: { type: astMatch.responseType } } }
1025
1190
  };
1026
1191
  }
1027
1192
  const params = [];
@@ -1072,15 +1237,7 @@ async function generateOpenApi(rootRouter, options = {}) {
1072
1237
  deepMerge(operation, inferredSpec);
1073
1238
  }
1074
1239
  if (route.handlerSpec) {
1075
- const spec = route.handlerSpec;
1076
- if (spec.summary) operation.summary = spec.summary;
1077
- if (spec.description) operation.description = spec.description;
1078
- if (spec.operationId) operation.operationId = spec.operationId;
1079
- if (spec.tags) operation.tags = spec.tags;
1080
- if (spec.security) operation.security = spec.security;
1081
- if (spec.responses) {
1082
- operation.responses = { ...operation.responses, ...spec.responses };
1083
- }
1240
+ deepMerge(operation, route.handlerSpec);
1084
1241
  }
1085
1242
  if (!operation.tags || operation.tags.length === 0) operation.tags = [tag];
1086
1243
  if (operation.tags) {
@@ -1099,11 +1256,13 @@ async function generateOpenApi(rootRouter, options = {}) {
1099
1256
  paths[fullPath][methodLower] = operation;
1100
1257
  }
1101
1258
  }
1102
- for (const controller of router[$childControllers]) {
1259
+ const controllers = router[$childControllers];
1260
+ for (const controller of controllers) {
1103
1261
  const controllerName = controller.constructor.name || "UnknownController";
1104
1262
  tagGroups.get(group)?.add(controllerName);
1105
1263
  }
1106
- for (const child of router[$childRouters]) {
1264
+ const childRouters = router[$childRouters];
1265
+ for (const child of childRouters) {
1107
1266
  const mountPath = child[$mountPath];
1108
1267
  const cleanPrefix = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
1109
1268
  const cleanMount = mountPath.startsWith("/") ? mountPath : "/" + mountPath;
@@ -1113,7 +1272,7 @@ async function generateOpenApi(rootRouter, options = {}) {
1113
1272
  };
1114
1273
  collect(rootRouter);
1115
1274
  const xTagGroups = [];
1116
- for (const [name, tags] of tagGroups) {
1275
+ for (const [name, tags] of tagGroups.entries()) {
1117
1276
  xTagGroups.push({ name, tags: Array.from(tags).sort() });
1118
1277
  }
1119
1278
  return {
@@ -1148,7 +1307,8 @@ function serveStatic(config, prefix) {
1148
1307
  if (res) return res;
1149
1308
  }
1150
1309
  if (config.exclude) {
1151
- for (const pattern of config.exclude) {
1310
+ for (let i = 0; i < config.exclude.length; i++) {
1311
+ const pattern = config.exclude[i];
1152
1312
  if (pattern instanceof RegExp) {
1153
1313
  if (pattern.test(relative)) return ctx.json({ error: "Forbidden" }, 403);
1154
1314
  } else if (typeof pattern === "string") {
@@ -1167,7 +1327,8 @@ function serveStatic(config, prefix) {
1167
1327
  stats = await stat(requestPath);
1168
1328
  } catch (e) {
1169
1329
  if (config.extensions) {
1170
- for (const ext of config.extensions) {
1330
+ for (let i = 0; i < config.extensions.length; i++) {
1331
+ const ext = config.extensions[i];
1171
1332
  const p = requestPath + (ext.startsWith(".") ? ext : "." + ext);
1172
1333
  try {
1173
1334
  const s = await stat(p);
@@ -1196,7 +1357,8 @@ function serveStatic(config, prefix) {
1196
1357
  indexes = [config.index];
1197
1358
  }
1198
1359
  let foundIndex = false;
1199
- for (const idx of indexes) {
1360
+ for (let i = 0; i < indexes.length; i++) {
1361
+ const idx = indexes[i];
1200
1362
  const idxPath = join(finalPath, idx);
1201
1363
  try {
1202
1364
  const idxStats = await stat(idxPath);
@@ -1278,7 +1440,8 @@ class RouterTrie {
1278
1440
  insert(method, path, handler) {
1279
1441
  let node = this.root;
1280
1442
  const segments = this.splitPath(path);
1281
- for (const segment of segments) {
1443
+ for (let i = 0; i < segments.length; i++) {
1444
+ const segment = segments[i];
1282
1445
  if (segment === "**") {
1283
1446
  if (!node.recursiveChild) {
1284
1447
  node.recursiveChild = this.createNode();
@@ -1365,40 +1528,68 @@ class RouterTrie {
1365
1528
  }
1366
1529
  }
1367
1530
  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
- DEFINE TABLE OVERWRITE requests SCHEMALESS COMMENT "Created by Shokupan";
1380
- `);
1381
- });
1531
+ let db;
1532
+ let dbPromise = null;
1533
+ let RecordId;
1534
+ async function ensureDb() {
1535
+ if (db) return db;
1536
+ if (dbPromise) return dbPromise;
1537
+ dbPromise = (async () => {
1538
+ try {
1539
+ const { createNodeEngines } = await import("@surrealdb/node");
1540
+ const surreal = await import("surrealdb");
1541
+ const Surreal = surreal.Surreal;
1542
+ RecordId = surreal.RecordId;
1543
+ const engine = process.env["SHOKUPAN_DB_ENGINE"] === "memory" ? "mem://" : "rocksdb://database";
1544
+ const _db = new Surreal({
1545
+ engines: createNodeEngines()
1546
+ });
1547
+ await _db.connect(engine, { namespace: "vendor", database: "shokupan" });
1548
+ await _db.query(`
1549
+ DEFINE TABLE OVERWRITE failed_requests SCHEMALESS COMMENT "Created by Shokupan";
1550
+ DEFINE TABLE OVERWRITE sessions SCHEMALESS COMMENT "Created by Shokupan";
1551
+ DEFINE TABLE OVERWRITE users SCHEMALESS COMMENT "Created by Shokupan";
1552
+ DEFINE TABLE OVERWRITE idempotency_keys SCHEMALESS COMMENT "Created by Shokupan";
1553
+ DEFINE TABLE OVERWRITE middleware_tracking SCHEMALESS COMMENT "Created by Shokupan";
1554
+ DEFINE TABLE OVERWRITE requests SCHEMALESS COMMENT "Created by Shokupan";
1555
+ `);
1556
+ db = _db;
1557
+ return db;
1558
+ } catch (e) {
1559
+ dbPromise = null;
1560
+ if (e.code === "ERR_MODULE_NOT_FOUND" || e.message.includes("Cannot find module")) {
1561
+ throw new Error("SurrealDB dependencies not found. To use the datastore, please install 'surrealdb' and '@surrealdb/node'.");
1562
+ }
1563
+ throw e;
1564
+ }
1565
+ })();
1566
+ return dbPromise;
1567
+ }
1382
1568
  const datastore = {
1383
- get(store, key) {
1569
+ async get(store, key) {
1570
+ await ensureDb();
1384
1571
  return db.select(new RecordId(store, key));
1385
1572
  },
1386
- set(store, key, value) {
1573
+ async set(store, key, value) {
1574
+ await ensureDb();
1387
1575
  return db.create(new RecordId(store, key)).content(value);
1388
1576
  },
1389
1577
  async query(query, vars) {
1578
+ await ensureDb();
1390
1579
  try {
1391
- const r = await db.query(query, vars).collect();
1392
- return r;
1580
+ const r = await db.query(query, vars);
1581
+ return Array.isArray(r) ? r : r?.collect ? await r.collect() : r;
1393
1582
  } catch (e) {
1394
1583
  console.error("DS ERROR:", e);
1395
1584
  throw e;
1396
1585
  }
1397
1586
  },
1398
- ready
1587
+ get ready() {
1588
+ return ensureDb().then(() => void 0);
1589
+ }
1399
1590
  };
1400
1591
  process.on("exit", async () => {
1401
- await db.close();
1592
+ if (db) await db.close();
1402
1593
  });
1403
1594
  const tracer = trace.getTracer("shokupan.middleware");
1404
1595
  function traceHandler(fn, name) {
@@ -1471,6 +1662,8 @@ class ShokupanRouter {
1471
1662
  [$parent] = null;
1472
1663
  [$childRouters] = [];
1473
1664
  [$childControllers] = [];
1665
+ hookCache = /* @__PURE__ */ new Map();
1666
+ hooksInitialized = false;
1474
1667
  middleware = [];
1475
1668
  get rootConfig() {
1476
1669
  return this[$appRoot]?.applicationConfig;
@@ -1488,7 +1681,8 @@ class ShokupanRouter {
1488
1681
  getComponentRegistry() {
1489
1682
  const controllerRoutesMap = /* @__PURE__ */ new Map();
1490
1683
  const localRoutes = [];
1491
- for (const r of this[$routes]) {
1684
+ for (let i = 0; i < this[$routes].length; i++) {
1685
+ const r = this[$routes][i];
1492
1686
  const entry = {
1493
1687
  type: "route",
1494
1688
  path: r.path,
@@ -1625,7 +1819,8 @@ class ShokupanRouter {
1625
1819
  const decoratedArgs = instance[$routeArgs] || proto && proto[$routeArgs];
1626
1820
  const methodMiddlewareMap = instance[$middleware] || proto && proto[$middleware];
1627
1821
  let routesAttached = 0;
1628
- for (const name of Array.from(methods)) {
1822
+ for (let i = 0; i < Array.from(methods).length; i++) {
1823
+ const name = Array.from(methods)[i];
1629
1824
  if (name === "constructor") continue;
1630
1825
  if (["arguments", "caller", "callee"].includes(name)) continue;
1631
1826
  const originalHandler = instance[name];
@@ -1637,7 +1832,8 @@ class ShokupanRouter {
1637
1832
  method = config.method;
1638
1833
  subPath = config.path;
1639
1834
  } else {
1640
- for (const m of HTTPMethods) {
1835
+ for (let j = 0; j < HTTPMethods.length; j++) {
1836
+ const m = HTTPMethods[j];
1641
1837
  if (name.toUpperCase().startsWith(m)) {
1642
1838
  method = m;
1643
1839
  const rest = name.slice(m.length);
@@ -1652,8 +1848,8 @@ class ShokupanRouter {
1652
1848
  buffer = "";
1653
1849
  }
1654
1850
  };
1655
- for (let i = 0; i < rest.length; i++) {
1656
- const char = rest[i];
1851
+ for (let i2 = 0; i2 < rest.length; i2++) {
1852
+ const char = rest[i2];
1657
1853
  if (char === "$") {
1658
1854
  flush();
1659
1855
  subPath += "/:";
@@ -1691,7 +1887,8 @@ class ShokupanRouter {
1691
1887
  if (routeArgs?.length > 0) {
1692
1888
  args = [];
1693
1889
  const sortedArgs = [...routeArgs].sort((a, b) => a.index - b.index);
1694
- for (const arg of sortedArgs) {
1890
+ for (let k = 0; k < sortedArgs.length; k++) {
1891
+ const arg = sortedArgs[k];
1695
1892
  switch (arg.type) {
1696
1893
  case RouteParamType.BODY:
1697
1894
  try {
@@ -1721,7 +1918,9 @@ class ShokupanRouter {
1721
1918
  args[arg.index] = vals.length > 1 ? vals : vals[0];
1722
1919
  } else {
1723
1920
  const query = {};
1724
- for (const key of url.searchParams.keys()) {
1921
+ const keys = Object.keys(url.searchParams);
1922
+ for (let k2 = 0; k2 < keys.length; k2++) {
1923
+ const key = keys[k2];
1725
1924
  const vals = url.searchParams.getAll(key);
1726
1925
  query[key] = vals.length > 1 ? vals : vals[0];
1727
1926
  }
@@ -1778,9 +1977,11 @@ class ShokupanRouter {
1778
1977
  path: r.path,
1779
1978
  handler: r.handler
1780
1979
  }));
1781
- for (const child of this[$childRouters]) {
1980
+ for (let i = 0; i < this[$childRouters].length; i++) {
1981
+ const child = this[$childRouters][i];
1782
1982
  const childRoutes = child.getRoutes();
1783
- for (const route of childRoutes) {
1983
+ for (let j = 0; j < childRoutes.length; j++) {
1984
+ const route = childRoutes[j];
1784
1985
  const cleanPrefix = child[$mountPath].endsWith("/") ? child[$mountPath].slice(0, -1) : child[$mountPath];
1785
1986
  const cleanPath = route.path.startsWith("/") ? route.path : "/" + route.path;
1786
1987
  const fullPath = cleanPrefix + cleanPath || "/";
@@ -1794,12 +1995,12 @@ class ShokupanRouter {
1794
1995
  return routes;
1795
1996
  }
1796
1997
  /**
1797
- * Makes a sub request to this router.
1798
- * This is useful for triggering other methods or route handlers.
1998
+ * Makes an internal request through this router's full routing pipeline.
1999
+ * This is useful for calling other routes internally and supports streaming responses.
1799
2000
  * @param options The request options.
1800
- * @returns The response.
2001
+ * @returns The raw Response object.
1801
2002
  */
1802
- async subRequest(arg) {
2003
+ async internalRequest(arg) {
1803
2004
  const options = typeof arg === "string" ? { path: arg } : arg;
1804
2005
  const store = asyncContext.getStore();
1805
2006
  store?.get("req");
@@ -1818,9 +2019,10 @@ class ShokupanRouter {
1818
2019
  return this.root[$dispatch](req);
1819
2020
  }
1820
2021
  /**
1821
- * Processes a request directly.
2022
+ * Processes a request for testing purposes.
2023
+ * Returns a simplified { status, headers, data } object instead of a Response.
1822
2024
  */
1823
- async processRequest(options) {
2025
+ async testRequest(options) {
1824
2026
  let url = options.url || options.path || "/";
1825
2027
  if (!url.startsWith("http")) {
1826
2028
  const base = `http://${this.rootConfig?.hostname || "localhost"}:${this.rootConfig?.port || 3e3}`;
@@ -1829,7 +2031,9 @@ class ShokupanRouter {
1829
2031
  }
1830
2032
  if (options.query) {
1831
2033
  const u = new URL(url);
1832
- for (const [k, v] of Object.entries(options.query)) {
2034
+ const entries = Object.entries(options.query);
2035
+ for (let i = 0; i < entries.length; i++) {
2036
+ const [k, v] = entries[i];
1833
2037
  u.searchParams.set(k, v);
1834
2038
  }
1835
2039
  url = u.toString();
@@ -1874,28 +2078,17 @@ class ShokupanRouter {
1874
2078
  data: result
1875
2079
  };
1876
2080
  }
1877
- applyRouterHooks(match) {
1878
- if (!this.config?.hooks) return match;
1879
- const hooks = this.config.hooks;
1880
- return {
1881
- ...match,
1882
- handler: this.wrapWithHooks(match.handler, hooks)
1883
- };
1884
- }
1885
- wrapWithHooks(handler, hooks) {
1886
- const hookList = Array.isArray(hooks) ? hooks : [hooks];
1887
- const hasStart = hookList.some((h) => !!h.onRequestStart);
1888
- const hasEnd = hookList.some((h) => !!h.onRequestEnd);
1889
- const hasError = hookList.some((h) => !!h.onError);
2081
+ wrapWithHooks(handler) {
2082
+ if (!this.hooksInitialized) {
2083
+ this.ensureHooksInitialized();
2084
+ }
2085
+ const hasStart = this.hookCache.get("onRequestStart")?.length > 0;
2086
+ const hasEnd = this.hookCache.get("onRequestEnd")?.length > 0;
2087
+ const hasError = this.hookCache.get("onError")?.length > 0;
1890
2088
  if (!hasStart && !hasEnd && !hasError) return handler;
1891
2089
  const originalHandler = handler;
1892
2090
  const wrapped = async (ctx) => {
1893
- if (hasStart) {
1894
- for (let i = 0; i < hookList.length; i++) {
1895
- const h = hookList[i];
1896
- if (typeof h.onRequestStart === "function") await h.onRequestStart(ctx);
1897
- }
1898
- }
2091
+ await this.runHooks("onRequestStart", ctx);
1899
2092
  const debug = ctx._debug;
1900
2093
  let debugId;
1901
2094
  let previousNode;
@@ -1909,17 +2102,11 @@ class ShokupanRouter {
1909
2102
  try {
1910
2103
  const res = await originalHandler(ctx);
1911
2104
  debug?.trackStep(debugId, "handler", performance.now() - start, "success");
1912
- for (let i = 0; i < hookList.length; i++) {
1913
- const h = hookList[i];
1914
- if (typeof h.onRequestEnd === "function") await h.onRequestEnd(ctx);
1915
- }
2105
+ await this.runHooks("onRequestEnd", ctx);
1916
2106
  return res;
1917
2107
  } catch (err) {
1918
2108
  debug?.trackStep(debugId, "handler", performance.now() - start, "error", err);
1919
- for (let i = 0; i < hookList.length; i++) {
1920
- const h = hookList[i];
1921
- if (typeof h.onError === "function") await h.onError(err, ctx);
1922
- }
2109
+ await this.runHooks("onError", ctx, err);
1923
2110
  throw err;
1924
2111
  } finally {
1925
2112
  if (debug && previousNode) debug.setNode(previousNode);
@@ -1941,18 +2128,19 @@ class ShokupanRouter {
1941
2128
  result = this.trie.search("GET", path);
1942
2129
  if (result) return result;
1943
2130
  }
1944
- for (const child of this[$childRouters]) {
2131
+ for (let i = 0; i < this[$childRouters].length; i++) {
2132
+ const child = this[$childRouters][i];
1945
2133
  const prefix = child[$mountPath];
1946
2134
  if (path === prefix || path.startsWith(prefix + "/")) {
1947
2135
  const subPath = path.slice(prefix.length) || "/";
1948
2136
  const match = child.find(method, subPath);
1949
- if (match) return this.applyRouterHooks(match);
2137
+ if (match) return match;
1950
2138
  }
1951
2139
  if (prefix.endsWith("/")) {
1952
2140
  if (path.startsWith(prefix)) {
1953
2141
  const subPath = path.slice(prefix.length) || "/";
1954
2142
  const match = child.find(method, subPath);
1955
- if (match) return this.applyRouterHooks(match);
2143
+ if (match) return match;
1956
2144
  }
1957
2145
  }
1958
2146
  }
@@ -1974,17 +2162,23 @@ class ShokupanRouter {
1974
2162
  /**
1975
2163
  * Adds a route to the router.
1976
2164
  *
1977
- * @param method - HTTP method
1978
- * @param path - URL path
1979
- * @param spec - OpenAPI specification for the route
1980
- * @param handler - Route handler function
1981
- * @param requestTimeout - Timeout for this route in milliseconds
2165
+ * @param arg - Route configuration object
2166
+ * @param arg.method - HTTP method
2167
+ * @param arg.path - URL path
2168
+ * @param arg.spec - OpenAPI specification for the route
2169
+ * @param arg.handler - Route handler function
2170
+ * @param arg.regex - Custom regex for path matching
2171
+ * @param arg.group - Group for the route
2172
+ * @param arg.requestTimeout - Timeout for this route in milliseconds
2173
+ * @param arg.renderer - JSX renderer for the route
2174
+ * @param arg.controller - Controller for the route
1982
2175
  */
1983
2176
  add({ method, path, spec, handler, regex: customRegex, group, requestTimeout, renderer, controller }) {
1984
2177
  const { regex, keys } = customRegex ? { regex: customRegex, keys: [] } : this.parsePath(path);
1985
2178
  if (this.currentGuards.length > 0) {
1986
2179
  spec = spec || {};
1987
- for (const guard of this.currentGuards) {
2180
+ for (let i = 0; i < this.currentGuards.length; i++) {
2181
+ const guard = this.currentGuards[i];
1988
2182
  if (guard.spec) {
1989
2183
  if (guard.spec.responses) {
1990
2184
  spec.responses = spec.responses || {};
@@ -2013,7 +2207,8 @@ class ShokupanRouter {
2013
2207
  if (routeGuards.length > 0) {
2014
2208
  const innerHandler = wrappedHandler;
2015
2209
  wrappedHandler = async (ctx) => {
2016
- for (const guard of routeGuards) {
2210
+ for (let i = 0; i < routeGuards.length; i++) {
2211
+ const guard = routeGuards[i];
2017
2212
  let guardPassed = false;
2018
2213
  let nextCalled = false;
2019
2214
  const next = () => {
@@ -2107,7 +2302,7 @@ class ShokupanRouter {
2107
2302
  wrappedHandler.originalHandler = trackingHandler.originalHandler || trackingHandler;
2108
2303
  let bakedHandler = wrappedHandler;
2109
2304
  if (this.config?.hooks) {
2110
- bakedHandler = this.wrapWithHooks(wrappedHandler, this.config.hooks);
2305
+ bakedHandler = this.wrapWithHooks(wrappedHandler);
2111
2306
  }
2112
2307
  this[$routes].push({
2113
2308
  method,
@@ -2264,6 +2459,67 @@ class ShokupanRouter {
2264
2459
  generateApiSpec(options = {}) {
2265
2460
  return generateOpenApi(this, options);
2266
2461
  }
2462
+ ensureHooksInitialized() {
2463
+ const hooks = this.config?.hooks;
2464
+ if (hooks) {
2465
+ const hookList = Array.isArray(hooks) ? hooks : [hooks];
2466
+ const hookTypes = [
2467
+ "onRequestStart",
2468
+ "onRequestEnd",
2469
+ "onResponseStart",
2470
+ "onResponseEnd",
2471
+ "onError",
2472
+ "beforeValidate",
2473
+ "afterValidate",
2474
+ "onRequestTimeout",
2475
+ "onReadTimeout",
2476
+ "onWriteTimeout"
2477
+ ];
2478
+ for (let i = 0; i < hookTypes.length; i++) {
2479
+ const type = hookTypes[i];
2480
+ const fns = [];
2481
+ for (let j = 0; j < hookList.length; j++) {
2482
+ const h = hookList[j];
2483
+ if (h[type]) fns.push(h[type]);
2484
+ }
2485
+ if (fns.length > 0) {
2486
+ this.hookCache.set(type, fns);
2487
+ }
2488
+ }
2489
+ }
2490
+ this.hooksInitialized = true;
2491
+ }
2492
+ async runHooks(name, ...args) {
2493
+ if (!this.hooksInitialized) {
2494
+ this.ensureHooksInitialized();
2495
+ }
2496
+ const fns = this.hookCache.get(name);
2497
+ if (!fns) return;
2498
+ const ctx = args?.[0] instanceof ShokupanContext ? args[0] : void 0;
2499
+ const debug = ctx?._debug;
2500
+ if (debug) {
2501
+ await Promise.all(fns.map(async (fn, index) => {
2502
+ const hookId = `hook_${name}_${fn.name || index}`;
2503
+ const previousNode = debug.getCurrentNode();
2504
+ debug.trackEdge(previousNode, hookId);
2505
+ debug.setNode(hookId);
2506
+ const start = performance.now();
2507
+ try {
2508
+ await fn(...args);
2509
+ const duration = performance.now() - start;
2510
+ debug.trackStep(hookId, "hook", duration, "success");
2511
+ } catch (error) {
2512
+ const duration = performance.now() - start;
2513
+ debug.trackStep(hookId, "hook", duration, "error", error);
2514
+ throw error;
2515
+ } finally {
2516
+ if (previousNode) debug.setNode(previousNode);
2517
+ }
2518
+ }));
2519
+ } else {
2520
+ await Promise.all(fns.map((fn) => fn(...args)));
2521
+ }
2522
+ }
2267
2523
  }
2268
2524
  class SystemCpuMonitor {
2269
2525
  constructor(intervalMs = 1e3) {
@@ -2321,15 +2577,13 @@ class Shokupan extends ShokupanRouter {
2321
2577
  openApiSpec;
2322
2578
  composedMiddleware;
2323
2579
  cpuMonitor;
2324
- hookCache = /* @__PURE__ */ new Map();
2325
- hooksInitialized = false;
2326
2580
  get logger() {
2327
2581
  return this.applicationConfig.logger;
2328
2582
  }
2329
2583
  constructor(applicationConfig = {}) {
2330
2584
  const config = Object.assign({}, defaults, applicationConfig);
2331
2585
  const { hooks, ...routerConfig } = config;
2332
- super(routerConfig);
2586
+ super({ ...routerConfig, hooks });
2333
2587
  this[$isApplication] = true;
2334
2588
  this[$appRoot] = this;
2335
2589
  this.applicationConfig = config;
@@ -2344,7 +2598,6 @@ class Shokupan extends ShokupanRouter {
2344
2598
  * Adds middleware to the application.
2345
2599
  */
2346
2600
  use(middleware) {
2347
- let trackedMiddleware = middleware;
2348
2601
  const { file, line } = getCallerInfo();
2349
2602
  if (!middleware.metadata) {
2350
2603
  middleware.metadata = {
@@ -2355,32 +2608,36 @@ class Shokupan extends ShokupanRouter {
2355
2608
  pluginName: middleware.pluginName
2356
2609
  };
2357
2610
  }
2358
- trackedMiddleware = async (ctx, next) => {
2359
- const c = ctx;
2360
- if (c.handlerStack && c.app?.applicationConfig.enableMiddlewareTracking) {
2361
- const metadata = middleware.metadata || {};
2362
- const start = performance.now();
2363
- const item = {
2364
- name: metadata.pluginName ? `${metadata.pluginName} (${metadata.name})` : metadata.name || middleware.name || "middleware",
2365
- file: metadata.file || file,
2366
- line: metadata.line || line,
2367
- isBuiltin: metadata.isBuiltin,
2368
- startTime: start,
2369
- duration: -1
2370
- };
2371
- c.handlerStack.push(item);
2372
- try {
2373
- return await middleware(ctx, next);
2374
- } finally {
2375
- item.duration = performance.now() - start;
2611
+ if (this.applicationConfig.enableMiddlewareTracking) {
2612
+ const trackedMiddleware = async (ctx, next) => {
2613
+ const c = ctx;
2614
+ if (c.handlerStack && c.app?.applicationConfig.enableMiddlewareTracking) {
2615
+ const metadata = middleware.metadata || {};
2616
+ const start = performance.now();
2617
+ const item = {
2618
+ name: metadata.pluginName ? `${metadata.pluginName} (${metadata.name})` : metadata.name || middleware.name || "middleware",
2619
+ file: metadata.file || file,
2620
+ line: metadata.line || line,
2621
+ isBuiltin: metadata.isBuiltin,
2622
+ startTime: start,
2623
+ duration: -1
2624
+ };
2625
+ c.handlerStack.push(item);
2626
+ try {
2627
+ return await middleware(ctx, next);
2628
+ } finally {
2629
+ item.duration = performance.now() - start;
2630
+ }
2376
2631
  }
2377
- }
2378
- return middleware(ctx, next);
2379
- };
2380
- trackedMiddleware.metadata = middleware.metadata;
2381
- Object.defineProperty(trackedMiddleware, "name", { value: middleware.name || "middleware" });
2382
- trackedMiddleware.order = this.middleware.length;
2383
- this.middleware.push(trackedMiddleware);
2632
+ return middleware(ctx, next);
2633
+ };
2634
+ trackedMiddleware.metadata = middleware.metadata;
2635
+ Object.defineProperty(trackedMiddleware, "name", { value: middleware.name || "middleware" });
2636
+ trackedMiddleware.order = this.middleware.length;
2637
+ this.middleware.push(trackedMiddleware);
2638
+ } else {
2639
+ this.middleware.push(middleware);
2640
+ }
2384
2641
  return this;
2385
2642
  }
2386
2643
  startupHooks = [];
@@ -2411,17 +2668,13 @@ class Shokupan extends ShokupanRouter {
2411
2668
  if (finalPort < 0 || finalPort > 65535) {
2412
2669
  throw new Error("Invalid port number");
2413
2670
  }
2414
- for (const hook of this.startupHooks) {
2415
- await hook();
2416
- }
2671
+ await Promise.all(this.startupHooks.map((hook) => hook()));
2417
2672
  if (this.applicationConfig.enableOpenApiGen) {
2418
2673
  this.openApiSpec = await generateOpenApi(this);
2419
- for (const hook of this.specAvailableHooks) {
2420
- await hook(this.openApiSpec);
2421
- }
2674
+ await Promise.all(this.specAvailableHooks.map((hook) => hook(this.openApiSpec)));
2422
2675
  }
2423
2676
  if (port === 0 && process.platform === "linux") ;
2424
- if (this.applicationConfig.autoBackpressureFeedback) {
2677
+ if (this.applicationConfig.autoBackpressureFeedback === true) {
2425
2678
  this.cpuMonitor = new SystemCpuMonitor();
2426
2679
  this.cpuMonitor.start();
2427
2680
  }
@@ -2449,7 +2702,7 @@ class Shokupan extends ShokupanRouter {
2449
2702
  };
2450
2703
  let factory = this.applicationConfig.serverFactory;
2451
2704
  if (!factory && typeof Bun === "undefined") {
2452
- const { createHttpServer } = await import("./server-adapter-BWrEJbKL.js");
2705
+ const { createHttpServer } = await import("./server-adapter-0xH174zz.js");
2453
2706
  factory = createHttpServer();
2454
2707
  }
2455
2708
  const server = factory ? await factory(serveOptions) : Bun.serve(serveOptions);
@@ -2462,7 +2715,7 @@ class Shokupan extends ShokupanRouter {
2462
2715
  /**
2463
2716
  * Processes a request by wrapping the standard fetch method.
2464
2717
  */
2465
- async processRequest(options) {
2718
+ async testRequest(options) {
2466
2719
  let url = options.url || options.path || "/";
2467
2720
  if (!url.startsWith("http")) {
2468
2721
  const base = `http://${this.applicationConfig.hostname || "localhost"}:${this.applicationConfig.port || 3e3}`;
@@ -2471,7 +2724,9 @@ class Shokupan extends ShokupanRouter {
2471
2724
  }
2472
2725
  if (options.query) {
2473
2726
  const u = new URL(url);
2474
- for (const [k, v] of Object.entries(options.query)) {
2727
+ const entries = Object.entries(options.query);
2728
+ for (let i = 0; i < entries.length; i++) {
2729
+ const [k, v] = entries[i];
2475
2730
  u.searchParams.set(k, v);
2476
2731
  }
2477
2732
  url = u.toString();
@@ -2540,18 +2795,18 @@ class Shokupan extends ShokupanRouter {
2540
2795
  if (this.cpuMonitor && this.cpuMonitor.getUsage() > (this.applicationConfig.autoBackpressureLevel ?? 60)) {
2541
2796
  const msg = "Too Many Requests (CPU Backpressure)";
2542
2797
  const res = ctx.text(msg, 429);
2543
- await this.executeHook("onResponseEnd", ctx, res);
2798
+ await this.runHooks("onResponseEnd", ctx, res);
2544
2799
  return res;
2545
2800
  }
2546
2801
  try {
2547
- if (this.hasHook("onRequestStart")) {
2548
- await this.executeHook("onRequestStart", ctx);
2549
- }
2802
+ await this.runHooks("onRequestStart", ctx);
2550
2803
  const fn = this.composedMiddleware ??= compose(this.middleware);
2551
2804
  const result = await fn(ctx, async () => {
2805
+ const bodyParsing = ["POST", "PUT", "PATCH", "DELETE"].includes(req.method) ? ctx.parseBody() : Promise.resolve();
2552
2806
  const match = this.find(req.method, ctx.path);
2553
2807
  if (match) {
2554
2808
  ctx.params = match.params;
2809
+ await bodyParsing;
2555
2810
  return match.handler(ctx);
2556
2811
  }
2557
2812
  return null;
@@ -2574,12 +2829,8 @@ class Shokupan extends ShokupanRouter {
2574
2829
  } else {
2575
2830
  response = ctx.text(String(result));
2576
2831
  }
2577
- if (this.hasHook("onRequestEnd")) {
2578
- await this.executeHook("onRequestEnd", ctx);
2579
- }
2580
- if (this.hasHook("onResponseStart")) {
2581
- await this.executeHook("onResponseStart", ctx, response);
2582
- }
2832
+ await this.runHooks("onRequestEnd", ctx);
2833
+ await this.runHooks("onResponseStart", ctx, response);
2583
2834
  return response;
2584
2835
  } catch (err) {
2585
2836
  console.error(err);
@@ -2588,9 +2839,7 @@ class Shokupan extends ShokupanRouter {
2588
2839
  const status = err.status || err.statusCode || 500;
2589
2840
  const body = { error: err.message || "Internal Server Error" };
2590
2841
  if (err.errors) body.errors = err.errors;
2591
- if (this.hasHook("onError")) {
2592
- await this.executeHook("onError", err, ctx);
2593
- }
2842
+ await this.runHooks("onError", ctx, err);
2594
2843
  return ctx.json(body, status);
2595
2844
  }
2596
2845
  };
@@ -2601,9 +2850,7 @@ class Shokupan extends ShokupanRouter {
2601
2850
  const timeoutPromise = new Promise((_, reject) => {
2602
2851
  timeoutId = setTimeout(async () => {
2603
2852
  controller.abort();
2604
- if (this.hasHook("onRequestTimeout")) {
2605
- await this.executeHook("onRequestTimeout", ctx);
2606
- }
2853
+ await this.runHooks("onRequestTimeout", ctx);
2607
2854
  reject(new Error("Request Timeout"));
2608
2855
  }, timeoutMs);
2609
2856
  });
@@ -2616,56 +2863,10 @@ class Shokupan extends ShokupanRouter {
2616
2863
  console.error("Unexpected error in request execution:", err);
2617
2864
  return ctx.text("Internal Server Error", 500);
2618
2865
  }).then(async (res) => {
2619
- if (this.hasHook("onResponseEnd")) {
2620
- await this.executeHook("onResponseEnd", ctx, res);
2621
- }
2866
+ await this.runHooks("onResponseEnd", ctx, res);
2622
2867
  return res;
2623
2868
  });
2624
2869
  }
2625
- ensureHooksInitialized() {
2626
- const hooks = this.applicationConfig.hooks;
2627
- if (hooks) {
2628
- const hookList = Array.isArray(hooks) ? hooks : [hooks];
2629
- const hookTypes = [
2630
- "onRequestStart",
2631
- "onRequestEnd",
2632
- "onResponseStart",
2633
- "onResponseEnd",
2634
- "onError",
2635
- "beforeValidate",
2636
- "afterValidate",
2637
- "onRequestTimeout",
2638
- "onReadTimeout",
2639
- "onWriteTimeout"
2640
- ];
2641
- for (const type of hookTypes) {
2642
- const fns = [];
2643
- for (const h of hookList) {
2644
- if (h[type]) fns.push(h[type]);
2645
- }
2646
- if (fns.length > 0) {
2647
- this.hookCache.set(type, fns);
2648
- }
2649
- }
2650
- }
2651
- this.hooksInitialized = true;
2652
- }
2653
- async executeHook(name, ...args) {
2654
- if (!this.hooksInitialized) {
2655
- this.ensureHooksInitialized();
2656
- }
2657
- const fns = this.hookCache.get(name);
2658
- if (!fns) return;
2659
- for (const fn of fns) {
2660
- await fn(...args);
2661
- }
2662
- }
2663
- hasHook(name) {
2664
- if (!this.hooksInitialized) {
2665
- this.ensureHooksInitialized();
2666
- }
2667
- return this.hookCache.has(name);
2668
- }
2669
2870
  }
2670
2871
  class AuthPlugin extends ShokupanRouter {
2671
2872
  constructor(authConfig) {
@@ -2713,7 +2914,9 @@ class AuthPlugin extends ShokupanRouter {
2713
2914
  return jwt;
2714
2915
  }
2715
2916
  init() {
2716
- for (const [providerName, providerConfig] of Object.entries(this.authConfig.providers)) {
2917
+ const providerEntries = Object.entries(this.authConfig.providers);
2918
+ for (let i = 0; i < providerEntries.length; i++) {
2919
+ const [providerName, providerConfig] = providerEntries[i];
2717
2920
  if (!providerConfig) continue;
2718
2921
  const provider = this.getProviderInstance(providerName, providerConfig);
2719
2922
  if (!provider) {
@@ -3041,7 +3244,9 @@ function Cors(options = {}) {
3041
3244
  }
3042
3245
  const response = await next();
3043
3246
  if (response instanceof Response) {
3044
- for (const [key, value] of headers.entries()) {
3247
+ const headerEntries = Array.from(headers.entries());
3248
+ for (let i = 0; i < headerEntries.length; i++) {
3249
+ const [key, value] = headerEntries[i];
3045
3250
  response.headers.set(key, value);
3046
3251
  }
3047
3252
  }
@@ -3111,6 +3316,8 @@ function useExpress(expressMiddleware) {
3111
3316
  });
3112
3317
  };
3113
3318
  }
3319
+ let plainToInstance;
3320
+ let validateOrReject;
3114
3321
  class ValidationError extends Error {
3115
3322
  constructor(errors) {
3116
3323
  super("Validation Error");
@@ -3175,6 +3382,18 @@ function isClass(schema) {
3175
3382
  }
3176
3383
  }
3177
3384
  async function validateClassValidator(schema, data) {
3385
+ if (!plainToInstance || !validateOrReject) {
3386
+ try {
3387
+ const ct = await import("class-transformer");
3388
+ const cv = await import("class-validator");
3389
+ plainToInstance = ct.plainToInstance;
3390
+ validateOrReject = cv.validateOrReject;
3391
+ } catch (e) {
3392
+ throw new Error(
3393
+ "class-transformer and class-validator are required for class-based validation. Install them with: bun add class-transformer class-validator reflect-metadata"
3394
+ );
3395
+ }
3396
+ }
3178
3397
  const object = plainToInstance(schema, data);
3179
3398
  try {
3180
3399
  await validateOrReject(object);
@@ -3189,30 +3408,8 @@ async function validateClassValidator(schema, data) {
3189
3408
  }
3190
3409
  }
3191
3410
  const safelyGetBody = async (ctx) => {
3192
- const req = ctx.req;
3193
- if (req._bodyParsed) {
3194
- return req._bodyValue;
3195
- }
3196
3411
  try {
3197
- let data;
3198
- if (typeof req.json === "function") {
3199
- data = await req.json();
3200
- } else {
3201
- data = req.body;
3202
- if (typeof data === "string") {
3203
- try {
3204
- data = JSON.parse(data);
3205
- } catch {
3206
- }
3207
- }
3208
- }
3209
- req._bodyParsed = true;
3210
- req._bodyValue = data;
3211
- Object.defineProperty(req, "json", {
3212
- value: async () => req._bodyValue,
3213
- configurable: true
3214
- });
3215
- return data;
3412
+ return await ctx.body();
3216
3413
  } catch (e) {
3217
3414
  return {};
3218
3415
  }
@@ -3259,9 +3456,7 @@ function validate(config) {
3259
3456
  body = await safelyGetBody(ctx);
3260
3457
  dataToValidate.body = body;
3261
3458
  }
3262
- if (ctx.app?.hasHook("beforeValidate")) {
3263
- await ctx.app.executeHook("beforeValidate", ctx, dataToValidate);
3264
- }
3459
+ await ctx.app.runHooks("beforeValidate", ctx, dataToValidate);
3265
3460
  if (validators.params) {
3266
3461
  ctx.params = await validators.params(ctx.params);
3267
3462
  }
@@ -3277,21 +3472,20 @@ function validate(config) {
3277
3472
  if (validators.body) {
3278
3473
  const b = body ?? await safelyGetBody(ctx);
3279
3474
  validBody = await validators.body(b);
3475
+ ctx._cachedBody = validBody;
3280
3476
  const req = ctx.req;
3281
- req._bodyValue = validBody;
3282
3477
  Object.defineProperty(req, "json", {
3283
3478
  value: async () => validBody,
3479
+ writable: true,
3284
3480
  configurable: true
3285
3481
  });
3286
3482
  ctx.body = validBody;
3287
3483
  }
3288
- if (ctx.app?.hasHook("afterValidate")) {
3289
- const validatedData = { ...dataToValidate };
3290
- if (config.params) validatedData.params = ctx.params;
3291
- if (config.query) validatedData.query = validQuery;
3292
- if (config.body) validatedData.body = validBody;
3293
- await ctx.app?.executeHook("afterValidate", ctx, validatedData);
3294
- }
3484
+ const validatedData = { ...dataToValidate };
3485
+ if (config.params) validatedData.params = ctx.params;
3486
+ if (config.query) validatedData.query = validQuery;
3487
+ if (config.body) validatedData.body = validBody;
3488
+ await ctx.app.runHooks("afterValidate", ctx, validatedData);
3295
3489
  return next();
3296
3490
  };
3297
3491
  }
@@ -3314,12 +3508,14 @@ function openApiValidator() {
3314
3508
  if (cache.validators.has(ctx.path)) {
3315
3509
  matchPath = ctx.path;
3316
3510
  } else {
3317
- for (const [path, { regex, paramNames }] of cache.paths) {
3511
+ const pathEntries = Array.from(cache.paths.entries());
3512
+ for (let i = 0; i < pathEntries.length; i++) {
3513
+ const [path, { regex, paramNames }] = pathEntries[i];
3318
3514
  const match = regex.exec(ctx.path);
3319
3515
  if (match) {
3320
3516
  matchPath = path;
3321
- paramNames.forEach((name, i) => {
3322
- matchParams[name] = match[i + 1];
3517
+ paramNames.forEach((name, i2) => {
3518
+ matchParams[name] = match[i2 + 1];
3323
3519
  });
3324
3520
  break;
3325
3521
  }
@@ -3376,7 +3572,9 @@ function openApiValidator() {
3376
3572
  function compileValidators(spec) {
3377
3573
  const validators = /* @__PURE__ */ new Map();
3378
3574
  const paths = /* @__PURE__ */ new Map();
3379
- for (const [path, pathItem] of Object.entries(spec.paths || {})) {
3575
+ const pathEntries = Object.entries(spec.paths || {});
3576
+ for (let i = 0; i < pathEntries.length; i++) {
3577
+ const [path, pathItem] = pathEntries[i];
3380
3578
  if (path.includes("{")) {
3381
3579
  const paramNames = [];
3382
3580
  const regexStr = "^" + path.replace(/{([^}]+)}/g, (_, name) => {
@@ -3389,7 +3587,9 @@ function compileValidators(spec) {
3389
3587
  });
3390
3588
  }
3391
3589
  const pathValidators = {};
3392
- for (const [method, operation] of Object.entries(pathItem)) {
3590
+ const methodEntries = Object.entries(pathItem);
3591
+ for (let k = 0; k < methodEntries.length; k++) {
3592
+ const [method, operation] = methodEntries[k];
3393
3593
  if (method === "parameters" || method === "summary" || method === "description") continue;
3394
3594
  const oper = operation;
3395
3595
  const opValidators = {};
@@ -3403,7 +3603,8 @@ function compileValidators(spec) {
3403
3603
  const queryRequired = [];
3404
3604
  const pathRequired = [];
3405
3605
  const headerRequired = [];
3406
- for (const param of parameters) {
3606
+ for (let j = 0; j < parameters.length; j++) {
3607
+ const param = parameters[j];
3407
3608
  if (param.in === "query") {
3408
3609
  queryProps[param.name] = param.schema || {};
3409
3610
  if (param.required) queryRequired.push(param.name);
@@ -3564,14 +3765,18 @@ function SecurityHeaders(options = {}) {
3564
3765
  if (opt === void 0 || opt === true) {
3565
3766
  set("Content-Security-Policy", "default-src 'self';base-uri 'self';font-src 'self' https: data:;form-action 'self';frame-ancestors 'self';img-src 'self' data:;object-src 'none';script-src 'self';style-src 'self' https: 'unsafe-inline';upgrade-insecure-requests");
3566
3767
  } else if (typeof opt === "object") {
3567
- for (const [key, val] of Object.entries(opt)) {
3768
+ const optEntries = Object.entries(opt);
3769
+ for (let i = 0; i < optEntries.length; i++) {
3770
+ const [key, val] = optEntries[i];
3568
3771
  }
3569
3772
  }
3570
3773
  }
3571
3774
  if (options.hidePoweredBy !== false) ;
3572
3775
  const response = await next();
3573
3776
  if (response instanceof Response) {
3574
- for (const [k, v] of Object.entries(headers)) {
3777
+ const headerEntries = Object.entries(headers);
3778
+ for (let i = 0; i < headerEntries.length; i++) {
3779
+ const [k, v] = headerEntries[i];
3575
3780
  response.headers.set(k, v);
3576
3781
  }
3577
3782
  return response;
@@ -3657,7 +3862,9 @@ class MemoryStore extends EventEmitter {
3657
3862
  }
3658
3863
  all(cb) {
3659
3864
  const result = {};
3660
- for (const sid in this.sessions) {
3865
+ const sessionKeys = Object.keys(this.sessions);
3866
+ for (let i = 0; i < sessionKeys.length; i++) {
3867
+ const sid = sessionKeys[i];
3661
3868
  try {
3662
3869
  result[sid] = JSON.parse(this.sessions[sid]);
3663
3870
  } catch {
@@ -3743,7 +3950,9 @@ function Session(options) {
3743
3950
  sessObj.regenerate = (cb) => {
3744
3951
  store.destroy(sessObj.id, (err) => {
3745
3952
  sessionID = generateId(ctx);
3746
- for (const key in sessObj) {
3953
+ const keys = Object.keys(sessObj);
3954
+ for (let i = 0; i < keys.length; i++) {
3955
+ const key = keys[i];
3747
3956
  if (key !== "cookie" && key !== "id" && typeof sessObj[key] !== "function") {
3748
3957
  delete sessObj[key];
3749
3958
  }
@@ -3758,7 +3967,9 @@ function Session(options) {
3758
3967
  store.get(sessObj.id, (err, sess2) => {
3759
3968
  if (err) return cb(err);
3760
3969
  if (!sess2) return cb(new Error("Session not found"));
3761
- for (const key in sessObj) {
3970
+ const keys = Object.keys(sessObj);
3971
+ for (let i = 0; i < keys.length; i++) {
3972
+ const key = keys[i];
3762
3973
  if (key !== "cookie" && key !== "id" && typeof sessObj[key] !== "function") {
3763
3974
  delete sessObj[key];
3764
3975
  }