shokupan 0.0.1 → 0.2.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.
package/dist/index.cjs CHANGED
@@ -1,17 +1,17 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
- const api = require("@opentelemetry/api");
4
- const exporterTraceOtlpProto = require("@opentelemetry/exporter-trace-otlp-proto");
5
- const resources = require("@opentelemetry/resources");
6
- const sdkTraceBase = require("@opentelemetry/sdk-trace-base");
7
- const sdkTraceNode = require("@opentelemetry/sdk-trace-node");
8
- const semanticConventions = require("@opentelemetry/semantic-conventions");
9
3
  const eta$2 = require("eta");
10
4
  const promises = require("fs/promises");
11
5
  const path = require("path");
12
6
  const node_async_hooks = require("node:async_hooks");
7
+ const api = require("@opentelemetry/api");
13
8
  const arctic = require("arctic");
14
9
  const jose = require("jose");
10
+ const Ajv = require("ajv");
11
+ const addFormats = require("ajv-formats");
12
+ const classTransformer = require("class-transformer");
13
+ const classValidator = require("class-validator");
14
+ const openapiAnalyzer = require("./openapi-analyzer-BN0wFCML.cjs");
15
15
  const crypto = require("crypto");
16
16
  const events = require("events");
17
17
  function _interopNamespaceDefault(e) {
@@ -32,12 +32,13 @@ function _interopNamespaceDefault(e) {
32
32
  }
33
33
  const jose__namespace = /* @__PURE__ */ _interopNamespaceDefault(jose);
34
34
  class ShokupanResponse {
35
- _headers = new Headers();
35
+ _headers = null;
36
36
  _status = 200;
37
37
  /**
38
38
  * Get the current headers
39
39
  */
40
40
  get headers() {
41
+ if (!this._headers) this._headers = new Headers();
41
42
  return this._headers;
42
43
  }
43
44
  /**
@@ -58,6 +59,7 @@ class ShokupanResponse {
58
59
  * @param value Header value
59
60
  */
60
61
  set(key, value) {
62
+ if (!this._headers) this._headers = new Headers();
61
63
  this._headers.set(key, value);
62
64
  return this;
63
65
  }
@@ -67,6 +69,7 @@ class ShokupanResponse {
67
69
  * @param value Header value
68
70
  */
69
71
  append(key, value) {
72
+ if (!this._headers) this._headers = new Headers();
70
73
  this._headers.append(key, value);
71
74
  return this;
72
75
  }
@@ -75,27 +78,58 @@ class ShokupanResponse {
75
78
  * @param key Header name
76
79
  */
77
80
  get(key) {
78
- return this._headers.get(key);
81
+ return this._headers?.get(key) || null;
79
82
  }
80
83
  /**
81
84
  * Check if a header exists
82
85
  * @param key Header name
83
86
  */
84
87
  has(key) {
85
- return this._headers.has(key);
88
+ return this._headers?.has(key) || false;
89
+ }
90
+ /**
91
+ * Internal: check if headers have been initialized/modified
92
+ */
93
+ get hasPopulatedHeaders() {
94
+ return this._headers !== null;
86
95
  }
87
96
  }
88
97
  class ShokupanContext {
89
- constructor(request, state) {
98
+ constructor(request, server, state, app, enableMiddlewareTracking = false) {
90
99
  this.request = request;
91
- this.url = new URL(request.url);
100
+ this.server = server;
101
+ this.app = app;
92
102
  this.state = state || {};
103
+ if (enableMiddlewareTracking) {
104
+ const self = this;
105
+ this.state = new Proxy(this.state, {
106
+ set(target, p, newValue, receiver) {
107
+ const result = Reflect.set(target, p, newValue, receiver);
108
+ const currentHandler = self.handlerStack[self.handlerStack.length - 1];
109
+ if (currentHandler) {
110
+ if (!currentHandler.stateChanges) currentHandler.stateChanges = {};
111
+ currentHandler.stateChanges[p] = newValue;
112
+ }
113
+ return result;
114
+ }
115
+ });
116
+ }
93
117
  this.response = new ShokupanResponse();
94
118
  }
95
- url;
119
+ _url;
96
120
  params = {};
121
+ // Router assigns this, but default to empty object
97
122
  state;
123
+ handlerStack = [];
98
124
  response;
125
+ _finalResponse;
126
+ get url() {
127
+ if (!this._url) {
128
+ const urlString = this.request.url || "http://localhost/";
129
+ this._url = new URL(urlString);
130
+ }
131
+ return this._url;
132
+ }
99
133
  /**
100
134
  * Base request
101
135
  */
@@ -112,7 +146,26 @@ class ShokupanContext {
112
146
  * Request path
113
147
  */
114
148
  get path() {
115
- return this.url.pathname;
149
+ if (this._url) return this._url.pathname;
150
+ const url = this.request.url;
151
+ let queryIndex = url.indexOf("?");
152
+ const end = queryIndex === -1 ? url.length : queryIndex;
153
+ let start = 0;
154
+ const protocolIndex = url.indexOf("://");
155
+ if (protocolIndex !== -1) {
156
+ const hostStart = protocolIndex + 3;
157
+ const pathStart = url.indexOf("/", hostStart);
158
+ if (pathStart !== -1 && pathStart < end) {
159
+ start = pathStart;
160
+ } else {
161
+ return "/";
162
+ }
163
+ } else {
164
+ if (url.charCodeAt(0) === 47) {
165
+ start = 0;
166
+ }
167
+ }
168
+ return url.substring(start, end);
116
169
  }
117
170
  /**
118
171
  * Request query params
@@ -120,12 +173,55 @@ class ShokupanContext {
120
173
  get query() {
121
174
  return Object.fromEntries(this.url.searchParams);
122
175
  }
176
+ /**
177
+ * Client IP address
178
+ */
179
+ get ip() {
180
+ return this.server?.requestIP(this.request);
181
+ }
182
+ /**
183
+ * Request hostname (e.g. "localhost")
184
+ */
185
+ get hostname() {
186
+ return this.url.hostname;
187
+ }
188
+ /**
189
+ * Request host (e.g. "localhost:3000")
190
+ */
191
+ get host() {
192
+ return this.url.host;
193
+ }
194
+ /**
195
+ * Request protocol (e.g. "http:", "https:")
196
+ */
197
+ get protocol() {
198
+ return this.url.protocol;
199
+ }
200
+ /**
201
+ * Whether request is secure (https)
202
+ */
203
+ get secure() {
204
+ return this.url.protocol === "https:";
205
+ }
206
+ /**
207
+ * Request origin (e.g. "http://localhost:3000")
208
+ */
209
+ get origin() {
210
+ return this.url.origin;
211
+ }
123
212
  /**
124
213
  * Request headers
125
214
  */
126
215
  get headers() {
127
216
  return this.request.headers;
128
217
  }
218
+ /**
219
+ * Get a request header
220
+ * @param name Header name
221
+ */
222
+ get(name) {
223
+ return this.request.headers.get(name);
224
+ }
129
225
  /**
130
226
  * Base response object
131
227
  */
@@ -134,6 +230,8 @@ class ShokupanContext {
134
230
  }
135
231
  /**
136
232
  * Helper to set a header on the response
233
+ * @param key Header key
234
+ * @param value Header value
137
235
  */
138
236
  set(key, value) {
139
237
  this.response.set(key, value);
@@ -164,9 +262,25 @@ class ShokupanContext {
164
262
  return this;
165
263
  }
166
264
  mergeHeaders(headers) {
167
- const h = new Headers(this.response.headers);
265
+ let h;
266
+ if (this.response.hasPopulatedHeaders) {
267
+ h = new Headers(this.response.headers);
268
+ } else {
269
+ h = new Headers();
270
+ }
168
271
  if (headers) {
169
- new Headers(headers).forEach((v, k) => h.set(k, v));
272
+ if (headers instanceof Headers) {
273
+ headers.forEach((v, k) => h.set(k, v));
274
+ } else if (Array.isArray(headers)) {
275
+ headers.forEach(([k, v]) => h.set(k, v));
276
+ } else {
277
+ const keys = Object.keys(headers);
278
+ for (let i = 0; i < keys.length; i++) {
279
+ const key = keys[i];
280
+ const val = headers[key];
281
+ h.set(key, val);
282
+ }
283
+ }
170
284
  }
171
285
  return h;
172
286
  }
@@ -179,7 +293,8 @@ class ShokupanContext {
179
293
  send(body, options) {
180
294
  const headers = this.mergeHeaders(options?.headers);
181
295
  const status = options?.status ?? this.response.status;
182
- return new Response(body, { status, headers });
296
+ this._finalResponse = new Response(body, { status, headers });
297
+ return this._finalResponse;
183
298
  }
184
299
  /**
185
300
  * Read request body
@@ -198,19 +313,36 @@ class ShokupanContext {
198
313
  * Respond with a JSON object
199
314
  */
200
315
  json(data, status, headers) {
316
+ const finalStatus = status ?? this.response.status;
317
+ const jsonString = JSON.stringify(data);
318
+ if (!headers && !this.response.hasPopulatedHeaders) {
319
+ this._finalResponse = new Response(jsonString, {
320
+ status: finalStatus,
321
+ headers: { "content-type": "application/json" }
322
+ });
323
+ return this._finalResponse;
324
+ }
201
325
  const finalHeaders = this.mergeHeaders(headers);
202
326
  finalHeaders.set("content-type", "application/json");
203
- const finalStatus = status ?? this.response.status;
204
- return new Response(JSON.stringify(data), { status: finalStatus, headers: finalHeaders });
327
+ this._finalResponse = new Response(jsonString, { status: finalStatus, headers: finalHeaders });
328
+ return this._finalResponse;
205
329
  }
206
330
  /**
207
331
  * Respond with a text string
208
332
  */
209
333
  text(data, status, headers) {
334
+ const finalStatus = status ?? this.response.status;
335
+ if (!headers && !this.response.hasPopulatedHeaders) {
336
+ this._finalResponse = new Response(data, {
337
+ status: finalStatus,
338
+ headers: { "content-type": "text/plain" }
339
+ });
340
+ return this._finalResponse;
341
+ }
210
342
  const finalHeaders = this.mergeHeaders(headers);
211
343
  finalHeaders.set("content-type", "text/plain");
212
- const finalStatus = status ?? this.response.status;
213
- return new Response(data, { status: finalStatus, headers: finalHeaders });
344
+ this._finalResponse = new Response(data, { status: finalStatus, headers: finalHeaders });
345
+ return this._finalResponse;
214
346
  }
215
347
  /**
216
348
  * Respond with HTML content
@@ -219,7 +351,8 @@ class ShokupanContext {
219
351
  const finalHeaders = this.mergeHeaders(headers);
220
352
  finalHeaders.set("content-type", "text/html");
221
353
  const finalStatus = status ?? this.response.status;
222
- return new Response(html, { status: finalStatus, headers: finalHeaders });
354
+ this._finalResponse = new Response(html, { status: finalStatus, headers: finalHeaders });
355
+ return this._finalResponse;
223
356
  }
224
357
  /**
225
358
  * Respond with a redirect
@@ -227,7 +360,8 @@ class ShokupanContext {
227
360
  redirect(url, status = 302) {
228
361
  const headers = this.mergeHeaders();
229
362
  headers.set("Location", url);
230
- return new Response(null, { status, headers });
363
+ this._finalResponse = new Response(null, { status, headers });
364
+ return this._finalResponse;
231
365
  }
232
366
  /**
233
367
  * Respond with a status code
@@ -235,7 +369,8 @@ class ShokupanContext {
235
369
  */
236
370
  status(status) {
237
371
  const headers = this.mergeHeaders();
238
- return new Response(null, { status, headers });
372
+ this._finalResponse = new Response(null, { status, headers });
373
+ return this._finalResponse;
239
374
  }
240
375
  /**
241
376
  * Respond with a file
@@ -243,7 +378,25 @@ class ShokupanContext {
243
378
  file(path2, fileOptions, responseOptions) {
244
379
  const headers = this.mergeHeaders(responseOptions?.headers);
245
380
  const status = responseOptions?.status ?? this.response.status;
246
- return new Response(Bun.file(path2, fileOptions), { status, headers });
381
+ this._finalResponse = new Response(Bun.file(path2, fileOptions), { status, headers });
382
+ return this._finalResponse;
383
+ }
384
+ /**
385
+ * JSX Rendering Function
386
+ */
387
+ renderer;
388
+ /**
389
+ * Render a JSX element
390
+ * @param element JSX Element
391
+ * @param status HTTP Status
392
+ * @param headers HTTP Headers
393
+ */
394
+ async jsx(element, args, status, headers) {
395
+ if (!this.renderer) {
396
+ throw new Error("No JSX renderer configured");
397
+ }
398
+ const html = await this.renderer(element, args);
399
+ return this.html(html, status, headers);
247
400
  }
248
401
  }
249
402
  const $isApplication = /* @__PURE__ */ Symbol.for("Shokupan.app");
@@ -259,6 +412,8 @@ const $childRouters = /* @__PURE__ */ Symbol.for("Shokupan.child-routers");
259
412
  const $childControllers = /* @__PURE__ */ Symbol.for("Shokupan.child-controllers");
260
413
  const $mountPath = /* @__PURE__ */ Symbol.for("Shokupan.mount-path");
261
414
  const $dispatch = /* @__PURE__ */ Symbol.for("Shokupan.dispatch");
415
+ const $routes = /* @__PURE__ */ Symbol.for("Shokupan.routes");
416
+ const $routeSpec = /* @__PURE__ */ Symbol.for("Shokupan.routeSpec");
262
417
  const HTTPMethods = ["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS", "ALL"];
263
418
  var RouteParamType = /* @__PURE__ */ ((RouteParamType2) => {
264
419
  RouteParamType2["BODY"] = "BODY";
@@ -311,6 +466,14 @@ const Query = createParamDecorator(RouteParamType.QUERY);
311
466
  const Headers$1 = createParamDecorator(RouteParamType.HEADER);
312
467
  const Req = createParamDecorator(RouteParamType.REQUEST);
313
468
  const Ctx = createParamDecorator(RouteParamType.CONTEXT);
469
+ function Spec(spec) {
470
+ return (target, propertyKey, descriptor) => {
471
+ if (!target[$routeSpec]) {
472
+ target[$routeSpec] = /* @__PURE__ */ new Map();
473
+ }
474
+ target[$routeSpec].set(propertyKey, spec);
475
+ };
476
+ }
314
477
  function createMethodDecorator(method) {
315
478
  return (path2 = "/") => {
316
479
  return (target, propertyKey, descriptor) => {
@@ -365,83 +528,29 @@ function Inject(token) {
365
528
  });
366
529
  };
367
530
  }
368
- const provider = new sdkTraceNode.NodeTracerProvider({
369
- resource: resources.resourceFromAttributes({
370
- [semanticConventions.ATTR_SERVICE_NAME]: "basic-service"
371
- }),
372
- spanProcessors: [
373
- new sdkTraceBase.SimpleSpanProcessor(
374
- new exporterTraceOtlpProto.OTLPTraceExporter({
375
- url: "http://localhost:4318/v1/traces"
376
- // Default OTLP port
377
- })
378
- )
379
- ]
380
- });
381
- provider.register();
382
- const tracer = api.trace.getTracer("shokupan.middleware");
383
- function traceMiddleware(fn, name) {
384
- const middlewareName = fn.name || "anonymous middleware";
385
- return async (ctx, next) => {
386
- return tracer.startActiveSpan(`middleware - ${middlewareName}`, {
387
- kind: api.SpanKind.INTERNAL,
388
- attributes: {
389
- "code.function": middlewareName,
390
- "component": "shokupan.middleware"
391
- }
392
- }, async (span) => {
393
- try {
394
- const result = await fn(ctx, next);
395
- return result;
396
- } catch (err) {
397
- span.recordException(err);
398
- span.setStatus({ code: api.SpanStatusCode.ERROR, message: err.message });
399
- throw err;
400
- } finally {
401
- span.end();
402
- }
403
- });
404
- };
405
- }
406
- function traceHandler(fn, name) {
407
- return async function(...args) {
408
- return tracer.startActiveSpan(`route handler - ${name}`, {
409
- kind: api.SpanKind.INTERNAL,
410
- attributes: {
411
- "http.route": name,
412
- "component": "shokupan.route"
531
+ const compose = (middleware) => {
532
+ if (!middleware.length) {
533
+ return (context, next) => {
534
+ return next ? next() : Promise.resolve();
535
+ };
536
+ }
537
+ return function dispatch(context, next) {
538
+ let index = -1;
539
+ function runner(i) {
540
+ if (i <= index) return Promise.reject(new Error("next() called multiple times"));
541
+ index = i;
542
+ if (i >= middleware.length) {
543
+ return next ? next() : Promise.resolve();
413
544
  }
414
- }, async (span) => {
545
+ const fn = middleware[i];
415
546
  try {
416
- const result = await fn.apply(this, args);
417
- return result;
547
+ return Promise.resolve(fn(context, () => runner(i + 1)));
418
548
  } catch (err) {
419
- span.recordException(err);
420
- span.setStatus({ code: api.SpanStatusCode.ERROR, message: err.message });
421
- throw err;
422
- } finally {
423
- span.end();
549
+ return Promise.reject(err);
424
550
  }
425
- });
426
- };
427
- }
428
- const compose = (middleware) => {
429
- function fn(context, next) {
430
- let runner = next || (async () => {
431
- });
432
- for (let i = middleware.length - 1; i >= 0; i--) {
433
- const fn2 = traceMiddleware(middleware[i]);
434
- const nextStep = runner;
435
- let called = false;
436
- runner = async () => {
437
- if (called) throw new Error("next() called multiple times");
438
- called = true;
439
- return fn2(context, nextStep);
440
- };
441
551
  }
442
- return runner();
443
- }
444
- return fn;
552
+ return runner(0);
553
+ };
445
554
  };
446
555
  class ShokupanRequestBase {
447
556
  method;
@@ -468,7 +577,6 @@ class ShokupanRequestBase {
468
577
  }
469
578
  }
470
579
  const ShokupanRequest = ShokupanRequestBase;
471
- const asyncContext = new node_async_hooks.AsyncLocalStorage();
472
580
  function isObject(item) {
473
581
  return item && typeof item === "object" && !Array.isArray(item);
474
582
  }
@@ -482,7 +590,17 @@ function deepMerge(target, ...sources) {
482
590
  deepMerge(target[key], source[key]);
483
591
  } else if (Array.isArray(source[key])) {
484
592
  if (!target[key]) Object.assign(target, { [key]: [] });
485
- target[key] = target[key].concat(source[key]);
593
+ if (key === "tags") {
594
+ target[key] = source[key];
595
+ continue;
596
+ }
597
+ const mergedArray = target[key].concat(source[key]);
598
+ const isPrimitive = (item) => typeof item === "string" || typeof item === "number" || typeof item === "boolean";
599
+ if (mergedArray.every(isPrimitive)) {
600
+ target[key] = Array.from(new Set(mergedArray));
601
+ } else {
602
+ target[key] = mergedArray;
603
+ }
486
604
  } else {
487
605
  Object.assign(target, { [key]: source[key] });
488
606
  }
@@ -490,98 +608,709 @@ function deepMerge(target, ...sources) {
490
608
  }
491
609
  return deepMerge(target, ...sources);
492
610
  }
493
- const eta$1 = new eta$2.Eta();
494
- const RouterRegistry = /* @__PURE__ */ new Map();
495
- const ShokupanApplicationTree = {};
496
- class ShokupanRouter {
497
- constructor(config) {
498
- this.config = config;
611
+ function analyzeHandler(handler) {
612
+ const handlerSource = handler.toString();
613
+ const inferredSpec = {};
614
+ if (handlerSource.includes("ctx.body") || handlerSource.includes("await ctx.req.json()")) {
615
+ inferredSpec.requestBody = {
616
+ content: { "application/json": { schema: { type: "object" } } }
617
+ };
499
618
  }
500
- // Internal marker to identify Router vs. Application
501
- [$isApplication] = false;
502
- [$isMounted] = false;
503
- [$isRouter] = true;
504
- [$appRoot];
505
- [$mountPath] = "/";
506
- [$parent] = null;
507
- [$childRouters] = [];
508
- [$childControllers] = [];
509
- get rootConfig() {
510
- return this[$appRoot]?.applicationConfig;
619
+ const queryParams = /* @__PURE__ */ new Map();
620
+ const queryIntMatch = handlerSource.match(/parseInt\(ctx\.query\.(\w+)\)/g);
621
+ if (queryIntMatch) {
622
+ queryIntMatch.forEach((match) => {
623
+ const paramName = match.match(/ctx\.query\.(\w+)/)?.[1];
624
+ if (paramName) queryParams.set(paramName, { type: "integer", format: "int32" });
625
+ });
511
626
  }
512
- get root() {
513
- return this[$appRoot];
627
+ const queryFloatMatch = handlerSource.match(/parseFloat\(ctx\.query\.(\w+)\)/g);
628
+ if (queryFloatMatch) {
629
+ queryFloatMatch.forEach((match) => {
630
+ const paramName = match.match(/ctx\.query\.(\w+)/)?.[1];
631
+ if (paramName) queryParams.set(paramName, { type: "number", format: "float" });
632
+ });
514
633
  }
515
- routes = [];
516
- currentGuards = [];
517
- isRouterInstance(target) {
518
- return typeof target === "object" && target !== null && $isRouter in target;
634
+ const queryNumberMatch = handlerSource.match(/Number\(ctx\.query\.(\w+)\)/g);
635
+ if (queryNumberMatch) {
636
+ queryNumberMatch.forEach((match) => {
637
+ const paramName = match.match(/ctx\.query\.(\w+)/)?.[1];
638
+ if (paramName && !queryParams.has(paramName)) {
639
+ queryParams.set(paramName, { type: "number" });
640
+ }
641
+ });
519
642
  }
520
- /**
521
- * Mounts a controller instance to a path prefix.
522
- *
523
- * Controller can be a convection router or an arbitrary class.
524
- *
525
- * Routes are derived from method names:
526
- * - get(ctx) -> GET /prefix/
527
- * - getUsers(ctx) -> GET /prefix/users
528
- * - postCreate(ctx) -> POST /prefix/create
529
- */
530
- mount(prefix, controller) {
531
- if (this.isRouterInstance(controller)) {
532
- if (controller[$isMounted]) {
533
- throw new Error("Router is already mounted");
643
+ const queryBoolMatch = handlerSource.match(/(?:Boolean\(ctx\.query\.(\w+)\)|!+ctx\.query\.(\w+))/g);
644
+ if (queryBoolMatch) {
645
+ queryBoolMatch.forEach((match) => {
646
+ const paramName = match.match(/ctx\.query\.(\w+)/)?.[1];
647
+ if (paramName && !queryParams.has(paramName)) {
648
+ queryParams.set(paramName, { type: "boolean" });
534
649
  }
535
- controller[$mountPath] = prefix;
536
- this[$childRouters].push(controller);
537
- controller[$parent] = this;
538
- const setRouterContext = (router) => {
539
- router[$appRoot] = this.root;
540
- router[$childRouters].forEach((child) => setRouterContext(child));
541
- };
542
- setRouterContext(controller);
543
- if (this[$appRoot]) ;
544
- controller[$appRoot] = this.root;
545
- controller[$isMounted] = true;
650
+ });
651
+ }
652
+ const queryMatch = handlerSource.match(/ctx\.query\.(\w+)/g);
653
+ if (queryMatch) {
654
+ queryMatch.forEach((match) => {
655
+ const paramName = match.split(".")[2];
656
+ if (paramName && !queryParams.has(paramName)) {
657
+ queryParams.set(paramName, { type: "string" });
658
+ }
659
+ });
660
+ }
661
+ if (queryParams.size > 0) {
662
+ if (!inferredSpec.parameters) inferredSpec.parameters = [];
663
+ queryParams.forEach((schema, paramName) => {
664
+ inferredSpec.parameters.push({
665
+ name: paramName,
666
+ in: "query",
667
+ schema: { type: schema.type, ...schema.format ? { format: schema.format } : {} }
668
+ });
669
+ });
670
+ }
671
+ const pathParams = /* @__PURE__ */ new Map();
672
+ const paramIntMatch = handlerSource.match(/parseInt\(ctx\.params\.(\w+)\)/g);
673
+ if (paramIntMatch) {
674
+ paramIntMatch.forEach((match) => {
675
+ const paramName = match.match(/ctx\.params\.(\w+)/)?.[1];
676
+ if (paramName) pathParams.set(paramName, { type: "integer", format: "int32" });
677
+ });
678
+ }
679
+ const paramFloatMatch = handlerSource.match(/parseFloat\(ctx\.params\.(\w+)\)/g);
680
+ if (paramFloatMatch) {
681
+ paramFloatMatch.forEach((match) => {
682
+ const paramName = match.match(/ctx\.params\.(\w+)/)?.[1];
683
+ if (paramName) pathParams.set(paramName, { type: "number", format: "float" });
684
+ });
685
+ }
686
+ if (pathParams.size > 0) {
687
+ if (!inferredSpec.parameters) inferredSpec.parameters = [];
688
+ pathParams.forEach((schema, paramName) => {
689
+ inferredSpec.parameters.push({
690
+ name: paramName,
691
+ in: "path",
692
+ required: true,
693
+ schema: { type: schema.type, ...schema.format ? { format: schema.format } : {} }
694
+ });
695
+ });
696
+ }
697
+ const headerMatch = handlerSource.match(/ctx\.get\(['"](\w+)['"]\)/g);
698
+ if (headerMatch) {
699
+ if (!inferredSpec.parameters) inferredSpec.parameters = [];
700
+ headerMatch.forEach((match) => {
701
+ const headerName = match.match(/['"](\w+)['"]/)?.[1];
702
+ if (headerName) {
703
+ inferredSpec.parameters.push({
704
+ name: headerName,
705
+ in: "header",
706
+ schema: { type: "string" }
707
+ });
708
+ }
709
+ });
710
+ }
711
+ const responses = {};
712
+ if (handlerSource.includes("ctx.json(")) {
713
+ responses["200"] = {
714
+ description: "Successful response",
715
+ content: {
716
+ "application/json": { schema: { type: "object" } }
717
+ }
718
+ };
719
+ }
720
+ if (handlerSource.includes("ctx.html(")) {
721
+ responses["200"] = {
722
+ description: "Successful response",
723
+ content: {
724
+ "text/html": { schema: { type: "string" } }
725
+ }
726
+ };
727
+ }
728
+ if (handlerSource.includes("ctx.text(")) {
729
+ responses["200"] = {
730
+ description: "Successful response",
731
+ content: {
732
+ "text/plain": { schema: { type: "string" } }
733
+ }
734
+ };
735
+ }
736
+ if (handlerSource.includes("ctx.file(")) {
737
+ responses["200"] = {
738
+ description: "File download",
739
+ content: {
740
+ "application/octet-stream": { schema: { type: "string", format: "binary" } }
741
+ }
742
+ };
743
+ }
744
+ if (handlerSource.includes("ctx.redirect(")) {
745
+ responses["302"] = {
746
+ description: "Redirect"
747
+ };
748
+ }
749
+ if (!responses["200"] && /return\s+\{/.test(handlerSource)) {
750
+ responses["200"] = {
751
+ description: "Successful response",
752
+ content: {
753
+ "application/json": { schema: { type: "object" } }
754
+ }
755
+ };
756
+ }
757
+ const errorStatusMatch = handlerSource.match(/ctx\.(?:json|text|html)\([^)]+,\s*(\d{3,})\)/g);
758
+ if (errorStatusMatch) {
759
+ errorStatusMatch.forEach((match) => {
760
+ const statusCode = match.match(/,\s*(\d{3,})\)/)?.[1];
761
+ if (statusCode && statusCode !== "200") {
762
+ responses[statusCode] = {
763
+ description: `Error response (${statusCode})`
764
+ };
765
+ }
766
+ });
767
+ }
768
+ if (Object.keys(responses).length > 0) {
769
+ inferredSpec.responses = responses;
770
+ }
771
+ return { inferredSpec };
772
+ }
773
+ async function generateOpenApi(rootRouter, options = {}) {
774
+ const paths = {};
775
+ const tagGroups = /* @__PURE__ */ new Map();
776
+ const defaultTagGroup = options.defaultTagGroup || "General";
777
+ const defaultTagName = options.defaultTag || "Application";
778
+ let astRoutes = [];
779
+ try {
780
+ const { OpenAPIAnalyzer } = await Promise.resolve().then(() => require("./openapi-analyzer-BN0wFCML.cjs"));
781
+ const analyzer = new OpenAPIAnalyzer(process.cwd());
782
+ const { applications } = await analyzer.analyze();
783
+ const appMap = /* @__PURE__ */ new Map();
784
+ applications.forEach((app) => {
785
+ appMap.set(app.name, app);
786
+ if (app.name !== app.className) {
787
+ appMap.set(app.className, app);
788
+ }
789
+ });
790
+ const getExpandedRoutes = (app, prefix = "", seen = /* @__PURE__ */ new Set()) => {
791
+ if (seen.has(app.name)) return [];
792
+ const newSeen = new Set(seen);
793
+ newSeen.add(app.name);
794
+ const expanded = [];
795
+ for (const route of app.routes) {
796
+ const cleanPrefix = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
797
+ const cleanPath = route.path.startsWith("/") ? route.path : "/" + route.path;
798
+ let joined = cleanPrefix + cleanPath;
799
+ if (joined.length > 1 && joined.endsWith("/")) {
800
+ joined = joined.slice(0, -1);
801
+ }
802
+ expanded.push({
803
+ ...route,
804
+ path: joined || "/"
805
+ });
806
+ }
807
+ if (app.mounted) {
808
+ for (const mount of app.mounted) {
809
+ const targetApp = appMap.get(mount.target);
810
+ if (targetApp) {
811
+ const cleanPrefix = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
812
+ const mountPrefix = mount.prefix.startsWith("/") ? mount.prefix : "/" + mount.prefix;
813
+ expanded.push(...getExpandedRoutes(targetApp, cleanPrefix + mountPrefix, newSeen));
814
+ }
815
+ }
816
+ }
817
+ return expanded;
818
+ };
819
+ applications.forEach((app) => {
820
+ astRoutes.push(...getExpandedRoutes(app));
821
+ });
822
+ const dedupedRoutes = /* @__PURE__ */ new Map();
823
+ for (const route of astRoutes) {
824
+ const key = `${route.method.toUpperCase()}:${route.path}`;
825
+ let score = 0;
826
+ if (route.responseSchema) score += 10;
827
+ if (route.handlerSource) score += 5;
828
+ if (!dedupedRoutes.has(key) || score > dedupedRoutes.get(key).score) {
829
+ dedupedRoutes.set(key, { route, score });
830
+ }
831
+ }
832
+ astRoutes = Array.from(dedupedRoutes.values()).map((v) => v.route);
833
+ } catch (e) {
834
+ console.warn("OpenAPI AST analysis failed or skipped:", e);
835
+ }
836
+ const collect = (router, prefix = "", currentGroup = defaultTagGroup, defaultTag = defaultTagName) => {
837
+ let group = currentGroup;
838
+ let tag = defaultTag;
839
+ if (router.config?.group) group = router.config.group;
840
+ if (router.config?.name) {
841
+ tag = router.config.name;
546
842
  } else {
547
- let instance = controller;
548
- if (typeof controller === "function") {
549
- instance = Container.resolve(controller);
550
- const controllerPath = controller[$controllerPath];
551
- if (controllerPath) {
552
- const p1 = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
553
- const p2 = controllerPath.startsWith("/") ? controllerPath : "/" + controllerPath;
554
- prefix = p1 + p2;
555
- if (!prefix) prefix = "/";
843
+ const mountPath = router[$mountPath];
844
+ if (mountPath && mountPath !== "/") {
845
+ const segments = mountPath.split("/").filter(Boolean);
846
+ if (segments.length > 0) {
847
+ const lastSegment = segments[segments.length - 1];
848
+ tag = lastSegment.replace(/[-_]/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
556
849
  }
557
850
  }
558
- instance[$mountPath] = prefix;
559
- this[$childControllers].push(instance);
560
- const controllerMiddleware = (typeof controller === "function" ? controller[$middleware] : instance[$middleware]) || [];
561
- const proto = Object.getPrototypeOf(instance);
562
- const methods = /* @__PURE__ */ new Set();
563
- let current = proto;
564
- while (current && current !== Object.prototype) {
565
- Object.getOwnPropertyNames(current).forEach((name) => methods.add(name));
566
- current = Object.getPrototypeOf(current);
851
+ }
852
+ if (!tagGroups.has(group)) tagGroups.set(group, /* @__PURE__ */ new Set());
853
+ const routes = router[$routes] || [];
854
+ for (const route of routes) {
855
+ const routeGroup = route.group || group;
856
+ const cleanPrefix = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
857
+ const cleanSubPath = route.path.startsWith("/") ? route.path : "/" + route.path;
858
+ let fullPath = cleanPrefix + cleanSubPath || "/";
859
+ fullPath = fullPath.replace(/:([a-zA-Z0-9_]+)/g, "{$1}");
860
+ if (fullPath.length > 1 && fullPath.endsWith("/")) {
861
+ fullPath = fullPath.slice(0, -1);
567
862
  }
568
- Object.getOwnPropertyNames(instance).forEach((name) => methods.add(name));
569
- const decoratedRoutes = instance[$routeMethods] || proto && proto[$routeMethods];
570
- const decoratedArgs = instance[$routeArgs] || proto && proto[$routeArgs];
571
- const methodMiddlewareMap = instance[$middleware] || proto && proto[$middleware];
572
- let routesAttached = 0;
573
- for (const name of methods) {
574
- if (name === "constructor") continue;
575
- if (["arguments", "caller", "callee"].includes(name)) continue;
576
- const originalHandler = instance[name];
577
- if (typeof originalHandler !== "function") continue;
578
- let method;
579
- let subPath = "";
580
- if (decoratedRoutes && decoratedRoutes.has(name)) {
581
- const config = decoratedRoutes.get(name);
582
- method = config.method;
583
- subPath = config.path;
584
- } else {
863
+ if (!paths[fullPath]) paths[fullPath] = {};
864
+ const operation = {
865
+ responses: { "200": { description: "Successful response" } },
866
+ tags: [tag]
867
+ };
868
+ if (route.guards) {
869
+ for (const guard of route.guards) {
870
+ if (guard.spec) {
871
+ if (guard.spec.security) {
872
+ const existing = operation.security || [];
873
+ for (const req of guard.spec.security) {
874
+ const reqStr = JSON.stringify(req);
875
+ if (!existing.some((e) => JSON.stringify(e) === reqStr)) {
876
+ existing.push(req);
877
+ }
878
+ }
879
+ operation.security = existing;
880
+ }
881
+ if (guard.spec.responses) {
882
+ operation.responses = { ...operation.responses, ...guard.spec.responses };
883
+ }
884
+ }
885
+ }
886
+ }
887
+ let astMatch = astRoutes.find(
888
+ (r) => r.method.toUpperCase() === route.method.toUpperCase() && r.path === fullPath
889
+ );
890
+ if (!astMatch) {
891
+ let runtimeSource = route.handler.toString();
892
+ if (route.handler.originalHandler) {
893
+ runtimeSource = route.handler.originalHandler.toString();
894
+ }
895
+ const runtimeHandlerSrc = runtimeSource.replace(/\s+/g, " ");
896
+ const sameMethodRoutes = astRoutes.filter((r) => r.method.toUpperCase() === route.method.toUpperCase());
897
+ astMatch = sameMethodRoutes.find((r) => {
898
+ const astHandlerSrc = (r.handlerSource || r.handlerName || "").replace(/\s+/g, " ");
899
+ if (!astHandlerSrc || astHandlerSrc.length < 20) return false;
900
+ const match = runtimeHandlerSrc.includes(astHandlerSrc) || astHandlerSrc.includes(runtimeHandlerSrc) || r.handlerSource && runtimeHandlerSrc.includes(r.handlerSource.substring(0, 50));
901
+ return match;
902
+ });
903
+ }
904
+ const potentialMatches = astRoutes.filter(
905
+ (r) => r.method.toUpperCase() === route.method.toUpperCase() && r.path === fullPath
906
+ );
907
+ if (potentialMatches.length > 1) {
908
+ const runtimeHandlerSrc = route.handler.toString().replace(/\s+/g, " ");
909
+ const preciseMatch = potentialMatches.find((r) => {
910
+ const astHandlerSrc = (r.handlerSource || r.handlerName || "").replace(/\s+/g, " ");
911
+ const match = runtimeHandlerSrc.includes(astHandlerSrc) || astHandlerSrc.includes(runtimeHandlerSrc) || r.handlerSource && runtimeHandlerSrc.includes(r.handlerSource.substring(0, 50));
912
+ return match;
913
+ });
914
+ if (preciseMatch) {
915
+ astMatch = preciseMatch;
916
+ }
917
+ }
918
+ if (astMatch) {
919
+ if (astMatch.summary) operation.summary = astMatch.summary;
920
+ if (astMatch.description) operation.description = astMatch.description;
921
+ if (astMatch.tags) operation.tags = astMatch.tags;
922
+ if (astMatch.operationId) operation.operationId = astMatch.operationId;
923
+ if (astMatch.requestTypes?.body) {
924
+ operation.requestBody = {
925
+ content: {
926
+ "application/json": { schema: astMatch.requestTypes.body }
927
+ }
928
+ };
929
+ }
930
+ if (astMatch.responseSchema) {
931
+ operation.responses["200"] = {
932
+ description: "Successful response",
933
+ content: {
934
+ "application/json": { schema: astMatch.responseSchema }
935
+ }
936
+ };
937
+ } else if (astMatch.responseType) {
938
+ const contentType = astMatch.responseType === "string" ? "text/plain" : "application/json";
939
+ operation.responses["200"] = {
940
+ description: "Successful response",
941
+ content: {
942
+ [contentType]: { schema: { type: astMatch.responseType } }
943
+ }
944
+ };
945
+ }
946
+ const params = [];
947
+ if (astMatch.requestTypes?.query) {
948
+ for (const [name, _type] of Object.entries(astMatch.requestTypes.query)) {
949
+ params.push({ name, in: "query", schema: { type: "string" } });
950
+ }
951
+ }
952
+ if (params.length > 0) {
953
+ operation.parameters = params;
954
+ }
955
+ }
956
+ if (route.keys.length > 0) {
957
+ const pathParams = route.keys.map((key) => ({
958
+ name: key,
959
+ in: "path",
960
+ required: true,
961
+ schema: { type: "string" }
962
+ }));
963
+ const existingParams = operation.parameters || [];
964
+ const mergedParams = [...existingParams];
965
+ pathParams.forEach((p) => {
966
+ const idx = mergedParams.findIndex((ep) => ep.in === "path" && ep.name === p.name);
967
+ if (idx >= 0) {
968
+ mergedParams[idx] = deepMerge(mergedParams[idx], p);
969
+ } else {
970
+ mergedParams.push(p);
971
+ }
972
+ });
973
+ operation.parameters = mergedParams;
974
+ }
975
+ const { inferredSpec } = analyzeHandler(route.handler);
976
+ if (inferredSpec) {
977
+ if (inferredSpec.parameters) {
978
+ const existingParams = operation.parameters || [];
979
+ const mergedParams = [...existingParams];
980
+ for (const p of inferredSpec.parameters) {
981
+ const idx = mergedParams.findIndex((ep) => ep.name === p.name && ep.in === p.in);
982
+ if (idx >= 0) {
983
+ mergedParams[idx] = deepMerge(mergedParams[idx], p);
984
+ } else {
985
+ mergedParams.push(p);
986
+ }
987
+ }
988
+ operation.parameters = mergedParams;
989
+ delete inferredSpec.parameters;
990
+ }
991
+ deepMerge(operation, inferredSpec);
992
+ }
993
+ if (route.handlerSpec) {
994
+ const spec = route.handlerSpec;
995
+ if (spec.summary) operation.summary = spec.summary;
996
+ if (spec.description) operation.description = spec.description;
997
+ if (spec.operationId) operation.operationId = spec.operationId;
998
+ if (spec.tags) operation.tags = spec.tags;
999
+ if (spec.security) operation.security = spec.security;
1000
+ if (spec.responses) {
1001
+ operation.responses = { ...operation.responses, ...spec.responses };
1002
+ }
1003
+ }
1004
+ if (!operation.tags || operation.tags.length === 0) operation.tags = [tag];
1005
+ if (operation.tags) {
1006
+ operation.tags = Array.from(new Set(operation.tags));
1007
+ for (const t of operation.tags) {
1008
+ if (!tagGroups.has(routeGroup)) tagGroups.set(routeGroup, /* @__PURE__ */ new Set());
1009
+ tagGroups.get(routeGroup)?.add(t);
1010
+ }
1011
+ }
1012
+ const methodLower = route.method.toLowerCase();
1013
+ if (methodLower === "all") {
1014
+ ["get", "post", "put", "delete", "patch"].forEach((m) => {
1015
+ if (!paths[fullPath][m]) paths[fullPath][m] = { ...operation };
1016
+ });
1017
+ } else {
1018
+ paths[fullPath][methodLower] = operation;
1019
+ }
1020
+ }
1021
+ for (const controller of router[$childControllers]) {
1022
+ const controllerName = controller.constructor.name || "UnknownController";
1023
+ tagGroups.get(group)?.add(controllerName);
1024
+ }
1025
+ for (const child of router[$childRouters]) {
1026
+ const mountPath = child[$mountPath];
1027
+ const cleanPrefix = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
1028
+ const cleanMount = mountPath.startsWith("/") ? mountPath : "/" + mountPath;
1029
+ const nextPrefix = cleanPrefix + cleanMount || "/";
1030
+ collect(child, nextPrefix, group, tag);
1031
+ }
1032
+ };
1033
+ collect(rootRouter);
1034
+ const xTagGroups = [];
1035
+ for (const [name, tags] of tagGroups) {
1036
+ xTagGroups.push({ name, tags: Array.from(tags).sort() });
1037
+ }
1038
+ return {
1039
+ openapi: "3.1.0",
1040
+ info: { title: "Shokupan API", version: "1.0.0", ...options.info },
1041
+ paths,
1042
+ components: options.components,
1043
+ servers: options.servers,
1044
+ tags: options.tags,
1045
+ externalDocs: options.externalDocs,
1046
+ "x-tagGroups": xTagGroups
1047
+ };
1048
+ }
1049
+ const eta$1 = new eta$2.Eta();
1050
+ function serveStatic(ctx, config, prefix) {
1051
+ const rootPath = path.resolve(config.root || ".");
1052
+ const normalizedPrefix = prefix.endsWith("/") && prefix !== "/" ? prefix.slice(0, -1) : prefix;
1053
+ return async () => {
1054
+ let relative = ctx.path.slice(normalizedPrefix.length);
1055
+ if (!relative.startsWith("/") && relative.length > 0) relative = "/" + relative;
1056
+ if (relative.length === 0) relative = "/";
1057
+ relative = decodeURIComponent(relative);
1058
+ const requestPath = path.join(rootPath, relative);
1059
+ if (!requestPath.startsWith(rootPath)) {
1060
+ return ctx.json({ error: "Forbidden" }, 403);
1061
+ }
1062
+ if (requestPath.includes("\0")) {
1063
+ return ctx.json({ error: "Forbidden" }, 403);
1064
+ }
1065
+ if (config.hooks?.onRequest) {
1066
+ const res = await config.hooks.onRequest(ctx);
1067
+ if (res) return res;
1068
+ }
1069
+ if (config.exclude) {
1070
+ for (const pattern of config.exclude) {
1071
+ if (pattern instanceof RegExp) {
1072
+ if (pattern.test(relative)) return ctx.json({ error: "Forbidden" }, 403);
1073
+ } else if (typeof pattern === "string") {
1074
+ if (relative.includes(pattern)) return ctx.json({ error: "Forbidden" }, 403);
1075
+ }
1076
+ }
1077
+ }
1078
+ if (path.basename(requestPath).startsWith(".")) {
1079
+ const behavior = config.dotfiles || "ignore";
1080
+ if (behavior === "deny") return ctx.json({ error: "Forbidden" }, 403);
1081
+ if (behavior === "ignore") return ctx.json({ error: "Not Found" }, 404);
1082
+ }
1083
+ let finalPath = requestPath;
1084
+ let stats;
1085
+ try {
1086
+ stats = await promises.stat(requestPath);
1087
+ } catch (e) {
1088
+ if (config.extensions) {
1089
+ for (const ext of config.extensions) {
1090
+ const p = requestPath + (ext.startsWith(".") ? ext : "." + ext);
1091
+ try {
1092
+ const s = await promises.stat(p);
1093
+ if (s.isFile()) {
1094
+ finalPath = p;
1095
+ stats = s;
1096
+ break;
1097
+ }
1098
+ } catch {
1099
+ }
1100
+ }
1101
+ }
1102
+ if (!stats) return ctx.json({ error: "Not Found" }, 404);
1103
+ }
1104
+ if (stats.isDirectory()) {
1105
+ if (!ctx.path.endsWith("/")) {
1106
+ const query = ctx.url.search;
1107
+ return ctx.redirect(ctx.path + "/" + query, 302);
1108
+ }
1109
+ let indexes = [];
1110
+ if (config.index === void 0) {
1111
+ indexes = ["index.html", "index.htm"];
1112
+ } else if (Array.isArray(config.index)) {
1113
+ indexes = config.index;
1114
+ } else if (config.index) {
1115
+ indexes = [config.index];
1116
+ }
1117
+ let foundIndex = false;
1118
+ for (const idx of indexes) {
1119
+ const idxPath = path.join(finalPath, idx);
1120
+ try {
1121
+ const idxStats = await promises.stat(idxPath);
1122
+ if (idxStats.isFile()) {
1123
+ finalPath = idxPath;
1124
+ foundIndex = true;
1125
+ break;
1126
+ }
1127
+ } catch {
1128
+ }
1129
+ }
1130
+ if (!foundIndex) {
1131
+ if (config.listDirectory) {
1132
+ try {
1133
+ const files = await promises.readdir(requestPath);
1134
+ const listing = eta$1.renderString(`
1135
+ <!DOCTYPE html>
1136
+ <html>
1137
+ <head>
1138
+ <title>Index of <%= it.relative %></title>
1139
+ <style>
1140
+ body { font-family: system-ui, -apple-system, sans-serif; padding: 2rem; max-width: 800px; margin: 0 auto; }
1141
+ ul { list-style: none; padding: 0; }
1142
+ li { padding: 0.5rem 0; border-bottom: 1px solid #eee; }
1143
+ a { text-decoration: none; color: #0066cc; }
1144
+ a:hover { text-decoration: underline; }
1145
+ h1 { font-size: 1.5rem; margin-bottom: 1rem; }
1146
+ </style>
1147
+ </head>
1148
+ <body>
1149
+ <h1>Index of <%= it.relative %></h1>
1150
+ <ul>
1151
+ <% if (it.relative !== '/') { %>
1152
+ <li><a href="../">../</a></li>
1153
+ <% } %>
1154
+ <% it.files.forEach(function(f) { %>
1155
+ <li><a href="<%= f %>"><%= f %></a></li>
1156
+ <% }) %>
1157
+ </ul>
1158
+ </body>
1159
+ </html>
1160
+ `, { relative, files, join: path.join });
1161
+ return new Response(listing, { headers: { "Content-Type": "text/html" } });
1162
+ } catch (e) {
1163
+ return ctx.json({ error: "Internal Server Error" }, 500);
1164
+ }
1165
+ } else {
1166
+ return ctx.json({ error: "Forbidden" }, 403);
1167
+ }
1168
+ }
1169
+ }
1170
+ const file = Bun.file(finalPath);
1171
+ let response = new Response(file);
1172
+ if (config.hooks?.onResponse) {
1173
+ const hooked = await config.hooks.onResponse(ctx, response);
1174
+ if (hooked) response = hooked;
1175
+ }
1176
+ return response;
1177
+ };
1178
+ }
1179
+ const asyncContext = new node_async_hooks.AsyncLocalStorage();
1180
+ const tracer = api.trace.getTracer("shokupan.middleware");
1181
+ function traceHandler(fn, name) {
1182
+ return async function(...args) {
1183
+ return tracer.startActiveSpan(`route handler - ${name}`, {
1184
+ kind: api.SpanKind.INTERNAL,
1185
+ attributes: {
1186
+ "http.route": name,
1187
+ "component": "shokupan.route"
1188
+ }
1189
+ }, async (span) => {
1190
+ try {
1191
+ const result = await fn.apply(this, args);
1192
+ return result;
1193
+ } catch (err) {
1194
+ span.recordException(err);
1195
+ span.setStatus({ code: api.SpanStatusCode.ERROR, message: err.message });
1196
+ throw err;
1197
+ } finally {
1198
+ span.end();
1199
+ }
1200
+ });
1201
+ };
1202
+ }
1203
+ const RouterRegistry = /* @__PURE__ */ new Map();
1204
+ const ShokupanApplicationTree = {};
1205
+ class ShokupanRouter {
1206
+ constructor(config) {
1207
+ this.config = config;
1208
+ if (config?.requestTimeout) {
1209
+ this.requestTimeout = config.requestTimeout;
1210
+ }
1211
+ }
1212
+ // Internal marker to identify Router vs. Application
1213
+ [$isApplication] = false;
1214
+ [$isMounted] = false;
1215
+ [$isRouter] = true;
1216
+ [$appRoot];
1217
+ [$mountPath] = "/";
1218
+ // Public via Symbol for OpenAPI generator
1219
+ [$parent] = null;
1220
+ [$childRouters] = [];
1221
+ [$childControllers] = [];
1222
+ get rootConfig() {
1223
+ return this[$appRoot]?.applicationConfig;
1224
+ }
1225
+ get root() {
1226
+ return this[$appRoot];
1227
+ }
1228
+ [$routes] = [];
1229
+ // Public via Symbol for OpenAPI generator
1230
+ currentGuards = [];
1231
+ isRouterInstance(target) {
1232
+ return typeof target === "object" && target !== null && $isRouter in target;
1233
+ }
1234
+ /**
1235
+ * Mounts a controller instance to a path prefix.
1236
+ *
1237
+ * Controller can be a convection router or an arbitrary class.
1238
+ *
1239
+ * Routes are derived from method names:
1240
+ * - get(ctx) -> GET /prefix/
1241
+ * - getUsers(ctx) -> GET /prefix/users
1242
+ * - postCreate(ctx) -> POST /prefix/create
1243
+ */
1244
+ mount(prefix, controller) {
1245
+ const isRouter = this.isRouterInstance(controller);
1246
+ const isFunction = typeof controller === "function";
1247
+ const controllersOnly = this.config?.controllersOnly ?? this.rootConfig?.controllersOnly ?? false;
1248
+ if (controllersOnly && !isFunction && !isRouter) {
1249
+ throw new Error(`[Shokupan] strict controller check failed: ${controller.constructor.name || typeof controller} is not a class constructor.`);
1250
+ }
1251
+ if (this.isRouterInstance(controller)) {
1252
+ if (controller[$isMounted]) {
1253
+ throw new Error("Router is already mounted");
1254
+ }
1255
+ controller[$mountPath] = prefix;
1256
+ this[$childRouters].push(controller);
1257
+ controller[$parent] = this;
1258
+ const setRouterContext = (router) => {
1259
+ router[$appRoot] = this.root;
1260
+ router[$childRouters].forEach((child) => setRouterContext(child));
1261
+ };
1262
+ setRouterContext(controller);
1263
+ if (this[$appRoot]) ;
1264
+ controller[$appRoot] = this.root;
1265
+ controller[$isMounted] = true;
1266
+ } else {
1267
+ let instance = controller;
1268
+ if (typeof controller === "function") {
1269
+ instance = Container.resolve(controller);
1270
+ const controllerPath = controller[$controllerPath];
1271
+ if (controllerPath) {
1272
+ const p1 = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
1273
+ const p2 = controllerPath.startsWith("/") ? controllerPath : "/" + controllerPath;
1274
+ prefix = p1 + p2;
1275
+ if (!prefix) prefix = "/";
1276
+ }
1277
+ } else {
1278
+ const ctor = instance.constructor;
1279
+ const controllerPath = ctor[$controllerPath];
1280
+ if (controllerPath) {
1281
+ const p1 = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
1282
+ const p2 = controllerPath.startsWith("/") ? controllerPath : "/" + controllerPath;
1283
+ prefix = p1 + p2;
1284
+ if (!prefix) prefix = "/";
1285
+ }
1286
+ }
1287
+ instance[$mountPath] = prefix;
1288
+ this[$childControllers].push(instance);
1289
+ const controllerMiddleware = (typeof controller === "function" ? controller[$middleware] : instance[$middleware]) || [];
1290
+ const proto = Object.getPrototypeOf(instance);
1291
+ const methods = /* @__PURE__ */ new Set();
1292
+ let current = proto;
1293
+ while (current && current !== Object.prototype) {
1294
+ Object.getOwnPropertyNames(current).forEach((name) => methods.add(name));
1295
+ current = Object.getPrototypeOf(current);
1296
+ }
1297
+ Object.getOwnPropertyNames(instance).forEach((name) => methods.add(name));
1298
+ const decoratedRoutes = instance[$routeMethods] || proto && proto[$routeMethods];
1299
+ const decoratedArgs = instance[$routeArgs] || proto && proto[$routeArgs];
1300
+ const methodMiddlewareMap = instance[$middleware] || proto && proto[$middleware];
1301
+ let routesAttached = 0;
1302
+ for (const name of methods) {
1303
+ if (name === "constructor") continue;
1304
+ if (["arguments", "caller", "callee"].includes(name)) continue;
1305
+ const originalHandler = instance[name];
1306
+ if (typeof originalHandler !== "function") continue;
1307
+ let method;
1308
+ let subPath = "";
1309
+ if (decoratedRoutes && decoratedRoutes.has(name)) {
1310
+ const config = decoratedRoutes.get(name);
1311
+ method = config.method;
1312
+ subPath = config.path;
1313
+ } else {
585
1314
  for (const m of HTTPMethods) {
586
1315
  if (name.toUpperCase().startsWith(m)) {
587
1316
  method = m;
@@ -661,7 +1390,7 @@ class ShokupanRouter {
661
1390
  }
662
1391
  }
663
1392
  }
664
- const tracedOriginalHandler = traceHandler(originalHandler, normalizedPath);
1393
+ const tracedOriginalHandler = ctx.app?.applicationConfig.enableTracing ? traceHandler(originalHandler, normalizedPath) : originalHandler;
665
1394
  return tracedOriginalHandler.apply(instance, args);
666
1395
  };
667
1396
  let finalHandler = wrappedHandler;
@@ -671,8 +1400,14 @@ class ShokupanRouter {
671
1400
  return composed(ctx, () => wrappedHandler(ctx));
672
1401
  };
673
1402
  }
1403
+ finalHandler.originalHandler = originalHandler;
1404
+ if (finalHandler !== wrappedHandler) {
1405
+ wrappedHandler.originalHandler = originalHandler;
1406
+ }
674
1407
  const tagName = instance.constructor.name;
675
- const spec = { tags: [tagName] };
1408
+ const decoratedSpecs = instance[$routeSpec] || proto && proto[$routeSpec];
1409
+ const userSpec = decoratedSpecs && decoratedSpecs.get(name);
1410
+ const spec = { tags: [tagName], ...userSpec };
676
1411
  this.add({ method, path: normalizedPath, handler: finalHandler, spec });
677
1412
  }
678
1413
  }
@@ -687,7 +1422,7 @@ class ShokupanRouter {
687
1422
  * Returns all routes attached to this router and its descendants.
688
1423
  */
689
1424
  getRoutes() {
690
- const routes = this.routes.map((r) => ({
1425
+ const routes = this[$routes].map((r) => ({
691
1426
  method: r.method,
692
1427
  path: r.path,
693
1428
  handler: r.handler
@@ -788,6 +1523,31 @@ class ShokupanRouter {
788
1523
  data: result
789
1524
  };
790
1525
  }
1526
+ applyHooks(match) {
1527
+ if (!this.config?.hooks) return match;
1528
+ const hooks = this.config.hooks;
1529
+ if (!hooks.onRequestStart && !hooks.onRequestEnd && !hooks.onError) return match;
1530
+ const originalHandler = match.handler;
1531
+ match.handler = async (ctx) => {
1532
+ if (hooks.onRequestStart) await hooks.onRequestStart(ctx);
1533
+ try {
1534
+ const result = await originalHandler(ctx);
1535
+ if (hooks.onRequestEnd) await hooks.onRequestEnd(ctx);
1536
+ return result;
1537
+ } catch (err) {
1538
+ if (hooks.onError) {
1539
+ try {
1540
+ await hooks.onError(err, ctx);
1541
+ } catch (e) {
1542
+ console.error("Error in router onError hook:", e);
1543
+ }
1544
+ }
1545
+ throw err;
1546
+ }
1547
+ };
1548
+ match.handler.originalHandler = originalHandler.originalHandler || originalHandler;
1549
+ return match;
1550
+ }
791
1551
  /**
792
1552
  * Find a route matching the given method and path.
793
1553
  * @param method HTTP method
@@ -795,29 +1555,38 @@ class ShokupanRouter {
795
1555
  * @returns Route handler and parameters if found, otherwise null
796
1556
  */
797
1557
  find(method, path2) {
798
- for (const route of this.routes) {
799
- if (route.method !== "ALL" && route.method !== method) continue;
800
- const match = route.regex.exec(path2);
801
- if (match) {
802
- const params = {};
803
- route.keys.forEach((key, index) => {
804
- params[key] = match[index + 1];
805
- });
806
- return { handler: route.handler, params };
1558
+ const findInRoutes = (routes, m) => {
1559
+ for (const route of routes) {
1560
+ if (route.method !== "ALL" && route.method !== m) continue;
1561
+ const match = route.regex.exec(path2);
1562
+ if (match) {
1563
+ const params = {};
1564
+ route.keys.forEach((key, index) => {
1565
+ params[key] = match[index + 1];
1566
+ });
1567
+ return this.applyHooks({ handler: route.handler, params });
1568
+ }
807
1569
  }
1570
+ return null;
1571
+ };
1572
+ let result = findInRoutes(this[$routes], method);
1573
+ if (result) return result;
1574
+ if (method === "HEAD") {
1575
+ result = findInRoutes(this[$routes], "GET");
1576
+ if (result) return result;
808
1577
  }
809
1578
  for (const child of this[$childRouters]) {
810
1579
  const prefix = child[$mountPath];
811
1580
  if (path2 === prefix || path2.startsWith(prefix + "/")) {
812
1581
  const subPath = path2.slice(prefix.length) || "/";
813
1582
  const match = child.find(method, subPath);
814
- if (match) return match;
1583
+ if (match) return this.applyHooks(match);
815
1584
  }
816
1585
  if (prefix.endsWith("/")) {
817
1586
  if (path2.startsWith(prefix)) {
818
1587
  const subPath = path2.slice(prefix.length) || "/";
819
1588
  const match = child.find(method, subPath);
820
- if (match) return match;
1589
+ if (match) return this.applyHooks(match);
821
1590
  }
822
1591
  }
823
1592
  }
@@ -835,6 +1604,7 @@ class ShokupanRouter {
835
1604
  };
836
1605
  }
837
1606
  // --- Functional Routing ---
1607
+ requestTimeout;
838
1608
  /**
839
1609
  * Adds a route to the router.
840
1610
  *
@@ -842,12 +1612,25 @@ class ShokupanRouter {
842
1612
  * @param path - URL path
843
1613
  * @param spec - OpenAPI specification for the route
844
1614
  * @param handler - Route handler function
1615
+ * @param requestTimeout - Timeout for this route in milliseconds
845
1616
  */
846
- add({ method, path: path2, spec, handler, regex: customRegex, group }) {
1617
+ add({ method, path: path2, spec, handler, regex: customRegex, group, requestTimeout, renderer }) {
847
1618
  const { regex, keys } = customRegex ? { regex: customRegex, keys: [] } : this.parsePath(path2);
848
1619
  let wrappedHandler = handler;
849
1620
  const routeGuards = [...this.currentGuards];
1621
+ const effectiveTimeout = requestTimeout ?? this.requestTimeout ?? this.rootConfig?.requestTimeout;
1622
+ if (effectiveTimeout !== void 0 && effectiveTimeout > 0) {
1623
+ const originalHandler = wrappedHandler;
1624
+ wrappedHandler = async (ctx) => {
1625
+ if (ctx.server) {
1626
+ ctx.server.timeout(ctx.req, effectiveTimeout / 1e3);
1627
+ }
1628
+ return originalHandler(ctx);
1629
+ };
1630
+ wrappedHandler.originalHandler = originalHandler.originalHandler || originalHandler;
1631
+ }
850
1632
  if (routeGuards.length > 0) {
1633
+ const innerHandler = wrappedHandler;
851
1634
  wrappedHandler = async (ctx) => {
852
1635
  for (const guard of routeGuards) {
853
1636
  let guardPassed = false;
@@ -872,10 +1655,47 @@ class ShokupanRouter {
872
1655
  return ctx.json({ error: "Forbidden" }, 403);
873
1656
  }
874
1657
  }
875
- return handler(ctx);
1658
+ return innerHandler(ctx);
1659
+ };
1660
+ }
1661
+ const effectiveRenderer = renderer ?? this.config?.renderer ?? this.rootConfig?.renderer;
1662
+ if (effectiveRenderer) {
1663
+ const innerHandler = wrappedHandler;
1664
+ wrappedHandler = async (ctx) => {
1665
+ ctx.renderer = effectiveRenderer;
1666
+ return innerHandler(ctx);
876
1667
  };
877
1668
  }
878
- this.routes.push({
1669
+ let file = "unknown";
1670
+ let line = 0;
1671
+ try {
1672
+ const err = new Error();
1673
+ const stack = err.stack?.split("\n") || [];
1674
+ const callerLine = stack.find(
1675
+ (l) => l.includes(":") && !l.includes("router.ts") && !l.includes("shokupan.ts") && !l.includes("node_modules") && !l.includes("bun:main")
1676
+ );
1677
+ if (callerLine) {
1678
+ const match = callerLine.match(/\((.*):(\d+):(\d+)\)/) || callerLine.match(/at (.*):(\d+):(\d+)/);
1679
+ if (match) {
1680
+ file = match[1];
1681
+ line = parseInt(match[2], 10);
1682
+ }
1683
+ }
1684
+ } catch (e) {
1685
+ }
1686
+ const trackedHandler = wrappedHandler;
1687
+ wrappedHandler = async (ctx) => {
1688
+ if (ctx.app?.applicationConfig.enableMiddlewareTracking) {
1689
+ ctx.handlerStack.push({
1690
+ name: handler.name || "anonymous",
1691
+ file,
1692
+ line
1693
+ });
1694
+ }
1695
+ return trackedHandler(ctx);
1696
+ };
1697
+ wrappedHandler.originalHandler = trackedHandler.originalHandler || trackedHandler;
1698
+ this[$routes].push({
879
1699
  method,
880
1700
  path: path2,
881
1701
  regex,
@@ -883,7 +1703,9 @@ class ShokupanRouter {
883
1703
  handler: wrappedHandler,
884
1704
  handlerSpec: spec,
885
1705
  group,
886
- guards: routeGuards.length > 0 ? routeGuards : void 0
1706
+ guards: routeGuards.length > 0 ? routeGuards : void 0,
1707
+ requestTimeout: effectiveTimeout
1708
+ // Save for inspection? Or just relying on closure
887
1709
  });
888
1710
  return this;
889
1711
  }
@@ -918,7 +1740,35 @@ class ShokupanRouter {
918
1740
  guard(specOrHandler, handler) {
919
1741
  const spec = typeof specOrHandler === "function" ? void 0 : specOrHandler;
920
1742
  const guardHandler = typeof specOrHandler === "function" ? specOrHandler : handler;
921
- this.currentGuards.push({ handler: guardHandler, spec });
1743
+ let file = "unknown";
1744
+ let line = 0;
1745
+ try {
1746
+ const err = new Error();
1747
+ const stack = err.stack?.split("\n") || [];
1748
+ const callerLine = stack.find(
1749
+ (l) => l.includes(":") && !l.includes("router.ts") && !l.includes("shokupan.ts") && !l.includes("node_modules") && !l.includes("bun:main")
1750
+ );
1751
+ if (callerLine) {
1752
+ const match = callerLine.match(/\((.*):(\d+):(\d+)\)/) || callerLine.match(/at (.*):(\d+):(\d+)/);
1753
+ if (match) {
1754
+ file = match[1];
1755
+ line = parseInt(match[2], 10);
1756
+ }
1757
+ }
1758
+ } catch (e) {
1759
+ }
1760
+ const trackedGuard = async (ctx, next) => {
1761
+ if (ctx.app?.applicationConfig.enableMiddlewareTracking) {
1762
+ ctx.handlerStack.push({
1763
+ name: guardHandler.name || "guard",
1764
+ file,
1765
+ line
1766
+ });
1767
+ }
1768
+ return guardHandler(ctx, next);
1769
+ };
1770
+ trackedGuard.originalHandler = guardHandler.originalHandler || guardHandler;
1771
+ this.currentGuards.push({ handler: trackedGuard, spec });
922
1772
  return this;
923
1773
  }
924
1774
  /**
@@ -928,133 +1778,12 @@ class ShokupanRouter {
928
1778
  */
929
1779
  static(uriPath, options) {
930
1780
  const config = typeof options === "string" ? { root: options } : options;
931
- const rootPath = path.resolve(config.root || ".");
932
1781
  const prefix = uriPath.startsWith("/") ? uriPath : "/" + uriPath;
933
1782
  const normalizedPrefix = prefix.endsWith("/") && prefix !== "/" ? prefix.slice(0, -1) : prefix;
934
- const handler = async (ctx) => {
935
- let relative = ctx.path.slice(normalizedPrefix.length);
936
- if (!relative.startsWith("/") && relative.length > 0) relative = "/" + relative;
937
- if (relative.length === 0) relative = "/";
938
- relative = decodeURIComponent(relative);
939
- const requestPath = path.join(rootPath, relative);
940
- if (!requestPath.startsWith(rootPath)) {
941
- return ctx.json({ error: "Forbidden" }, 403);
942
- }
943
- if (requestPath.includes("\0")) {
944
- return ctx.json({ error: "Forbidden" }, 403);
945
- }
946
- if (config.hooks?.onRequest) {
947
- const res = await config.hooks.onRequest(ctx);
948
- if (res) return res;
949
- }
950
- if (config.exclude) {
951
- for (const pattern2 of config.exclude) {
952
- if (pattern2 instanceof RegExp) {
953
- if (pattern2.test(relative)) return ctx.json({ error: "Forbidden" }, 403);
954
- } else if (typeof pattern2 === "string") {
955
- if (relative.includes(pattern2)) return ctx.json({ error: "Forbidden" }, 403);
956
- }
957
- }
958
- }
959
- if (path.basename(requestPath).startsWith(".")) {
960
- const behavior = config.dotfiles || "ignore";
961
- if (behavior === "deny") return ctx.json({ error: "Forbidden" }, 403);
962
- if (behavior === "ignore") return ctx.json({ error: "Not Found" }, 404);
963
- }
964
- let finalPath = requestPath;
965
- let stats;
966
- try {
967
- stats = await promises.stat(requestPath);
968
- } catch (e) {
969
- if (config.extensions) {
970
- for (const ext of config.extensions) {
971
- const p = requestPath + (ext.startsWith(".") ? ext : "." + ext);
972
- try {
973
- const s = await promises.stat(p);
974
- if (s.isFile()) {
975
- finalPath = p;
976
- stats = s;
977
- break;
978
- }
979
- } catch {
980
- }
981
- }
982
- }
983
- if (!stats) return ctx.json({ error: "Not Found" }, 404);
984
- }
985
- if (stats.isDirectory()) {
986
- if (!ctx.path.endsWith("/")) {
987
- const query = ctx.url.search;
988
- return ctx.redirect(ctx.path + "/" + query, 302);
989
- }
990
- let indexes = [];
991
- if (config.index === void 0) {
992
- indexes = ["index.html", "index.htm"];
993
- } else if (Array.isArray(config.index)) {
994
- indexes = config.index;
995
- } else if (config.index) {
996
- indexes = [config.index];
997
- }
998
- let foundIndex = false;
999
- for (const idx of indexes) {
1000
- const idxPath = path.join(finalPath, idx);
1001
- try {
1002
- const idxStats = await promises.stat(idxPath);
1003
- if (idxStats.isFile()) {
1004
- finalPath = idxPath;
1005
- foundIndex = true;
1006
- break;
1007
- }
1008
- } catch {
1009
- }
1010
- }
1011
- if (!foundIndex) {
1012
- if (config.listDirectory) {
1013
- try {
1014
- const files = await promises.readdir(requestPath);
1015
- const listing = eta$1.renderString(`
1016
- <!DOCTYPE html>
1017
- <html>
1018
- <head>
1019
- <title>Index of <%= it.relative %></title>
1020
- <style>
1021
- body { font-family: system-ui, -apple-system, sans-serif; padding: 2rem; max-width: 800px; margin: 0 auto; }
1022
- ul { list-style: none; padding: 0; }
1023
- li { padding: 0.5rem 0; border-bottom: 1px solid #eee; }
1024
- a { text-decoration: none; color: #0066cc; }
1025
- a:hover { text-decoration: underline; }
1026
- h1 { font-size: 1.5rem; margin-bottom: 1rem; }
1027
- </style>
1028
- </head>
1029
- <body>
1030
- <h1>Index of <%= it.relative %></h1>
1031
- <ul>
1032
- <% if (it.relative !== '/') { %>
1033
- <li><a href="../">../</a></li>
1034
- <% } %>
1035
- <% it.files.forEach(function(f) { %>
1036
- <li><a href="<%= f %>"><%= f %></a></li>
1037
- <% }) %>
1038
- </ul>
1039
- </body>
1040
- </html>
1041
- `, { relative, files, join: path.join });
1042
- return new Response(listing, { headers: { "Content-Type": "text/html" } });
1043
- } catch (e) {
1044
- return ctx.json({ error: "Internal Server Error" }, 500);
1045
- }
1046
- } else {
1047
- return ctx.json({ error: "Forbidden" }, 403);
1048
- }
1049
- }
1050
- }
1051
- const file = Bun.file(finalPath);
1052
- let response = new Response(file);
1053
- if (config.hooks?.onResponse) {
1054
- const hooked = await config.hooks.onResponse(ctx, response);
1055
- if (hooked) response = hooked;
1056
- }
1057
- return response;
1783
+ serveStatic(null, config, prefix);
1784
+ const routeHandler = async (ctx) => {
1785
+ const runner = serveStatic(ctx, config, prefix);
1786
+ return runner();
1058
1787
  };
1059
1788
  let groupName = "Static";
1060
1789
  const segments = normalizedPrefix.split("/").filter(Boolean);
@@ -1073,8 +1802,8 @@ class ShokupanRouter {
1073
1802
  const pattern = `^${normalizedPrefix}(/.*)?$`;
1074
1803
  const regex = new RegExp(pattern);
1075
1804
  const displayPath = normalizedPrefix === "/" ? "/*" : normalizedPrefix + "/*";
1076
- this.add({ method: "GET", path: displayPath, handler, spec, regex });
1077
- this.add({ method: "HEAD", path: displayPath, handler, spec, regex });
1805
+ this.add({ method: "GET", path: displayPath, handler: routeHandler, spec, regex });
1806
+ this.add({ method: "HEAD", path: displayPath, handler: routeHandler, spec, regex });
1078
1807
  return this;
1079
1808
  }
1080
1809
  /**
@@ -1109,138 +1838,25 @@ class ShokupanRouter {
1109
1838
  }
1110
1839
  /**
1111
1840
  * Generates an OpenAPI 3.1 Document by recursing through the router and its descendants.
1841
+ * Now includes runtime analysis of handler functions to infer request/response types.
1112
1842
  */
1113
1843
  generateApiSpec(options = {}) {
1114
- const paths = {};
1115
- const tagGroups = /* @__PURE__ */ new Map();
1116
- const defaultTagGroup = options.defaultTagGroup || "General";
1117
- const defaultTagName = options.defaultTag || "Application";
1118
- const collect = (router, prefix = "", currentGroup = defaultTagGroup, defaultTag = defaultTagName) => {
1119
- let group = currentGroup;
1120
- let tag = defaultTag;
1121
- if (router.config?.group) {
1122
- group = router.config.group;
1123
- }
1124
- if (router.config?.name) {
1125
- tag = router.config.name;
1126
- } else {
1127
- const mountPath = router[$mountPath];
1128
- if (mountPath && mountPath !== "/") {
1129
- const segments = mountPath.split("/").filter(Boolean);
1130
- if (segments.length > 0) {
1131
- const lastSegment = segments[segments.length - 1];
1132
- const humanized = lastSegment.replace(/[-_]/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
1133
- tag = humanized;
1134
- }
1135
- }
1136
- }
1137
- if (!tagGroups.has(group)) {
1138
- tagGroups.set(group, /* @__PURE__ */ new Set());
1139
- }
1140
- for (const route of router.routes) {
1141
- const routeGroup = route.group || group;
1142
- const cleanPrefix = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
1143
- const cleanSubPath = route.path.startsWith("/") ? route.path : "/" + route.path;
1144
- let fullPath = cleanPrefix + cleanSubPath || "/";
1145
- fullPath = fullPath.replace(/:([a-zA-Z0-9_]+)/g, "{$1}");
1146
- if (!paths[fullPath]) {
1147
- paths[fullPath] = {};
1148
- }
1149
- const operation = {
1150
- responses: {
1151
- 200: { description: "OK" }
1152
- }
1153
- };
1154
- if (route.keys.length > 0) {
1155
- operation.parameters = route.keys.map((key) => ({
1156
- name: key,
1157
- in: "path",
1158
- required: true,
1159
- schema: { type: "string" }
1160
- }));
1161
- }
1162
- if (route.guards) {
1163
- for (const guard of route.guards) {
1164
- if (guard.spec) {
1165
- deepMerge(operation, guard.spec);
1166
- }
1167
- }
1168
- }
1169
- if (route.handlerSpec) {
1170
- deepMerge(operation, route.handlerSpec);
1171
- }
1172
- if (!operation.tags || operation.tags.length === 0) {
1173
- operation.tags = [tag];
1174
- }
1175
- if (operation.tags) {
1176
- operation.tags = Array.from(new Set(operation.tags));
1177
- for (const t of operation.tags) {
1178
- if (!tagGroups.has(routeGroup)) {
1179
- tagGroups.set(routeGroup, /* @__PURE__ */ new Set());
1180
- }
1181
- tagGroups.get(routeGroup)?.add(t);
1182
- }
1183
- }
1184
- const methodLower = route.method.toLowerCase();
1185
- if (methodLower === "all") {
1186
- ["get", "post", "put", "delete", "patch"].forEach((m) => {
1187
- if (!paths[fullPath][m]) {
1188
- paths[fullPath][m] = { ...operation };
1189
- }
1190
- });
1191
- } else {
1192
- paths[fullPath][methodLower] = operation;
1193
- }
1194
- }
1195
- for (const controller of router[$childControllers]) {
1196
- const mountPath = controller[$mountPath] || "";
1197
- prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
1198
- mountPath.startsWith("/") ? mountPath : "/" + mountPath;
1199
- const controllerName = controller.constructor.name || "UnknownController";
1200
- tagGroups.get(group)?.add(controllerName);
1201
- }
1202
- for (const child of router[$childRouters]) {
1203
- const mountPath = child[$mountPath];
1204
- const cleanPrefix = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
1205
- const cleanMount = mountPath.startsWith("/") ? mountPath : "/" + mountPath;
1206
- const nextPrefix = cleanPrefix + cleanMount || "/";
1207
- collect(child, nextPrefix, group, tag);
1208
- }
1209
- };
1210
- collect(this);
1211
- const xTagGroups = [];
1212
- for (const [name, tags] of tagGroups) {
1213
- xTagGroups.push({
1214
- name,
1215
- tags: Array.from(tags).sort()
1216
- });
1217
- }
1218
- return {
1219
- openapi: "3.1.0",
1220
- info: {
1221
- title: "Shokupan API",
1222
- version: "1.0.0",
1223
- ...options.info
1224
- },
1225
- paths,
1226
- components: options.components,
1227
- servers: options.servers,
1228
- tags: options.tags,
1229
- externalDocs: options.externalDocs,
1230
- "x-tagGroups": xTagGroups
1231
- };
1844
+ return generateOpenApi(this, options);
1232
1845
  }
1233
1846
  }
1234
1847
  const defaults = {
1235
1848
  port: 3e3,
1236
1849
  hostname: "localhost",
1237
1850
  development: process.env.NODE_ENV !== "production",
1238
- enableAsyncLocalStorage: false
1851
+ enableAsyncLocalStorage: false,
1852
+ reusePort: false
1239
1853
  };
1240
1854
  api.trace.getTracer("shokupan.application");
1241
1855
  class Shokupan extends ShokupanRouter {
1242
1856
  applicationConfig = {};
1857
+ openApiSpec;
1243
1858
  middleware = [];
1859
+ composedMiddleware;
1244
1860
  get logger() {
1245
1861
  return this.applicationConfig.logger;
1246
1862
  }
@@ -1251,10 +1867,48 @@ class Shokupan extends ShokupanRouter {
1251
1867
  Object.assign(this.applicationConfig, defaults, applicationConfig);
1252
1868
  }
1253
1869
  /**
1254
- * Adds middleware to the application.
1870
+ * Adds middleware to the application.
1871
+ */
1872
+ use(middleware) {
1873
+ let trackedMiddleware = middleware;
1874
+ let file = "unknown";
1875
+ let line = 0;
1876
+ try {
1877
+ const err = new Error();
1878
+ const stack = err.stack?.split("\n") || [];
1879
+ const callerLine = stack.find(
1880
+ (l) => l.includes(":") && !l.includes("shokupan.ts") && !l.includes("router.ts") && // In case called from router?
1881
+ !l.includes("node_modules") && !l.includes("bun:main")
1882
+ );
1883
+ if (callerLine) {
1884
+ const match = callerLine.match(/\((.*):(\d+):(\d+)\)/) || callerLine.match(/at (.*):(\d+):(\d+)/);
1885
+ if (match) {
1886
+ file = match[1];
1887
+ line = parseInt(match[2], 10);
1888
+ }
1889
+ }
1890
+ } catch (e) {
1891
+ }
1892
+ trackedMiddleware = async (ctx, next) => {
1893
+ const c = ctx;
1894
+ if (c.handlerStack && c.app?.applicationConfig.enableMiddlewareTracking) {
1895
+ c.handlerStack.push({
1896
+ name: middleware.name || "middleware",
1897
+ file,
1898
+ line
1899
+ });
1900
+ }
1901
+ return middleware(ctx, next);
1902
+ };
1903
+ this.middleware.push(trackedMiddleware);
1904
+ return this;
1905
+ }
1906
+ startupHooks = [];
1907
+ /**
1908
+ * Registers a callback to be executed before the server starts listening.
1255
1909
  */
1256
- use(middleware) {
1257
- this.middleware.push(middleware);
1910
+ onStart(callback) {
1911
+ this.startupHooks.push(callback);
1258
1912
  return this;
1259
1913
  }
1260
1914
  /**
@@ -1263,18 +1917,46 @@ class Shokupan extends ShokupanRouter {
1263
1917
  * @param port - The port to listen on. If not specified, the port from the configuration is used. If that is not specified, port 3000 is used.
1264
1918
  * @returns The server instance.
1265
1919
  */
1266
- listen(port) {
1920
+ async listen(port) {
1267
1921
  const finalPort = port ?? this.applicationConfig.port ?? 3e3;
1268
1922
  if (finalPort < 0 || finalPort > 65535) {
1269
1923
  throw new Error("Invalid port number");
1270
1924
  }
1925
+ for (const hook of this.startupHooks) {
1926
+ await hook();
1927
+ }
1928
+ if (this.applicationConfig.enableOpenApiGen) {
1929
+ this.openApiSpec = await generateOpenApi(this);
1930
+ }
1271
1931
  if (port === 0 && process.platform === "linux") ;
1272
- const server = Bun.serve({
1932
+ const serveOptions = {
1273
1933
  port: finalPort,
1274
1934
  hostname: this.applicationConfig.hostname,
1275
1935
  development: this.applicationConfig.development,
1276
- fetch: this.fetch.bind(this)
1277
- });
1936
+ fetch: this.fetch.bind(this),
1937
+ reusePort: this.applicationConfig.reusePort,
1938
+ idleTimeout: this.applicationConfig.readTimeout ? this.applicationConfig.readTimeout / 1e3 : void 0,
1939
+ websocket: {
1940
+ open(ws) {
1941
+ ws.data?.handler?.open?.(ws);
1942
+ },
1943
+ message(ws, message) {
1944
+ ws.data?.handler?.message?.(ws, message);
1945
+ },
1946
+ drain(ws) {
1947
+ ws.data?.handler?.drain?.(ws);
1948
+ },
1949
+ close(ws, code, reason) {
1950
+ ws.data?.handler?.close?.(ws, code, reason);
1951
+ }
1952
+ }
1953
+ };
1954
+ let factory = this.applicationConfig.serverFactory;
1955
+ if (!factory && typeof Bun === "undefined") {
1956
+ const { createHttpServer } = await Promise.resolve().then(() => require("./server-adapter-BD6oKEto.cjs"));
1957
+ factory = createHttpServer();
1958
+ }
1959
+ const server = factory ? await factory(serveOptions) : Bun.serve(serveOptions);
1278
1960
  console.log(`Shokupan server listening on http://${server.hostname}:${server.port}`);
1279
1961
  return server;
1280
1962
  }
@@ -1325,65 +2007,122 @@ class Shokupan extends ShokupanRouter {
1325
2007
  * This logic contains the middleware chain and router dispatch.
1326
2008
  *
1327
2009
  * @param req - The request to handle.
2010
+ * @param server - The server instance.
1328
2011
  * @returns The response to send.
1329
2012
  */
1330
- async fetch(req) {
1331
- const tracer2 = api.trace.getTracer("shokupan.application");
1332
- const store = asyncContext.getStore();
1333
- const attrs = {
1334
- attributes: {
1335
- "http.url": req.url,
1336
- "http.method": req.method
1337
- }
1338
- };
1339
- const parent = store?.get("span");
1340
- const ctx = parent ? api.trace.setSpan(api.context.active(), parent) : void 0;
1341
- return tracer2.startActiveSpan(`${req.method} ${new URL(req.url).pathname}`, attrs, ctx, (span) => {
2013
+ async fetch(req, server) {
2014
+ if (this.applicationConfig.enableTracing) {
2015
+ const tracer2 = api.trace.getTracer("shokupan.application");
2016
+ const store = asyncContext.getStore();
2017
+ const attrs = {
2018
+ attributes: {
2019
+ "http.url": req.url,
2020
+ "http.method": req.method
2021
+ }
2022
+ };
2023
+ const parent = store?.get("span");
2024
+ const ctx = parent ? api.trace.setSpan(api.context.active(), parent) : void 0;
2025
+ return tracer2.startActiveSpan(`${req.method} ${new URL(req.url).pathname}`, attrs, ctx, (span) => {
2026
+ const ctxMap = /* @__PURE__ */ new Map();
2027
+ ctxMap.set("span", span);
2028
+ ctxMap.set("request", req);
2029
+ return asyncContext.run(ctxMap, () => this.handleRequest(req, server).finally(() => span.end()));
2030
+ });
2031
+ }
2032
+ if (this.applicationConfig.enableAsyncLocalStorage) {
1342
2033
  const ctxMap = /* @__PURE__ */ new Map();
1343
- ctxMap.set("span", span);
1344
2034
  ctxMap.set("request", req);
1345
- const runCallback = () => {
1346
- const request = req;
1347
- const handle = async () => {
1348
- const ctx2 = new ShokupanContext(request);
1349
- const fn = compose(this.middleware);
2035
+ return asyncContext.run(ctxMap, () => this.handleRequest(req, server));
2036
+ }
2037
+ return this.handleRequest(req, server);
2038
+ }
2039
+ async handleRequest(req, server) {
2040
+ const request = req;
2041
+ const ctx = new ShokupanContext(request, server, void 0, this, this.applicationConfig.enableMiddlewareTracking);
2042
+ const handle = async () => {
2043
+ try {
2044
+ if (this.applicationConfig.hooks?.onRequestStart) {
2045
+ await this.applicationConfig.hooks.onRequestStart(ctx);
2046
+ }
2047
+ const fn = this.composedMiddleware ??= compose(this.middleware);
2048
+ const result = await fn(ctx, async () => {
2049
+ const match = this.find(req.method, ctx.path);
2050
+ if (match) {
2051
+ ctx.params = match.params;
2052
+ return match.handler(ctx);
2053
+ }
2054
+ return null;
2055
+ });
2056
+ let response;
2057
+ if (result instanceof Response) {
2058
+ response = result;
2059
+ } else if ((result === null || result === void 0) && ctx._finalResponse instanceof Response) {
2060
+ response = ctx._finalResponse;
2061
+ } else if ((result === null || result === void 0) && ctx.response.status === 404) {
2062
+ const span = asyncContext.getStore()?.get("span");
2063
+ if (span) span.setAttribute("http.status_code", 404);
2064
+ response = ctx.text("Not Found", 404);
2065
+ } else if (result === null || result === void 0) {
2066
+ if (ctx._finalResponse) response = ctx._finalResponse;
2067
+ else response = ctx.text("Not Found", 404);
2068
+ } else if (typeof result === "object") {
2069
+ response = ctx.json(result);
2070
+ } else {
2071
+ response = ctx.text(String(result));
2072
+ }
2073
+ if (this.applicationConfig.hooks?.onRequestEnd) {
2074
+ await this.applicationConfig.hooks.onRequestEnd(ctx);
2075
+ }
2076
+ if (this.applicationConfig.hooks?.onResponseStart) {
2077
+ await this.applicationConfig.hooks.onResponseStart(ctx, response);
2078
+ }
2079
+ return response;
2080
+ } catch (err) {
2081
+ console.error(err);
2082
+ const span = asyncContext.getStore()?.get("span");
2083
+ if (span) span.setStatus({ code: 2 });
2084
+ const status = err.status || err.statusCode || 500;
2085
+ const body = { error: err.message || "Internal Server Error" };
2086
+ if (err.errors) body.errors = err.errors;
2087
+ if (this.applicationConfig.hooks?.onError) {
1350
2088
  try {
1351
- const result = await fn(ctx2, async () => {
1352
- const match = this.find(req.method, ctx2.path);
1353
- if (match) {
1354
- ctx2.params = match.params;
1355
- return match.handler(ctx2);
1356
- }
1357
- return null;
1358
- });
1359
- if (result instanceof Response) {
1360
- return result;
1361
- }
1362
- if (result === null || result === void 0) {
1363
- span.setAttribute("http.status_code", 404);
1364
- return ctx2.text("Not Found", 404);
1365
- }
1366
- if (typeof result === "object") {
1367
- return ctx2.json(result);
2089
+ await this.applicationConfig.hooks.onError(err, ctx);
2090
+ } catch (hookErr) {
2091
+ console.error("Error in onError hook:", hookErr);
2092
+ }
2093
+ }
2094
+ return ctx.json(body, status);
2095
+ }
2096
+ };
2097
+ let executionPromise = handle();
2098
+ const timeoutMs = this.applicationConfig.requestTimeout;
2099
+ if (timeoutMs && timeoutMs > 0 && this.applicationConfig.hooks?.onRequestTimeout) {
2100
+ let timeoutId;
2101
+ const timeoutPromise = new Promise((_, reject) => {
2102
+ timeoutId = setTimeout(async () => {
2103
+ try {
2104
+ if (this.applicationConfig.hooks?.onRequestTimeout) {
2105
+ await this.applicationConfig.hooks.onRequestTimeout(ctx);
1368
2106
  }
1369
- return ctx2.text(String(result));
1370
- } catch (err) {
1371
- console.error(err);
1372
- span.recordException(err);
1373
- span.setStatus({ code: 2 });
1374
- const status = err.status || err.statusCode || 500;
1375
- const body = { error: err.message || "Internal Server Error" };
1376
- if (err.errors) body.errors = err.errors;
1377
- return ctx2.json(body, status);
2107
+ } catch (e) {
2108
+ console.error("Error in onRequestTimeout hook:", e);
1378
2109
  }
1379
- };
1380
- return handle().finally(() => span.end());
1381
- };
1382
- if (this.applicationConfig.enableAsyncLocalStorage) {
1383
- return asyncContext.run(ctxMap, runCallback);
1384
- } else {
1385
- return runCallback();
2110
+ reject(new Error("Request Timeout"));
2111
+ }, timeoutMs);
2112
+ });
2113
+ executionPromise = Promise.race([executionPromise, timeoutPromise]).finally(() => clearTimeout(timeoutId));
2114
+ }
2115
+ return executionPromise.catch((err) => {
2116
+ if (err.message === "Request Timeout") {
2117
+ return ctx.text("Request Timeout", 408);
2118
+ }
2119
+ console.error("Unexpected error in request execution:", err);
2120
+ return ctx.text("Internal Server Error", 500);
2121
+ }).then(async (res) => {
2122
+ if (this.applicationConfig.hooks?.onResponseEnd) {
2123
+ await this.applicationConfig.hooks.onResponseEnd(ctx, res);
1386
2124
  }
2125
+ return res;
1387
2126
  });
1388
2127
  }
1389
2128
  }
@@ -1435,8 +2174,8 @@ class AuthPlugin extends ShokupanRouter {
1435
2174
  init() {
1436
2175
  for (const [providerName, providerConfig] of Object.entries(this.authConfig.providers)) {
1437
2176
  if (!providerConfig) continue;
1438
- const provider2 = this.getProviderInstance(providerName, providerConfig);
1439
- if (!provider2) {
2177
+ const provider = this.getProviderInstance(providerName, providerConfig);
2178
+ if (!provider) {
1440
2179
  continue;
1441
2180
  }
1442
2181
  this.get(`/auth/${providerName}/login`, async (ctx) => {
@@ -1444,15 +2183,15 @@ class AuthPlugin extends ShokupanRouter {
1444
2183
  const codeVerifier = providerName === "google" || providerName === "microsoft" || providerName === "auth0" || providerName === "okta" ? arctic.generateCodeVerifier() : void 0;
1445
2184
  const scopes = providerConfig.scopes || [];
1446
2185
  let url;
1447
- if (provider2 instanceof arctic.GitHub) {
1448
- url = await provider2.createAuthorizationURL(state, scopes);
1449
- } else if (provider2 instanceof arctic.Google || provider2 instanceof arctic.MicrosoftEntraId || provider2 instanceof arctic.Auth0 || provider2 instanceof arctic.Okta) {
1450
- url = await provider2.createAuthorizationURL(state, codeVerifier, scopes);
1451
- } else if (provider2 instanceof arctic.Apple) {
1452
- url = await provider2.createAuthorizationURL(state, scopes);
1453
- } else if (provider2 instanceof arctic.OAuth2Client) {
2186
+ if (provider instanceof arctic.GitHub) {
2187
+ url = await provider.createAuthorizationURL(state, scopes);
2188
+ } else if (provider instanceof arctic.Google || provider instanceof arctic.MicrosoftEntraId || provider instanceof arctic.Auth0 || provider instanceof arctic.Okta) {
2189
+ url = await provider.createAuthorizationURL(state, codeVerifier, scopes);
2190
+ } else if (provider instanceof arctic.Apple) {
2191
+ url = await provider.createAuthorizationURL(state, scopes);
2192
+ } else if (provider instanceof arctic.OAuth2Client) {
1454
2193
  if (!providerConfig.authUrl) return ctx.text("Config error: authUrl required for oauth2", 500);
1455
- url = await provider2.createAuthorizationURL(providerConfig.authUrl, state, scopes);
2194
+ url = await provider.createAuthorizationURL(providerConfig.authUrl, state, scopes);
1456
2195
  } else {
1457
2196
  return ctx.text("Provider config error", 500);
1458
2197
  }
@@ -1475,19 +2214,19 @@ class AuthPlugin extends ShokupanRouter {
1475
2214
  try {
1476
2215
  let tokens;
1477
2216
  let idToken;
1478
- if (provider2 instanceof arctic.GitHub) {
1479
- tokens = await provider2.validateAuthorizationCode(code);
1480
- } else if (provider2 instanceof arctic.Google || provider2 instanceof arctic.MicrosoftEntraId) {
2217
+ if (provider instanceof arctic.GitHub) {
2218
+ tokens = await provider.validateAuthorizationCode(code);
2219
+ } else if (provider instanceof arctic.Google || provider instanceof arctic.MicrosoftEntraId) {
1481
2220
  if (!storedVerifier) return ctx.text("Missing verifier", 400);
1482
- tokens = await provider2.validateAuthorizationCode(code, storedVerifier);
1483
- } else if (provider2 instanceof arctic.Auth0 || provider2 instanceof arctic.Okta) {
1484
- tokens = await provider2.validateAuthorizationCode(code, storedVerifier || "");
1485
- } else if (provider2 instanceof arctic.Apple) {
1486
- tokens = await provider2.validateAuthorizationCode(code);
2221
+ tokens = await provider.validateAuthorizationCode(code, storedVerifier);
2222
+ } else if (provider instanceof arctic.Auth0 || provider instanceof arctic.Okta) {
2223
+ tokens = await provider.validateAuthorizationCode(code, storedVerifier || "");
2224
+ } else if (provider instanceof arctic.Apple) {
2225
+ tokens = await provider.validateAuthorizationCode(code);
1487
2226
  idToken = tokens.idToken;
1488
- } else if (provider2 instanceof arctic.OAuth2Client) {
2227
+ } else if (provider instanceof arctic.OAuth2Client) {
1489
2228
  if (!providerConfig.tokenUrl) return ctx.text("Config error: tokenUrl required for oauth2", 500);
1490
- tokens = await provider2.validateAuthorizationCode(providerConfig.tokenUrl, code, null);
2229
+ tokens = await provider.validateAuthorizationCode(providerConfig.tokenUrl, code, null);
1491
2230
  }
1492
2231
  const accessToken = tokens.accessToken || tokens.access_token;
1493
2232
  const user = await this.fetchUser(providerName, accessToken, providerConfig, idToken);
@@ -1504,9 +2243,9 @@ class AuthPlugin extends ShokupanRouter {
1504
2243
  });
1505
2244
  }
1506
2245
  }
1507
- async fetchUser(provider2, token, config, idToken) {
1508
- let user = { id: "unknown", provider: provider2 };
1509
- if (provider2 === "github") {
2246
+ async fetchUser(provider, token, config, idToken) {
2247
+ let user = { id: "unknown", provider };
2248
+ if (provider === "github") {
1510
2249
  const res = await fetch("https://api.github.com/user", {
1511
2250
  headers: { Authorization: `Bearer ${token}` }
1512
2251
  });
@@ -1516,10 +2255,10 @@ class AuthPlugin extends ShokupanRouter {
1516
2255
  name: data.name || data.login,
1517
2256
  email: data.email,
1518
2257
  picture: data.avatar_url,
1519
- provider: provider2,
2258
+ provider,
1520
2259
  raw: data
1521
2260
  };
1522
- } else if (provider2 === "google") {
2261
+ } else if (provider === "google") {
1523
2262
  const res = await fetch("https://openidconnect.googleapis.com/v1/userinfo", {
1524
2263
  headers: { Authorization: `Bearer ${token}` }
1525
2264
  });
@@ -1529,10 +2268,10 @@ class AuthPlugin extends ShokupanRouter {
1529
2268
  name: data.name,
1530
2269
  email: data.email,
1531
2270
  picture: data.picture,
1532
- provider: provider2,
2271
+ provider,
1533
2272
  raw: data
1534
2273
  };
1535
- } else if (provider2 === "microsoft") {
2274
+ } else if (provider === "microsoft") {
1536
2275
  const res = await fetch("https://graph.microsoft.com/v1.0/me", {
1537
2276
  headers: { Authorization: `Bearer ${token}` }
1538
2277
  });
@@ -1541,12 +2280,12 @@ class AuthPlugin extends ShokupanRouter {
1541
2280
  id: data.id,
1542
2281
  name: data.displayName,
1543
2282
  email: data.mail || data.userPrincipalName,
1544
- provider: provider2,
2283
+ provider,
1545
2284
  raw: data
1546
2285
  };
1547
- } else if (provider2 === "auth0" || provider2 === "okta") {
2286
+ } else if (provider === "auth0" || provider === "okta") {
1548
2287
  const domain = config.domain.startsWith("http") ? config.domain : `https://${config.domain}`;
1549
- const endpoint = provider2 === "auth0" ? `${domain}/userinfo` : `${domain}/oauth2/v1/userinfo`;
2288
+ const endpoint = provider === "auth0" ? `${domain}/userinfo` : `${domain}/oauth2/v1/userinfo`;
1550
2289
  const res = await fetch(endpoint, {
1551
2290
  headers: { Authorization: `Bearer ${token}` }
1552
2291
  });
@@ -1556,20 +2295,20 @@ class AuthPlugin extends ShokupanRouter {
1556
2295
  name: data.name,
1557
2296
  email: data.email,
1558
2297
  picture: data.picture,
1559
- provider: provider2,
2298
+ provider,
1560
2299
  raw: data
1561
2300
  };
1562
- } else if (provider2 === "apple") {
2301
+ } else if (provider === "apple") {
1563
2302
  if (idToken) {
1564
2303
  const payload = jose__namespace.decodeJwt(idToken);
1565
2304
  user = {
1566
2305
  id: payload.sub,
1567
2306
  email: payload["email"],
1568
- provider: provider2,
2307
+ provider,
1569
2308
  raw: payload
1570
2309
  };
1571
2310
  }
1572
- } else if (provider2 === "oauth2") {
2311
+ } else if (provider === "oauth2") {
1573
2312
  if (config.userInfoUrl) {
1574
2313
  const res = await fetch(config.userInfoUrl, {
1575
2314
  headers: { Authorization: `Bearer ${token}` }
@@ -1580,7 +2319,7 @@ class AuthPlugin extends ShokupanRouter {
1580
2319
  name: data.name,
1581
2320
  email: data.email,
1582
2321
  picture: data.picture,
1583
- provider: provider2,
2322
+ provider,
1584
2323
  raw: data
1585
2324
  };
1586
2325
  }
@@ -1610,15 +2349,19 @@ class AuthPlugin extends ShokupanRouter {
1610
2349
  }
1611
2350
  }
1612
2351
  function Compression(options = {}) {
1613
- const threshold = options.threshold ?? 1024;
2352
+ const threshold = options.threshold ?? 512;
1614
2353
  return async (ctx, next) => {
1615
2354
  const acceptEncoding = ctx.headers.get("accept-encoding") || "";
1616
2355
  let method = null;
1617
2356
  if (acceptEncoding.includes("br")) method = "br";
2357
+ else if (acceptEncoding.includes("zstd")) method = "zstd";
1618
2358
  else if (acceptEncoding.includes("gzip")) method = "gzip";
1619
2359
  else if (acceptEncoding.includes("deflate")) method = "deflate";
1620
2360
  if (!method) return next();
1621
- const response = await next();
2361
+ let response = await next();
2362
+ if (!(response instanceof Response) && ctx._finalResponse instanceof Response) {
2363
+ response = ctx._finalResponse;
2364
+ }
1622
2365
  if (response instanceof Response) {
1623
2366
  if (response.headers.has("Content-Encoding")) return response;
1624
2367
  const body = await response.arrayBuffer();
@@ -1630,97 +2373,506 @@ function Compression(options = {}) {
1630
2373
  });
1631
2374
  }
1632
2375
  let compressed;
1633
- if (method === "br") {
1634
- compressed = require("node:zlib").brotliCompressSync(body);
1635
- } else if (method === "gzip") {
1636
- compressed = Bun.gzipSync(body);
1637
- } else {
1638
- compressed = Bun.deflateSync(body);
2376
+ switch (method) {
2377
+ case "br":
2378
+ const zlib = require("node:zlib");
2379
+ compressed = await new Promise((res, rej) => zlib.brotliCompress(body, {
2380
+ params: {
2381
+ [zlib.constants.BROTLI_PARAM_QUALITY]: 4
2382
+ }
2383
+ }, (err, data) => {
2384
+ if (err) return rej(err);
2385
+ res(data);
2386
+ }));
2387
+ break;
2388
+ case "gzip":
2389
+ compressed = Bun.gzipSync(body);
2390
+ break;
2391
+ case "zstd":
2392
+ compressed = await Bun.zstdCompress(body);
2393
+ break;
2394
+ default:
2395
+ compressed = Bun.deflateSync(body);
2396
+ break;
1639
2397
  }
1640
2398
  const headers = new Headers(response.headers);
1641
2399
  headers.set("Content-Encoding", method);
1642
2400
  headers.set("Content-Length", String(compressed.length));
1643
- headers.delete("Content-Length");
1644
2401
  return new Response(compressed, {
1645
2402
  status: response.status,
1646
2403
  statusText: response.statusText,
1647
2404
  headers
1648
2405
  });
1649
2406
  }
1650
- return response;
2407
+ return response;
2408
+ };
2409
+ }
2410
+ function Cors(options = {}) {
2411
+ const defaults2 = {
2412
+ origin: "*",
2413
+ methods: "GET,HEAD,PUT,PATCH,POST,DELETE",
2414
+ preflightContinue: false,
2415
+ optionsSuccessStatus: 204
2416
+ };
2417
+ const opts = { ...defaults2, ...options };
2418
+ return async (ctx, next) => {
2419
+ const headers = new Headers();
2420
+ const origin = ctx.headers.get("origin");
2421
+ const set = (k, v) => headers.set(k, v);
2422
+ const append = (k, v) => headers.append(k, v);
2423
+ if (opts.origin === "*") {
2424
+ set("Access-Control-Allow-Origin", "*");
2425
+ } else if (typeof opts.origin === "string") {
2426
+ set("Access-Control-Allow-Origin", opts.origin);
2427
+ } else if (Array.isArray(opts.origin)) {
2428
+ if (origin && opts.origin.includes(origin)) {
2429
+ set("Access-Control-Allow-Origin", origin);
2430
+ append("Vary", "Origin");
2431
+ }
2432
+ } else if (typeof opts.origin === "function") {
2433
+ const allowed = opts.origin(ctx);
2434
+ if (allowed === true && origin) {
2435
+ set("Access-Control-Allow-Origin", origin);
2436
+ append("Vary", "Origin");
2437
+ } else if (typeof allowed === "string") {
2438
+ set("Access-Control-Allow-Origin", allowed);
2439
+ append("Vary", "Origin");
2440
+ }
2441
+ }
2442
+ if (opts.credentials) {
2443
+ set("Access-Control-Allow-Credentials", "true");
2444
+ }
2445
+ if (opts.exposedHeaders) {
2446
+ const exposed = Array.isArray(opts.exposedHeaders) ? opts.exposedHeaders.join(",") : opts.exposedHeaders;
2447
+ if (exposed) set("Access-Control-Expose-Headers", exposed);
2448
+ }
2449
+ if (ctx.method === "OPTIONS") {
2450
+ if (opts.methods) {
2451
+ const methods = Array.isArray(opts.methods) ? opts.methods.join(",") : opts.methods;
2452
+ set("Access-Control-Allow-Methods", methods);
2453
+ }
2454
+ if (opts.allowedHeaders) {
2455
+ const h = Array.isArray(opts.allowedHeaders) ? opts.allowedHeaders.join(",") : opts.allowedHeaders;
2456
+ set("Access-Control-Allow-Headers", h);
2457
+ } else {
2458
+ const reqHeaders = ctx.headers.get("access-control-request-headers");
2459
+ if (reqHeaders) {
2460
+ set("Access-Control-Allow-Headers", reqHeaders);
2461
+ append("Vary", "Access-Control-Request-Headers");
2462
+ }
2463
+ }
2464
+ if (opts.maxAge) {
2465
+ set("Access-Control-Max-Age", String(opts.maxAge));
2466
+ }
2467
+ return new Response(null, {
2468
+ status: opts.optionsSuccessStatus || 204,
2469
+ headers
2470
+ });
2471
+ }
2472
+ const response = await next();
2473
+ if (response instanceof Response) {
2474
+ for (const [key, value] of headers.entries()) {
2475
+ response.headers.set(key, value);
2476
+ }
2477
+ }
2478
+ return response;
2479
+ };
2480
+ }
2481
+ function useExpress(expressMiddleware) {
2482
+ return async (ctx, next) => {
2483
+ return new Promise((resolve, reject) => {
2484
+ const reqStore = {
2485
+ method: ctx.method,
2486
+ url: ctx.url.pathname + ctx.url.search,
2487
+ path: ctx.url.pathname,
2488
+ query: ctx.query,
2489
+ headers: ctx.headers,
2490
+ get: (name) => ctx.headers.get(name)
2491
+ };
2492
+ const req = new Proxy(ctx.request, {
2493
+ get(target, prop) {
2494
+ if (prop in reqStore) return reqStore[prop];
2495
+ const val = target[prop];
2496
+ if (typeof val === "function") return val.bind(target);
2497
+ return val;
2498
+ },
2499
+ set(target, prop, value) {
2500
+ reqStore[prop] = value;
2501
+ ctx.state[prop] = value;
2502
+ return true;
2503
+ }
2504
+ });
2505
+ const res = {
2506
+ locals: {},
2507
+ statusCode: 200,
2508
+ setHeader: (name, value) => {
2509
+ ctx.response.headers.set(name, value);
2510
+ },
2511
+ set: (name, value) => {
2512
+ ctx.response.headers.set(name, value);
2513
+ },
2514
+ end: (chunk) => {
2515
+ resolve(new Response(chunk, { status: res.statusCode }));
2516
+ },
2517
+ status: (code) => {
2518
+ res.statusCode = code;
2519
+ return res;
2520
+ },
2521
+ send: (body) => {
2522
+ let content = body;
2523
+ if (typeof body === "object") content = JSON.stringify(body);
2524
+ resolve(new Response(content, { status: res.statusCode }));
2525
+ },
2526
+ json: (body) => {
2527
+ resolve(Response.json(body, { status: res.statusCode }));
2528
+ }
2529
+ };
2530
+ try {
2531
+ expressMiddleware(req, res, (err) => {
2532
+ if (err) return reject(err);
2533
+ resolve(next());
2534
+ });
2535
+ } catch (err) {
2536
+ reject(err);
2537
+ }
2538
+ });
2539
+ };
2540
+ }
2541
+ class ValidationError extends Error {
2542
+ constructor(errors) {
2543
+ super("Validation Error");
2544
+ this.errors = errors;
2545
+ }
2546
+ status = 400;
2547
+ }
2548
+ function isZod(schema) {
2549
+ return typeof schema?.safeParse === "function";
2550
+ }
2551
+ async function validateZod(schema, data) {
2552
+ const result = await schema.safeParseAsync(data);
2553
+ if (!result.success) {
2554
+ throw new ValidationError(result.error.errors);
2555
+ }
2556
+ return result.data;
2557
+ }
2558
+ function isTypeBox(schema) {
2559
+ return typeof schema?.Check === "function" && typeof schema?.Errors === "function";
2560
+ }
2561
+ function validateTypeBox(schema, data) {
2562
+ if (!schema.Check(data)) {
2563
+ throw new ValidationError([...schema.Errors(data)]);
2564
+ }
2565
+ return data;
2566
+ }
2567
+ function isAjv(schema) {
2568
+ return typeof schema === "function" && "errors" in schema;
2569
+ }
2570
+ function validateAjv(schema, data) {
2571
+ const valid = schema(data);
2572
+ if (!valid) {
2573
+ throw new ValidationError(schema.errors);
2574
+ }
2575
+ return data;
2576
+ }
2577
+ const valibot = (schema, parser) => {
2578
+ return {
2579
+ _valibot: true,
2580
+ schema,
2581
+ parser
2582
+ };
2583
+ };
2584
+ function isValibotWrapper(schema) {
2585
+ return schema?._valibot === true;
2586
+ }
2587
+ async function validateValibotWrapper(wrapper, data) {
2588
+ const result = await wrapper.parser(wrapper.schema, data);
2589
+ if (!result.success) {
2590
+ throw new ValidationError(result.issues);
2591
+ }
2592
+ return result.output;
2593
+ }
2594
+ function isClass(schema) {
2595
+ try {
2596
+ if (typeof schema === "function" && /^\s*class\s+/.test(schema.toString())) {
2597
+ return true;
2598
+ }
2599
+ return typeof schema === "function" && schema.prototype && schema.name;
2600
+ } catch {
2601
+ return false;
2602
+ }
2603
+ }
2604
+ async function validateClassValidator(schema, data) {
2605
+ const object = classTransformer.plainToInstance(schema, data);
2606
+ try {
2607
+ await classValidator.validateOrReject(object);
2608
+ return object;
2609
+ } catch (errors) {
2610
+ const formattedErrors = Array.isArray(errors) ? errors.map((err) => ({
2611
+ property: err.property,
2612
+ constraints: err.constraints,
2613
+ children: err.children
2614
+ })) : errors;
2615
+ throw new ValidationError(formattedErrors);
2616
+ }
2617
+ }
2618
+ const safelyGetBody = async (ctx) => {
2619
+ const req = ctx.req;
2620
+ if (req._bodyParsed) {
2621
+ return req._bodyValue;
2622
+ }
2623
+ try {
2624
+ let data;
2625
+ if (typeof req.json === "function") {
2626
+ data = await req.json();
2627
+ } else {
2628
+ data = req.body;
2629
+ if (typeof data === "string") {
2630
+ try {
2631
+ data = JSON.parse(data);
2632
+ } catch {
2633
+ }
2634
+ }
2635
+ }
2636
+ req._bodyParsed = true;
2637
+ req._bodyValue = data;
2638
+ Object.defineProperty(req, "json", {
2639
+ value: async () => req._bodyValue,
2640
+ configurable: true
2641
+ });
2642
+ return data;
2643
+ } catch (e) {
2644
+ return {};
2645
+ }
2646
+ };
2647
+ function validate(config) {
2648
+ return async (ctx, next) => {
2649
+ const dataToValidate = {};
2650
+ if (config.params) dataToValidate.params = ctx.params;
2651
+ let queryObj;
2652
+ if (config.query) {
2653
+ const url = new URL(ctx.req.url);
2654
+ queryObj = Object.fromEntries(url.searchParams.entries());
2655
+ dataToValidate.query = queryObj;
2656
+ }
2657
+ if (config.headers) dataToValidate.headers = Object.fromEntries(ctx.req.headers.entries());
2658
+ let body;
2659
+ if (config.body) {
2660
+ body = await safelyGetBody(ctx);
2661
+ dataToValidate.body = body;
2662
+ }
2663
+ if (ctx.app?.applicationConfig.hooks?.beforeValidate) {
2664
+ await ctx.app.applicationConfig.hooks.beforeValidate(ctx, dataToValidate);
2665
+ }
2666
+ if (config.params) {
2667
+ ctx.params = await runValidation(config.params, ctx.params);
2668
+ }
2669
+ let validQuery;
2670
+ if (config.query && queryObj) {
2671
+ validQuery = await runValidation(config.query, queryObj);
2672
+ }
2673
+ if (config.headers) {
2674
+ const headersObj = Object.fromEntries(ctx.req.headers.entries());
2675
+ await runValidation(config.headers, headersObj);
2676
+ }
2677
+ let validBody;
2678
+ if (config.body) {
2679
+ const b = body ?? await safelyGetBody(ctx);
2680
+ validBody = await runValidation(config.body, b);
2681
+ const req = ctx.req;
2682
+ req._bodyValue = validBody;
2683
+ Object.defineProperty(req, "json", {
2684
+ value: async () => validBody,
2685
+ configurable: true
2686
+ });
2687
+ ctx.body = validBody;
2688
+ }
2689
+ if (ctx.app?.applicationConfig.hooks?.afterValidate) {
2690
+ const validatedData = { ...dataToValidate };
2691
+ if (config.params) validatedData.params = ctx.params;
2692
+ if (config.query) validatedData.query = validQuery;
2693
+ if (config.body) validatedData.body = validBody;
2694
+ await ctx.app.applicationConfig.hooks.afterValidate(ctx, validatedData);
2695
+ }
2696
+ return next();
1651
2697
  };
1652
2698
  }
1653
- function Cors(options = {}) {
1654
- const defaults2 = {
1655
- origin: "*",
1656
- methods: "GET,HEAD,PUT,PATCH,POST,DELETE",
1657
- preflightContinue: false,
1658
- optionsSuccessStatus: 204
1659
- };
1660
- const opts = { ...defaults2, ...options };
2699
+ async function runValidation(schema, data) {
2700
+ if (isZod(schema)) {
2701
+ return validateZod(schema, data);
2702
+ }
2703
+ if (isTypeBox(schema)) {
2704
+ return validateTypeBox(schema, data);
2705
+ }
2706
+ if (isAjv(schema)) {
2707
+ return validateAjv(schema, data);
2708
+ }
2709
+ if (isValibotWrapper(schema)) {
2710
+ return validateValibotWrapper(schema, data);
2711
+ }
2712
+ if (isClass(schema)) {
2713
+ return validateClassValidator(schema, data);
2714
+ }
2715
+ if (isTypeBox(schema)) {
2716
+ return validateTypeBox(schema, data);
2717
+ }
2718
+ if (isAjv(schema)) {
2719
+ return validateAjv(schema, data);
2720
+ }
2721
+ if (isValibotWrapper(schema)) {
2722
+ return validateValibotWrapper(schema, data);
2723
+ }
2724
+ if (typeof schema === "function") {
2725
+ return schema(data);
2726
+ }
2727
+ throw new Error("Unknown validator type provided. Please use a supported library (Zod, Ajv, TypeBox) or a custom function.");
2728
+ }
2729
+ const ajv = new Ajv({ coerceTypes: true, allErrors: true });
2730
+ addFormats(ajv);
2731
+ const compiledValidators = /* @__PURE__ */ new WeakMap();
2732
+ function openApiValidator() {
1661
2733
  return async (ctx, next) => {
1662
- const headers = new Headers();
1663
- const origin = ctx.headers.get("origin");
1664
- const set = (k, v) => headers.set(k, v);
1665
- const append = (k, v) => headers.append(k, v);
1666
- if (opts.origin === "*") {
1667
- set("Access-Control-Allow-Origin", "*");
1668
- } else if (typeof opts.origin === "string") {
1669
- set("Access-Control-Allow-Origin", opts.origin);
1670
- } else if (Array.isArray(opts.origin)) {
1671
- if (origin && opts.origin.includes(origin)) {
1672
- set("Access-Control-Allow-Origin", origin);
1673
- append("Vary", "Origin");
1674
- }
1675
- } else if (typeof opts.origin === "function") {
1676
- const allowed = opts.origin(ctx);
1677
- if (allowed === true && origin) {
1678
- set("Access-Control-Allow-Origin", origin);
1679
- append("Vary", "Origin");
1680
- } else if (typeof allowed === "string") {
1681
- set("Access-Control-Allow-Origin", allowed);
1682
- append("Vary", "Origin");
2734
+ const app = ctx.app;
2735
+ if (!app || !app.openApiSpec) {
2736
+ return next();
2737
+ }
2738
+ let cache = compiledValidators.get(app);
2739
+ if (!cache) {
2740
+ cache = compileValidators(app.openApiSpec);
2741
+ compiledValidators.set(app, cache);
2742
+ }
2743
+ const method = ctx.req.method.toLowerCase();
2744
+ let matchPath;
2745
+ if (cache.has(ctx.path)) {
2746
+ matchPath = ctx.path;
2747
+ } else {
2748
+ for (const specPath of cache.keys()) {
2749
+ const regexStr = "^" + specPath.replace(/{([^}]+)}/g, "([^/]+)") + "$";
2750
+ const regex = new RegExp(regexStr);
2751
+ const match = regex.exec(ctx.path);
2752
+ if (match) {
2753
+ matchPath = specPath;
2754
+ break;
2755
+ }
1683
2756
  }
1684
2757
  }
1685
- if (opts.credentials) {
1686
- set("Access-Control-Allow-Credentials", "true");
2758
+ if (!matchPath) {
2759
+ return next();
1687
2760
  }
1688
- if (opts.exposedHeaders) {
1689
- const exposed = Array.isArray(opts.exposedHeaders) ? opts.exposedHeaders.join(",") : opts.exposedHeaders;
1690
- if (exposed) set("Access-Control-Expose-Headers", exposed);
2761
+ const validators = cache.get(matchPath)?.[method];
2762
+ if (!validators) {
2763
+ return next();
1691
2764
  }
1692
- if (ctx.method === "OPTIONS") {
1693
- if (opts.methods) {
1694
- const methods = Array.isArray(opts.methods) ? opts.methods.join(",") : opts.methods;
1695
- set("Access-Control-Allow-Methods", methods);
2765
+ const errors = [];
2766
+ if (validators.body) {
2767
+ let body;
2768
+ try {
2769
+ body = await ctx.req.json().catch(() => ({}));
2770
+ } catch {
2771
+ body = {};
1696
2772
  }
1697
- if (opts.allowedHeaders) {
1698
- const h = Array.isArray(opts.allowedHeaders) ? opts.allowedHeaders.join(",") : opts.allowedHeaders;
1699
- set("Access-Control-Allow-Headers", h);
1700
- } else {
1701
- const reqHeaders = ctx.headers.get("access-control-request-headers");
1702
- if (reqHeaders) {
1703
- set("Access-Control-Allow-Headers", reqHeaders);
1704
- append("Vary", "Access-Control-Request-Headers");
2773
+ const valid = validators.body(body);
2774
+ if (!valid && validators.body.errors) {
2775
+ errors.push(...validators.body.errors.map((e) => ({ ...e, location: "body" })));
2776
+ }
2777
+ }
2778
+ if (validators.query) {
2779
+ const query = Object.fromEntries(new URL(ctx.req.url).searchParams.entries());
2780
+ const valid = validators.query(query);
2781
+ if (!valid && validators.query.errors) {
2782
+ errors.push(...validators.query.errors.map((e) => ({ ...e, location: "query" })));
2783
+ }
2784
+ }
2785
+ if (validators.params) {
2786
+ let params = ctx.params;
2787
+ if (Object.keys(params).length === 0 && matchPath) {
2788
+ const paramNames = (matchPath.match(/{([^}]+)}/g) || []).map((s) => s.slice(1, -1));
2789
+ if (paramNames.length > 0) {
2790
+ const regexStr = "^" + matchPath.replace(/{([^}]+)}/g, "([^/]+)") + "$";
2791
+ const regex = new RegExp(regexStr);
2792
+ const match = regex.exec(ctx.path);
2793
+ if (match) {
2794
+ params = {};
2795
+ paramNames.forEach((name, i) => {
2796
+ params[name] = match[i + 1];
2797
+ });
2798
+ }
1705
2799
  }
1706
2800
  }
1707
- if (opts.maxAge) {
1708
- set("Access-Control-Max-Age", String(opts.maxAge));
2801
+ const valid = validators.params(params);
2802
+ if (!valid && validators.params.errors) {
2803
+ errors.push(...validators.params.errors.map((e) => ({ ...e, location: "path" })));
1709
2804
  }
1710
- return new Response(null, {
1711
- status: opts.optionsSuccessStatus || 204,
1712
- headers
1713
- });
1714
2805
  }
1715
- const response = await next();
1716
- if (response instanceof Response) {
1717
- for (const [key, value] of headers.entries()) {
1718
- response.headers.set(key, value);
2806
+ if (validators.headers) {
2807
+ const headers = Object.fromEntries(ctx.req.headers.entries());
2808
+ const valid = validators.headers(headers);
2809
+ if (!valid && validators.headers.errors) {
2810
+ errors.push(...validators.headers.errors.map((e) => ({ ...e, location: "header" })));
1719
2811
  }
1720
2812
  }
1721
- return response;
2813
+ if (errors.length > 0) {
2814
+ throw new ValidationError(errors);
2815
+ }
2816
+ return next();
1722
2817
  };
1723
2818
  }
2819
+ function compileValidators(spec) {
2820
+ const cache = /* @__PURE__ */ new Map();
2821
+ for (const [path2, pathItem] of Object.entries(spec.paths || {})) {
2822
+ const pathValidators = {};
2823
+ for (const [method, operation] of Object.entries(pathItem)) {
2824
+ if (method === "parameters" || method === "summary" || method === "description") continue;
2825
+ const oper = operation;
2826
+ const validators = {};
2827
+ if (oper.requestBody?.content?.["application/json"]?.schema) {
2828
+ validators.body = ajv.compile(oper.requestBody.content["application/json"].schema);
2829
+ }
2830
+ const parameters = [...oper.parameters || [], ...pathItem.parameters || []];
2831
+ const queryProps = {};
2832
+ const pathProps = {};
2833
+ const headerProps = {};
2834
+ const queryRequired = [];
2835
+ const pathRequired = [];
2836
+ const headerRequired = [];
2837
+ for (const param of parameters) {
2838
+ if (param.in === "query") {
2839
+ queryProps[param.name] = param.schema || {};
2840
+ if (param.required) queryRequired.push(param.name);
2841
+ } else if (param.in === "path") {
2842
+ pathProps[param.name] = param.schema || {};
2843
+ pathRequired.push(param.name);
2844
+ } else if (param.in === "header") {
2845
+ headerProps[param.name] = param.schema || {};
2846
+ if (param.required) headerRequired.push(param.name);
2847
+ }
2848
+ }
2849
+ if (Object.keys(queryProps).length > 0) {
2850
+ validators.query = ajv.compile({
2851
+ type: "object",
2852
+ properties: queryProps,
2853
+ required: queryRequired.length > 0 ? queryRequired : void 0
2854
+ });
2855
+ }
2856
+ if (Object.keys(pathProps).length > 0) {
2857
+ validators.params = ajv.compile({
2858
+ type: "object",
2859
+ properties: pathProps,
2860
+ required: pathRequired.length > 0 ? pathRequired : void 0
2861
+ });
2862
+ }
2863
+ if (Object.keys(headerProps).length > 0) {
2864
+ validators.headers = ajv.compile({
2865
+ type: "object",
2866
+ properties: headerProps,
2867
+ required: headerRequired.length > 0 ? headerRequired : void 0
2868
+ });
2869
+ }
2870
+ pathValidators[method] = validators;
2871
+ }
2872
+ cache.set(path2, pathValidators);
2873
+ }
2874
+ return cache;
2875
+ }
1724
2876
  function RateLimit(options = {}) {
1725
2877
  const windowMs = options.windowMs || 60 * 1e3;
1726
2878
  const max = options.max || 5;
@@ -1740,7 +2892,7 @@ function RateLimit(options = {}) {
1740
2892
  }
1741
2893
  }
1742
2894
  }, windowMs);
1743
- if (interval.unref) interval.unref();
2895
+ interval.unref?.();
1744
2896
  return async (ctx, next) => {
1745
2897
  if (skip(ctx)) return next();
1746
2898
  const key = keyGenerator(ctx);
@@ -1811,10 +2963,43 @@ class ScalarPlugin extends ShokupanRouter {
1811
2963
  this.get("/scalar.js", (ctx) => {
1812
2964
  return ctx.file(__dirname + "/../../node_modules/@scalar/api-reference/dist/browser/standalone.js");
1813
2965
  });
1814
- this.get("/openapi.json", (ctx) => {
1815
- return (this.root || this).generateApiSpec();
2966
+ this.get("/openapi.json", async (ctx) => {
2967
+ let spec;
2968
+ if (this.root.openApiSpec) {
2969
+ try {
2970
+ spec = structuredClone(this.root.openApiSpec);
2971
+ } catch (e) {
2972
+ spec = Object.assign({}, this.root.openApiSpec);
2973
+ }
2974
+ } else {
2975
+ spec = await (this.root || this).generateApiSpec();
2976
+ }
2977
+ if (this.pluginOptions.baseDocument) {
2978
+ deepMerge(spec, this.pluginOptions.baseDocument);
2979
+ }
2980
+ return ctx.json(spec);
1816
2981
  });
1817
2982
  }
2983
+ // New lifecycle method to be called by router.mount
2984
+ onMount(parent) {
2985
+ if (parent.onStart) {
2986
+ parent.onStart(async () => {
2987
+ if (this.pluginOptions.enableStaticAnalysis) {
2988
+ try {
2989
+ const entrypoint = process.argv[1];
2990
+ console.log(`[ScalarPlugin] Running eager static analysis on entrypoint: ${entrypoint}`);
2991
+ const analyzer = new openapiAnalyzer.OpenAPIAnalyzer(process.cwd(), entrypoint);
2992
+ let staticSpec = await analyzer.analyze();
2993
+ if (!this.pluginOptions.baseDocument) this.pluginOptions.baseDocument = {};
2994
+ deepMerge(this.pluginOptions.baseDocument, staticSpec);
2995
+ console.log("[ScalarPlugin] Static analysis completed successfully.");
2996
+ } catch (err) {
2997
+ console.error("[ScalarPlugin] Failed to run static analysis:", err);
2998
+ }
2999
+ }
3000
+ });
3001
+ }
3002
+ }
1818
3003
  }
1819
3004
  function SecurityHeaders(options = {}) {
1820
3005
  return async (ctx, next) => {
@@ -2121,134 +3306,6 @@ function Session(options) {
2121
3306
  return result;
2122
3307
  };
2123
3308
  }
2124
- class ValidationError extends Error {
2125
- constructor(errors) {
2126
- super("Validation Error");
2127
- this.errors = errors;
2128
- }
2129
- status = 400;
2130
- }
2131
- function isZod(schema) {
2132
- return typeof schema?.safeParse === "function";
2133
- }
2134
- async function validateZod(schema, data) {
2135
- const result = await schema.safeParseAsync(data);
2136
- if (!result.success) {
2137
- throw new ValidationError(result.error.errors);
2138
- }
2139
- return result.data;
2140
- }
2141
- function isTypeBox(schema) {
2142
- return typeof schema?.Check === "function" && typeof schema?.Errors === "function";
2143
- }
2144
- function validateTypeBox(schema, data) {
2145
- if (!schema.Check(data)) {
2146
- throw new ValidationError([...schema.Errors(data)]);
2147
- }
2148
- return data;
2149
- }
2150
- function isAjv(schema) {
2151
- return typeof schema === "function" && "errors" in schema;
2152
- }
2153
- function validateAjv(schema, data) {
2154
- const valid = schema(data);
2155
- if (!valid) {
2156
- throw new ValidationError(schema.errors);
2157
- }
2158
- return data;
2159
- }
2160
- const valibot = (schema, parser) => {
2161
- return {
2162
- _valibot: true,
2163
- schema,
2164
- parser
2165
- };
2166
- };
2167
- function isValibotWrapper(schema) {
2168
- return schema?._valibot === true;
2169
- }
2170
- async function validateValibotWrapper(wrapper, data) {
2171
- const result = await wrapper.parser(wrapper.schema, data);
2172
- if (!result.success) {
2173
- throw new ValidationError(result.issues);
2174
- }
2175
- return result.output;
2176
- }
2177
- const safelyGetBody = async (ctx) => {
2178
- const req = ctx.req;
2179
- if (req._bodyParsed) {
2180
- return req._bodyValue;
2181
- }
2182
- try {
2183
- let data;
2184
- if (typeof req.json === "function") {
2185
- data = await req.json();
2186
- } else {
2187
- data = req.body;
2188
- if (typeof data === "string") {
2189
- try {
2190
- data = JSON.parse(data);
2191
- } catch {
2192
- }
2193
- }
2194
- }
2195
- req._bodyParsed = true;
2196
- req._bodyValue = data;
2197
- Object.defineProperty(req, "json", {
2198
- value: async () => req._bodyValue,
2199
- configurable: true
2200
- });
2201
- return data;
2202
- } catch (e) {
2203
- return {};
2204
- }
2205
- };
2206
- function validate(config) {
2207
- return async (ctx, next) => {
2208
- if (config.params) {
2209
- ctx.params = await runValidation(config.params, ctx.params);
2210
- }
2211
- if (config.query) {
2212
- const url = new URL(ctx.req.url);
2213
- const queryObj = Object.fromEntries(url.searchParams.entries());
2214
- await runValidation(config.query, queryObj);
2215
- }
2216
- if (config.headers) {
2217
- const headersObj = Object.fromEntries(ctx.req.headers.entries());
2218
- await runValidation(config.headers, headersObj);
2219
- }
2220
- if (config.body) {
2221
- const body = await safelyGetBody(ctx);
2222
- const validBody = await runValidation(config.body, body);
2223
- const req = ctx.req;
2224
- req._bodyValue = validBody;
2225
- Object.defineProperty(req, "json", {
2226
- value: async () => validBody,
2227
- configurable: true
2228
- });
2229
- ctx.body = validBody;
2230
- }
2231
- return next();
2232
- };
2233
- }
2234
- async function runValidation(schema, data) {
2235
- if (isZod(schema)) {
2236
- return validateZod(schema, data);
2237
- }
2238
- if (isTypeBox(schema)) {
2239
- return validateTypeBox(schema, data);
2240
- }
2241
- if (isAjv(schema)) {
2242
- return validateAjv(schema, data);
2243
- }
2244
- if (isValibotWrapper(schema)) {
2245
- return validateValibotWrapper(schema, data);
2246
- }
2247
- if (typeof schema === "function") {
2248
- return schema(data);
2249
- }
2250
- throw new Error("Unknown validator type provided. Please use a supported library (Zod, Ajv, TypeBox) or a custom function.");
2251
- }
2252
3309
  exports.$appRoot = $appRoot;
2253
3310
  exports.$childControllers = $childControllers;
2254
3311
  exports.$childRouters = $childRouters;
@@ -2262,6 +3319,8 @@ exports.$mountPath = $mountPath;
2262
3319
  exports.$parent = $parent;
2263
3320
  exports.$routeArgs = $routeArgs;
2264
3321
  exports.$routeMethods = $routeMethods;
3322
+ exports.$routeSpec = $routeSpec;
3323
+ exports.$routes = $routes;
2265
3324
  exports.All = All;
2266
3325
  exports.AuthPlugin = AuthPlugin;
2267
3326
  exports.Body = Body;
@@ -2297,9 +3356,12 @@ exports.ShokupanContext = ShokupanContext;
2297
3356
  exports.ShokupanRequest = ShokupanRequest;
2298
3357
  exports.ShokupanResponse = ShokupanResponse;
2299
3358
  exports.ShokupanRouter = ShokupanRouter;
3359
+ exports.Spec = Spec;
2300
3360
  exports.Use = Use;
2301
3361
  exports.ValidationError = ValidationError;
2302
3362
  exports.compose = compose;
3363
+ exports.openApiValidator = openApiValidator;
3364
+ exports.useExpress = useExpress;
2303
3365
  exports.valibot = valibot;
2304
3366
  exports.validate = validate;
2305
3367
  //# sourceMappingURL=index.cjs.map