shokupan 0.4.5 → 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 (57) hide show
  1. package/README.md +10 -9
  2. package/dist/analysis/openapi-analyzer.d.ts +0 -4
  3. package/dist/cli.cjs +1 -1
  4. package/dist/cli.js +1 -1
  5. package/dist/context.d.ts +30 -8
  6. package/dist/index.cjs +692 -461
  7. package/dist/index.cjs.map +1 -1
  8. package/dist/index.js +635 -426
  9. package/dist/index.js.map +1 -1
  10. package/dist/json-parser-B3dnQmCC.js +35 -0
  11. package/dist/json-parser-B3dnQmCC.js.map +1 -0
  12. package/dist/json-parser-COdZ0fqY.cjs +35 -0
  13. package/dist/json-parser-COdZ0fqY.cjs.map +1 -0
  14. package/dist/{openapi-analyzer-D9YB3IkV.cjs → openapi-analyzer-Bei1sVWp.cjs} +63 -49
  15. package/dist/openapi-analyzer-Bei1sVWp.cjs.map +1 -0
  16. package/dist/{openapi-analyzer-BtIaHIfe.js → openapi-analyzer-Ce_7JxZh.js} +63 -49
  17. package/dist/openapi-analyzer-Ce_7JxZh.js.map +1 -0
  18. package/dist/plugins/scalar.d.ts +1 -1
  19. package/dist/router.d.ts +33 -22
  20. package/dist/{server-adapter-BWrEJbKL.js → server-adapter-0xH174zz.js} +4 -2
  21. package/dist/server-adapter-0xH174zz.js.map +1 -0
  22. package/dist/{server-adapter-fVKP60e0.cjs → server-adapter-DFhwlK8e.cjs} +4 -2
  23. package/dist/server-adapter-DFhwlK8e.cjs.map +1 -0
  24. package/dist/shokupan.d.ts +4 -8
  25. package/dist/types.d.ts +32 -3
  26. package/dist/util/datastore.d.ts +6 -0
  27. package/dist/util/json-parser.d.ts +12 -0
  28. package/dist/util/plugin-deps.d.ts +25 -0
  29. package/package.json +74 -14
  30. package/dist/benchmarking/advanced-cases/elysia.d.ts +0 -1
  31. package/dist/benchmarking/advanced-cases/express.d.ts +0 -1
  32. package/dist/benchmarking/advanced-cases/fastify.d.ts +0 -1
  33. package/dist/benchmarking/advanced-cases/hapi.d.ts +0 -1
  34. package/dist/benchmarking/advanced-cases/hono.d.ts +0 -1
  35. package/dist/benchmarking/advanced-cases/koa.d.ts +0 -1
  36. package/dist/benchmarking/advanced-cases/nest.d.ts +0 -1
  37. package/dist/benchmarking/advanced-cases/shokupan.d.ts +0 -1
  38. package/dist/benchmarking/advanced-data.d.ts +0 -33
  39. package/dist/benchmarking/advanced-runner.d.ts +0 -1
  40. package/dist/benchmarking/advanced-worker.d.ts +0 -0
  41. package/dist/benchmarking/cases/elysia.d.ts +0 -1
  42. package/dist/benchmarking/cases/express.d.ts +0 -1
  43. package/dist/benchmarking/cases/fastify.d.ts +0 -1
  44. package/dist/benchmarking/cases/hapi.d.ts +0 -1
  45. package/dist/benchmarking/cases/hono.d.ts +0 -1
  46. package/dist/benchmarking/cases/koa.d.ts +0 -1
  47. package/dist/benchmarking/cases/nest.d.ts +0 -1
  48. package/dist/benchmarking/cases/shokupan.d.ts +0 -1
  49. package/dist/benchmarking/data.d.ts +0 -15
  50. package/dist/benchmarking/quick_bench.d.ts +0 -1
  51. package/dist/benchmarking/runner.d.ts +0 -1
  52. package/dist/benchmarking/worker.d.ts +0 -0
  53. package/dist/buntest.d.ts +0 -1
  54. package/dist/openapi-analyzer-BtIaHIfe.js.map +0 -1
  55. package/dist/openapi-analyzer-D9YB3IkV.cjs.map +0 -1
  56. package/dist/server-adapter-BWrEJbKL.js.map +0 -1
  57. 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-D9YB3IkV.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-D9YB3IkV.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,32 +1201,14 @@ 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;
