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.cjs CHANGED
@@ -1,12 +1,32 @@
1
1
  "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __copyProps = (to, from, except, desc) => {
9
+ if (from && typeof from === "object" || typeof from === "function") {
10
+ for (let key of __getOwnPropNames(from))
11
+ if (!__hasOwnProp.call(to, key) && key !== except)
12
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
13
+ }
14
+ return to;
15
+ };
16
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
17
+ // If the importer is in node compatibility mode or this is not an ESM
18
+ // file that has been converted to a CommonJS file using a Babel-
19
+ // compatible transform (i.e. "__esModule" has not been set), then set
20
+ // "default" to the CommonJS "module.exports" for node compatibility.
21
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
22
+ mod
23
+ ));
2
24
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
25
  const promises = require("node:fs/promises");
4
26
  const eta$2 = require("eta");
5
27
  const promises$1 = require("fs/promises");
6
28
  const path = require("path");
7
29
  const node_async_hooks = require("node:async_hooks");
8
- const node = require("@surrealdb/node");
9
- const surrealdb = require("surrealdb");
10
30
  const api = require("@opentelemetry/api");
11
31
  const os = require("node:os");
12
32
  const arctic = require("arctic");
@@ -14,9 +34,7 @@ const jose = require("jose");
14
34
  const zlib = require("node:zlib");
15
35
  const Ajv = require("ajv");
16
36
  const addFormats = require("ajv-formats");
17
- const classTransformer = require("class-transformer");
18
- const classValidator = require("class-validator");
19
- const openapiAnalyzer = require("./openapi-analyzer-z-7AoFRC.cjs");
37
+ const openapiAnalyzer = require("./openapi-analyzer-Bei1sVWp.cjs");
20
38
  const crypto = require("crypto");
21
39
  const events = require("events");
22
40
  function _interopNamespaceDefault(e) {
@@ -101,8 +119,73 @@ class ShokupanResponse {
101
119
  return this._headers !== null;
102
120
  }
103
121
  }
