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