1004
- });
1005
- }
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;
1210
+ return runtimeHandlerSrc.includes(astHandlerSrc) || astHandlerSrc.includes(runtimeHandlerSrc) || r.handlerSource && runtimeHandlerSrc.includes(r.handlerSource.substring(0, 50));
1015
1211
  });
1016
- if (preciseMatch) {
1017
- astMatch = preciseMatch;
1018
- }
1019
1212
  }
1020
1213
  if (astMatch) {
1021
1214
  if (astMatch.summary) operation.summary = astMatch.summary;
@@ -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 = () => {
@@ -2092,41 +2309,43 @@ class ShokupanRouter {
2092
2309
  if (ctx.app?.applicationConfig.enableMiddlewareTracking) {
2093
2310
  const duration = performance.now() - startTime;
2094
2311
  const config = ctx.app.applicationConfig;
2095
- try {
2096
- const timestamp = Date.now();
2097
- const key = `${timestamp}-${handler.name || "anonymous"}-${Math.random().toString(36).substring(7)}`;
2098
- await datastore.set("middleware_tracking", key, {
2099
- name: handler.name || "anonymous",
2100
- path: ctx.path,
2101
- timestamp,
2102
- duration,
2103
- file,
2104
- line,
2105
- error: error ? String(error) : void 0,
2106
- metadata: {
2107
- isBuiltin: handler.isBuiltin,
2108
- pluginName: handler.pluginName
2312
+ Promise.resolve().then(async () => {
2313
+ try {
2314
+ const timestamp = Date.now();
2315
+ const key = `${timestamp}-${handler.name || "anonymous"}-${Math.random().toString(36).substring(7)}`;
2316
+ await datastore.set("middleware_tracking", key, {
2317
+ name: handler.name || "anonymous",
2318
+ path: ctx.path,
2319
+ timestamp,
2320
+ duration,
2321
+ file,
2322
+ line,
2323
+ error: error ? String(error) : void 0,
2324
+ metadata: {
2325
+ isBuiltin: handler.isBuiltin,
2326
+ pluginName: handler.pluginName
2327
+ }
2328
+ });
2329
+ const ttl = config.middlewareTrackingTTL ?? 864e5;
2330
+ const maxCapacity = config.middlewareTrackingMaxCapacity ?? 1e4;
2331
+ const cutoff = Date.now() - ttl;
2332
+ await datastore.query(`DELETE middleware_tracking WHERE timestamp < ${cutoff}`);
2333
+ const results = await datastore.query("SELECT count() FROM middleware_tracking GROUP ALL");
2334
+ if (results && results[0] && results[0].count > maxCapacity) {
2335
+ const toDelete = results[0].count - maxCapacity;
2336
+ await datastore.query(`DELETE middleware_tracking ORDER BY timestamp ASC LIMIT ${toDelete}`);
2109
2337
  }
2110
- });
2111
- const ttl = config.middlewareTrackingTTL ?? 864e5;
2112
- const maxCapacity = config.middlewareTrackingMaxCapacity ?? 1e4;
2113
- const cutoff = Date.now() - ttl;
2114
- await datastore.query(`DELETE middleware_tracking WHERE timestamp < ${cutoff}`);
2115
- const results = await datastore.query("SELECT count() FROM middleware_tracking GROUP ALL");
2116
- if (results && results[0] && results[0].count > maxCapacity) {
2117
- const toDelete = results[0].count - maxCapacity;
2118
- await datastore.query(`DELETE middleware_tracking ORDER BY timestamp ASC LIMIT ${toDelete}`);
2338
+ } catch (datastoreError) {
2339
+ console.error("Failed to store middleware tracking:", datastoreError);
2119
2340
  }
2120
- } catch (datastoreError) {
2121
- console.error("Failed to store middleware tracking:", datastoreError);
2122
- }
2341
+ });
2123
2342
  }
2124
2343
  }
2125
2344
  };
2126
2345
  wrappedHandler.originalHandler = trackingHandler.originalHandler || trackingHandler;
2127
2346
  let bakedHandler = wrappedHandler;
2128
2347
  if (this.config?.hooks) {
2129
- bakedHandler = this.wrapWithHooks(wrappedHandler, this.config.hooks);
2348
+ bakedHandler = this.wrapWithHooks(wrappedHandler);
2130
2349
  }
2131
2350
  this[$routes].push({
2132
2351
  method,
@@ -2283,6 +2502,67 @@ class ShokupanRouter {
2283
2502
  generateApiSpec(options = {}) {
2284
2503
  return generateOpenApi(this, options);
2285
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
+ }
2286
2566
  }
2287
2567
  class SystemCpuMonitor {
2288
2568
  constructor(intervalMs = 1e3) {
@@ -2340,15 +2620,13 @@ class Shokupan extends ShokupanRouter {
2340
2620
  openApiSpec;
2341
2621
  composedMiddleware;
2342
2622
  cpuMonitor;
2343
- hookCache = /* @__PURE__ */ new Map();
2344
- hooksInitialized = false;
2345
2623
  get logger() {
2346
2624
  return this.applicationConfig.logger;
2347
2625
  }
2348
2626
  constructor(applicationConfig = {}) {
2349
2627
  const config = Object.assign({}, defaults, applicationConfig);
2350
2628
  const { hooks, ...routerConfig } = config;
2351
- super(routerConfig);
2629
+ super({ ...routerConfig, hooks });
2352
2630
  this[$isApplication] = true;
2353
2631
  this[$appRoot] = this;
2354
2632
  this.applicationConfig = config;
@@ -2363,7 +2641,6 @@ class Shokupan extends ShokupanRouter {
2363
2641
  * Adds middleware to the application.
2364
2642
  */
2365
2643
  use(middleware) {
2366
- let trackedMiddleware = middleware;
2367
2644
  const { file, line } = getCallerInfo();
2368
2645
  if (!middleware.metadata) {
2369
2646
  middleware.metadata = {
@@ -2374,32 +2651,36 @@ class Shokupan extends ShokupanRouter {
2374
2651
  pluginName: middleware.pluginName
2375
2652
  };
2376
2653
  }
2377
- trackedMiddleware = async (ctx, next) => {
2378
- const c = ctx;
2379
- if (c.handlerStack && c.app?.applicationConfig.enableMiddlewareTracking) {
2380
- const metadata = middleware.metadata || {};
2381
- const start = performance.now();
2382
- const item = {
2383
- name: metadata.pluginName ? `${metadata.pluginName} (${metadata.name})` : metadata.name || middleware.name || "middleware",
2384
- file: metadata.file || file,
2385
- line: metadata.line || line,
2386
- isBuiltin: metadata.isBuiltin,
2387
- startTime: start,
2388
- duration: -1
2389
- };
2390
- c.handlerStack.push(item);
2391
- try {
2392
- return await middleware(ctx, next);
2393
- } finally {
2394
- 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
+ }
2395
2674
  }
2396
- }
2397
- return middleware(ctx, next);
2398
- };
2399
- trackedMiddleware.metadata = middleware.metadata;
2400
- Object.defineProperty(trackedMiddleware, "name", { value: middleware.name || "middleware" });
2401
- trackedMiddleware.order = this.middleware.length;
2402
- 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
+ }
2403
2684
  return this;
2404
2685
  }
2405
2686
  startupHooks = [];
@@ -2430,17 +2711,13 @@ class Shokupan extends ShokupanRouter {
2430
2711
  if (finalPort < 0 || finalPort > 65535) {
2431
2712
  throw new Error("Invalid port number");
2432
2713
  }
2433
- for (const hook of this.startupHooks) {
2434
- await hook();
2435
- }
2714
+ await Promise.all(this.startupHooks.map((hook) => hook()));
2436
2715
  if (this.applicationConfig.enableOpenApiGen) {
2437
2716
  this.openApiSpec = await generateOpenApi(this);
2438
- for (const hook of this.specAvailableHooks) {
2439
- await hook(this.openApiSpec);
2440
- }
2717
+ await Promise.all(this.specAvailableHooks.map((hook) => hook(this.openApiSpec)));
2441
2718
  }
2442
2719
  if (port === 0 && process.platform === "linux") ;
2443
- if (this.applicationConfig.autoBackpressureFeedback) {
2720
+ if (this.applicationConfig.autoBackpressureFeedback === true) {
2444
2721
  this.cpuMonitor = new SystemCpuMonitor();
2445
2722
  this.cpuMonitor.start();
2446
2723
  }
@@ -2468,11 +2745,11 @@ class Shokupan extends ShokupanRouter {
2468
2745
  };
2469
2746
  let factory = this.applicationConfig.serverFactory;
2470
2747
  if (!factory && typeof Bun === "undefined") {
2471
- const { createHttpServer } = await Promise.resolve().then(() => require("./server-adapter-fVKP60e0.cjs"));
2748
+ const { createHttpServer } = await Promise.resolve().then(() => require("./server-adapter-DFhwlK8e.cjs"));
2472
2749
  factory = createHttpServer();
2473
2750
  }
2474
2751
  const server = factory ? await factory(serveOptions) : Bun.serve(serveOptions);
2475
- console.log(`Shokupan server listening on http://${server.hostname}:${server.port}`);
2752
+ console.log(`Shokupan server listening on http://${serveOptions.hostname}:${serveOptions.port}`);
2476
2753
  return server;
2477
2754
  }
2478
2755
  [$dispatch](req) {
@@ -2481,7 +2758,7 @@ class Shokupan extends ShokupanRouter {
2481
2758
  /**
2482
2759
  * Processes a request by wrapping the standard fetch method.
2483
2760
  */
2484
- async processRequest(options) {
2761
+ async testRequest(options) {
2485
2762
  let url = options.url || options.path || "/";
2486
2763
  if (!url.startsWith("http")) {
2487
2764
  const base = `http://${this.applicationConfig.hostname || "localhost"}:${this.applicationConfig.port || 3e3}`;
@@ -2490,7 +2767,9 @@ class Shokupan extends ShokupanRouter {
2490
2767
  }
2491
2768
  if (options.query) {
2492
2769
  const u = new URL(url);
2493
- 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];
2494
2773
  u.searchParams.set(k, v);
2495
2774
  }
2496
2775
  url = u.toString();
@@ -2559,18 +2838,18 @@ class Shokupan extends ShokupanRouter {
2559
2838
  if (this.cpuMonitor && this.cpuMonitor.getUsage() > (this.applicationConfig.autoBackpressureLevel ?? 60)) {
2560
2839
  const msg = "Too Many Requests (CPU Backpressure)";
2561
2840
  const res = ctx.text(msg, 429);
2562
- await this.executeHook("onResponseEnd", ctx, res);
2841
+ await this.runHooks("onResponseEnd", ctx, res);
2563
2842
  return res;
2564
2843
  }
2565
2844
  try {
2566
- if (this.hasHook("onRequestStart")) {
2567
- await this.executeHook("onRequestStart", ctx);
2568
- }
2845
+ await this.runHooks("onRequestStart", ctx);
2569
2846
  const fn = this.composedMiddleware ??= compose(this.middleware);
2570
2847
  const result = await fn(ctx, async () => {
2848
+ const bodyParsing = ["POST", "PUT", "PATCH", "DELETE"].includes(req.method) ? ctx.parseBody() : Promise.resolve();
2571
2849
  const match = this.find(req.method, ctx.path);
2572
2850
  if (match) {
2573
2851
  ctx.params = match.params;
2852
+ await bodyParsing;
2574
2853
  return match.handler(ctx);
2575
2854
  }
2576
2855
  return null;
@@ -2593,12 +2872,8 @@ class Shokupan extends ShokupanRouter {
2593
2872
  } else {
2594
2873
  response = ctx.text(String(result));
2595
2874
  }
2596
- if (this.hasHook("onRequestEnd")) {
2597
- await this.executeHook("onRequestEnd", ctx);
2598
- }
2599
- if (this.hasHook("onResponseStart")) {
2600
- await this.executeHook("onResponseStart", ctx, response);
2601
- }
2875
+ await this.runHooks("onRequestEnd", ctx);
2876
+ await this.runHooks("onResponseStart", ctx, response);
2602
2877
  return response;
2603
2878
  } catch (err) {
2604
2879
  console.error(err);
@@ -2607,9 +2882,7 @@ class Shokupan extends ShokupanRouter {
2607
2882
  const status = err.status || err.statusCode || 500;
2608
2883
  const body = { error: err.message || "Internal Server Error" };
2609
2884
  if (err.errors) body.errors = err.errors;
2610
- if (this.hasHook("onError")) {
2611
- await this.executeHook("onError", err, ctx);
2612
- }
2885
+ await this.runHooks("onError", ctx, err);
2613
2886
  return ctx.json(body, status);
2614
2887
  }
2615
2888
  };
@@ -2620,9 +2893,7 @@ class Shokupan extends ShokupanRouter {
2620
2893
  const timeoutPromise = new Promise((_, reject) => {
2621
2894
  timeoutId = setTimeout(async () => {
2622
2895
  controller.abort();
2623
- if (this.hasHook("onRequestTimeout")) {
2624
- await this.executeHook("onRequestTimeout", ctx);
2625
- }
2896
+ await this.runHooks("onRequestTimeout", ctx);
2626
2897
  reject(new Error("Request Timeout"));
2627
2898
  }, timeoutMs);
2628
2899
  });
@@ -2635,56 +2906,10 @@ class Shokupan extends ShokupanRouter {
2635
2906
  console.error("Unexpected error in request execution:", err);
2636
2907
  return ctx.text("Internal Server Error", 500);
2637
2908
  }).then(async (res) => {
2638
- if (this.hasHook("onResponseEnd")) {
2639
- await this.executeHook("onResponseEnd", ctx, res);
2640
- }
2909
+ await this.runHooks("onResponseEnd", ctx, res);
2641
2910
  return res;
2642
2911
  });
2643
2912
  }
2644
- ensureHooksInitialized() {
2645
- const hooks = this.applicationConfig.hooks;
2646
- if (hooks) {
2647
- const hookList = Array.isArray(hooks) ? hooks : [hooks];
2648
- const hookTypes = [
2649
- "onRequestStart",
2650
- "onRequestEnd",
2651
- "onResponseStart",
2652
- "onResponseEnd",
2653
- "onError",
2654
- "beforeValidate",
2655
- "afterValidate",
2656
- "onRequestTimeout",
2657
- "onReadTimeout",
2658
- "onWriteTimeout"
2659
- ];
2660
- for (const type of hookTypes) {
2661
- const fns = [];
2662
- for (const h of hookList) {
2663
- if (h[type]) fns.push(h[type]);
2664
- }
2665
- if (fns.length > 0) {
2666
- this.hookCache.set(type, fns);
2667
- }
2668
- }
2669
- }
2670
- this.hooksInitialized = true;
2671
- }
2672
- async executeHook(name, ...args) {
2673
- if (!this.hooksInitialized) {
2674
- this.ensureHooksInitialized();
2675
- }
2676
- const fns = this.hookCache.get(name);
2677
- if (!fns) return;
2678
- for (const fn of fns) {
2679
- await fn(...args);
2680
- }
2681
- }
2682
- hasHook(name) {
2683
- if (!this.hooksInitialized) {
2684
- this.ensureHooksInitialized();
2685
- }
2686
- return this.hookCache.has(name);
2687
- }
2688
2913
  }
2689
2914
  class AuthPlugin extends ShokupanRouter {
2690
2915
  constructor(authConfig) {
@@ -2732,7 +2957,9 @@ class AuthPlugin extends ShokupanRouter {
2732
2957
  return jwt;
2733
2958
  }
2734
2959
  init() {
2735
- 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];
2736
2963
  if (!providerConfig) continue;
2737
2964
  const provider = this.getProviderInstance(providerName, providerConfig);
2738
2965
  if (!provider) {
@@ -3060,7 +3287,9 @@ function Cors(options = {}) {
3060
3287
  }
3061
3288
  const response = await next();
3062
3289
  if (response instanceof Response) {
3063
- 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];
3064
3293
  response.headers.set(key, value);
3065
3294
  }
3066
3295
  }
@@ -3130,6 +3359,8 @@ function useExpress(expressMiddleware) {
3130
3359
  });
3131
3360
  };
3132
3361
  }
3362
+ let plainToInstance;
3363
+ let validateOrReject;
3133
3364
  class ValidationError extends Error {
3134
3365
  constructor(errors) {
3135
3366
  super("Validation Error");
@@ -3194,9 +3425,21 @@ function isClass(schema) {
3194
3425
  }
3195
3426
  }
3196
3427
  async function validateClassValidator(schema, data) {
3197
- 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);
3198
3441
  try {
3199
- await classValidator.validateOrReject(object);
3442
+ await validateOrReject(object);
3200
3443
  return object;
3201
3444
  } catch (errors) {
3202
3445
  const formattedErrors = Array.isArray(errors) ? errors.map((err) => ({
@@ -3208,30 +3451,8 @@ async function validateClassValidator(schema, data) {
3208
3451
  }
3209
3452
  }
3210
3453
  const safelyGetBody = async (ctx) => {
3211
- const req = ctx.req;
3212
- if (req._bodyParsed) {
3213
- return req._bodyValue;
3214
- }
3215
3454
  try {
3216
- let data;
3217
- if (typeof req.json === "function") {
3218
- data = await req.json();
3219
- } else {
3220
- data = req.body;
3221
- if (typeof data === "string") {
3222
- try {
3223
- data = JSON.parse(data);
3224
- } catch {
3225
- }
3226
- }
3227
- }
3228
- req._bodyParsed = true;
3229
- req._bodyValue = data;
3230
- Object.defineProperty(req, "json", {
3231
- value: async () => req._bodyValue,
3232
- configurable: true
3233
- });
3234
- return data;
3455
+ return await ctx.body();
3235
3456
  } catch (e) {
3236
3457
  return {};
3237
3458
  }
@@ -3278,9 +3499,7 @@ function validate(config) {
3278
3499
  body = await safelyGetBody(ctx);
3279
3500
  dataToValidate.body = body;
3280
3501
  }
3281
- if (ctx.app?.applicationConfig.hooks?.beforeValidate) {
3282
- await ctx.app.applicationConfig.hooks.beforeValidate(ctx, dataToValidate);
3283
- }
3502
+ await ctx.app.runHooks("beforeValidate", ctx, dataToValidate);
3284
3503
  if (validators.params) {
3285
3504
  ctx.params = await validators.params(ctx.params);
3286
3505
  }
@@ -3296,21 +3515,20 @@ function validate(config) {
3296
3515
  if (validators.body) {
3297
3516
  const b = body ?? await safelyGetBody(ctx);
3298
3517
  validBody = await validators.body(b);
3518
+ ctx._cachedBody = validBody;
3299
3519
  const req = ctx.req;
3300
- req._bodyValue = validBody;
3301
3520
  Object.defineProperty(req, "json", {
3302
3521
  value: async () => validBody,
3522
+ writable: true,
3303
3523
  configurable: true
3304
3524
  });
3305
3525
  ctx.body = validBody;
3306
3526
  }
3307
- if (ctx.app?.applicationConfig.hooks?.afterValidate) {
3308
- const validatedData = { ...dataToValidate };
3309
- if (config.params) validatedData.params = ctx.params;
3310
- if (config.query) validatedData.query = validQuery;
3311
- if (config.body) validatedData.body = validBody;
3312
- await ctx.app.applicationConfig.hooks.afterValidate(ctx, validatedData);
3313
- }
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);
3314
3532
  return next();
3315
3533
  };
3316
3534
  }
@@ -3333,12 +3551,14 @@ function openApiValidator() {
3333
3551
  if (cache.validators.has(ctx.path)) {
3334
3552
  matchPath = ctx.path;
3335
3553
  } else {
3336
- 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];
3337
3557
  const match = regex.exec(ctx.path);
3338
3558
  if (match) {
3339
3559
  matchPath = path2;
3340
- paramNames.forEach((name, i) => {
3341
- matchParams[name] = match[i + 1];
3560
+ paramNames.forEach((name, i2) => {
3561
+ matchParams[name] = match[i2 + 1];
3342
3562
  });
3343
3563
  break;
3344
3564
  }
@@ -3395,7 +3615,9 @@ function openApiValidator() {
3395
3615
  function compileValidators(spec) {
3396
3616
  const validators = /* @__PURE__ */ new Map();
3397
3617
  const paths = /* @__PURE__ */ new Map();
3398
- 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];
3399
3621
  if (path2.includes("{")) {
3400
3622
  const paramNames = [];
3401
3623
  const regexStr = "^" + path2.replace(/{([^}]+)}/g, (_, name) => {
@@ -3408,7 +3630,9 @@ function compileValidators(spec) {
3408
3630
  });
3409
3631
  }
3410
3632
  const pathValidators = {};
3411
- 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];
3412
3636
  if (method === "parameters" || method === "summary" || method === "description") continue;
3413
3637
  const oper = operation;
3414
3638
  const opValidators = {};
@@ -3422,7 +3646,8 @@ function compileValidators(spec) {
3422
3646
  const queryRequired = [];
3423
3647
  const pathRequired = [];
3424
3648
  const headerRequired = [];
3425
- for (const param of parameters) {
3649
+ for (let j = 0; j < parameters.length; j++) {
3650
+ const param = parameters[j];
3426
3651
  if (param.in === "query") {
3427
3652
  queryProps[param.name] = param.schema || {};
3428
3653
  if (param.required) queryRequired.push(param.name);
@@ -3493,8 +3718,7 @@ class ScalarPlugin extends ShokupanRouter {
3493
3718
 
3494
3719
  <body>
3495
3720
  <div id="app"></div>
3496
-
3497
- <script src="<%= it.path %>scalar.js"><\/script>
3721
+ <script src="https://cdn.jsdelivr.net/npm/@scalar/api-reference"><\/script>
3498
3722
  <script>
3499
3723
  Scalar.createApiReference('#app', [{ ...<%~ JSON.stringify(it.config.baseDocument) %>,
3500
3724
  url: "<%= it.path %>openapi.json",
@@ -3505,9 +3729,6 @@ class ScalarPlugin extends ShokupanRouter {
3505
3729
 
3506
3730
  </html>`, { path: path2, config: this.pluginOptions }));
3507
3731
  });
3508
- this.get("/scalar.js", (ctx) => {
3509
- return ctx.file(__dirname + "/../../node_modules/@scalar/api-reference/dist/browser/standalone.js");
3510
- });
3511
3732
  this.get("/openapi.json", async (ctx) => {
3512
3733
  let spec;
3513
3734
  if (this.root.openApiSpec) {
@@ -3587,14 +3808,18 @@ function SecurityHeaders(options = {}) {
3587
3808
  if (opt === void 0 || opt === true) {
3588
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");
3589
3810
  } else if (typeof opt === "object") {
3590
- 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];
3591
3814
  }
3592
3815
  }
3593
3816
  }
3594
3817
  if (options.hidePoweredBy !== false) ;
3595
3818
  const response = await next();
3596
3819
  if (response instanceof Response) {
3597
- 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];
3598
3823
  response.headers.set(k, v);
3599
3824
  }
3600
3825
  return response;
@@ -3680,7 +3905,9 @@ class MemoryStore extends events.EventEmitter {
3680
3905
  }
3681
3906
  all(cb) {
3682
3907
  const result = {};
3683
- 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];
3684
3911
  try {
3685
3912
  result[sid] = JSON.parse(this.sessions[sid]);
3686
3913
  } catch {
@@ -3766,7 +3993,9 @@ function Session(options) {
3766
3993
  sessObj.regenerate = (cb) => {
3767
3994
  store.destroy(sessObj.id, (err) => {
3768
3995
  sessionID = generateId(ctx);
3769
- 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];
3770
3999
  if (key !== "cookie" && key !== "id" && typeof sessObj[key] !== "function") {
3771
4000
  delete sessObj[key];
3772
4001
  }
@@ -3781,7 +4010,9 @@ function Session(options) {
3781
4010
  store.get(sessObj.id, (err, sess2) => {
3782
4011
  if (err) return cb(err);
3783
4012
  if (!sess2) return cb(new Error("Session not found"));
3784
- 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];
3785
4016
  if (key !== "cookie" && key !== "id" && typeof sessObj[key] !== "function") {
3786
4017
  delete sessObj[key];
3787
4018
  }