122
+ const VALID_HTTP_STATUSES = /* @__PURE__ */ new Set([
123
+ 100,
124
+ 101,
125
+ 102,
126
+ 103,
127
+ 200,
128
+ 201,
129
+ 202,
130
+ 203,
131
+ 204,
132
+ 205,
133
+ 206,
134
+ 207,
135
+ 208,
136
+ 226,
137
+ 300,
138
+ 301,
139
+ 302,
140
+ 303,
141
+ 304,
142
+ 305,
143
+ 306,
144
+ 307,
145
+ 308,
146
+ 400,
147
+ 401,
148
+ 402,
149
+ 403,
150
+ 404,
151
+ 405,
152
+ 406,
153
+ 407,
154
+ 408,
155
+ 409,
156
+ 410,
157
+ 411,
158
+ 412,
159
+ 413,
160
+ 414,
161
+ 415,
162
+ 416,
163
+ 417,
164
+ 418,
165
+ 421,
166
+ 422,
167
+ 423,
168
+ 424,
169
+ 425,
170
+ 426,
171
+ 428,
172
+ 429,
173
+ 431,
174
+ 451,
175
+ 500,
176
+ 501,
177
+ 502,
178
+ 503,
179
+ 504,
180
+ 505,
181
+ 506,
182
+ 507,
183
+ 508,
184
+ 510,
185
+ 511
186
+ ]);
187
+ const VALID_REDIRECT_STATUSES = /* @__PURE__ */ new Set([301, 302, 303, 307, 308]);
104
188
  class ShokupanContext {
105
- // Raw body for compression optimization
106
189
  constructor(request, server, state, app, signal, enableMiddlewareTracking = false) {
107
190
  this.request = request;
108
191
  this.server = server;
@@ -125,7 +208,6 @@ class ShokupanContext {
125
208
  }
126
209
  this.response = new ShokupanResponse();
127
210
  }
128
- _url;
129
211
  params = {};
130
212
  // Router assigns this, but default to empty object
131
213
  state;
@@ -134,6 +216,19 @@ class ShokupanContext {
134
216
  _debug;
135
217
  _finalResponse;
136
218
  _rawBody;
219
+ // Raw body for compression optimization
220
+ // Body caching to avoid double parsing
221
+ _url;
222
+ _cachedBody;
223
+ _bodyType;
224
+ _bodyParsed = false;
225
+ _bodyParseError;
226
+ // Cached URL properties to avoid repeated parsing
227
+ _cachedHostname;
228
+ _cachedProtocol;
229
+ _cachedHost;
230
+ _cachedOrigin;
231
+ _cachedQuery;
137
232
  get url() {
138
233
  if (!this._url) {
139
234
  const urlString = this.request.url || "http://localhost/";
@@ -182,8 +277,11 @@ class ShokupanContext {
182
277
  * Request query params
183
278
  */
184
279
  get query() {
280
+ if (this._cachedQuery) return this._cachedQuery;
185
281
  const q = {};
186
- for (const [key, value] of this.url.searchParams) {
282
+ const entries = Object.entries(this.url.searchParams);
283
+ for (let i = 0; i < entries.length; i++) {
284
+ const [key, value] = entries[i];
187
285
  if (q[key] === void 0) {
188
286
  q[key] = value;
189
287
  } else if (Array.isArray(q[key])) {
@@ -192,6 +290,7 @@ class ShokupanContext {
192
290
  q[key] = [q[key], value];
193
291
  }
194
292
  }
293
+ this._cachedQuery = q;
195
294
  return q;
196
295
  }
197
296
  /**
@@ -204,31 +303,31 @@ class ShokupanContext {
204
303
  * Request hostname (e.g. "localhost")
205
304
  */
206
305
  get hostname() {
207
- return this.url.hostname;
306
+ return this._cachedHostname ??= this.url.hostname;
208
307
  }
209
308
  /**
210
309
  * Request host (e.g. "localhost:3000")
211
310
  */
212
311
  get host() {
213
- return this.url.host;
312
+ return this._cachedHost ??= this.url.host;
214
313
  }
215
314
  /**
216
315
  * Request protocol (e.g. "http:", "https:")
217
316
  */
218
317
  get protocol() {
219
- return this.url.protocol;
318
+ return this._cachedProtocol ??= this.url.protocol;
220
319
  }
221
320
  /**
222
321
  * Whether request is secure (https)
223
322
  */
224
323
  get secure() {
225
- return this.url.protocol === "https:";
324
+ return this.protocol === "https:";
226
325
  }
227
326
  /**
228
327
  * Request origin (e.g. "http://localhost:3000")
229
328
  */
230
329
  get origin() {
231
- return this.url.origin;
330
+ return this._cachedOrigin ??= this.url.origin;
232
331
  }
233
332
  /**
234
333
  * Request headers
@@ -324,6 +423,91 @@ class ShokupanContext {
324
423
  }
325
424
  return h;
326
425
  }
426
+ /**
427
+ * Read request body with caching to avoid double parsing.
428
+ * The body is only parsed once and cached for subsequent reads.
429
+ */
430
+ async body() {
431
+ if (this._bodyParseError) {
432
+ throw this._bodyParseError;
433
+ }
434
+ if (this._bodyParsed) {
435
+ return this._cachedBody;
436
+ }
437
+ const contentType = this.request.headers.get("content-type") || "";
438
+ if (contentType.includes("application/json") || contentType.includes("+json")) {
439
+ const rawText = await this.readRawBody();
440
+ const parserType = this.app?.applicationConfig?.jsonParser || "native";
441
+ if (parserType === "native") {
442
+ this._cachedBody = JSON.parse(rawText);
443
+ } else {
444
+ const { getJSONParser } = await Promise.resolve().then(() => require("./json-parser-COdZ0fqY.cjs"));
445
+ const parser = getJSONParser(parserType);
446
+ this._cachedBody = parser(rawText);
447
+ }
448
+ this._bodyType = "json";
449
+ } else if (contentType.includes("multipart/form-data") || contentType.includes("application/x-www-form-urlencoded")) {
450
+ this._cachedBody = await this.request.formData();
451
+ this._bodyType = "formData";
452
+ } else {
453
+ this._cachedBody = await this.readRawBody();
454
+ this._bodyType = "text";
455
+ }
456
+ this._bodyParsed = true;
457
+ return this._cachedBody;
458
+ }
459
+ /**
460
+ * Pre-parse the request body before handler execution.
461
+ * This improves performance and enables Node.js compatibility for large payloads.
462
+ * Errors are deferred until the body is actually accessed in the handler.
463
+ */
464
+ async parseBody() {
465
+ if (this._bodyParsed) {
466
+ return;
467
+ }
468
+ if (this.request.method === "GET" || this.request.method === "HEAD") {
469
+ return;
470
+ }
471
+ try {
472
+ await this.body();
473
+ } catch (error) {
474
+ this._bodyParseError = error;
475
+ }
476
+ }
477
+ /**
478
+ * Read raw body from ReadableStream efficiently.
479
+ * This is much faster than request.text() for large payloads.
480
+ * Also handles the case where body is already a string (e.g., in tests).
481
+ */
482
+ async readRawBody() {
483
+ if (typeof this.request.body === "string") {
484
+ return this.request.body;
485
+ }
486
+ const reader = this.request.body?.getReader();
487
+ if (!reader) {
488
+ return "";
489
+ }
490
+ const chunks = [];
491
+ let totalSize = 0;
492
+ try {
493
+ while (true) {
494
+ const { done, value } = await reader.read();
495
+ if (done) break;
496
+ chunks.push(value);
497
+ totalSize += value.length;
498
+ }
499
+ } finally {
500
+ reader.releaseLock();
501
+ }
502
+ const result = new Uint8Array(totalSize);
503
+ let offset = 0;
504
+ for (let i = 0; i < chunks.length; i++) {
505
+ const chunk = chunks[i];
506
+ result.set(chunk, offset);
507
+ offset += chunk.length;
508
+ }
509
+ return new TextDecoder().decode(result);
510
+ }
327
511
  /**
328
512
  * Send a response
329
513
  * @param body Response body
@@ -332,31 +516,24 @@ class ShokupanContext {
332
516
  */
333
517
  send(body, options) {
334
518
  const headers = this.mergeHeaders(options?.headers);
335
- const status = options?.status ?? this.response.status;
519
+ const status = options?.status ?? this.response.status ?? 200;
520
+ if (this.app.applicationConfig.validateStatusCodes && !VALID_HTTP_STATUSES.has(status)) {
521
+ throw new Error(`Invalid HTTP status code: ${status}`);
522
+ }
336
523
  if (typeof body === "string" || body instanceof ArrayBuffer || body instanceof Uint8Array) {
337
524
  this._rawBody = body;
338
525
  }
339
526
  this._finalResponse = new Response(body, { status, headers });
340
527
  return this._finalResponse;
341
528
  }
342
- /**
343
- * Read request body
344
- */
345
- async body() {
346
- const contentType = this.request.headers.get("content-type") || "";
347
- if (contentType.includes("application/json") || contentType.includes("+json")) {
348
- return this.request.json();
349
- }
350
- if (contentType.includes("multipart/form-data") || contentType.includes("application/x-www-form-urlencoded")) {
351
- return this.request.formData();
352
- }
353
- return this.request.text();
354
- }
355
529
  /**
356
530
  * Respond with a JSON object
357
531
  */
358
532
  json(data, status, headers) {
359
- const finalStatus = status ?? this.response.status;
533
+ const finalStatus = status ?? this.response.status ?? 200;
534
+ if (!VALID_HTTP_STATUSES.has(finalStatus)) {
535
+ throw new Error(`Invalid HTTP status code: ${finalStatus}`);
536
+ }
360
537
  const jsonString = JSON.stringify(data);
361
538
  this._rawBody = jsonString;
362
539
  if (!headers && !this.response.hasPopulatedHeaders) {
@@ -375,7 +552,10 @@ class ShokupanContext {
375
552
  * Respond with a text string
376
553
  */
377
554
  text(data, status, headers) {
378
- const finalStatus = status ?? this.response.status;
555
+ const finalStatus = status ?? this.response.status ?? 200;
556
+ if (this.app.applicationConfig.validateStatusCodes && !VALID_HTTP_STATUSES.has(finalStatus)) {
557
+ throw new Error(`Invalid HTTP status code: ${finalStatus}`);
558
+ }
379
559
  this._rawBody = data;
380
560
  if (!headers && !this.response.hasPopulatedHeaders) {
381
561
  this._finalResponse = new Response(data, {
@@ -393,7 +573,10 @@ class ShokupanContext {
393
573
  * Respond with HTML content
394
574
  */
395
575
  html(html, status, headers) {
396
- const finalStatus = status ?? this.response.status;
576
+ const finalStatus = status ?? this.response.status ?? 200;
577
+ if (this.app.applicationConfig.validateStatusCodes && !VALID_HTTP_STATUSES.has(finalStatus)) {
578
+ throw new Error(`Invalid HTTP status code: ${finalStatus}`);
579
+ }
397
580
  const finalHeaders = this.mergeHeaders(headers);
398
581
  finalHeaders.set("content-type", "text/html; charset=utf-8");
399
582
  this._rawBody = html;
@@ -404,6 +587,9 @@ class ShokupanContext {
404
587
  * Respond with a redirect
405
588
  */
406
589
  redirect(url, status = 302) {
590
+ if (this.app.applicationConfig.validateStatusCodes && !VALID_REDIRECT_STATUSES.has(status)) {
591
+ throw new Error(`Invalid redirect status code: ${status}`);
592
+ }
407
593
  const headers = this.mergeHeaders();
408
594
  headers.set("Location", url);
409
595
  this._finalResponse = new Response(null, { status, headers });
@@ -414,6 +600,9 @@ class ShokupanContext {
414
600
  * DOES NOT CHAIN!
415
601
  */
416
602
  status(status) {
603
+ if (this.app.applicationConfig.validateStatusCodes && !VALID_HTTP_STATUSES.has(status)) {
604
+ throw new Error(`Invalid HTTP status code: ${status}`);
605
+ }
417
606
  const headers = this.mergeHeaders();
418
607
  this._finalResponse = new Response(null, { status, headers });
419
608
  return this._finalResponse;
@@ -424,6 +613,9 @@ class ShokupanContext {
424
613
  async file(path2, fileOptions, responseOptions) {
425
614
  const headers = this.mergeHeaders(responseOptions?.headers);
426
615
  const status = responseOptions?.status ?? this.response.status;
616
+ if (this.app.applicationConfig.validateStatusCodes && !VALID_HTTP_STATUSES.has(status)) {
617
+ throw new Error(`Invalid HTTP status code: ${status}`);
618
+ }
427
619
  if (typeof Bun !== "undefined") {
428
620
  this._finalResponse = new Response(Bun.file(path2, fileOptions), { status, headers });
429
621
  return this._finalResponse;
@@ -447,6 +639,10 @@ class ShokupanContext {
447
639
  * @param headers HTTP Headers
448
640
  */
449
641
  async jsx(element, args, status, headers) {
642
+ status ??= 200;
643
+ if (this.app.applicationConfig.validateStatusCodes && !VALID_HTTP_STATUSES.has(status)) {
644
+ throw new Error(`Invalid HTTP status code: ${status}`);
645
+ }
450
646
  if (!this.renderer) {
451
647
  throw new Error("No JSX renderer configured");
452
648
  }
@@ -471,7 +667,9 @@ function RateLimitMiddleware(options = {}) {
471
667
  const hits = /* @__PURE__ */ new Map();
472
668
  const interval = setInterval(() => {
473
669
  const now = Date.now();
474
- for (const [key, record] of hits.entries()) {
670
+ const entries = Array.from(hits.entries());
671
+ for (let i = 0; i < entries.length; i++) {
672
+ const [key, record] = entries[i];
475
673
  if (record.resetTime <= now) {
476
674
  hits.delete(key);
477
675
  }
@@ -724,7 +922,9 @@ function deepMerge(target, ...sources) {
724
922
  if (!sources.length) return target;
725
923
  const source = sources.shift();
726
924
  if (isObject(target) && isObject(source)) {
727
- for (const key in source) {
925
+ const sourceKeys = Object.keys(source);
926
+ for (let i = 0; i < sourceKeys.length; i++) {
927
+ const key = sourceKeys[i];
728
928
  if (isObject(source[key])) {
729
929
  if (!target[key]) Object.assign(target, { [key]: {} });
730
930
  deepMerge(target[key], source[key]);
@@ -748,15 +948,17 @@ function deepMerge(target, ...sources) {
748
948
  }
749
949
  return deepMerge(target, ...sources);
750
950
  }
751
- const REGEX_QUERY_INT = /parseInt\(ctx\.query\.(\w+)\)/g;
752
- const REGEX_QUERY_FLOAT = /parseFloat\(ctx\.query\.(\w+)\)/g;
753
- const REGEX_QUERY_NUMBER = /Number\(ctx\.query\.(\w+)\)/g;
754
- const REGEX_QUERY_BOOL = /(?:Boolean\(ctx\.query\.(\w+)\)|!+ctx\.query\.(\w+))/g;
755
- const REGEX_QUERY_GENERIC = /ctx\.query\.(\w+)/g;
756
- const REGEX_PARAM_INT = /parseInt\(ctx\.params\.(\w+)\)/g;
757
- const REGEX_PARAM_FLOAT = /parseFloat\(ctx\.params\.(\w+)\)/g;
758
- const REGEX_HEADER_GET = /ctx\.get\(['"](\w+)['"]\)/g;
759
- const REGEX_ERROR_STATUS = /ctx\.(?:json|text|html)\([^)]+,\s*(\d{3,})\)/g;
951
+ const REGEX_PATTERNS = {
952
+ QUERY_INT: /parseInt\(ctx\.query\.(\w+)\)/g,
953
+ QUERY_FLOAT: /parseFloat\(ctx\.query\.(\w+)\)/g,
954
+ QUERY_NUMBER: /Number\(ctx\.query\.(\w+)\)/g,
955
+ QUERY_BOOL: /(?:Boolean\(ctx\.query\.(\w+)\)|!+ctx\.query\.(\w+))/g,
956
+ QUERY_GENERIC: /ctx\.query\.(\w+)/g,
957
+ PARAM_INT: /parseInt\(ctx\.params\.(\w+)\)/g,
958
+ PARAM_FLOAT: /parseFloat\(ctx\.params\.(\w+)\)/g,
959
+ HEADER_GET: /ctx\.get\(['"](\w+)['"]\)/g,
960
+ ERROR_STATUS: /ctx\.(?:json|text|html)\([^)]+,\s*(\d{3,})\)/g
961
+ };
760
962
  function analyzeHandler(handler) {
761
963
  const handlerSource = handler.toString();
762
964
  const inferredSpec = {};
@@ -766,29 +968,20 @@ function analyzeHandler(handler) {
766
968
  };
767
969
  }
768
970
  const queryParams = /* @__PURE__ */ new Map();
769
- for (const match of handlerSource.matchAll(REGEX_QUERY_INT)) {
770
- if (match[1]) queryParams.set(match[1], { type: "integer", format: "int32" });
771
- }
772
- for (const match of handlerSource.matchAll(REGEX_QUERY_FLOAT)) {
773
- if (match[1]) queryParams.set(match[1], { type: "number", format: "float" });
774
- }
775
- for (const match of handlerSource.matchAll(REGEX_QUERY_NUMBER)) {
776
- if (match[1] && !queryParams.has(match[1])) {
777
- queryParams.set(match[1], { type: "number" });
778
- }
779
- }
780
- for (const match of handlerSource.matchAll(REGEX_QUERY_BOOL)) {
781
- const name = match[1] || match[2];
782
- if (name && !queryParams.has(name)) {
783
- queryParams.set(name, { type: "boolean" });
784
- }
785
- }
786
- for (const match of handlerSource.matchAll(REGEX_QUERY_GENERIC)) {
787
- const name = match[1];
788
- if (name && !queryParams.has(name)) {
789
- queryParams.set(name, { type: "string" });
971
+ const processMatches = (regex, type, format) => {
972
+ const matches = Array.from(handlerSource.matchAll(regex));
973
+ for (const match of matches) {
974
+ const name = match[1] || match[2];
975
+ if (name && !queryParams.has(name)) {
976
+ queryParams.set(name, { type, format });
977
+ }
790
978
  }
791
- }
979
+ };
980
+ processMatches(REGEX_PATTERNS.QUERY_INT, "integer", "int32");
981
+ processMatches(REGEX_PATTERNS.QUERY_FLOAT, "number", "float");
982
+ processMatches(REGEX_PATTERNS.QUERY_NUMBER, "number");
983
+ processMatches(REGEX_PATTERNS.QUERY_BOOL, "boolean");
984
+ processMatches(REGEX_PATTERNS.QUERY_GENERIC, "string");
792
985
  if (queryParams.size > 0) {
793
986
  if (!inferredSpec.parameters) inferredSpec.parameters = [];
794
987
  queryParams.forEach((schema, paramName) => {
@@ -800,12 +993,15 @@ function analyzeHandler(handler) {
800
993
  });
801
994
  }
802
995
  const pathParams = /* @__PURE__ */ new Map();
803
- for (const match of handlerSource.matchAll(REGEX_PARAM_INT)) {
804
- if (match[1]) pathParams.set(match[1], { type: "integer", format: "int32" });
805
- }
806
- for (const match of handlerSource.matchAll(REGEX_PARAM_FLOAT)) {
807
- if (match[1]) pathParams.set(match[1], { type: "number", format: "float" });
808
- }
996
+ const processPathMatches = (regex, type, format) => {
997
+ const matches = Array.from(handlerSource.matchAll(regex));
998
+ for (const match of matches) {
999
+ const name = match[1];
1000
+ if (name) pathParams.set(name, { type, format });
1001
+ }
1002
+ };
1003
+ processPathMatches(REGEX_PATTERNS.PARAM_INT, "integer", "int32");
1004
+ processPathMatches(REGEX_PATTERNS.PARAM_FLOAT, "number", "float");
809
1005
  if (pathParams.size > 0) {
810
1006
  if (!inferredSpec.parameters) inferredSpec.parameters = [];
811
1007
  pathParams.forEach((schema, paramName) => {
@@ -817,7 +1013,8 @@ function analyzeHandler(handler) {
817
1013
  });
818
1014
  });
819
1015
  }
820
- for (const match of handlerSource.matchAll(REGEX_HEADER_GET)) {
1016
+ const headerMatches = Array.from(handlerSource.matchAll(REGEX_PATTERNS.HEADER_GET));
1017
+ for (const match of headerMatches) {
821
1018
  if (match[1]) {
822
1019
  if (!inferredSpec.parameters) inferredSpec.parameters = [];
823
1020
  inferredSpec.parameters.push({
@@ -836,13 +1033,19 @@ function analyzeHandler(handler) {
836
1033
  }
837
1034
  if (handlerSource.includes("ctx.html(")) {
838
1035
  responses["200"] = {
839
- description: "Successful response",
1036
+ description: "Successful HTML response",
1037
+ content: { "text/html": { schema: { type: "string" } } }
1038
+ };
1039
+ }
1040
+ if (handlerSource.includes("ctx.jsx(")) {
1041
+ responses["200"] = {
1042
+ description: "Successful HTML response (Rendered JSX)",
840
1043
  content: { "text/html": { schema: { type: "string" } } }
841
1044
  };
842
1045
  }
843
1046
  if (handlerSource.includes("ctx.text(")) {
844
1047
  responses["200"] = {
845
- description: "Successful response",
1048
+ description: "Successful text response",
846
1049
  content: { "text/plain": { schema: { type: "string" } } }
847
1050
  };
848
1051
  }
@@ -853,7 +1056,18 @@ function analyzeHandler(handler) {
853
1056
  };
854
1057
  }
855
1058
  if (handlerSource.includes("ctx.redirect(")) {
856
- responses["302"] = { description: "Redirect" };
1059
+ let hasSpecificRedirect = false;
1060
+ const redirectMatches = Array.from(handlerSource.matchAll(/ctx\.redirect\([^,]+,\s*(\d{3})\)/g));
1061
+ for (const match of redirectMatches) {
1062
+ const status = match[1];
1063
+ if (/^30[12378]$/.test(status)) {
1064
+ responses[status] = { description: `Redirect (${status})` };
1065
+ hasSpecificRedirect = true;
1066
+ }
1067
+ }
1068
+ if (!hasSpecificRedirect) {
1069
+ responses["302"] = { description: "Redirect" };
1070
+ }
857
1071
  }
858
1072
  if (!responses["200"] && /return\s+\{/.test(handlerSource)) {
859
1073
  responses["200"] = {
@@ -861,7 +1075,8 @@ function analyzeHandler(handler) {
861
1075
  content: { "application/json": { schema: { type: "object" } } }
862
1076
  };
863
1077
  }
864
- for (const match of handlerSource.matchAll(REGEX_ERROR_STATUS)) {
1078
+ const errorStatusMatches = Array.from(handlerSource.matchAll(REGEX_PATTERNS.ERROR_STATUS));
1079
+ for (const match of errorStatusMatches) {
865
1080
  const statusCode = match[1];
866
1081
  if (statusCode && statusCode !== "200") {
867
1082
  responses[statusCode] = { description: `Error response (${statusCode})` };
@@ -872,6 +1087,52 @@ function analyzeHandler(handler) {
872
1087
  }
873
1088
  return { inferredSpec };
874
1089
  }
1090
+ async function getAstRoutes(applications) {
1091
+ const astRoutes = [];
1092
+ const getExpandedRoutes = (app, prefix = "", seen = /* @__PURE__ */ new Set()) => {
1093
+ if (seen.has(app.name)) return [];
1094
+ const newSeen = new Set(seen);
1095
+ newSeen.add(app.name);
1096
+ const expanded = [];
1097
+ for (const route of app.routes) {
1098
+ const cleanPrefix = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
1099
+ const cleanPath = route.path.startsWith("/") ? route.path : "/" + route.path;
1100
+ let joined = cleanPrefix + cleanPath;
1101
+ if (joined.length > 1 && joined.endsWith("/")) {
1102
+ joined = joined.slice(0, -1);
1103
+ }
1104
+ expanded.push({
1105
+ ...route,
1106
+ path: joined || "/"
1107
+ });
1108
+ }
1109
+ if (app.mounted) {
1110
+ for (const mount of app.mounted) {
1111
+ const targetApp = applications.find((a) => a.name === mount.target || a.className === mount.target);
1112
+ if (targetApp) {
1113
+ const cleanPrefix = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
1114
+ const mountPrefix = mount.prefix.startsWith("/") ? mount.prefix : "/" + mount.prefix;
1115
+ expanded.push(...getExpandedRoutes(targetApp, cleanPrefix + mountPrefix, newSeen));
1116
+ }
1117
+ }
1118
+ }
1119
+ return expanded;
1120
+ };
1121
+ applications.forEach((app) => {
1122
+ astRoutes.push(...getExpandedRoutes(app));
1123
+ });
1124
+ const dedupedRoutes = /* @__PURE__ */ new Map();
1125
+ for (const route of astRoutes) {
1126
+ const key = `${route.method.toUpperCase()}:${route.path}`;
1127
+ let score = 0;
1128
+ if (route.responseSchema) score += 10;
1129
+ if (route.handlerSource) score += 5;
1130
+ if (!dedupedRoutes.has(key) || score > dedupedRoutes.get(key).score) {
1131
+ dedupedRoutes.set(key, { route, score });
1132
+ }
1133
+ }
1134
+ return Array.from(dedupedRoutes.values()).map((v) => v.route);
1135
+ }
875
1136
  async function generateOpenApi(rootRouter, options = {}) {
876
1137
  const paths = {};
877
1138
  const tagGroups = /* @__PURE__ */ new Map();
@@ -879,61 +1140,11 @@ async function generateOpenApi(rootRouter, options = {}) {
879
1140
  const defaultTagName = options.defaultTag || "Application";
880
1141
  let astRoutes = [];
881
1142
  try {
882
- const { OpenAPIAnalyzer } = await Promise.resolve().then(() => require("./openapi-analyzer-z-7AoFRC.cjs"));
1143
+ const { OpenAPIAnalyzer } = await Promise.resolve().then(() => require("./openapi-analyzer-Bei1sVWp.cjs"));
883
1144
  const analyzer = new OpenAPIAnalyzer(process.cwd());
884
1145
  const { applications } = await analyzer.analyze();
885
- const appMap = /* @__PURE__ */ new Map();
886
- applications.forEach((app) => {
887
- appMap.set(app.name, app);
888
- if (app.name !== app.className) {
889
- appMap.set(app.className, app);
890
- }
891
- });
892
- const getExpandedRoutes = (app, prefix = "", seen = /* @__PURE__ */ new Set()) => {
893
- if (seen.has(app.name)) return [];
894
- const newSeen = new Set(seen);
895
- newSeen.add(app.name);
896
- const expanded = [];
897
- for (const route of app.routes) {
898
- const cleanPrefix = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
899
- const cleanPath = route.path.startsWith("/") ? route.path : "/" + route.path;
900
- let joined = cleanPrefix + cleanPath;
901
- if (joined.length > 1 && joined.endsWith("/")) {
902
- joined = joined.slice(0, -1);
903
- }
904
- expanded.push({
905
- ...route,
906
- path: joined || "/"
907
- });
908
- }
909
- if (app.mounted) {
910
- for (const mount of app.mounted) {
911
- const targetApp = appMap.get(mount.target);
912
- if (targetApp) {
913
- const cleanPrefix = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
914
- const mountPrefix = mount.prefix.startsWith("/") ? mount.prefix : "/" + mount.prefix;
915
- expanded.push(...getExpandedRoutes(targetApp, cleanPrefix + mountPrefix, newSeen));
916
- }
917
- }
918
- }
919
- return expanded;
920
- };
921
- applications.forEach((app) => {
922
- astRoutes.push(...getExpandedRoutes(app));
923
- });
924
- const dedupedRoutes = /* @__PURE__ */ new Map();
925
- for (const route of astRoutes) {
926
- const key = `${route.method.toUpperCase()}:${route.path}`;
927
- let score = 0;
928
- if (route.responseSchema) score += 10;
929
- if (route.handlerSource) score += 5;
930
- if (!dedupedRoutes.has(key) || score > dedupedRoutes.get(key).score) {
931
- dedupedRoutes.set(key, { route, score });
932
- }
933
- }
934
- astRoutes = Array.from(dedupedRoutes.values()).map((v) => v.route);
1146
+ astRoutes = await getAstRoutes(applications);
935
1147
  } catch (e) {
936
- console.warn("OpenAPI AST analysis failed or skipped:", e);
937
1148
  }
938
1149
  const collect = (router, prefix = "", currentGroup = defaultTagGroup, defaultTag = defaultTagName) => {
939
1150
  let group = currentGroup;
@@ -990,33 +1201,15 @@ async function generateOpenApi(rootRouter, options = {}) {
990
1201
  (r) => r.method.toUpperCase() === route.method.toUpperCase() && r.path === fullPath
991
1202
  );
992
1203
  if (!astMatch) {
993
- let runtimeSource = route.handler.toString();
994
- if (route.handler.originalHandler) {
995
- runtimeSource = route.handler.originalHandler.toString();
996
- }
1204
+ const runtimeSource = (route.handler.originalHandler || route.handler).toString();
997
1205
  const runtimeHandlerSrc = runtimeSource.replace(/\s+/g, " ");
998
1206
  const sameMethodRoutes = astRoutes.filter((r) => r.method.toUpperCase() === route.method.toUpperCase());
999
1207
  astMatch = sameMethodRoutes.find((r) => {
1000
1208
  const astHandlerSrc = (r.handlerSource || r.handlerName || "").replace(/\s+/g, " ");
1001
1209
  if (!astHandlerSrc || astHandlerSrc.length < 20) return false;
1002
- const match = runtimeHandlerSrc.includes(astHandlerSrc) || astHandlerSrc.includes(runtimeHandlerSrc) || r.handlerSource && runtimeHandlerSrc.includes(r.handlerSource.substring(0, 50));
1003
- return match;
1210
+ return runtimeHandlerSrc.includes(astHandlerSrc) || astHandlerSrc.includes(runtimeHandlerSrc) || r.handlerSource && runtimeHandlerSrc.includes(r.handlerSource.substring(0, 50));
1004
1211
  });
1005
1212
  }
1006
- const potentialMatches = astRoutes.filter(
1007
- (r) => r.method.toUpperCase() === route.method.toUpperCase() && r.path === fullPath
1008
- );
1009
- if (potentialMatches.length > 1) {
1010
- const runtimeHandlerSrc = route.handler.toString().replace(/\s+/g, " ");
1011
- const preciseMatch = potentialMatches.find((r) => {
1012
- const astHandlerSrc = (r.handlerSource || r.handlerName || "").replace(/\s+/g, " ");
1013
- const match = runtimeHandlerSrc.includes(astHandlerSrc) || astHandlerSrc.includes(runtimeHandlerSrc) || r.handlerSource && runtimeHandlerSrc.includes(r.handlerSource.substring(0, 50));
1014
- return match;
1015
- });
1016
- if (preciseMatch) {
1017
- astMatch = preciseMatch;
1018
- }
1019
- }
1020
1213
  if (astMatch) {
1021
1214
  if (astMatch.summary) operation.summary = astMatch.summary;
1022
1215
  if (astMatch.description) operation.description = astMatch.description;
@@ -1024,25 +1217,19 @@ async function generateOpenApi(rootRouter, options = {}) {
1024
1217
  if (astMatch.operationId) operation.operationId = astMatch.operationId;
1025
1218
  if (astMatch.requestTypes?.body) {
1026
1219
  operation.requestBody = {
1027
- content: {
1028
- "application/json": { schema: astMatch.requestTypes.body }
1029
- }
1220
+ content: { "application/json": { schema: astMatch.requestTypes.body } }
1030
1221
  };
1031
1222
  }
1032
1223
  if (astMatch.responseSchema) {
1033
1224
  operation.responses["200"] = {
1034
1225
  description: "Successful response",
1035
- content: {
1036
- "application/json": { schema: astMatch.responseSchema }
1037
- }
1226
+ content: { "application/json": { schema: astMatch.responseSchema } }
1038
1227
  };
1039
1228
  } else if (astMatch.responseType) {
1040
1229
  const contentType = astMatch.responseType === "string" ? "text/plain" : "application/json";
1041
1230
  operation.responses["200"] = {
1042
1231
  description: "Successful response",
1043
- content: {
1044
- [contentType]: { schema: { type: astMatch.responseType } }
1045
- }
1232
+ content: { [contentType]: { schema: { type: astMatch.responseType } } }
1046
1233
  };
1047
1234
  }
1048
1235
  const params = [];
@@ -1093,15 +1280,7 @@ async function generateOpenApi(rootRouter, options = {}) {
1093
1280
  deepMerge(operation, inferredSpec);
1094
1281
  }
1095
1282
  if (route.handlerSpec) {
1096
- const spec = route.handlerSpec;
1097
- if (spec.summary) operation.summary = spec.summary;
1098
- if (spec.description) operation.description = spec.description;
1099
- if (spec.operationId) operation.operationId = spec.operationId;
1100
- if (spec.tags) operation.tags = spec.tags;
1101
- if (spec.security) operation.security = spec.security;
1102
- if (spec.responses) {
1103
- operation.responses = { ...operation.responses, ...spec.responses };
1104
- }
1283
+ deepMerge(operation, route.handlerSpec);
1105
1284
  }
1106
1285
  if (!operation.tags || operation.tags.length === 0) operation.tags = [tag];
1107
1286
  if (operation.tags) {
@@ -1120,11 +1299,13 @@ async function generateOpenApi(rootRouter, options = {}) {
1120
1299
  paths[fullPath][methodLower] = operation;
1121
1300
  }
1122
1301
  }
1123
- for (const controller of router[$childControllers]) {
1302
+ const controllers = router[$childControllers];
1303
+ for (const controller of controllers) {
1124
1304
  const controllerName = controller.constructor.name || "UnknownController";
1125
1305
  tagGroups.get(group)?.add(controllerName);
1126
1306
  }
1127
- for (const child of router[$childRouters]) {
1307
+ const childRouters = router[$childRouters];
1308
+ for (const child of childRouters) {
1128
1309
  const mountPath = child[$mountPath];
1129
1310
  const cleanPrefix = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
1130
1311
  const cleanMount = mountPath.startsWith("/") ? mountPath : "/" + mountPath;
@@ -1134,7 +1315,7 @@ async function generateOpenApi(rootRouter, options = {}) {
1134
1315
  };
1135
1316
  collect(rootRouter);
1136
1317
  const xTagGroups = [];
1137
- for (const [name, tags] of tagGroups) {
1318
+ for (const [name, tags] of tagGroups.entries()) {
1138
1319
  xTagGroups.push({ name, tags: Array.from(tags).sort() });
1139
1320
  }
1140
1321
  return {
@@ -1169,7 +1350,8 @@ function serveStatic(config, prefix) {
1169
1350
  if (res) return res;
1170
1351
  }
1171
1352
  if (config.exclude) {
1172
- for (const pattern of config.exclude) {
1353
+ for (let i = 0; i < config.exclude.length; i++) {
1354
+ const pattern = config.exclude[i];
1173
1355
  if (pattern instanceof RegExp) {
1174
1356
  if (pattern.test(relative)) return ctx.json({ error: "Forbidden" }, 403);
1175
1357
  } else if (typeof pattern === "string") {
@@ -1188,7 +1370,8 @@ function serveStatic(config, prefix) {
1188
1370
  stats = await promises$1.stat(requestPath);
1189
1371
  } catch (e) {
1190
1372
  if (config.extensions) {
1191
- for (const ext of config.extensions) {
1373
+ for (let i = 0; i < config.extensions.length; i++) {
1374
+ const ext = config.extensions[i];
1192
1375
  const p = requestPath + (ext.startsWith(".") ? ext : "." + ext);
1193
1376
  try {
1194
1377
  const s = await promises$1.stat(p);
@@ -1217,7 +1400,8 @@ function serveStatic(config, prefix) {
1217
1400
  indexes = [config.index];
1218
1401
  }
1219
1402
  let foundIndex = false;
1220
- for (const idx of indexes) {
1403
+ for (let i = 0; i < indexes.length; i++) {
1404
+ const idx = indexes[i];
1221
1405
  const idxPath = path.join(finalPath, idx);
1222
1406
  try {
1223
1407
  const idxStats = await promises$1.stat(idxPath);
@@ -1297,38 +1481,39 @@ class RouterTrie {
1297
1481
  };
1298
1482
  }
1299
1483
  insert(method, path2, handler) {
1300
- let node2 = this.root;
1484
+ let node = this.root;
1301
1485
  const segments = this.splitPath(path2);
1302
- for (const segment of segments) {
1486
+ for (let i = 0; i < segments.length; i++) {
1487
+ const segment = segments[i];
1303
1488
  if (segment === "**") {
1304
- if (!node2.recursiveChild) {
1305
- node2.recursiveChild = this.createNode();
1489
+ if (!node.recursiveChild) {
1490
+ node.recursiveChild = this.createNode();
1306
1491
  }
1307
- node2 = node2.recursiveChild;
1492
+ node = node.recursiveChild;
1308
1493
  } else if (segment === "*") {
1309
- if (!node2.wildcardChild) {
1310
- node2.wildcardChild = this.createNode();
1494
+ if (!node.wildcardChild) {
1495
+ node.wildcardChild = this.createNode();
1311
1496
  }
1312
- node2 = node2.wildcardChild;
1497
+ node = node.wildcardChild;
1313
1498
  } else if (segment.startsWith(":")) {
1314
1499
  const paramName = segment.slice(1);
1315
- if (!node2.paramChild) {
1316
- node2.paramChild = this.createNode();
1317
- node2.paramChild.paramName = paramName;
1500
+ if (!node.paramChild) {
1501
+ node.paramChild = this.createNode();
1502
+ node.paramChild.paramName = paramName;
1318
1503
  }
1319
- node2 = node2.paramChild;
1320
- node2.paramName = paramName;
1504
+ node = node.paramChild;
1505
+ node.paramName = paramName;
1321
1506
  } else {
1322
- if (!node2.children[segment]) {
1323
- node2.children[segment] = this.createNode();
1507
+ if (!node.children[segment]) {
1508
+ node.children[segment] = this.createNode();
1324
1509
  }
1325
- node2 = node2.children[segment];
1510
+ node = node.children[segment];
1326
1511
  }
1327
1512
  }
1328
- if (!node2.handlers) {
1329
- node2.handlers = {};
1513
+ if (!node.handlers) {
1514
+ node.handlers = {};
1330
1515
  }
1331
- node2.handlers[method] = handler;
1516
+ node.handlers[method] = handler;
1332
1517
  }
1333
1518
  search(method, path2) {
1334
1519
  const segments = this.splitPath(path2);
@@ -1345,34 +1530,34 @@ class RouterTrie {
1345
1530
  }
1346
1531
  return null;
1347
1532
  }
1348
- findNode(node2, segments, index, params) {
1533
+ findNode(node, segments, index, params) {
1349
1534
  if (index === segments.length) {
1350
- if (node2.handlers) return node2;
1351
- if (node2.recursiveChild && node2.recursiveChild.handlers) {
1352
- return node2.recursiveChild;
1535
+ if (node.handlers) return node;
1536
+ if (node.recursiveChild && node.recursiveChild.handlers) {
1537
+ return node.recursiveChild;
1353
1538
  }
1354
1539
  return null;
1355
1540
  }
1356
1541
  const segment = segments[index];
1357
- const child = node2.children[segment];
1542
+ const child = node.children[segment];
1358
1543
  if (child) {
1359
1544
  const result = this.findNode(child, segments, index + 1, params);
1360
1545
  if (result) return result;
1361
1546
  }
1362
- if (node2.paramChild) {
1363
- params[node2.paramChild.paramName] = segment;
1364
- const result = this.findNode(node2.paramChild, segments, index + 1, params);
1547
+ if (node.paramChild) {
1548
+ params[node.paramChild.paramName] = segment;
1549
+ const result = this.findNode(node.paramChild, segments, index + 1, params);
1365
1550
  if (result) return result;
1366
- delete params[node2.paramChild.paramName];
1551
+ delete params[node.paramChild.paramName];
1367
1552
  }
1368
- if (node2.wildcardChild) {
1369
- const result = this.findNode(node2.wildcardChild, segments, index + 1, params);
1553
+ if (node.wildcardChild) {
1554
+ const result = this.findNode(node.wildcardChild, segments, index + 1, params);
1370
1555
  if (result) return result;
1371
1556
  }
1372
- if (node2.recursiveChild) {
1557
+ if (node.recursiveChild) {
1373
1558
  const remaining = segments.length - index;
1374
1559
  for (let k = 0; k <= remaining; k++) {
1375
- const result = this.findNode(node2.recursiveChild, segments, index + k, params);
1560
+ const result = this.findNode(node.recursiveChild, segments, index + k, params);
1376
1561
  if (result) return result;
1377
1562
  }
1378
1563
  }
@@ -1386,40 +1571,68 @@ class RouterTrie {
1386
1571
  }
1387
1572
  }
1388
1573
  const asyncContext = new node_async_hooks.AsyncLocalStorage();
1389
- const engine = process.env["SHOKUPAN_DB_ENGINE"] === "memory" ? "mem://" : "rocksdb://database";
1390
- const db = new surrealdb.Surreal({
1391
- engines: node.createNodeEngines()
1392
- });
1393
- const ready = db.connect(engine, { namespace: "vendor", database: "shokupan" }).then(() => {
1394
- return db.query(`
1395
- DEFINE TABLE OVERWRITE failed_requests SCHEMALESS COMMENT "Created by Shokupan";
1396
- DEFINE TABLE OVERWRITE sessions SCHEMALESS COMMENT "Created by Shokupan";
1397
- DEFINE TABLE OVERWRITE users SCHEMALESS COMMENT "Created by Shokupan";
1398
- DEFINE TABLE OVERWRITE idempotency_keys SCHEMALESS COMMENT "Created by Shokupan";
1399
- DEFINE TABLE OVERWRITE middleware_tracking SCHEMALESS COMMENT "Created by Shokupan";
1400
- DEFINE TABLE OVERWRITE requests SCHEMALESS COMMENT "Created by Shokupan";
1401
- `);
1402
- });
1574
+ let db;
1575
+ let dbPromise = null;
1576
+ let RecordId;
1577
+ async function ensureDb() {
1578
+ if (db) return db;
1579
+ if (dbPromise) return dbPromise;
1580
+ dbPromise = (async () => {
1581
+ try {
1582
+ const { createNodeEngines } = await import("@surrealdb/node");
1583
+ const surreal = await import("surrealdb");
1584
+ const Surreal = surreal.Surreal;
1585
+ RecordId = surreal.RecordId;
1586
+ const engine = process.env["SHOKUPAN_DB_ENGINE"] === "memory" ? "mem://" : "rocksdb://database";
1587
+ const _db = new Surreal({
1588
+ engines: createNodeEngines()
1589
+ });
1590
+ await _db.connect(engine, { namespace: "vendor", database: "shokupan" });
1591
+ await _db.query(`
1592
+ DEFINE TABLE OVERWRITE failed_requests SCHEMALESS COMMENT "Created by Shokupan";
1593
+ DEFINE TABLE OVERWRITE sessions SCHEMALESS COMMENT "Created by Shokupan";
1594
+ DEFINE TABLE OVERWRITE users SCHEMALESS COMMENT "Created by Shokupan";
1595
+ DEFINE TABLE OVERWRITE idempotency_keys SCHEMALESS COMMENT "Created by Shokupan";
1596
+ DEFINE TABLE OVERWRITE middleware_tracking SCHEMALESS COMMENT "Created by Shokupan";
1597
+ DEFINE TABLE OVERWRITE requests SCHEMALESS COMMENT "Created by Shokupan";
1598
+ `);
1599
+ db = _db;
1600
+ return db;
1601
+ } catch (e) {
1602
+ dbPromise = null;
1603
+ if (e.code === "ERR_MODULE_NOT_FOUND" || e.message.includes("Cannot find module")) {
1604
+ throw new Error("SurrealDB dependencies not found. To use the datastore, please install 'surrealdb' and '@surrealdb/node'.");
1605
+ }
1606
+ throw e;
1607
+ }
1608
+ })();
1609
+ return dbPromise;
1610
+ }
1403
1611
  const datastore = {
1404
- get(store, key) {
1405
- return db.select(new surrealdb.RecordId(store, key));
1612
+ async get(store, key) {
1613
+ await ensureDb();
1614
+ return db.select(new RecordId(store, key));
1406
1615
  },
1407
- set(store, key, value) {
1408
- return db.create(new surrealdb.RecordId(store, key)).content(value);
1616
+ async set(store, key, value) {
1617
+ await ensureDb();
1618
+ return db.create(new RecordId(store, key)).content(value);
1409
1619
  },
1410
1620
  async query(query, vars) {
1621
+ await ensureDb();
1411
1622
  try {
1412
- const r = await db.query(query, vars).collect();
1413
- return r;
1623
+ const r = await db.query(query, vars);
1624
+ return Array.isArray(r) ? r : r?.collect ? await r.collect() : r;
1414
1625
  } catch (e) {
1415
1626
  console.error("DS ERROR:", e);
1416
1627
  throw e;
1417
1628
  }
1418
1629
  },
1419
- ready
1630
+ get ready() {
1631
+ return ensureDb().then(() => void 0);
1632
+ }
1420
1633
  };
1421
1634
  process.on("exit", async () => {
1422
- await db.close();
1635
+ if (db) await db.close();
1423
1636
  });
1424
1637
  const tracer = api.trace.getTracer("shokupan.middleware");
1425
1638
  function traceHandler(fn, name) {
@@ -1492,6 +1705,8 @@ class ShokupanRouter {
1492
1705
  [$parent] = null;
1493
1706
  [$childRouters] = [];
1494
1707
  [$childControllers] = [];
1708
+ hookCache = /* @__PURE__ */ new Map();
1709
+ hooksInitialized = false;
1495
1710
  middleware = [];
1496
1711
  get rootConfig() {
1497
1712
  return this[$appRoot]?.applicationConfig;
@@ -1509,7 +1724,8 @@ class ShokupanRouter {
1509
1724
  getComponentRegistry() {
1510
1725
  const controllerRoutesMap = /* @__PURE__ */ new Map();
1511
1726
  const localRoutes = [];
1512
- for (const r of this[$routes]) {
1727
+ for (let i = 0; i < this[$routes].length; i++) {
1728
+ const r = this[$routes][i];
1513
1729
  const entry = {
1514
1730
  type: "route",
1515
1731
  path: r.path,
@@ -1646,7 +1862,8 @@ class ShokupanRouter {
1646
1862
  const decoratedArgs = instance[$routeArgs] || proto && proto[$routeArgs];
1647
1863
  const methodMiddlewareMap = instance[$middleware] || proto && proto[$middleware];
1648
1864
  let routesAttached = 0;
1649
- for (const name of Array.from(methods)) {
1865
+ for (let i = 0; i < Array.from(methods).length; i++) {
1866
+ const name = Array.from(methods)[i];
1650
1867
  if (name === "constructor") continue;
1651
1868
  if (["arguments", "caller", "callee"].includes(name)) continue;
1652
1869
  const originalHandler = instance[name];
@@ -1658,7 +1875,8 @@ class ShokupanRouter {
1658
1875
  method = config.method;
1659
1876
  subPath = config.path;
1660
1877
  } else {
1661
- for (const m of HTTPMethods) {
1878
+ for (let j = 0; j < HTTPMethods.length; j++) {
1879
+ const m = HTTPMethods[j];
1662
1880
  if (name.toUpperCase().startsWith(m)) {
1663
1881
  method = m;
1664
1882
  const rest = name.slice(m.length);
@@ -1673,8 +1891,8 @@ class ShokupanRouter {
1673
1891
  buffer = "";
1674
1892
  }
1675
1893
  };
1676
- for (let i = 0; i < rest.length; i++) {
1677
- const char = rest[i];
1894
+ for (let i2 = 0; i2 < rest.length; i2++) {
1895
+ const char = rest[i2];
1678
1896
  if (char === "$") {
1679
1897
  flush();
1680
1898
  subPath += "/:";
@@ -1712,7 +1930,8 @@ class ShokupanRouter {
1712
1930
  if (routeArgs?.length > 0) {
1713
1931
  args = [];
1714
1932
  const sortedArgs = [...routeArgs].sort((a, b) => a.index - b.index);
1715
- for (const arg of sortedArgs) {
1933
+ for (let k = 0; k < sortedArgs.length; k++) {
1934
+ const arg = sortedArgs[k];
1716
1935
  switch (arg.type) {
1717
1936
  case RouteParamType.BODY:
1718
1937
  try {
@@ -1742,7 +1961,9 @@ class ShokupanRouter {
1742
1961
  args[arg.index] = vals.length > 1 ? vals : vals[0];
1743
1962
  } else {
1744
1963
  const query = {};
1745
- for (const key of url.searchParams.keys()) {
1964
+ const keys = Object.keys(url.searchParams);
1965
+ for (let k2 = 0; k2 < keys.length; k2++) {
1966
+ const key = keys[k2];
1746
1967
  const vals = url.searchParams.getAll(key);
1747
1968
  query[key] = vals.length > 1 ? vals : vals[0];
1748
1969
  }
@@ -1799,9 +2020,11 @@ class ShokupanRouter {
1799
2020
  path: r.path,
1800
2021
  handler: r.handler
1801
2022
  }));
1802
- for (const child of this[$childRouters]) {
2023
+ for (let i = 0; i < this[$childRouters].length; i++) {
2024
+ const child = this[$childRouters][i];
1803
2025
  const childRoutes = child.getRoutes();
1804
- for (const route of childRoutes) {
2026
+ for (let j = 0; j < childRoutes.length; j++) {
2027
+ const route = childRoutes[j];
1805
2028
  const cleanPrefix = child[$mountPath].endsWith("/") ? child[$mountPath].slice(0, -1) : child[$mountPath];
1806
2029
  const cleanPath = route.path.startsWith("/") ? route.path : "/" + route.path;
1807
2030
  const fullPath = cleanPrefix + cleanPath || "/";
@@ -1815,12 +2038,12 @@ class ShokupanRouter {
1815
2038
  return routes;
1816
2039
  }
1817
2040
  /**
1818
- * Makes a sub request to this router.
1819
- * This is useful for triggering other methods or route handlers.
2041
+ * Makes an internal request through this router's full routing pipeline.
2042
+ * This is useful for calling other routes internally and supports streaming responses.
1820
2043
  * @param options The request options.
1821
- * @returns The response.
2044
+ * @returns The raw Response object.
1822
2045
  */
1823
- async subRequest(arg) {
2046
+ async internalRequest(arg) {
1824
2047
  const options = typeof arg === "string" ? { path: arg } : arg;
1825
2048
  const store = asyncContext.getStore();
1826
2049
  store?.get("req");
@@ -1839,9 +2062,10 @@ class ShokupanRouter {
1839
2062
  return this.root[$dispatch](req);
1840
2063
  }
1841
2064
  /**
1842
- * Processes a request directly.
2065
+ * Processes a request for testing purposes.
2066
+ * Returns a simplified { status, headers, data } object instead of a Response.
1843
2067
  */
1844
- async processRequest(options) {
2068
+ async testRequest(options) {
1845
2069
  let url = options.url || options.path || "/";
1846
2070
  if (!url.startsWith("http")) {
1847
2071
  const base = `http://${this.rootConfig?.hostname || "localhost"}:${this.rootConfig?.port || 3e3}`;
@@ -1850,7 +2074,9 @@ class ShokupanRouter {
1850
2074
  }
1851
2075
  if (options.query) {
1852
2076
  const u = new URL(url);
1853
- for (const [k, v] of Object.entries(options.query)) {
2077
+ const entries = Object.entries(options.query);
2078
+ for (let i = 0; i < entries.length; i++) {
2079
+ const [k, v] = entries[i];
1854
2080
  u.searchParams.set(k, v);
1855
2081
  }
1856
2082
  url = u.toString();
@@ -1895,28 +2121,17 @@ class ShokupanRouter {
1895
2121
  data: result
1896
2122
  };
1897
2123
  }
1898
- applyRouterHooks(match) {
1899
- if (!this.config?.hooks) return match;
1900
- const hooks = this.config.hooks;
1901
- return {
1902
- ...match,
1903
- handler: this.wrapWithHooks(match.handler, hooks)
1904
- };
1905
- }
1906
- wrapWithHooks(handler, hooks) {
1907
- const hookList = Array.isArray(hooks) ? hooks : [hooks];
1908
- const hasStart = hookList.some((h) => !!h.onRequestStart);
1909
- const hasEnd = hookList.some((h) => !!h.onRequestEnd);
1910
- const hasError = hookList.some((h) => !!h.onError);
2124
+ wrapWithHooks(handler) {
2125
+ if (!this.hooksInitialized) {
2126
+ this.ensureHooksInitialized();
2127
+ }
2128
+ const hasStart = this.hookCache.get("onRequestStart")?.length > 0;
2129
+ const hasEnd = this.hookCache.get("onRequestEnd")?.length > 0;
2130
+ const hasError = this.hookCache.get("onError")?.length > 0;
1911
2131
  if (!hasStart && !hasEnd && !hasError) return handler;
1912
2132
  const originalHandler = handler;
1913
2133
  const wrapped = async (ctx) => {
1914
- if (hasStart) {
1915
- for (let i = 0; i < hookList.length; i++) {
1916
- const h = hookList[i];
1917
- if (typeof h.onRequestStart === "function") await h.onRequestStart(ctx);
1918
- }
1919
- }
2134
+ await this.runHooks("onRequestStart", ctx);
1920
2135
  const debug = ctx._debug;
1921
2136
  let debugId;
1922
2137
  let previousNode;
@@ -1930,17 +2145,11 @@ class ShokupanRouter {
1930
2145
  try {
1931
2146
  const res = await originalHandler(ctx);
1932
2147
  debug?.trackStep(debugId, "handler", performance.now() - start, "success");
1933
- for (let i = 0; i < hookList.length; i++) {
1934
- const h = hookList[i];
1935
- if (typeof h.onRequestEnd === "function") await h.onRequestEnd(ctx);
1936
- }
2148
+ await this.runHooks("onRequestEnd", ctx);
1937
2149
  return res;
1938
2150
  } catch (err) {
1939
2151
  debug?.trackStep(debugId, "handler", performance.now() - start, "error", err);
1940
- for (let i = 0; i < hookList.length; i++) {
1941
- const h = hookList[i];
1942
- if (typeof h.onError === "function") await h.onError(err, ctx);
1943
- }
2152
+ await this.runHooks("onError", ctx, err);
1944
2153
  throw err;
1945
2154
  } finally {
1946
2155
  if (debug && previousNode) debug.setNode(previousNode);
@@ -1962,18 +2171,19 @@ class ShokupanRouter {
1962
2171
  result = this.trie.search("GET", path2);
1963
2172
  if (result) return result;
1964
2173
  }
1965
- for (const child of this[$childRouters]) {
2174
+ for (let i = 0; i < this[$childRouters].length; i++) {
2175
+ const child = this[$childRouters][i];
1966
2176
  const prefix = child[$mountPath];
1967
2177
  if (path2 === prefix || path2.startsWith(prefix + "/")) {
1968
2178
  const subPath = path2.slice(prefix.length) || "/";
1969
2179
  const match = child.find(method, subPath);
1970
- if (match) return this.applyRouterHooks(match);
2180
+ if (match) return match;
1971
2181
  }
1972
2182
  if (prefix.endsWith("/")) {
1973
2183
  if (path2.startsWith(prefix)) {
1974
2184
  const subPath = path2.slice(prefix.length) || "/";
1975
2185
  const match = child.find(method, subPath);
1976
- if (match) return this.applyRouterHooks(match);
2186
+ if (match) return match;
1977
2187
  }
1978
2188
  }
1979
2189
  }
@@ -1995,17 +2205,23 @@ class ShokupanRouter {
1995
2205
  /**
1996
2206
  * Adds a route to the router.
1997
2207
  *
1998
- * @param method - HTTP method
1999
- * @param path - URL path
2000
- * @param spec - OpenAPI specification for the route
2001
- * @param handler - Route handler function
2002
- * @param requestTimeout - Timeout for this route in milliseconds
2208
+ * @param arg - Route configuration object
2209
+ * @param arg.method - HTTP method
2210
+ * @param arg.path - URL path
2211
+ * @param arg.spec - OpenAPI specification for the route
2212
+ * @param arg.handler - Route handler function
2213
+ * @param arg.regex - Custom regex for path matching
2214
+ * @param arg.group - Group for the route
2215
+ * @param arg.requestTimeout - Timeout for this route in milliseconds
2216
+ * @param arg.renderer - JSX renderer for the route
2217
+ * @param arg.controller - Controller for the route
2003
2218
  */
2004
2219
  add({ method, path: path2, spec, handler, regex: customRegex, group, requestTimeout, renderer, controller }) {
2005
2220
  const { regex, keys } = customRegex ? { regex: customRegex, keys: [] } : this.parsePath(path2);
2006
2221
  if (this.currentGuards.length > 0) {
2007
2222
  spec = spec || {};
2008
- for (const guard of this.currentGuards) {
2223
+ for (let i = 0; i < this.currentGuards.length; i++) {
2224
+ const guard = this.currentGuards[i];
2009
2225
  if (guard.spec) {
2010
2226
  if (guard.spec.responses) {
2011
2227
  spec.responses = spec.responses || {};
@@ -2034,7 +2250,8 @@ class ShokupanRouter {
2034
2250
  if (routeGuards.length > 0) {
2035
2251
  const innerHandler = wrappedHandler;
2036
2252
  wrappedHandler = async (ctx) => {
2037
- for (const guard of routeGuards) {
2253
+ for (let i = 0; i < routeGuards.length; i++) {
2254
+ const guard = routeGuards[i];
2038
2255
  let guardPassed = false;
2039
2256
  let nextCalled = false;
2040
2257
  const next = () => {
@@ -2128,7 +2345,7 @@ class ShokupanRouter {
2128
2345
  wrappedHandler.originalHandler = trackingHandler.originalHandler || trackingHandler;
2129
2346
  let bakedHandler = wrappedHandler;
2130
2347
  if (this.config?.hooks) {
2131
- bakedHandler = this.wrapWithHooks(wrappedHandler, this.config.hooks);
2348
+ bakedHandler = this.wrapWithHooks(wrappedHandler);
2132
2349
  }
2133
2350
  this[$routes].push({
2134
2351
  method,
@@ -2285,6 +2502,67 @@ class ShokupanRouter {
2285
2502
  generateApiSpec(options = {}) {
2286
2503
  return generateOpenApi(this, options);
2287
2504
  }
2505
+ ensureHooksInitialized() {
2506
+ const hooks = this.config?.hooks;
2507
+ if (hooks) {
2508
+ const hookList = Array.isArray(hooks) ? hooks : [hooks];
2509
+ const hookTypes = [
2510
+ "onRequestStart",
2511
+ "onRequestEnd",
2512
+ "onResponseStart",
2513
+ "onResponseEnd",
2514
+ "onError",
2515
+ "beforeValidate",
2516
+ "afterValidate",
2517
+ "onRequestTimeout",
2518
+ "onReadTimeout",
2519
+ "onWriteTimeout"
2520
+ ];
2521
+ for (let i = 0; i < hookTypes.length; i++) {
2522
+ const type = hookTypes[i];
2523
+ const fns = [];
2524
+ for (let j = 0; j < hookList.length; j++) {
2525
+ const h = hookList[j];
2526
+ if (h[type]) fns.push(h[type]);
2527
+ }
2528
+ if (fns.length > 0) {
2529
+ this.hookCache.set(type, fns);
2530
+ }
2531
+ }
2532
+ }
2533
+ this.hooksInitialized = true;
2534
+ }
2535
+ async runHooks(name, ...args) {
2536
+ if (!this.hooksInitialized) {
2537
+ this.ensureHooksInitialized();
2538
+ }
2539
+ const fns = this.hookCache.get(name);
2540
+ if (!fns) return;
2541
+ const ctx = args?.[0] instanceof ShokupanContext ? args[0] : void 0;
2542
+ const debug = ctx?._debug;
2543
+ if (debug) {
2544
+ await Promise.all(fns.map(async (fn, index) => {
2545
+ const hookId = `hook_${name}_${fn.name || index}`;
2546
+ const previousNode = debug.getCurrentNode();
2547
+ debug.trackEdge(previousNode, hookId);
2548
+ debug.setNode(hookId);
2549
+ const start = performance.now();
2550
+ try {
2551
+ await fn(...args);
2552
+ const duration = performance.now() - start;
2553
+ debug.trackStep(hookId, "hook", duration, "success");
2554
+ } catch (error) {
2555
+ const duration = performance.now() - start;
2556
+ debug.trackStep(hookId, "hook", duration, "error", error);
2557
+ throw error;
2558
+ } finally {
2559
+ if (previousNode) debug.setNode(previousNode);
2560
+ }
2561
+ }));
2562
+ } else {
2563
+ await Promise.all(fns.map((fn) => fn(...args)));
2564
+ }
2565
+ }
2288
2566
  }
2289
2567
  class SystemCpuMonitor {
2290
2568
  constructor(intervalMs = 1e3) {
@@ -2342,15 +2620,13 @@ class Shokupan extends ShokupanRouter {
2342
2620
  openApiSpec;
2343
2621
  composedMiddleware;
2344
2622
  cpuMonitor;
2345
- hookCache = /* @__PURE__ */ new Map();
2346
- hooksInitialized = false;
2347
2623
  get logger() {
2348
2624
  return this.applicationConfig.logger;
2349
2625
  }
2350
2626
  constructor(applicationConfig = {}) {
2351
2627
  const config = Object.assign({}, defaults, applicationConfig);
2352
2628
  const { hooks, ...routerConfig } = config;
2353
- super(routerConfig);
2629
+ super({ ...routerConfig, hooks });
2354
2630
  this[$isApplication] = true;
2355
2631
  this[$appRoot] = this;
2356
2632
  this.applicationConfig = config;
@@ -2365,7 +2641,6 @@ class Shokupan extends ShokupanRouter {
2365
2641
  * Adds middleware to the application.
2366
2642
  */
2367
2643
  use(middleware) {
2368
- let trackedMiddleware = middleware;
2369
2644
  const { file, line } = getCallerInfo();
2370
2645
  if (!middleware.metadata) {
2371
2646
  middleware.metadata = {
@@ -2376,32 +2651,36 @@ class Shokupan extends ShokupanRouter {
2376
2651
  pluginName: middleware.pluginName
2377
2652
  };
2378
2653
  }
2379
- trackedMiddleware = async (ctx, next) => {
2380
- const c = ctx;
2381
- if (c.handlerStack && c.app?.applicationConfig.enableMiddlewareTracking) {
2382
- const metadata = middleware.metadata || {};
2383
- const start = performance.now();
2384
- const item = {
2385
- name: metadata.pluginName ? `${metadata.pluginName} (${metadata.name})` : metadata.name || middleware.name || "middleware",
2386
- file: metadata.file || file,
2387
- line: metadata.line || line,
2388
- isBuiltin: metadata.isBuiltin,
2389
- startTime: start,
2390
- duration: -1
2391
- };
2392
- c.handlerStack.push(item);
2393
- try {
2394
- return await middleware(ctx, next);
2395
- } finally {
2396
- item.duration = performance.now() - start;
2654
+ if (this.applicationConfig.enableMiddlewareTracking) {
2655
+ const trackedMiddleware = async (ctx, next) => {
2656
+ const c = ctx;
2657
+ if (c.handlerStack && c.app?.applicationConfig.enableMiddlewareTracking) {
2658
+ const metadata = middleware.metadata || {};
2659
+ const start = performance.now();
2660
+ const item = {
2661
+ name: metadata.pluginName ? `${metadata.pluginName} (${metadata.name})` : metadata.name || middleware.name || "middleware",
2662
+ file: metadata.file || file,
2663
+ line: metadata.line || line,
2664
+ isBuiltin: metadata.isBuiltin,
2665
+ startTime: start,
2666
+ duration: -1
2667
+ };
2668
+ c.handlerStack.push(item);
2669
+ try {
2670
+ return await middleware(ctx, next);
2671
+ } finally {
2672
+ item.duration = performance.now() - start;
2673
+ }
2397
2674
  }
2398
- }
2399
- return middleware(ctx, next);
2400
- };
2401
- trackedMiddleware.metadata = middleware.metadata;
2402
- Object.defineProperty(trackedMiddleware, "name", { value: middleware.name || "middleware" });
2403
- trackedMiddleware.order = this.middleware.length;
2404
- this.middleware.push(trackedMiddleware);
2675
+ return middleware(ctx, next);
2676
+ };
2677
+ trackedMiddleware.metadata = middleware.metadata;
2678
+ Object.defineProperty(trackedMiddleware, "name", { value: middleware.name || "middleware" });
2679
+ trackedMiddleware.order = this.middleware.length;
2680
+ this.middleware.push(trackedMiddleware);
2681
+ } else {
2682
+ this.middleware.push(middleware);
2683
+ }
2405
2684
  return this;
2406
2685
  }
2407
2686
  startupHooks = [];
@@ -2432,17 +2711,13 @@ class Shokupan extends ShokupanRouter {
2432
2711
  if (finalPort < 0 || finalPort > 65535) {
2433
2712
  throw new Error("Invalid port number");
2434
2713
  }
2435
- for (const hook of this.startupHooks) {
2436
- await hook();
2437
- }
2714
+ await Promise.all(this.startupHooks.map((hook) => hook()));
2438
2715
  if (this.applicationConfig.enableOpenApiGen) {
2439
2716
  this.openApiSpec = await generateOpenApi(this);
2440
- for (const hook of this.specAvailableHooks) {
2441
- await hook(this.openApiSpec);
2442
- }
2717
+ await Promise.all(this.specAvailableHooks.map((hook) => hook(this.openApiSpec)));
2443
2718
  }
2444
2719
  if (port === 0 && process.platform === "linux") ;
2445
- if (this.applicationConfig.autoBackpressureFeedback) {
2720
+ if (this.applicationConfig.autoBackpressureFeedback === true) {
2446
2721
  this.cpuMonitor = new SystemCpuMonitor();
2447
2722
  this.cpuMonitor.start();
2448
2723
  }
@@ -2470,7 +2745,7 @@ class Shokupan extends ShokupanRouter {
2470
2745
  };
2471
2746
  let factory = this.applicationConfig.serverFactory;
2472
2747
  if (!factory && typeof Bun === "undefined") {
2473
- const { createHttpServer } = await Promise.resolve().then(() => require("./server-adapter-fVKP60e0.cjs"));
2748
+ const { createHttpServer } = await Promise.resolve().then(() => require("./server-adapter-DFhwlK8e.cjs"));
2474
2749
  factory = createHttpServer();
2475
2750
  }
2476
2751
  const server = factory ? await factory(serveOptions) : Bun.serve(serveOptions);
@@ -2483,7 +2758,7 @@ class Shokupan extends ShokupanRouter {
2483
2758
  /**
2484
2759
  * Processes a request by wrapping the standard fetch method.
2485
2760
  */
2486
- async processRequest(options) {
2761
+ async testRequest(options) {
2487
2762
  let url = options.url || options.path || "/";
2488
2763
  if (!url.startsWith("http")) {
2489
2764
  const base = `http://${this.applicationConfig.hostname || "localhost"}:${this.applicationConfig.port || 3e3}`;
@@ -2492,7 +2767,9 @@ class Shokupan extends ShokupanRouter {
2492
2767
  }
2493
2768
  if (options.query) {
2494
2769
  const u = new URL(url);
2495
- for (const [k, v] of Object.entries(options.query)) {
2770
+ const entries = Object.entries(options.query);
2771
+ for (let i = 0; i < entries.length; i++) {
2772
+ const [k, v] = entries[i];
2496
2773
  u.searchParams.set(k, v);
2497
2774
  }
2498
2775
  url = u.toString();
@@ -2561,18 +2838,18 @@ class Shokupan extends ShokupanRouter {
2561
2838
  if (this.cpuMonitor && this.cpuMonitor.getUsage() > (this.applicationConfig.autoBackpressureLevel ?? 60)) {
2562
2839
  const msg = "Too Many Requests (CPU Backpressure)";
2563
2840
  const res = ctx.text(msg, 429);
2564
- await this.executeHook("onResponseEnd", ctx, res);
2841
+ await this.runHooks("onResponseEnd", ctx, res);
2565
2842
  return res;
2566
2843
  }
2567
2844
  try {
2568
- if (this.hasHook("onRequestStart")) {
2569
- await this.executeHook("onRequestStart", ctx);
2570
- }
2845
+ await this.runHooks("onRequestStart", ctx);
2571
2846
  const fn = this.composedMiddleware ??= compose(this.middleware);
2572
2847
  const result = await fn(ctx, async () => {
2848
+ const bodyParsing = ["POST", "PUT", "PATCH", "DELETE"].includes(req.method) ? ctx.parseBody() : Promise.resolve();
2573
2849
  const match = this.find(req.method, ctx.path);
2574
2850
  if (match) {
2575
2851
  ctx.params = match.params;
2852
+ await bodyParsing;
2576
2853
  return match.handler(ctx);
2577
2854
  }
2578
2855
  return null;
@@ -2595,12 +2872,8 @@ class Shokupan extends ShokupanRouter {
2595
2872
  } else {
2596
2873
  response = ctx.text(String(result));
2597
2874
  }
2598
- if (this.hasHook("onRequestEnd")) {
2599
- await this.executeHook("onRequestEnd", ctx);
2600
- }
2601
- if (this.hasHook("onResponseStart")) {
2602
- await this.executeHook("onResponseStart", ctx, response);
2603
- }
2875
+ await this.runHooks("onRequestEnd", ctx);
2876
+ await this.runHooks("onResponseStart", ctx, response);
2604
2877
  return response;
2605
2878
  } catch (err) {
2606
2879
  console.error(err);
@@ -2609,9 +2882,7 @@ class Shokupan extends ShokupanRouter {
2609
2882
  const status = err.status || err.statusCode || 500;
2610
2883
  const body = { error: err.message || "Internal Server Error" };
2611
2884
  if (err.errors) body.errors = err.errors;
2612
- if (this.hasHook("onError")) {
2613
- await this.executeHook("onError", err, ctx);
2614
- }
2885
+ await this.runHooks("onError", ctx, err);
2615
2886
  return ctx.json(body, status);
2616
2887
  }
2617
2888
  };
@@ -2622,9 +2893,7 @@ class Shokupan extends ShokupanRouter {
2622
2893
  const timeoutPromise = new Promise((_, reject) => {
2623
2894
  timeoutId = setTimeout(async () => {
2624
2895
  controller.abort();
2625
- if (this.hasHook("onRequestTimeout")) {
2626
- await this.executeHook("onRequestTimeout", ctx);
2627
- }
2896
+ await this.runHooks("onRequestTimeout", ctx);
2628
2897
  reject(new Error("Request Timeout"));
2629
2898
  }, timeoutMs);
2630
2899
  });
@@ -2637,56 +2906,10 @@ class Shokupan extends ShokupanRouter {
2637
2906
  console.error("Unexpected error in request execution:", err);
2638
2907
  return ctx.text("Internal Server Error", 500);
2639
2908
  }).then(async (res) => {
2640
- if (this.hasHook("onResponseEnd")) {
2641
- await this.executeHook("onResponseEnd", ctx, res);
2642
- }
2909
+ await this.runHooks("onResponseEnd", ctx, res);
2643
2910
  return res;
2644
2911
  });
2645
2912
  }
2646
- ensureHooksInitialized() {
2647
- const hooks = this.applicationConfig.hooks;
2648
- if (hooks) {
2649
- const hookList = Array.isArray(hooks) ? hooks : [hooks];
2650
- const hookTypes = [
2651
- "onRequestStart",
2652
- "onRequestEnd",
2653
- "onResponseStart",
2654
- "onResponseEnd",
2655
- "onError",
2656
- "beforeValidate",
2657
- "afterValidate",
2658
- "onRequestTimeout",
2659
- "onReadTimeout",
2660
- "onWriteTimeout"
2661
- ];
2662
- for (const type of hookTypes) {
2663
- const fns = [];
2664
- for (const h of hookList) {
2665
- if (h[type]) fns.push(h[type]);
2666
- }
2667
- if (fns.length > 0) {
2668
- this.hookCache.set(type, fns);
2669
- }
2670
- }
2671
- }
2672
- this.hooksInitialized = true;
2673
- }
2674
- async executeHook(name, ...args) {
2675
- if (!this.hooksInitialized) {
2676
- this.ensureHooksInitialized();
2677
- }
2678
- const fns = this.hookCache.get(name);
2679
- if (!fns) return;
2680
- for (const fn of fns) {
2681
- await fn(...args);
2682
- }
2683
- }
2684
- hasHook(name) {
2685
- if (!this.hooksInitialized) {
2686
- this.ensureHooksInitialized();
2687
- }
2688
- return this.hookCache.has(name);
2689
- }
2690
2913
  }
2691
2914
  class AuthPlugin extends ShokupanRouter {
2692
2915
  constructor(authConfig) {
@@ -2734,7 +2957,9 @@ class AuthPlugin extends ShokupanRouter {
2734
2957
  return jwt;
2735
2958
  }
2736
2959
  init() {
2737
- for (const [providerName, providerConfig] of Object.entries(this.authConfig.providers)) {
2960
+ const providerEntries = Object.entries(this.authConfig.providers);
2961
+ for (let i = 0; i < providerEntries.length; i++) {
2962
+ const [providerName, providerConfig] = providerEntries[i];
2738
2963
  if (!providerConfig) continue;
2739
2964
  const provider = this.getProviderInstance(providerName, providerConfig);
2740
2965
  if (!provider) {
@@ -3062,7 +3287,9 @@ function Cors(options = {}) {
3062
3287
  }
3063
3288
  const response = await next();
3064
3289
  if (response instanceof Response) {
3065
- for (const [key, value] of headers.entries()) {
3290
+ const headerEntries = Array.from(headers.entries());
3291
+ for (let i = 0; i < headerEntries.length; i++) {
3292
+ const [key, value] = headerEntries[i];
3066
3293
  response.headers.set(key, value);
3067
3294
  }
3068
3295
  }
@@ -3132,6 +3359,8 @@ function useExpress(expressMiddleware) {
3132
3359
  });
3133
3360
  };
3134
3361
  }
3362
+ let plainToInstance;
3363
+ let validateOrReject;
3135
3364
  class ValidationError extends Error {
3136
3365
  constructor(errors) {
3137
3366
  super("Validation Error");
@@ -3196,9 +3425,21 @@ function isClass(schema) {
3196
3425
  }
3197
3426
  }
3198
3427
  async function validateClassValidator(schema, data) {
3199
- const object = classTransformer.plainToInstance(schema, data);
3428
+ if (!plainToInstance || !validateOrReject) {
3429
+ try {
3430
+ const ct = await import("class-transformer");
3431
+ const cv = await import("class-validator");
3432
+ plainToInstance = ct.plainToInstance;
3433
+ validateOrReject = cv.validateOrReject;
3434
+ } catch (e) {
3435
+ throw new Error(
3436
+ "class-transformer and class-validator are required for class-based validation. Install them with: bun add class-transformer class-validator reflect-metadata"
3437
+ );
3438
+ }
3439
+ }
3440
+ const object = plainToInstance(schema, data);
3200
3441
  try {
3201
- await classValidator.validateOrReject(object);
3442
+ await validateOrReject(object);
3202
3443
  return object;
3203
3444
  } catch (errors) {
3204
3445
  const formattedErrors = Array.isArray(errors) ? errors.map((err) => ({
@@ -3210,30 +3451,8 @@ async function validateClassValidator(schema, data) {
3210
3451
  }
3211
3452
  }
3212
3453
  const safelyGetBody = async (ctx) => {
3213
- const req = ctx.req;
3214
- if (req._bodyParsed) {
3215
- return req._bodyValue;
3216
- }
3217
3454
  try {
3218
- let data;
3219
- if (typeof req.json === "function") {
3220
- data = await req.json();
3221
- } else {
3222
- data = req.body;
3223
- if (typeof data === "string") {
3224
- try {
3225
- data = JSON.parse(data);
3226
- } catch {
3227
- }
3228
- }
3229
- }
3230
- req._bodyParsed = true;
3231
- req._bodyValue = data;
3232
- Object.defineProperty(req, "json", {
3233
- value: async () => req._bodyValue,
3234
- configurable: true
3235
- });
3236
- return data;
3455
+ return await ctx.body();
3237
3456
  } catch (e) {
3238
3457
  return {};
3239
3458
  }
@@ -3280,9 +3499,7 @@ function validate(config) {
3280
3499
  body = await safelyGetBody(ctx);
3281
3500
  dataToValidate.body = body;
3282
3501
  }
3283
- if (ctx.app?.hasHook("beforeValidate")) {
3284
- await ctx.app.executeHook("beforeValidate", ctx, dataToValidate);
3285
- }
3502
+ await ctx.app.runHooks("beforeValidate", ctx, dataToValidate);
3286
3503
  if (validators.params) {
3287
3504
  ctx.params = await validators.params(ctx.params);
3288
3505
  }
@@ -3298,21 +3515,20 @@ function validate(config) {
3298
3515
  if (validators.body) {
3299
3516
  const b = body ?? await safelyGetBody(ctx);
3300
3517
  validBody = await validators.body(b);
3518
+ ctx._cachedBody = validBody;
3301
3519
  const req = ctx.req;
3302
- req._bodyValue = validBody;
3303
3520
  Object.defineProperty(req, "json", {
3304
3521
  value: async () => validBody,
3522
+ writable: true,
3305
3523
  configurable: true
3306
3524
  });
3307
3525
  ctx.body = validBody;
3308
3526
  }
3309
- if (ctx.app?.hasHook("afterValidate")) {
3310
- const validatedData = { ...dataToValidate };
3311
- if (config.params) validatedData.params = ctx.params;
3312
- if (config.query) validatedData.query = validQuery;
3313
- if (config.body) validatedData.body = validBody;
3314
- await ctx.app?.executeHook("afterValidate", ctx, validatedData);
3315
- }
3527
+ const validatedData = { ...dataToValidate };
3528
+ if (config.params) validatedData.params = ctx.params;
3529
+ if (config.query) validatedData.query = validQuery;
3530
+ if (config.body) validatedData.body = validBody;
3531
+ await ctx.app.runHooks("afterValidate", ctx, validatedData);
3316
3532
  return next();
3317
3533
  };
3318
3534
  }
@@ -3335,12 +3551,14 @@ function openApiValidator() {
3335
3551
  if (cache.validators.has(ctx.path)) {
3336
3552
  matchPath = ctx.path;
3337
3553
  } else {
3338
- for (const [path2, { regex, paramNames }] of cache.paths) {
3554
+ const pathEntries = Array.from(cache.paths.entries());
3555
+ for (let i = 0; i < pathEntries.length; i++) {
3556
+ const [path2, { regex, paramNames }] = pathEntries[i];
3339
3557
  const match = regex.exec(ctx.path);
3340
3558
  if (match) {
3341
3559
  matchPath = path2;
3342
- paramNames.forEach((name, i) => {
3343
- matchParams[name] = match[i + 1];
3560
+ paramNames.forEach((name, i2) => {
3561
+ matchParams[name] = match[i2 + 1];
3344
3562
  });
3345
3563
  break;
3346
3564
  }
@@ -3397,7 +3615,9 @@ function openApiValidator() {
3397
3615
  function compileValidators(spec) {
3398
3616
  const validators = /* @__PURE__ */ new Map();
3399
3617
  const paths = /* @__PURE__ */ new Map();
3400
- for (const [path2, pathItem] of Object.entries(spec.paths || {})) {
3618
+ const pathEntries = Object.entries(spec.paths || {});
3619
+ for (let i = 0; i < pathEntries.length; i++) {
3620
+ const [path2, pathItem] = pathEntries[i];
3401
3621
  if (path2.includes("{")) {
3402
3622
  const paramNames = [];
3403
3623
  const regexStr = "^" + path2.replace(/{([^}]+)}/g, (_, name) => {
@@ -3410,7 +3630,9 @@ function compileValidators(spec) {
3410
3630
  });
3411
3631
  }
3412
3632
  const pathValidators = {};
3413
- for (const [method, operation] of Object.entries(pathItem)) {
3633
+ const methodEntries = Object.entries(pathItem);
3634
+ for (let k = 0; k < methodEntries.length; k++) {
3635
+ const [method, operation] = methodEntries[k];
3414
3636
  if (method === "parameters" || method === "summary" || method === "description") continue;
3415
3637
  const oper = operation;
3416
3638
  const opValidators = {};
@@ -3424,7 +3646,8 @@ function compileValidators(spec) {
3424
3646
  const queryRequired = [];
3425
3647
  const pathRequired = [];
3426
3648
  const headerRequired = [];
3427
- for (const param of parameters) {
3649
+ for (let j = 0; j < parameters.length; j++) {
3650
+ const param = parameters[j];
3428
3651
  if (param.in === "query") {
3429
3652
  queryProps[param.name] = param.schema || {};
3430
3653
  if (param.required) queryRequired.push(param.name);
@@ -3585,14 +3808,18 @@ function SecurityHeaders(options = {}) {
3585
3808
  if (opt === void 0 || opt === true) {
3586
3809
  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");
3587
3810
  } else if (typeof opt === "object") {
3588
- for (const [key, val] of Object.entries(opt)) {
3811
+ const optEntries = Object.entries(opt);
3812
+ for (let i = 0; i < optEntries.length; i++) {
3813
+ const [key, val] = optEntries[i];
3589
3814
  }
3590
3815
  }
3591
3816
  }
3592
3817
  if (options.hidePoweredBy !== false) ;
3593
3818
  const response = await next();
3594
3819
  if (response instanceof Response) {
3595
- for (const [k, v] of Object.entries(headers)) {
3820
+ const headerEntries = Object.entries(headers);
3821
+ for (let i = 0; i < headerEntries.length; i++) {
3822
+ const [k, v] = headerEntries[i];
3596
3823
  response.headers.set(k, v);
3597
3824
  }
3598
3825
  return response;
@@ -3678,7 +3905,9 @@ class MemoryStore extends events.EventEmitter {
3678
3905
  }
3679
3906
  all(cb) {
3680
3907
  const result = {};
3681
- for (const sid in this.sessions) {
3908
+ const sessionKeys = Object.keys(this.sessions);
3909
+ for (let i = 0; i < sessionKeys.length; i++) {
3910
+ const sid = sessionKeys[i];
3682
3911
  try {
3683
3912
  result[sid] = JSON.parse(this.sessions[sid]);
3684
3913
  } catch {
@@ -3764,7 +3993,9 @@ function Session(options) {
3764
3993
  sessObj.regenerate = (cb) => {
3765
3994
  store.destroy(sessObj.id, (err) => {
3766
3995
  sessionID = generateId(ctx);
3767
- for (const key in sessObj) {
3996
+ const keys = Object.keys(sessObj);
3997
+ for (let i = 0; i < keys.length; i++) {
3998
+ const key = keys[i];
3768
3999
  if (key !== "cookie" && key !== "id" && typeof sessObj[key] !== "function") {
3769
4000
  delete sessObj[key];
3770
4001
  }
@@ -3779,7 +4010,9 @@ function Session(options) {
3779
4010
  store.get(sessObj.id, (err, sess2) => {
3780
4011
  if (err) return cb(err);
3781
4012
  if (!sess2) return cb(new Error("Session not found"));
3782
- for (const key in sessObj) {
4013
+ const keys = Object.keys(sessObj);
4014
+ for (let i = 0; i < keys.length; i++) {
4015
+ const key = keys[i];
3783
4016
  if (key !== "cookie" && key !== "id" && typeof sessObj[key] !== "function") {
3784
4017
  delete sessObj[key];
3785
4018
  }