shokupan 0.6.1 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/README.md +2 -2
  2. package/dist/{openapi-analyzer-Bei1sVWp.cjs → analyzer-Bei1sVWp.cjs} +1 -1
  3. package/dist/analyzer-Bei1sVWp.cjs.map +1 -0
  4. package/dist/{openapi-analyzer-Ce_7JxZh.js → analyzer-Ce_7JxZh.js} +1 -1
  5. package/dist/analyzer-Ce_7JxZh.js.map +1 -0
  6. package/dist/cli.cjs +2 -2
  7. package/dist/cli.cjs.map +1 -1
  8. package/dist/cli.js +1 -1
  9. package/dist/cli.js.map +1 -1
  10. package/dist/context.d.ts +9 -9
  11. package/dist/{server-adapter-0xH174zz.js → http-server-0xH174zz.js} +1 -1
  12. package/dist/http-server-0xH174zz.js.map +1 -0
  13. package/dist/{server-adapter-DFhwlK8e.cjs → http-server-DFhwlK8e.cjs} +1 -1
  14. package/dist/http-server-DFhwlK8e.cjs.map +1 -0
  15. package/dist/index.cjs +928 -767
  16. package/dist/index.cjs.map +1 -1
  17. package/dist/index.d.ts +17 -17
  18. package/dist/index.js +953 -791
  19. package/dist/index.js.map +1 -1
  20. package/dist/middleware.d.ts +1 -1
  21. package/dist/plugins/{auth.d.ts → application/auth.d.ts} +72 -3
  22. package/dist/plugins/application/cluster.d.ts +33 -0
  23. package/dist/plugins/{failed-request-recorder.d.ts → application/dashboard/failed-request-recorder.d.ts} +1 -1
  24. package/dist/plugins/{debugview → application/dashboard}/plugin.d.ts +13 -6
  25. package/dist/plugins/{server-adapter.d.ts → application/http-server.d.ts} +1 -1
  26. package/dist/plugins/{idempotency → application/idempotency}/plugin.d.ts +7 -1
  27. package/dist/plugins/{openapi.d.ts → application/openapi/openapi.d.ts} +2 -2
  28. package/dist/plugins/application/scalar.d.ts +36 -0
  29. package/dist/plugins/middleware/compression.d.ts +17 -0
  30. package/dist/plugins/middleware/cors.d.ts +34 -0
  31. package/dist/plugins/{express.d.ts → middleware/express.d.ts} +1 -1
  32. package/dist/plugins/{openapi-validator.d.ts → middleware/openapi-validator.d.ts} +2 -2
  33. package/dist/plugins/middleware/proxy.d.ts +37 -0
  34. package/dist/plugins/middleware/rate-limit.d.ts +58 -0
  35. package/dist/plugins/{security-headers.d.ts → middleware/security-headers.d.ts} +51 -1
  36. package/dist/plugins/{serve-static.d.ts → middleware/serve-static.d.ts} +1 -1
  37. package/dist/plugins/{session.d.ts → middleware/session.d.ts} +89 -3
  38. package/dist/plugins/{validation.d.ts → middleware/validation.d.ts} +6 -1
  39. package/dist/router.d.ts +5 -5
  40. package/dist/shokupan.d.ts +10 -4
  41. package/dist/util/async-hooks.d.ts +8 -2
  42. package/dist/{decorators.d.ts → util/decorators.d.ts} +1 -1
  43. package/dist/util/http-status.d.ts +2 -0
  44. package/dist/util/instrumentation.d.ts +1 -1
  45. package/dist/{router → util}/trie.d.ts +1 -1
  46. package/dist/{types.d.ts → util/types.d.ts} +8 -1
  47. package/package.json +4 -4
  48. package/dist/openapi-analyzer-Bei1sVWp.cjs.map +0 -1
  49. package/dist/openapi-analyzer-Ce_7JxZh.js.map +0 -1
  50. package/dist/plugins/compression.d.ts +0 -5
  51. package/dist/plugins/cors.d.ts +0 -11
  52. package/dist/plugins/proxy.d.ts +0 -11
  53. package/dist/plugins/rate-limit.d.ts +0 -15
  54. package/dist/plugins/scalar.d.ts +0 -15
  55. package/dist/server-adapter-0xH174zz.js.map +0 -1
  56. package/dist/server-adapter-DFhwlK8e.cjs.map +0 -1
  57. /package/dist/{analysis/openapi-analyzer.d.ts → plugins/application/openapi/analyzer.d.ts} +0 -0
  58. /package/dist/{di.d.ts → util/di.d.ts} +0 -0
  59. /package/dist/{request.d.ts → util/request.d.ts} +0 -0
  60. /package/dist/{response.d.ts → util/response.d.ts} +0 -0
  61. /package/dist/{symbol.d.ts → util/symbol.d.ts} +0 -0
package/dist/index.js CHANGED
@@ -1,18 +1,87 @@
1
1
  import { readFile } from "node:fs/promises";
2
+ import { trace, SpanKind, SpanStatusCode, context } from "@opentelemetry/api";
3
+ import { AsyncLocalStorage } from "node:async_hooks";
2
4
  import { Eta } from "eta";
3
5
  import { stat, readdir, readFile as readFile$1 } from "fs/promises";
4
6
  import { resolve, join, sep, basename } from "path";
5
- import { AsyncLocalStorage } from "node:async_hooks";
6
- import { trace, SpanKind, SpanStatusCode, context } from "@opentelemetry/api";
7
7
  import * as os from "node:os";
8
+ import os__default from "node:os";
8
9
  import { OAuth2Client, Okta, Auth0, Apple, MicrosoftEntraId, Google, GitHub, generateState, generateCodeVerifier } from "arctic";
9
10
  import * as jose from "jose";
11
+ import cluster from "node:cluster";
12
+ import net from "node:net";
13
+ import { OpenAPIAnalyzer } from "./analyzer-Ce_7JxZh.js";
10
14
  import * as zlib from "node:zlib";
11
15
  import Ajv from "ajv";
12
16
  import addFormats from "ajv-formats";
13
- import { OpenAPIAnalyzer } from "./openapi-analyzer-Ce_7JxZh.js";
14
17
  import { randomUUID, createHmac } from "crypto";
15
18
  import { EventEmitter } from "events";
19
+ const VALID_HTTP_STATUSES = /* @__PURE__ */ new Set([
20
+ 100,
21
+ 101,
22
+ 102,
23
+ 103,
24
+ 200,
25
+ 201,
26
+ 202,
27
+ 203,
28
+ 204,
29
+ 205,
30
+ 206,
31
+ 207,
32
+ 208,
33
+ 226,
34
+ 300,
35
+ 301,
36
+ 302,
37
+ 303,
38
+ 304,
39
+ 305,
40
+ 306,
41
+ 307,
42
+ 308,
43
+ 400,
44
+ 401,
45
+ 402,
46
+ 403,
47
+ 404,
48
+ 405,
49
+ 406,
50
+ 407,
51
+ 408,
52
+ 409,
53
+ 410,
54
+ 411,
55
+ 412,
56
+ 413,
57
+ 414,
58
+ 415,
59
+ 416,
60
+ 417,
61
+ 418,
62
+ 421,
63
+ 422,
64
+ 423,
65
+ 424,
66
+ 425,
67
+ 426,
68
+ 428,
69
+ 429,
70
+ 431,
71
+ 451,
72
+ 500,
73
+ 501,
74
+ 502,
75
+ 503,
76
+ 504,
77
+ 505,
78
+ 506,
79
+ 507,
80
+ 508,
81
+ 510,
82
+ 511
83
+ ]);
84
+ const VALID_REDIRECT_STATUSES = /* @__PURE__ */ new Set([301, 302, 303, 307, 308]);
16
85
  class ShokupanResponse {
17
86
  _headers = null;
18
87
  _status = 200;
@@ -85,72 +154,6 @@ function isValidCookieDomain(domain, currentHost) {
85
154
  }
86
155
  return false;
87
156
  }
88
- const VALID_HTTP_STATUSES = /* @__PURE__ */ new Set([
89
- 100,
90
- 101,
91
- 102,
92
- 103,
93
- 200,
94
- 201,
95
- 202,
96
- 203,
97
- 204,
98
- 205,
99
- 206,
100
- 207,
101
- 208,
102
- 226,
103
- 300,
104
- 301,
105
- 302,
106
- 303,
107
- 304,
108
- 305,
109
- 306,
110
- 307,
111
- 308,
112
- 400,
113
- 401,
114
- 402,
115
- 403,
116
- 404,
117
- 405,
118
- 406,
119
- 407,
120
- 408,
121
- 409,
122
- 410,
123
- 411,
124
- 412,
125
- 413,
126
- 414,
127
- 415,
128
- 416,
129
- 417,
130
- 418,
131
- 421,
132
- 422,
133
- 423,
134
- 424,
135
- 425,
136
- 426,
137
- 428,
138
- 429,
139
- 431,
140
- 451,
141
- 500,
142
- 501,
143
- 502,
144
- 503,
145
- 504,
146
- 505,
147
- 506,
148
- 507,
149
- 508,
150
- 510,
151
- 511
152
- ]);
153
- const VALID_REDIRECT_STATUSES = /* @__PURE__ */ new Set([301, 302, 303, 307, 308]);
154
157
  class ShokupanContext {
155
158
  constructor(request, server, state, app, signal, enableMiddlewareTracking = false) {
156
159
  this.request = request;
@@ -189,12 +192,20 @@ class ShokupanContext {
189
192
  _bodyType;
190
193
  _bodyParsed = false;
191
194
  _bodyParseError;
195
+ _routeMatched = false;
192
196
  // Cached URL properties to avoid repeated parsing
193
197
  _cachedHostname;
194
198
  _cachedProtocol;
195
199
  _cachedHost;
196
200
  _cachedOrigin;
197
201
  _cachedQuery;
202
+ /**
203
+ * JSX Rendering Function
204
+ */
205
+ renderer;
206
+ setRenderer(renderer) {
207
+ this.renderer = renderer;
208
+ }
198
209
  get url() {
199
210
  if (!this._url) {
200
211
  const urlString = this.request.url || "http://localhost/";
@@ -412,11 +423,15 @@ class ShokupanContext {
412
423
  }
413
424
  const contentType = this.request.headers.get("content-type") || "";
414
425
  if (contentType.includes("application/json") || contentType.includes("+json")) {
415
- const rawText = await this.readRawBody();
416
426
  const parserType = this.app?.applicationConfig?.jsonParser || "native";
417
427
  if (parserType === "native") {
418
- this._cachedBody = JSON.parse(rawText);
428
+ try {
429
+ this._cachedBody = await this.request.json();
430
+ } catch (e) {
431
+ throw e;
432
+ }
419
433
  } else {
434
+ const rawText = await this.request.text();
420
435
  const { getJSONParser } = await import("./json-parser-B3dnQmCC.js");
421
436
  const parser = getJSONParser(parserType);
422
437
  this._cachedBody = parser(rawText);
@@ -426,7 +441,7 @@ class ShokupanContext {
426
441
  this._cachedBody = await this.request.formData();
427
442
  this._bodyType = "formData";
428
443
  } else {
429
- this._cachedBody = await this.readRawBody();
444
+ this._cachedBody = await this.request.text();
430
445
  this._bodyType = "text";
431
446
  }
432
447
  this._bodyParsed = true;
@@ -604,10 +619,6 @@ class ShokupanContext {
604
619
  return this._finalResponse;
605
620
  }
606
621
  }
607
- /**
608
- * JSX Rendering Function
609
- */
610
- renderer;
611
622
  /**
612
623
  * Render a JSX element
613
624
  * @param element JSX Element
@@ -626,284 +637,67 @@ class ShokupanContext {
626
637
  return this.html(html, status, headers);
627
638
  }
628
639
  }
629
- function RateLimitMiddleware(options = {}) {
630
- const windowMs = options.windowMs || 60 * 1e3;
631
- const max = options.limit || options.max || 5;
632
- const message = options.message || "Too many requests, please try again later.";
633
- const statusCode = options.statusCode || 429;
634
- const headers = options.headers !== false;
635
- const mode = options.mode || "user";
636
- const trustedProxies = options.trustedProxies || [];
637
- const keyGenerator = options.keyGenerator || ((ctx) => {
638
- if (mode === "absolute") {
639
- return "global";
640
- }
641
- const xForwardedFor = ctx.headers.get("x-forwarded-for");
642
- if (xForwardedFor && trustedProxies.length > 0) {
643
- const ips = xForwardedFor.split(",").map((ip) => ip.trim());
644
- for (let i = ips.length - 1; i >= 0; i--) {
645
- const ip = ips[i];
646
- if (!trustedProxies.includes(ip)) {
647
- if (/^[\d.:a-fA-F]+$/.test(ip)) {
648
- return ip;
649
- }
650
- }
640
+ const compose = (middleware) => {
641
+ if (!middleware.length) {
642
+ return (context2, next) => {
643
+ return next ? next() : Promise.resolve();
644
+ };
645
+ }
646
+ return function dispatch(context2, next) {
647
+ let index = -1;
648
+ async function runner(i) {
649
+ if (i <= index) return Promise.reject(new Error("next() called multiple times"));
650
+ index = i;
651
+ if (i >= middleware.length) {
652
+ return next ? next() : Promise.resolve();
651
653
  }
652
- }
653
- return ctx.server?.requestIP?.(ctx.request)?.address || "unknown";
654
- });
655
- const skip = options.skip || (() => false);
656
- const hits = /* @__PURE__ */ new Map();
657
- const interval = setInterval(() => {
658
- const now = Date.now();
659
- const entries = Array.from(hits.entries());
660
- for (let i = 0; i < entries.length; i++) {
661
- const [key, record] = entries[i];
662
- if (record.resetTime <= now) {
663
- hits.delete(key);
654
+ const fn = middleware[i];
655
+ if (!context2._debug) {
656
+ return fn(context2, () => runner(i + 1));
664
657
  }
665
- }
666
- }, windowMs);
667
- if (interval.unref) interval.unref();
668
- const rateLimitMiddleware = async function RateLimitMiddleware2(ctx, next) {
669
- if (skip(ctx)) return next();
670
- const key = keyGenerator(ctx);
671
- const now = Date.now();
672
- let record = hits.get(key);
673
- if (!record || record.resetTime <= now) {
674
- record = {
675
- hits: 0,
676
- resetTime: now + windowMs
677
- };
678
- hits.set(key, record);
679
- }
680
- record.hits++;
681
- const remaining = Math.max(0, max - record.hits);
682
- const resetTime = Math.ceil(record.resetTime / 1e3);
683
- const retryAfter = Math.ceil((record.resetTime - now) / 1e3);
684
- const setHeaders = (res) => {
685
- if (!headers || !res || !res.headers) return;
658
+ const debug = context2._debug;
659
+ const debugId = fn._debugId || fn.name || "anonymous";
660
+ const previousNode = debug.getCurrentNode();
661
+ debug.trackEdge(previousNode, debugId);
662
+ debug.setNode(debugId);
663
+ const start = performance.now();
686
664
  try {
687
- res.headers.set("X-RateLimit-Limit", String(max));
688
- res.headers.set("X-RateLimit-Remaining", String(remaining));
689
- res.headers.set("X-RateLimit-Reset", String(resetTime));
690
- } catch (e) {
691
- }
692
- };
693
- if (record.hits > max) {
694
- typeof message === "object" ? JSON.stringify(message) : String(message);
695
- const res = typeof message === "object" ? ctx.json(message, statusCode) : ctx.text(String(message), statusCode);
696
- if (headers) {
697
- setHeaders(res);
698
- res.headers.set("Retry-After", String(retryAfter));
665
+ const res = await Promise.resolve(fn(context2, () => runner(i + 1)));
666
+ debug.trackStep(debugId, "middleware", performance.now() - start, "success");
667
+ return res;
668
+ } catch (err) {
669
+ debug.trackStep(debugId, "middleware", performance.now() - start, "error", err);
670
+ return Promise.reject(err);
671
+ } finally {
672
+ if (previousNode) debug.setNode(previousNode);
699
673
  }
700
- return res;
701
- }
702
- const response = await next();
703
- if (response instanceof Response && headers) {
704
- setHeaders(response);
705
674
  }
706
- return response;
675
+ return runner(0);
707
676
  };
708
- rateLimitMiddleware.isBuiltin = true;
709
- rateLimitMiddleware.pluginName = "RateLimit";
710
- return rateLimitMiddleware;
711
- }
712
- const $isApplication = /* @__PURE__ */ Symbol.for("Shokupan.app");
713
- const $appRoot = /* @__PURE__ */ Symbol.for("Shokupan.app-root");
714
- const $isMounted = /* @__PURE__ */ Symbol("Shokupan.isMounted");
715
- const $routeMethods = /* @__PURE__ */ Symbol("Shokupan.routeMethods");
716
- const $routeArgs = /* @__PURE__ */ Symbol("Shokupan.routeArgs");
717
- const $controllerPath = /* @__PURE__ */ Symbol("Shokupan.controllerPath");
718
- const $middleware = /* @__PURE__ */ Symbol("Shokupan.middleware");
719
- const $isRouter = /* @__PURE__ */ Symbol.for("Shokupan.router");
720
- const $parent = /* @__PURE__ */ Symbol.for("Shokupan.parent");
721
- const $childRouters = /* @__PURE__ */ Symbol.for("Shokupan.child-routers");
722
- const $childControllers = /* @__PURE__ */ Symbol.for("Shokupan.child-controllers");
723
- const $mountPath = /* @__PURE__ */ Symbol.for("Shokupan.mount-path");
724
- const $dispatch = /* @__PURE__ */ Symbol.for("Shokupan.dispatch");
725
- const $routes = /* @__PURE__ */ Symbol.for("Shokupan.routes");
726
- const $routeSpec = /* @__PURE__ */ Symbol.for("Shokupan.routeSpec");
727
- const HTTPMethods = ["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS", "ALL"];
728
- var RouteParamType = /* @__PURE__ */ ((RouteParamType2) => {
729
- RouteParamType2["BODY"] = "BODY";
730
- RouteParamType2["PARAM"] = "PARAM";
731
- RouteParamType2["QUERY"] = "QUERY";
732
- RouteParamType2["HEADER"] = "HEADER";
733
- RouteParamType2["REQUEST"] = "REQUEST";
734
- RouteParamType2["CONTEXT"] = "CONTEXT";
735
- return RouteParamType2;
736
- })(RouteParamType || {});
737
- function Controller(path = "/") {
738
- return (target) => {
739
- target[$controllerPath] = path;
740
- };
741
- }
742
- function Use(...middleware) {
743
- return (target, propertyKey, descriptor) => {
744
- if (!propertyKey) {
745
- const existing = target[$middleware] || [];
746
- target[$middleware] = [...existing, ...middleware];
747
- } else {
748
- if (!target[$middleware]) {
749
- target[$middleware] = /* @__PURE__ */ new Map();
750
- }
751
- const existing = target[$middleware].get(propertyKey) || [];
752
- target[$middleware].set(propertyKey, [...existing, ...middleware]);
753
- }
754
- };
755
- }
756
- function createParamDecorator(type) {
757
- return (name) => {
758
- return (target, propertyKey, parameterIndex) => {
759
- if (!target[$routeArgs]) {
760
- target[$routeArgs] = /* @__PURE__ */ new Map();
761
- }
762
- if (!target[$routeArgs].has(propertyKey)) {
763
- target[$routeArgs].set(propertyKey, []);
764
- }
765
- target[$routeArgs].get(propertyKey).push({
766
- index: parameterIndex,
767
- type,
768
- name
769
- });
770
- };
771
- };
772
- }
773
- const Body = createParamDecorator(RouteParamType.BODY);
774
- const Param = createParamDecorator(RouteParamType.PARAM);
775
- const Query = createParamDecorator(RouteParamType.QUERY);
776
- const Headers$1 = createParamDecorator(RouteParamType.HEADER);
777
- const Req = createParamDecorator(RouteParamType.REQUEST);
778
- const Ctx = createParamDecorator(RouteParamType.CONTEXT);
779
- function Spec(spec) {
780
- return (target, propertyKey, descriptor) => {
781
- if (!target[$routeSpec]) {
782
- target[$routeSpec] = /* @__PURE__ */ new Map();
783
- }
784
- target[$routeSpec].set(propertyKey, spec);
785
- };
786
- }
787
- function createMethodDecorator(method) {
788
- return (path = "/") => {
789
- return (target, propertyKey, descriptor) => {
790
- if (!target[$routeMethods]) {
791
- target[$routeMethods] = /* @__PURE__ */ new Map();
792
- }
793
- target[$routeMethods].set(propertyKey, {
794
- method,
795
- path
796
- });
797
- };
798
- };
799
- }
800
- const Get = createMethodDecorator("GET");
801
- const Post = createMethodDecorator("POST");
802
- const Put = createMethodDecorator("PUT");
803
- const Delete = createMethodDecorator("DELETE");
804
- const Patch = createMethodDecorator("PATCH");
805
- const Options = createMethodDecorator("OPTIONS");
806
- const Head = createMethodDecorator("HEAD");
807
- const All = createMethodDecorator("ALL");
808
- function RateLimit(options) {
809
- return Use(RateLimitMiddleware(options));
810
- }
811
- class Container {
812
- static services = /* @__PURE__ */ new Map();
813
- static register(target, instance) {
814
- this.services.set(target, instance);
815
- }
816
- static get(target) {
817
- return this.services.get(target);
818
- }
819
- static has(target) {
820
- return this.services.has(target);
821
- }
822
- static resolve(target) {
823
- if (this.services.has(target)) {
824
- return this.services.get(target);
825
- }
826
- const instance = new target();
827
- this.services.set(target, instance);
828
- return instance;
829
- }
830
- }
831
- function Injectable() {
832
- return (target) => {
833
- };
834
- }
835
- function Inject(token) {
836
- return (target, key) => {
837
- Object.defineProperty(target, key, {
838
- get: () => Container.resolve(token),
839
- enumerable: true,
840
- configurable: true
841
- });
842
- };
843
- }
844
- const compose = (middleware) => {
845
- if (!middleware.length) {
846
- return (context2, next) => {
847
- return next ? next() : Promise.resolve();
848
- };
849
- }
850
- return function dispatch(context2, next) {
851
- let index = -1;
852
- async function runner(i) {
853
- if (i <= index) return Promise.reject(new Error("next() called multiple times"));
854
- index = i;
855
- if (i >= middleware.length) {
856
- return next ? next() : Promise.resolve();
857
- }
858
- const fn = middleware[i];
859
- if (!context2._debug) {
860
- return fn(context2, () => runner(i + 1));
677
+ };
678
+ const tracer = trace.getTracer("shokupan.middleware");
679
+ function traceHandler(fn, name) {
680
+ return async function(...args) {
681
+ return tracer.startActiveSpan(`route handler - ${name}`, {
682
+ kind: SpanKind.INTERNAL,
683
+ attributes: {
684
+ "http.route": name,
685
+ "component": "shokupan.route"
861
686
  }
862
- const debug = context2._debug;
863
- const debugId = fn._debugId || fn.name || "anonymous";
864
- const previousNode = debug.getCurrentNode();
865
- debug.trackEdge(previousNode, debugId);
866
- debug.setNode(debugId);
867
- const start = performance.now();
687
+ }, async (span) => {
868
688
  try {
869
- const res = await Promise.resolve(fn(context2, () => runner(i + 1)));
870
- debug.trackStep(debugId, "middleware", performance.now() - start, "success");
871
- return res;
689
+ const result = await fn.apply(this, args);
690
+ return result;
872
691
  } catch (err) {
873
- debug.trackStep(debugId, "middleware", performance.now() - start, "error", err);
874
- return Promise.reject(err);
692
+ span.recordException(err);
693
+ span.setStatus({ code: SpanStatusCode.ERROR, message: err.message });
694
+ throw err;
875
695
  } finally {
876
- if (previousNode) debug.setNode(previousNode);
696
+ span.end();
877
697
  }
878
- }
879
- return runner(0);
698
+ });
880
699
  };
881
- };
882
- class ShokupanRequestBase {
883
- method;
884
- url;
885
- headers;
886
- body;
887
- async json() {
888
- return JSON.parse(this.body);
889
- }
890
- async text() {
891
- return this.body;
892
- }
893
- async formData() {
894
- if (this.body instanceof FormData) {
895
- return this.body;
896
- }
897
- return new Response(this.body, { headers: this.headers }).formData();
898
- }
899
- constructor(props) {
900
- Object.assign(this, props);
901
- if (!(this.headers instanceof Headers)) {
902
- this.headers = new Headers(this.headers);
903
- }
904
- }
905
700
  }
906
- const ShokupanRequest = ShokupanRequestBase;
907
701
  function isObject(item) {
908
702
  return item && typeof item === "object" && !Array.isArray(item);
909
703
  }
@@ -937,6 +731,21 @@ function deepMerge(target, ...sources) {
937
731
  }
938
732
  return deepMerge(target, ...sources);
939
733
  }
734
+ const $isApplication = /* @__PURE__ */ Symbol.for("Shokupan.app");
735
+ const $appRoot = /* @__PURE__ */ Symbol.for("Shokupan.app-root");
736
+ const $isMounted = /* @__PURE__ */ Symbol("Shokupan.isMounted");
737
+ const $routeMethods = /* @__PURE__ */ Symbol("Shokupan.routeMethods");
738
+ const $routeArgs = /* @__PURE__ */ Symbol("Shokupan.routeArgs");
739
+ const $controllerPath = /* @__PURE__ */ Symbol("Shokupan.controllerPath");
740
+ const $middleware = /* @__PURE__ */ Symbol("Shokupan.middleware");
741
+ const $isRouter = /* @__PURE__ */ Symbol.for("Shokupan.router");
742
+ const $parent = /* @__PURE__ */ Symbol.for("Shokupan.parent");
743
+ const $childRouters = /* @__PURE__ */ Symbol.for("Shokupan.child-routers");
744
+ const $childControllers = /* @__PURE__ */ Symbol.for("Shokupan.child-controllers");
745
+ const $mountPath = /* @__PURE__ */ Symbol.for("Shokupan.mount-path");
746
+ const $dispatch = /* @__PURE__ */ Symbol.for("Shokupan.dispatch");
747
+ const $routes = /* @__PURE__ */ Symbol.for("Shokupan.routes");
748
+ const $routeSpec = /* @__PURE__ */ Symbol.for("Shokupan.routeSpec");
940
749
  const REGEX_PATTERNS = {
941
750
  QUERY_INT: /parseInt\(ctx\.query\.(\w+)\)/g,
942
751
  QUERY_FLOAT: /parseFloat\(ctx\.query\.(\w+)\)/g,
@@ -1129,7 +938,7 @@ async function generateOpenApi(rootRouter, options = {}) {
1129
938
  const defaultTagName = options.defaultTag || "Application";
1130
939
  let astRoutes = [];
1131
940
  try {
1132
- const { OpenAPIAnalyzer: OpenAPIAnalyzer2 } = await import("./openapi-analyzer-Ce_7JxZh.js");
941
+ const { OpenAPIAnalyzer: OpenAPIAnalyzer2 } = await import("./analyzer-Ce_7JxZh.js");
1133
942
  const analyzer = new OpenAPIAnalyzer2(process.cwd());
1134
943
  const { applications } = await analyzer.analyze();
1135
944
  astRoutes = await getAstRoutes(applications);
@@ -1318,6 +1127,11 @@ async function generateOpenApi(rootRouter, options = {}) {
1318
1127
  "x-tagGroups": xTagGroups
1319
1128
  };
1320
1129
  }
1130
+ class RequestContextStore {
1131
+ request;
1132
+ span;
1133
+ }
1134
+ const asyncContext = new AsyncLocalStorage();
1321
1135
  const eta$1 = new Eta();
1322
1136
  function serveStatic(config, prefix) {
1323
1137
  const rootPath = resolve(config.root || ".");
@@ -1470,107 +1284,6 @@ function serveStatic(config, prefix) {
1470
1284
  serveStaticMiddleware.pluginName = "ServeStatic";
1471
1285
  return serveStaticMiddleware;
1472
1286
  }
1473
- class RouterTrie {
1474
- root;
1475
- constructor() {
1476
- this.root = this.createNode();
1477
- }
1478
- createNode() {
1479
- return {
1480
- children: {}
1481
- };
1482
- }
1483
- insert(method, path, handler) {
1484
- let node = this.root;
1485
- const segments = this.splitPath(path);
1486
- for (let i = 0; i < segments.length; i++) {
1487
- const segment = segments[i];
1488
- if (segment === "**") {
1489
- if (!node.recursiveChild) {
1490
- node.recursiveChild = this.createNode();
1491
- }
1492
- node = node.recursiveChild;
1493
- } else if (segment === "*") {
1494
- if (!node.wildcardChild) {
1495
- node.wildcardChild = this.createNode();
1496
- }
1497
- node = node.wildcardChild;
1498
- } else if (segment.startsWith(":")) {
1499
- const paramName = segment.slice(1);
1500
- if (!node.paramChild) {
1501
- node.paramChild = this.createNode();
1502
- node.paramChild.paramName = paramName;
1503
- }
1504
- node = node.paramChild;
1505
- node.paramName = paramName;
1506
- } else {
1507
- if (!node.children[segment]) {
1508
- node.children[segment] = this.createNode();
1509
- }
1510
- node = node.children[segment];
1511
- }
1512
- }
1513
- if (!node.handlers) {
1514
- node.handlers = {};
1515
- }
1516
- node.handlers[method] = handler;
1517
- }
1518
- search(method, path) {
1519
- const segments = this.splitPath(path);
1520
- const params = {};
1521
- const match = this.findNode(this.root, segments, 0, params);
1522
- if (match && match.handlers) {
1523
- const handler = match.handlers[method] || match.handlers["ALL"];
1524
- if (handler) {
1525
- return { handler, params };
1526
- }
1527
- if (method === "HEAD" && match.handlers["GET"]) {
1528
- return { handler: match.handlers["GET"], params };
1529
- }
1530
- }
1531
- return null;
1532
- }
1533
- findNode(node, segments, index, params) {
1534
- if (index === segments.length) {
1535
- if (node.handlers) return node;
1536
- if (node.recursiveChild && node.recursiveChild.handlers) {
1537
- return node.recursiveChild;
1538
- }
1539
- return null;
1540
- }
1541
- const segment = segments[index];
1542
- const child = node.children[segment];
1543
- if (child) {
1544
- const result = this.findNode(child, segments, index + 1, params);
1545
- if (result) return result;
1546
- }
1547
- if (node.paramChild) {
1548
- params[node.paramChild.paramName] = segment;
1549
- const result = this.findNode(node.paramChild, segments, index + 1, params);
1550
- if (result) return result;
1551
- delete params[node.paramChild.paramName];
1552
- }
1553
- if (node.wildcardChild) {
1554
- const result = this.findNode(node.wildcardChild, segments, index + 1, params);
1555
- if (result) return result;
1556
- }
1557
- if (node.recursiveChild) {
1558
- const remaining = segments.length - index;
1559
- for (let k = 0; k <= remaining; k++) {
1560
- const result = this.findNode(node.recursiveChild, segments, index + k, params);
1561
- if (result) return result;
1562
- }
1563
- }
1564
- return null;
1565
- }
1566
- splitPath(path) {
1567
- if (path === "/" || path === "") return [];
1568
- const s = path.startsWith("/") ? path.slice(1) : path;
1569
- if (s === "") return [];
1570
- return s.split("/");
1571
- }
1572
- }
1573
- const asyncContext = new AsyncLocalStorage();
1574
1287
  let db;
1575
1288
  let dbPromise = null;
1576
1289
  let RecordId;
@@ -1634,29 +1347,64 @@ const datastore = {
1634
1347
  process.on("exit", async () => {
1635
1348
  if (db) await db.close();
1636
1349
  });
1637
- const tracer = trace.getTracer("shokupan.middleware");
1638
- function traceHandler(fn, name) {
1639
- return async function(...args) {
1640
- return tracer.startActiveSpan(`route handler - ${name}`, {
1641
- kind: SpanKind.INTERNAL,
1642
- attributes: {
1643
- "http.route": name,
1644
- "component": "shokupan.route"
1645
- }
1646
- }, async (span) => {
1647
- try {
1648
- const result = await fn.apply(this, args);
1649
- return result;
1650
- } catch (err) {
1651
- span.recordException(err);
1652
- span.setStatus({ code: SpanStatusCode.ERROR, message: err.message });
1653
- throw err;
1654
- } finally {
1655
- span.end();
1656
- }
1350
+ class Container {
1351
+ static services = /* @__PURE__ */ new Map();
1352
+ static register(target, instance) {
1353
+ this.services.set(target, instance);
1354
+ }
1355
+ static get(target) {
1356
+ return this.services.get(target);
1357
+ }
1358
+ static has(target) {
1359
+ return this.services.has(target);
1360
+ }
1361
+ static resolve(target) {
1362
+ if (this.services.has(target)) {
1363
+ return this.services.get(target);
1364
+ }
1365
+ const instance = new target();
1366
+ this.services.set(target, instance);
1367
+ return instance;
1368
+ }
1369
+ }
1370
+ function Injectable() {
1371
+ return (target) => {
1372
+ };
1373
+ }
1374
+ function Inject(token) {
1375
+ return (target, key) => {
1376
+ Object.defineProperty(target, key, {
1377
+ get: () => Container.resolve(token),
1378
+ enumerable: true,
1379
+ configurable: true
1657
1380
  });
1658
1381
  };
1659
1382
  }
1383
+ class ShokupanRequestBase {
1384
+ method;
1385
+ url;
1386
+ headers;
1387
+ body;
1388
+ async json() {
1389
+ return JSON.parse(this.body);
1390
+ }
1391
+ async text() {
1392
+ return this.body;
1393
+ }
1394
+ async formData() {
1395
+ if (this.body instanceof FormData) {
1396
+ return this.body;
1397
+ }
1398
+ return new Response(this.body, { headers: this.headers }).formData();
1399
+ }
1400
+ constructor(props) {
1401
+ Object.assign(this, props);
1402
+ if (!(this.headers instanceof Headers)) {
1403
+ this.headers = new Headers(this.headers);
1404
+ }
1405
+ }
1406
+ }
1407
+ const ShokupanRequest = ShokupanRequestBase;
1660
1408
  function getCallerInfo(skipFrames = 1) {
1661
1409
  let file = "unknown";
1662
1410
  let line = 0;
@@ -1682,12 +1430,120 @@ function getCallerInfo(skipFrames = 1) {
1682
1430
  }
1683
1431
  }
1684
1432
  }
1685
- } catch (e) {
1433
+ } catch (e) {
1434
+ }
1435
+ return { file, line };
1436
+ }
1437
+ class RouterTrie {
1438
+ root;
1439
+ constructor() {
1440
+ this.root = this.createNode();
1441
+ }
1442
+ createNode() {
1443
+ return {
1444
+ children: {}
1445
+ };
1446
+ }
1447
+ insert(method, path, handler) {
1448
+ let node = this.root;
1449
+ const segments = this.splitPath(path);
1450
+ for (let i = 0; i < segments.length; i++) {
1451
+ const segment = segments[i];
1452
+ if (segment === "**") {
1453
+ if (!node.recursiveChild) {
1454
+ node.recursiveChild = this.createNode();
1455
+ }
1456
+ node = node.recursiveChild;
1457
+ } else if (segment === "*") {
1458
+ if (!node.wildcardChild) {
1459
+ node.wildcardChild = this.createNode();
1460
+ }
1461
+ node = node.wildcardChild;
1462
+ } else if (segment.startsWith(":")) {
1463
+ const paramName = segment.slice(1);
1464
+ if (!node.paramChild) {
1465
+ node.paramChild = this.createNode();
1466
+ node.paramChild.paramName = paramName;
1467
+ }
1468
+ node = node.paramChild;
1469
+ node.paramName = paramName;
1470
+ } else {
1471
+ if (!node.children[segment]) {
1472
+ node.children[segment] = this.createNode();
1473
+ }
1474
+ node = node.children[segment];
1475
+ }
1476
+ }
1477
+ if (!node.handlers) {
1478
+ node.handlers = {};
1479
+ }
1480
+ node.handlers[method] = handler;
1481
+ }
1482
+ search(method, path) {
1483
+ const segments = this.splitPath(path);
1484
+ const params = {};
1485
+ const match = this.findNode(this.root, segments, 0, params);
1486
+ if (match && match.handlers) {
1487
+ const handler = match.handlers[method] || match.handlers["ALL"];
1488
+ if (handler) {
1489
+ return { handler, params };
1490
+ }
1491
+ if (method === "HEAD" && match.handlers["GET"]) {
1492
+ return { handler: match.handlers["GET"], params };
1493
+ }
1494
+ }
1495
+ return null;
1496
+ }
1497
+ findNode(node, segments, index, params) {
1498
+ if (index === segments.length) {
1499
+ if (node.handlers) return node;
1500
+ if (node.recursiveChild && node.recursiveChild.handlers) {
1501
+ return node.recursiveChild;
1502
+ }
1503
+ return null;
1504
+ }
1505
+ const segment = segments[index];
1506
+ const child = node.children[segment];
1507
+ if (child) {
1508
+ const result = this.findNode(child, segments, index + 1, params);
1509
+ if (result) return result;
1510
+ }
1511
+ if (node.paramChild) {
1512
+ params[node.paramChild.paramName] = segment;
1513
+ const result = this.findNode(node.paramChild, segments, index + 1, params);
1514
+ if (result) return result;
1515
+ delete params[node.paramChild.paramName];
1516
+ }
1517
+ if (node.wildcardChild) {
1518
+ const result = this.findNode(node.wildcardChild, segments, index + 1, params);
1519
+ if (result) return result;
1520
+ }
1521
+ if (node.recursiveChild) {
1522
+ const remaining = segments.length - index;
1523
+ for (let k = 0; k <= remaining; k++) {
1524
+ const result = this.findNode(node.recursiveChild, segments, index + k, params);
1525
+ if (result) return result;
1526
+ }
1527
+ }
1528
+ return null;
1529
+ }
1530
+ splitPath(path) {
1531
+ if (path === "/" || path === "") return [];
1532
+ const s = path.startsWith("/") ? path.slice(1) : path;
1533
+ if (s === "") return [];
1534
+ return s.split("/");
1686
1535
  }
1687
- return { file, line };
1688
1536
  }
1689
- const RouterRegistry = /* @__PURE__ */ new Map();
1690
- const ShokupanApplicationTree = {};
1537
+ const HTTPMethods = ["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS", "ALL"];
1538
+ var RouteParamType = /* @__PURE__ */ ((RouteParamType2) => {
1539
+ RouteParamType2["BODY"] = "BODY";
1540
+ RouteParamType2["PARAM"] = "PARAM";
1541
+ RouteParamType2["QUERY"] = "QUERY";
1542
+ RouteParamType2["HEADER"] = "HEADER";
1543
+ RouteParamType2["REQUEST"] = "REQUEST";
1544
+ RouteParamType2["CONTEXT"] = "CONTEXT";
1545
+ return RouteParamType2;
1546
+ })(RouteParamType || {});
1691
1547
  class ShokupanRouter {
1692
1548
  constructor(config) {
1693
1549
  this.config = config;
@@ -1798,216 +1654,9 @@ class ShokupanRouter {
1798
1654
  throw new Error(`[Shokupan] strict controller check failed: ${controller.constructor.name || typeof controller} is not a class constructor.`);
1799
1655
  }
1800
1656
  if (this.isRouterInstance(controller)) {
1801
- if (controller[$isMounted]) {
1802
- throw new Error("Router is already mounted");
1803
- }
1804
- controller[$mountPath] = prefix;
1805
- if (!controller.metadata) {
1806
- const info = getCallerInfo();
1807
- controller.metadata = {
1808
- file: info.file,
1809
- line: info.line,
1810
- name: "MountedRouter"
1811
- };
1812
- }
1813
- this[$childRouters].push(controller);
1814
- controller[$parent] = this;
1815
- const setRouterContext = (router) => {
1816
- router[$appRoot] = this.root;
1817
- router[$childRouters].forEach((child) => setRouterContext(child));
1818
- };
1819
- setRouterContext(controller);
1820
- if (this[$appRoot]) ;
1821
- controller[$appRoot] = this.root;
1822
- controller[$isMounted] = true;
1657
+ this.mountRouter(prefix, controller);
1823
1658
  } else {
1824
- let instance = controller;
1825
- if (typeof controller === "function") {
1826
- instance = Container.resolve(controller);
1827
- const controllerPath = controller[$controllerPath];
1828
- if (controllerPath) {
1829
- const p1 = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
1830
- const p2 = controllerPath.startsWith("/") ? controllerPath : "/" + controllerPath;
1831
- prefix = p1 + p2;
1832
- if (!prefix) prefix = "/";
1833
- }
1834
- } else {
1835
- const ctor = instance.constructor;
1836
- const controllerPath = ctor[$controllerPath];
1837
- if (controllerPath) {
1838
- const p1 = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
1839
- const p2 = controllerPath.startsWith("/") ? controllerPath : "/" + controllerPath;
1840
- prefix = p1 + p2;
1841
- if (!prefix) prefix = "/";
1842
- }
1843
- }
1844
- instance[$mountPath] = prefix;
1845
- const info = getCallerInfo();
1846
- instance.metadata = {
1847
- file: info.file,
1848
- line: info.line,
1849
- name: instance.constructor.name
1850
- };
1851
- this[$childControllers].push(instance);
1852
- const controllerMiddleware = (typeof controller === "function" ? controller[$middleware] : instance[$middleware]) || [];
1853
- const proto = Object.getPrototypeOf(instance);
1854
- const methods = /* @__PURE__ */ new Set();
1855
- let current = proto;
1856
- while (current && current !== Object.prototype) {
1857
- Object.getOwnPropertyNames(current).forEach((name) => methods.add(name));
1858
- current = Object.getPrototypeOf(current);
1859
- }
1860
- Object.getOwnPropertyNames(instance).forEach((name) => methods.add(name));
1861
- const decoratedRoutes = instance[$routeMethods] || proto && proto[$routeMethods];
1862
- const decoratedArgs = instance[$routeArgs] || proto && proto[$routeArgs];
1863
- const methodMiddlewareMap = instance[$middleware] || proto && proto[$middleware];
1864
- let routesAttached = 0;
1865
- for (let i = 0; i < Array.from(methods).length; i++) {
1866
- const name = Array.from(methods)[i];
1867
- if (name === "constructor") continue;
1868
- if (["arguments", "caller", "callee"].includes(name)) continue;
1869
- const originalHandler = instance[name];
1870
- if (typeof originalHandler !== "function") continue;
1871
- let method;
1872
- let subPath = "";
1873
- if (decoratedRoutes && decoratedRoutes.has(name)) {
1874
- const config = decoratedRoutes.get(name);
1875
- method = config.method;
1876
- subPath = config.path;
1877
- } else {
1878
- for (let j = 0; j < HTTPMethods.length; j++) {
1879
- const m = HTTPMethods[j];
1880
- if (name.toUpperCase().startsWith(m)) {
1881
- method = m;
1882
- const rest = name.slice(m.length);
1883
- if (rest.length === 0) {
1884
- subPath = "/";
1885
- } else {
1886
- subPath = "";
1887
- let buffer = "";
1888
- const flush = () => {
1889
- if (buffer.length > 0) {
1890
- subPath += "/" + buffer.toLowerCase();
1891
- buffer = "";
1892
- }
1893
- };
1894
- for (let i2 = 0; i2 < rest.length; i2++) {
1895
- const char = rest[i2];
1896
- if (char === "$") {
1897
- flush();
1898
- subPath += "/:";
1899
- continue;
1900
- }
1901
- }
1902
- subPath = rest.replace(/\$/g, "/:").replace(/([a-z0-9])([A-Z])/g, "$1/$2").toLowerCase();
1903
- if (!subPath.startsWith("/")) {
1904
- subPath = "/" + subPath;
1905
- }
1906
- }
1907
- break;
1908
- }
1909
- }
1910
- }
1911
- if (method) {
1912
- routesAttached++;
1913
- const cleanPrefix = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
1914
- const cleanSubPath = subPath === "/" ? "" : subPath;
1915
- let joined;
1916
- if (cleanSubPath.length === 0) {
1917
- joined = cleanPrefix;
1918
- } else if (cleanSubPath.startsWith("/")) {
1919
- joined = cleanPrefix + cleanSubPath;
1920
- } else {
1921
- joined = cleanPrefix + "/" + cleanSubPath;
1922
- }
1923
- const fullPath = joined || "/";
1924
- const normalizedPath = fullPath.replace(/\/+/g, "/");
1925
- const methodMw = methodMiddlewareMap instanceof Map ? methodMiddlewareMap.get(name) || [] : [];
1926
- const allMiddleware = [...controllerMiddleware, ...methodMw];
1927
- const routeArgs = decoratedArgs && decoratedArgs.get(name);
1928
- const wrappedHandler = async (ctx) => {
1929
- let args = [ctx];
1930
- if (routeArgs?.length > 0) {
1931
- args = [];
1932
- const sortedArgs = [...routeArgs].sort((a, b) => a.index - b.index);
1933
- for (let k = 0; k < sortedArgs.length; k++) {
1934
- const arg = sortedArgs[k];
1935
- switch (arg.type) {
1936
- case RouteParamType.BODY:
1937
- try {
1938
- if (ctx.req.headers.get("content-type")?.includes("application/json")) {
1939
- args[arg.index] = await ctx.req.json();
1940
- } else {
1941
- const text = await ctx.req.text();
1942
- if (!text) {
1943
- args[arg.index] = {};
1944
- } else {
1945
- args[arg.index] = JSON.parse(text);
1946
- }
1947
- }
1948
- } catch (e) {
1949
- const err = new Error("Invalid JSON body");
1950
- err.status = 400;
1951
- throw err;
1952
- }
1953
- break;
1954
- case RouteParamType.PARAM:
1955
- args[arg.index] = arg.name ? ctx.params[arg.name] : ctx.params;
1956
- break;
1957
- case RouteParamType.QUERY: {
1958
- const url = new URL(ctx.req.url);
1959
- if (arg.name) {
1960
- const vals = url.searchParams.getAll(arg.name);
1961
- args[arg.index] = vals.length > 1 ? vals : vals[0];
1962
- } else {
1963
- const query = {};
1964
- const keys = Object.keys(url.searchParams);
1965
- for (let k2 = 0; k2 < keys.length; k2++) {
1966
- const key = keys[k2];
1967
- const vals = url.searchParams.getAll(key);
1968
- query[key] = vals.length > 1 ? vals : vals[0];
1969
- }
1970
- args[arg.index] = query;
1971
- }
1972
- break;
1973
- }
1974
- case RouteParamType.HEADER:
1975
- args[arg.index] = arg.name ? ctx.req.headers.get(arg.name) : ctx.req.headers;
1976
- break;
1977
- case RouteParamType.REQUEST:
1978
- args[arg.index] = ctx.req;
1979
- break;
1980
- case RouteParamType.CONTEXT:
1981
- args[arg.index] = ctx;
1982
- break;
1983
- }
1984
- }
1985
- }
1986
- const tracedOriginalHandler = ctx.app?.applicationConfig.enableTracing ? traceHandler(originalHandler, normalizedPath) : originalHandler;
1987
- return tracedOriginalHandler.apply(instance, args);
1988
- };
1989
- let finalHandler = wrappedHandler;
1990
- if (allMiddleware.length > 0) {
1991
- const composed = compose(allMiddleware);
1992
- finalHandler = async (ctx) => {
1993
- return composed(ctx, () => wrappedHandler(ctx));
1994
- };
1995
- }
1996
- finalHandler.originalHandler = originalHandler;
1997
- if (finalHandler !== wrappedHandler) {
1998
- wrappedHandler.originalHandler = originalHandler;
1999
- }
2000
- const tagName = instance.constructor.name;
2001
- const decoratedSpecs = instance[$routeSpec] || proto && proto[$routeSpec];
2002
- const userSpec = decoratedSpecs && decoratedSpecs.get(name);
2003
- const spec = { tags: [tagName], ...userSpec };
2004
- this.add({ method, path: normalizedPath, handler: finalHandler, spec, controller: instance });
2005
- }
2006
- }
2007
- if (routesAttached === 0) {
2008
- console.warn(`No routes attached to controller ${instance.constructor.name}`);
2009
- }
2010
- instance[$isMounted] = true;
1659
+ this.scanControllerRoutes(prefix, controller);
2011
1660
  }
2012
1661
  return this;
2013
1662
  }
@@ -2045,8 +1694,6 @@ class ShokupanRouter {
2045
1694
  */
2046
1695
  async internalRequest(arg) {
2047
1696
  const options = typeof arg === "string" ? { path: arg } : arg;
2048
- const store = asyncContext.getStore();
2049
- store?.get("req");
2050
1697
  let url = options.path;
2051
1698
  if (!url.startsWith("http")) {
2052
1699
  const base = `http://${this.rootConfig?.hostname || "localhost"}:${this.rootConfig.port || 3e3}`;
@@ -2158,6 +1805,220 @@ class ShokupanRouter {
2158
1805
  wrapped.originalHandler = originalHandler.originalHandler ?? originalHandler;
2159
1806
  return wrapped;
2160
1807
  }
1808
+ mountRouter(prefix, router) {
1809
+ if (router[$isMounted]) {
1810
+ throw new Error("Router is already mounted");
1811
+ }
1812
+ router[$mountPath] = prefix;
1813
+ if (!router.metadata) {
1814
+ const info = getCallerInfo();
1815
+ router.metadata = {
1816
+ file: info.file,
1817
+ line: info.line,
1818
+ name: "MountedRouter"
1819
+ };
1820
+ }
1821
+ this[$childRouters].push(router);
1822
+ router[$parent] = this;
1823
+ const setRouterContext = (router2) => {
1824
+ router2[$appRoot] = this.root;
1825
+ router2[$childRouters].forEach((child) => setRouterContext(child));
1826
+ };
1827
+ setRouterContext(router);
1828
+ router[$appRoot] = this.root;
1829
+ router[$isMounted] = true;
1830
+ }
1831
+ scanControllerRoutes(prefix, controller) {
1832
+ let instance = controller;
1833
+ if (typeof controller === "function") {
1834
+ instance = Container.resolve(controller);
1835
+ const controllerPath = controller[$controllerPath];
1836
+ if (controllerPath) {
1837
+ const p1 = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
1838
+ const p2 = controllerPath.startsWith("/") ? controllerPath : "/" + controllerPath;
1839
+ prefix = p1 + p2;
1840
+ if (!prefix) prefix = "/";
1841
+ }
1842
+ } else {
1843
+ const ctor = instance.constructor;
1844
+ const controllerPath = ctor[$controllerPath];
1845
+ if (controllerPath) {
1846
+ const p1 = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
1847
+ const p2 = controllerPath.startsWith("/") ? controllerPath : "/" + controllerPath;
1848
+ prefix = p1 + p2;
1849
+ if (!prefix) prefix = "/";
1850
+ }
1851
+ }
1852
+ instance[$mountPath] = prefix;
1853
+ const info = getCallerInfo();
1854
+ instance.metadata = {
1855
+ file: info.file,
1856
+ line: info.line,
1857
+ name: instance.constructor.name
1858
+ };
1859
+ this[$childControllers].push(instance);
1860
+ const controllerMiddleware = (typeof controller === "function" ? controller[$middleware] : instance[$middleware]) || [];
1861
+ const proto = Object.getPrototypeOf(instance);
1862
+ const methods = /* @__PURE__ */ new Set();
1863
+ let current = proto;
1864
+ while (current && current !== Object.prototype) {
1865
+ Object.getOwnPropertyNames(current).forEach((name) => methods.add(name));
1866
+ current = Object.getPrototypeOf(current);
1867
+ }
1868
+ Object.getOwnPropertyNames(instance).forEach((name) => methods.add(name));
1869
+ const decoratedRoutes = instance[$routeMethods] || proto && proto[$routeMethods];
1870
+ const decoratedArgs = instance[$routeArgs] || proto && proto[$routeArgs];
1871
+ const methodMiddlewareMap = instance[$middleware] || proto && proto[$middleware];
1872
+ let routesAttached = 0;
1873
+ for (let i = 0; i < Array.from(methods).length; i++) {
1874
+ const name = Array.from(methods)[i];
1875
+ if (name === "constructor") continue;
1876
+ if (["arguments", "caller", "callee"].includes(name)) continue;
1877
+ const originalHandler = instance[name];
1878
+ if (typeof originalHandler !== "function") continue;
1879
+ let method;
1880
+ let subPath = "";
1881
+ if (decoratedRoutes && decoratedRoutes.has(name)) {
1882
+ const config = decoratedRoutes.get(name);
1883
+ method = config.method;
1884
+ subPath = config.path;
1885
+ } else {
1886
+ for (let j = 0; j < HTTPMethods.length; j++) {
1887
+ const m = HTTPMethods[j];
1888
+ if (name.toUpperCase().startsWith(m)) {
1889
+ method = m;
1890
+ const rest = name.slice(m.length);
1891
+ if (rest.length === 0) {
1892
+ subPath = "/";
1893
+ } else {
1894
+ subPath = "";
1895
+ let buffer = "";
1896
+ const flush = () => {
1897
+ if (buffer.length > 0) {
1898
+ subPath += "/" + buffer.toLowerCase();
1899
+ buffer = "";
1900
+ }
1901
+ };
1902
+ for (let i2 = 0; i2 < rest.length; i2++) {
1903
+ const char = rest[i2];
1904
+ if (char === "$") {
1905
+ flush();
1906
+ subPath += "/:";
1907
+ continue;
1908
+ }
1909
+ buffer += char;
1910
+ }
1911
+ if (buffer.length > 0) flush();
1912
+ subPath = rest.replace(/\$/g, "/:").replace(/([a-z0-9])([A-Z])/g, "$1/$2").toLowerCase();
1913
+ if (!subPath.startsWith("/")) {
1914
+ subPath = "/" + subPath;
1915
+ }
1916
+ }
1917
+ break;
1918
+ }
1919
+ }
1920
+ }
1921
+ if (method) {
1922
+ routesAttached++;
1923
+ const cleanPrefix = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
1924
+ const cleanSubPath = subPath === "/" ? "" : subPath;
1925
+ let joined;
1926
+ if (cleanSubPath.length === 0) {
1927
+ joined = cleanPrefix;
1928
+ } else if (cleanSubPath.startsWith("/")) {
1929
+ joined = cleanPrefix + cleanSubPath;
1930
+ } else {
1931
+ joined = cleanPrefix + "/" + cleanSubPath;
1932
+ }
1933
+ const fullPath = joined || "/";
1934
+ const normalizedPath = fullPath.replace(/\/+/g, "/");
1935
+ const methodMw = methodMiddlewareMap instanceof Map ? methodMiddlewareMap.get(name) || [] : [];
1936
+ const allMiddleware = [...controllerMiddleware, ...methodMw];
1937
+ const routeArgs = decoratedArgs && decoratedArgs.get(name);
1938
+ const wrappedHandler = async (ctx) => {
1939
+ let args = [ctx];
1940
+ if (routeArgs?.length > 0) {
1941
+ args = [];
1942
+ const sortedArgs = [...routeArgs].sort((a, b) => a.index - b.index);
1943
+ for (let k = 0; k < sortedArgs.length; k++) {
1944
+ const arg = sortedArgs[k];
1945
+ switch (arg.type) {
1946
+ case RouteParamType.BODY:
1947
+ try {
1948
+ if (ctx.req.headers.get("content-type")?.includes("application/json")) {
1949
+ args[arg.index] = await ctx.req.json();
1950
+ } else {
1951
+ const text = await ctx.req.text();
1952
+ if (!text) {
1953
+ args[arg.index] = {};
1954
+ } else {
1955
+ args[arg.index] = JSON.parse(text);
1956
+ }
1957
+ }
1958
+ } catch (e) {
1959
+ const err = new Error("Invalid JSON body");
1960
+ err.status = 400;
1961
+ throw err;
1962
+ }
1963
+ break;
1964
+ case RouteParamType.PARAM:
1965
+ args[arg.index] = arg.name ? ctx.params[arg.name] : ctx.params;
1966
+ break;
1967
+ case RouteParamType.QUERY: {
1968
+ const url = new URL(ctx.req.url);
1969
+ if (arg.name) {
1970
+ const vals = url.searchParams.getAll(arg.name);
1971
+ args[arg.index] = vals.length > 1 ? vals : vals[0];
1972
+ } else {
1973
+ const query = {};
1974
+ const keys = Object.keys(url.searchParams);
1975
+ for (let k2 = 0; k2 < keys.length; k2++) {
1976
+ const key = keys[k2];
1977
+ const vals = url.searchParams.getAll(key);
1978
+ query[key] = vals.length > 1 ? vals : vals[0];
1979
+ }
1980
+ args[arg.index] = query;
1981
+ }
1982
+ break;
1983
+ }
1984
+ case RouteParamType.HEADER:
1985
+ args[arg.index] = arg.name ? ctx.req.headers.get(arg.name) : ctx.req.headers;
1986
+ break;
1987
+ case RouteParamType.REQUEST:
1988
+ args[arg.index] = ctx.req;
1989
+ break;
1990
+ case RouteParamType.CONTEXT:
1991
+ args[arg.index] = ctx;
1992
+ break;
1993
+ }
1994
+ }
1995
+ }
1996
+ const tracedOriginalHandler = ctx.app?.applicationConfig.enableTracing ? traceHandler(originalHandler, normalizedPath) : originalHandler;
1997
+ return tracedOriginalHandler.apply(instance, args);
1998
+ };
1999
+ let finalHandler = wrappedHandler;
2000
+ if (allMiddleware.length > 0) {
2001
+ const composed = compose(allMiddleware);
2002
+ finalHandler = async (ctx) => {
2003
+ return composed(ctx, () => wrappedHandler(ctx));
2004
+ };
2005
+ }
2006
+ finalHandler.originalHandler = originalHandler;
2007
+ if (finalHandler !== wrappedHandler) {
2008
+ wrappedHandler.originalHandler = originalHandler;
2009
+ }
2010
+ const tagName = instance.constructor.name;
2011
+ const decoratedSpecs = instance[$routeSpec] || proto && proto[$routeSpec];
2012
+ const userSpec = decoratedSpecs && decoratedSpecs.get(name);
2013
+ const spec = { tags: [tagName], ...userSpec };
2014
+ this.add({ method, path: normalizedPath, handler: finalHandler, spec, controller: instance });
2015
+ }
2016
+ }
2017
+ if (routesAttached === 0) {
2018
+ console.warn(`No routes attached to controller ${instance.constructor.name}`);
2019
+ }
2020
+ instance[$isMounted] = true;
2021
+ }
2161
2022
  /**
2162
2023
  * Find a route matching the given method and path.
2163
2024
  * @param method HTTP method
@@ -2284,7 +2145,7 @@ class ShokupanRouter {
2284
2145
  if (effectiveRenderer) {
2285
2146
  const innerHandler = wrappedHandler;
2286
2147
  wrappedHandler = async (ctx) => {
2287
- ctx.renderer = effectiveRenderer;
2148
+ ctx.setRenderer(effectiveRenderer);
2288
2149
  return innerHandler(ctx);
2289
2150
  };
2290
2151
  }
@@ -2686,6 +2547,13 @@ class Shokupan extends ShokupanRouter {
2686
2547
  }
2687
2548
  return this;
2688
2549
  }
2550
+ /**
2551
+ * Registers a plugin.
2552
+ */
2553
+ register(plugin, options) {
2554
+ plugin.onInit(this, options);
2555
+ return this;
2556
+ }
2689
2557
  startupHooks = [];
2690
2558
  /**
2691
2559
  * Registers a callback to be executed before the server starts listening.
@@ -2748,7 +2616,7 @@ class Shokupan extends ShokupanRouter {
2748
2616
  };
2749
2617
  let factory = this.applicationConfig.serverFactory;
2750
2618
  if (!factory && typeof Bun === "undefined") {
2751
- const { createHttpServer } = await import("./server-adapter-0xH174zz.js");
2619
+ const { createHttpServer } = await import("./http-server-0xH174zz.js");
2752
2620
  factory = createHttpServer();
2753
2621
  }
2754
2622
  const server = factory ? await factory(serveOptions) : Bun.serve(serveOptions);
@@ -2817,19 +2685,19 @@ class Shokupan extends ShokupanRouter {
2817
2685
  "http.method": req.method
2818
2686
  }
2819
2687
  };
2820
- const parent = store?.get("span");
2688
+ const parent = store?.span;
2821
2689
  const ctx = parent ? trace.setSpan(context.active(), parent) : void 0;
2822
2690
  return tracer2.startActiveSpan(`${req.method} ${new URL(req.url).pathname}`, attrs, ctx, (span) => {
2823
- const ctxMap = /* @__PURE__ */ new Map();
2824
- ctxMap.set("span", span);
2825
- ctxMap.set("request", req);
2826
- return asyncContext.run(ctxMap, () => this.handleRequest(req, server).finally(() => span.end()));
2691
+ const ctxStore = new RequestContextStore();
2692
+ ctxStore.span = span;
2693
+ ctxStore.request = req;
2694
+ return asyncContext.run(ctxStore, () => this.handleRequest(req, server).finally(() => span.end()));
2827
2695
  });
2828
2696
  }
2829
2697
  if (this.applicationConfig.enableAsyncLocalStorage) {
2830
- const ctxMap = /* @__PURE__ */ new Map();
2831
- ctxMap.set("request", req);
2832
- return asyncContext.run(ctxMap, () => this.handleRequest(req, server));
2698
+ const ctxStore = new RequestContextStore();
2699
+ ctxStore.request = req;
2700
+ return asyncContext.run(ctxStore, () => this.handleRequest(req, server));
2833
2701
  }
2834
2702
  return this.handleRequest(req, server);
2835
2703
  }
@@ -2851,6 +2719,7 @@ class Shokupan extends ShokupanRouter {
2851
2719
  const bodyParsing = ["POST", "PUT", "PATCH", "DELETE"].includes(req.method) ? ctx.parseBody() : Promise.resolve();
2852
2720
  const match = this.find(req.method, ctx.path);
2853
2721
  if (match) {
2722
+ ctx._routeMatched = true;
2854
2723
  ctx.params = match.params;
2855
2724
  await bodyParsing;
2856
2725
  return match.handler(ctx);
@@ -2865,10 +2734,14 @@ class Shokupan extends ShokupanRouter {
2865
2734
  } else if (result === null || result === void 0) {
2866
2735
  if (ctx._finalResponse instanceof Response) {
2867
2736
  response = ctx._finalResponse;
2868
- } else if (ctx.response.status !== 200 || ctx.response.hasPopulatedHeaders) {
2737
+ } else if (ctx._routeMatched) {
2869
2738
  response = ctx.send(null, { status: ctx.response.status, headers: ctx.response.headers });
2870
2739
  } else {
2871
- response = ctx.text("Not Found", 404);
2740
+ if (ctx.response.status !== 200) {
2741
+ response = ctx.send(null, { status: ctx.response.status, headers: ctx.response.headers });
2742
+ } else {
2743
+ response = ctx.text("Not Found", 404);
2744
+ }
2872
2745
  }
2873
2746
  } else if (typeof result === "object") {
2874
2747
  response = ctx.json(result);
@@ -2880,7 +2753,7 @@ class Shokupan extends ShokupanRouter {
2880
2753
  return response;
2881
2754
  } catch (err) {
2882
2755
  console.error(err);
2883
- const span = asyncContext.getStore()?.get("span");
2756
+ const span = asyncContext.getStore()?.span;
2884
2757
  if (span) span.setStatus({ code: 2 });
2885
2758
  const status = err.status || err.statusCode || 500;
2886
2759
  const body = { error: err.message || "Internal Server Error" };
@@ -2888,31 +2761,195 @@ class Shokupan extends ShokupanRouter {
2888
2761
  await this.runHooks("onError", ctx, err);
2889
2762
  return ctx.json(body, status);
2890
2763
  }
2891
- };
2892
- let executionPromise = handle();
2893
- const timeoutMs = this.applicationConfig.requestTimeout;
2894
- if (timeoutMs && timeoutMs > 0) {
2895
- let timeoutId;
2896
- const timeoutPromise = new Promise((_, reject) => {
2897
- timeoutId = setTimeout(async () => {
2898
- controller.abort();
2899
- await this.runHooks("onRequestTimeout", ctx);
2900
- reject(new Error("Request Timeout"));
2901
- }, timeoutMs);
2764
+ };
2765
+ let executionPromise = handle();
2766
+ const timeoutMs = this.applicationConfig.requestTimeout;
2767
+ if (timeoutMs && timeoutMs > 0) {
2768
+ let timeoutId;
2769
+ const timeoutPromise = new Promise((_, reject) => {
2770
+ timeoutId = setTimeout(async () => {
2771
+ controller.abort();
2772
+ await this.runHooks("onRequestTimeout", ctx);
2773
+ reject(new Error("Request Timeout"));
2774
+ }, timeoutMs);
2775
+ });
2776
+ executionPromise = Promise.race([executionPromise, timeoutPromise]).finally(() => clearTimeout(timeoutId));
2777
+ }
2778
+ return executionPromise.catch((err) => {
2779
+ if (err.message === "Request Timeout") {
2780
+ return ctx.text("Request Timeout", 408);
2781
+ }
2782
+ console.error("Unexpected error in request execution:", err);
2783
+ return ctx.text("Internal Server Error", 500);
2784
+ }).then(async (res) => {
2785
+ await this.runHooks("onResponseEnd", ctx, res);
2786
+ return res;
2787
+ });
2788
+ }
2789
+ }
2790
+ function RateLimitMiddleware(options = {}) {
2791
+ const windowMs = options.windowMs || 60 * 1e3;
2792
+ const max = options.limit || options.max || 5;
2793
+ const message = options.message || "Too many requests, please try again later.";
2794
+ const statusCode = options.statusCode || 429;
2795
+ const headers = options.headers !== false;
2796
+ const mode = options.mode || "user";
2797
+ const trustedProxies = options.trustedProxies || [];
2798
+ const keyGenerator = options.keyGenerator || ((ctx) => {
2799
+ if (mode === "absolute") {
2800
+ return "global";
2801
+ }
2802
+ const xForwardedFor = ctx.headers.get("x-forwarded-for");
2803
+ if (xForwardedFor && trustedProxies.length > 0) {
2804
+ const ips = xForwardedFor.split(",").map((ip) => ip.trim());
2805
+ for (let i = ips.length - 1; i >= 0; i--) {
2806
+ const ip = ips[i];
2807
+ if (!trustedProxies.includes(ip)) {
2808
+ if (/^[\d.:a-fA-F]+$/.test(ip)) {
2809
+ return ip;
2810
+ }
2811
+ }
2812
+ }
2813
+ }
2814
+ return ctx.server?.requestIP?.(ctx.request)?.address || "unknown";
2815
+ });
2816
+ const skip = options.skip || (() => false);
2817
+ const hits = /* @__PURE__ */ new Map();
2818
+ const interval = setInterval(() => {
2819
+ const now = Date.now();
2820
+ const entries = Array.from(hits.entries());
2821
+ for (let i = 0; i < entries.length; i++) {
2822
+ const [key, record] = entries[i];
2823
+ if (record.resetTime <= now) {
2824
+ hits.delete(key);
2825
+ }
2826
+ }
2827
+ }, windowMs);
2828
+ if (interval.unref) interval.unref();
2829
+ const rateLimitMiddleware = async function RateLimitMiddleware2(ctx, next) {
2830
+ if (skip(ctx)) return next();
2831
+ const key = keyGenerator(ctx);
2832
+ const now = Date.now();
2833
+ let record = hits.get(key);
2834
+ if (!record || record.resetTime <= now) {
2835
+ record = {
2836
+ hits: 0,
2837
+ resetTime: now + windowMs
2838
+ };
2839
+ hits.set(key, record);
2840
+ }
2841
+ record.hits++;
2842
+ const remaining = Math.max(0, max - record.hits);
2843
+ const resetTime = Math.ceil(record.resetTime / 1e3);
2844
+ const retryAfter = Math.ceil((record.resetTime - now) / 1e3);
2845
+ const setHeaders = (res) => {
2846
+ if (!headers || !res || !res.headers) return;
2847
+ try {
2848
+ res.headers.set("X-RateLimit-Limit", String(max));
2849
+ res.headers.set("X-RateLimit-Remaining", String(remaining));
2850
+ res.headers.set("X-RateLimit-Reset", String(resetTime));
2851
+ } catch (e) {
2852
+ }
2853
+ };
2854
+ if (record.hits > max) {
2855
+ if (options.onRateLimited) {
2856
+ const result = await options.onRateLimited(ctx, key);
2857
+ if (result instanceof Response) {
2858
+ return result;
2859
+ }
2860
+ }
2861
+ const msg = typeof message === "function" ? message(ctx, key) : message;
2862
+ typeof msg === "object" ? JSON.stringify(msg) : String(msg);
2863
+ const res = typeof msg === "object" ? ctx.json(msg, statusCode) : ctx.text(String(msg), statusCode);
2864
+ if (headers) {
2865
+ setHeaders(res);
2866
+ res.headers.set("Retry-After", String(retryAfter));
2867
+ }
2868
+ return res;
2869
+ }
2870
+ const response = await next();
2871
+ if (response instanceof Response && headers) {
2872
+ setHeaders(response);
2873
+ }
2874
+ return response;
2875
+ };
2876
+ rateLimitMiddleware.isBuiltin = true;
2877
+ rateLimitMiddleware.pluginName = "RateLimit";
2878
+ return rateLimitMiddleware;
2879
+ }
2880
+ function Controller(path = "/") {
2881
+ return (target) => {
2882
+ target[$controllerPath] = path;
2883
+ };
2884
+ }
2885
+ function Use(...middleware) {
2886
+ return (target, propertyKey, descriptor) => {
2887
+ if (!propertyKey) {
2888
+ const existing = target[$middleware] || [];
2889
+ target[$middleware] = [...existing, ...middleware];
2890
+ } else {
2891
+ if (!target[$middleware]) {
2892
+ target[$middleware] = /* @__PURE__ */ new Map();
2893
+ }
2894
+ const existing = target[$middleware].get(propertyKey) || [];
2895
+ target[$middleware].set(propertyKey, [...existing, ...middleware]);
2896
+ }
2897
+ };
2898
+ }
2899
+ function createParamDecorator(type) {
2900
+ return (name) => {
2901
+ return (target, propertyKey, parameterIndex) => {
2902
+ if (!target[$routeArgs]) {
2903
+ target[$routeArgs] = /* @__PURE__ */ new Map();
2904
+ }
2905
+ if (!target[$routeArgs].has(propertyKey)) {
2906
+ target[$routeArgs].set(propertyKey, []);
2907
+ }
2908
+ target[$routeArgs].get(propertyKey).push({
2909
+ index: parameterIndex,
2910
+ type,
2911
+ name
2902
2912
  });
2903
- executionPromise = Promise.race([executionPromise, timeoutPromise]).finally(() => clearTimeout(timeoutId));
2913
+ };
2914
+ };
2915
+ }
2916
+ const Body = createParamDecorator(RouteParamType.BODY);
2917
+ const Param = createParamDecorator(RouteParamType.PARAM);
2918
+ const Query = createParamDecorator(RouteParamType.QUERY);
2919
+ const Headers$1 = createParamDecorator(RouteParamType.HEADER);
2920
+ const Req = createParamDecorator(RouteParamType.REQUEST);
2921
+ const Ctx = createParamDecorator(RouteParamType.CONTEXT);
2922
+ function Spec(spec) {
2923
+ return (target, propertyKey, descriptor) => {
2924
+ if (!target[$routeSpec]) {
2925
+ target[$routeSpec] = /* @__PURE__ */ new Map();
2904
2926
  }
2905
- return executionPromise.catch((err) => {
2906
- if (err.message === "Request Timeout") {
2907
- return ctx.text("Request Timeout", 408);
2927
+ target[$routeSpec].set(propertyKey, spec);
2928
+ };
2929
+ }
2930
+ function createMethodDecorator(method) {
2931
+ return (path = "/") => {
2932
+ return (target, propertyKey, descriptor) => {
2933
+ if (!target[$routeMethods]) {
2934
+ target[$routeMethods] = /* @__PURE__ */ new Map();
2908
2935
  }
2909
- console.error("Unexpected error in request execution:", err);
2910
- return ctx.text("Internal Server Error", 500);
2911
- }).then(async (res) => {
2912
- await this.runHooks("onResponseEnd", ctx, res);
2913
- return res;
2914
- });
2915
- }
2936
+ target[$routeMethods].set(propertyKey, {
2937
+ method,
2938
+ path
2939
+ });
2940
+ };
2941
+ };
2942
+ }
2943
+ const Get = createMethodDecorator("GET");
2944
+ const Post = createMethodDecorator("POST");
2945
+ const Put = createMethodDecorator("PUT");
2946
+ const Delete = createMethodDecorator("DELETE");
2947
+ const Patch = createMethodDecorator("PATCH");
2948
+ const Options = createMethodDecorator("OPTIONS");
2949
+ const Head = createMethodDecorator("HEAD");
2950
+ const All = createMethodDecorator("ALL");
2951
+ function RateLimit(options) {
2952
+ return Use(RateLimitMiddleware(options));
2916
2953
  }
2917
2954
  class AuthPlugin extends ShokupanRouter {
2918
2955
  constructor(authConfig) {
@@ -2922,6 +2959,13 @@ class AuthPlugin extends ShokupanRouter {
2922
2959
  this.init();
2923
2960
  }
2924
2961
  secret;
2962
+ onInit(app, options) {
2963
+ if (options?.path) {
2964
+ app.mount(options.path, this);
2965
+ } else {
2966
+ app.mount(options.path ?? "/", this);
2967
+ }
2968
+ }
2925
2969
  getProviderInstance(name, p) {
2926
2970
  switch (name) {
2927
2971
  case "github":
@@ -3139,8 +3183,196 @@ class AuthPlugin extends ShokupanRouter {
3139
3183
  };
3140
3184
  }
3141
3185
  }
3186
+ class ClusterPlugin {
3187
+ constructor(options = {}) {
3188
+ this.options = options;
3189
+ }
3190
+ onInit(app) {
3191
+ const originalListen = app.listen.bind(app);
3192
+ const { workers = "auto", silent = false, sticky = false } = this.options;
3193
+ const isBun = typeof Bun !== "undefined";
3194
+ const numCPUs = os__default.cpus().length;
3195
+ const numWorkers = workers === "auto" || workers === -1 ? numCPUs : workers;
3196
+ if (numWorkers <= 1) {
3197
+ return;
3198
+ }
3199
+ app.listen = async (port) => {
3200
+ const finalPort = port ?? app.applicationConfig.port ?? 3e3;
3201
+ if (isBun) {
3202
+ return this.handleBun(app, finalPort, numWorkers, originalListen);
3203
+ } else {
3204
+ return this.handleNode(app, finalPort, numWorkers, originalListen, silent, sticky);
3205
+ }
3206
+ };
3207
+ }
3208
+ async handleBun(app, port, workers, originalListen) {
3209
+ const workerId = process.env["SHOKUPAN_WORKER_ID"];
3210
+ if (workerId) {
3211
+ app.applicationConfig.reusePort = true;
3212
+ return originalListen(port);
3213
+ }
3214
+ console.log(`[Cluster] Starting ${workers} Bun workers on port ${port}...`);
3215
+ const spawnWorker = (id) => {
3216
+ Bun.spawn([process.argv0, ...process.argv.slice(1)], {
3217
+ env: { ...process.env, SHOKUPAN_WORKER_ID: id },
3218
+ stdio: ["inherit", "inherit", "inherit"],
3219
+ onExit(proc, exitCode, signalCode, error) {
3220
+ console.log(`[Cluster] Worker ${id} died (code: ${exitCode}). Restarting...`);
3221
+ spawnWorker(id);
3222
+ }
3223
+ });
3224
+ };
3225
+ for (let i = 0; i < workers; i++) {
3226
+ spawnWorker(process.pid + "_" + i + 1);
3227
+ }
3228
+ setInterval(() => {
3229
+ }, 1e3 * 60 * 60);
3230
+ return {
3231
+ stop: () => {
3232
+ },
3233
+ port
3234
+ };
3235
+ }
3236
+ async handleNode(app, port, workers, originalListen, silent, sticky) {
3237
+ if (cluster.isPrimary) {
3238
+ console.log(`[Cluster] Master ${process.pid} is running`);
3239
+ const fork = () => cluster.fork(process.env);
3240
+ for (let i = 0; i < workers; i++) {
3241
+ fork();
3242
+ }
3243
+ cluster.on("exit", (worker, code, signal) => {
3244
+ console.log(`[Cluster] Worker ${worker.process.pid} died. Restarting...`);
3245
+ fork();
3246
+ });
3247
+ if (sticky) {
3248
+ const server = net.createServer({ pauseOnConnect: true }, (connection) => {
3249
+ const remote = connection.remoteAddress || "";
3250
+ let hash = 0;
3251
+ for (let i = 0; i < remote.length; i++) {
3252
+ hash = (hash << 5) - hash + remote.charCodeAt(i);
3253
+ hash |= 0;
3254
+ }
3255
+ const index = Math.abs(hash) % workers;
3256
+ const worker = Object.values(cluster.workers)[index];
3257
+ if (worker) {
3258
+ worker.send("sticky-session:connection", connection);
3259
+ } else {
3260
+ connection.end();
3261
+ }
3262
+ });
3263
+ server.listen(port, () => {
3264
+ console.log(`[Cluster] Sticky Load Balancer listening on port ${port}`);
3265
+ });
3266
+ return {
3267
+ close: () => server.close(),
3268
+ port
3269
+ };
3270
+ } else {
3271
+ return {
3272
+ close: () => {
3273
+ },
3274
+ // Master controls
3275
+ port
3276
+ };
3277
+ }
3278
+ } else {
3279
+ if (sticky) {
3280
+ const server = await originalListen(0);
3281
+ process.on("message", (message, handle) => {
3282
+ if (message !== "sticky-session:connection") return;
3283
+ if (!handle) return;
3284
+ server.emit("connection", handle);
3285
+ handle.resume();
3286
+ });
3287
+ return server;
3288
+ } else {
3289
+ return originalListen(port);
3290
+ }
3291
+ }
3292
+ }
3293
+ }
3294
+ const eta = new Eta();
3295
+ class ScalarPlugin extends ShokupanRouter {
3296
+ constructor(pluginOptions = {}) {
3297
+ pluginOptions.config ??= {};
3298
+ super();
3299
+ this.pluginOptions = pluginOptions;
3300
+ this.init();
3301
+ }
3302
+ onInit(app, options) {
3303
+ if (options?.path) {
3304
+ app.mount(options.path, this);
3305
+ } else {
3306
+ app.mount(options.path ?? "/", this);
3307
+ }
3308
+ this.onMount(app);
3309
+ }
3310
+ init() {
3311
+ this.get("/", (ctx) => {
3312
+ let path = ctx.url.toString();
3313
+ if (!path.endsWith("/")) path += "/";
3314
+ return ctx.html(eta.renderString(`<!doctype html>
3315
+ <html>
3316
+ <head>
3317
+ <title>API Reference</title>
3318
+ <meta charset = "utf-8" />
3319
+ <meta name="viewport" content = "width=device-width, initial-scale=1" />
3320
+ </head>
3321
+
3322
+ <body>
3323
+ <div id="app"></div>
3324
+ <script src="https://cdn.jsdelivr.net/npm/@scalar/api-reference"><\/script>
3325
+ <script>
3326
+ Scalar.createApiReference('#app', [{ ...<%~ JSON.stringify(it.config.baseDocument) %>,
3327
+ url: "<%= it.path %>openapi.json",
3328
+ }
3329
+ ])
3330
+ <\/script>
3331
+ </body>
3332
+
3333
+ </html>`, { path, config: this.pluginOptions }));
3334
+ });
3335
+ this.get("/openapi.json", async (ctx) => {
3336
+ let spec;
3337
+ if (this.root.openApiSpec) {
3338
+ try {
3339
+ spec = structuredClone(this.root.openApiSpec);
3340
+ } catch (e) {
3341
+ spec = Object.assign({}, this.root.openApiSpec);
3342
+ }
3343
+ } else {
3344
+ spec = await (this.root || this).generateApiSpec();
3345
+ }
3346
+ if (this.pluginOptions.baseDocument) {
3347
+ deepMerge(spec, this.pluginOptions.baseDocument);
3348
+ }
3349
+ return ctx.json(spec);
3350
+ });
3351
+ }
3352
+ // New lifecycle method to be called by router.mount
3353
+ onMount(parent) {
3354
+ if (parent.onStart) {
3355
+ parent.onStart(async () => {
3356
+ if (this.pluginOptions.enableStaticAnalysis) {
3357
+ try {
3358
+ const entrypoint = process.argv[1];
3359
+ console.log(`[ScalarPlugin] Running eager static analysis on entrypoint: ${entrypoint}`);
3360
+ const analyzer = new OpenAPIAnalyzer(process.cwd(), entrypoint);
3361
+ let staticSpec = await analyzer.analyze();
3362
+ if (!this.pluginOptions.baseDocument) this.pluginOptions.baseDocument = {};
3363
+ deepMerge(this.pluginOptions.baseDocument, staticSpec);
3364
+ console.log("[ScalarPlugin] Static analysis completed successfully.");
3365
+ } catch (err) {
3366
+ console.error("[ScalarPlugin] Failed to run static analysis:", err);
3367
+ }
3368
+ }
3369
+ });
3370
+ }
3371
+ }
3372
+ }
3142
3373
  function Compression(options = {}) {
3143
3374
  const threshold = options.threshold ?? 512;
3375
+ const allowedAlgorithms = new Set(options.allowedAlgorithms ?? ["br", "gzip", "zstd", "deflate"]);
3144
3376
  const compressionMiddleware = async function CompressionMiddleware(ctx, next) {
3145
3377
  const acceptEncoding = ctx.headers.get("accept-encoding") || "";
3146
3378
  let method = null;
@@ -3153,6 +3385,9 @@ function Compression(options = {}) {
3153
3385
  } else if (acceptEncoding.includes("gzip")) method = "gzip";
3154
3386
  else if (acceptEncoding.includes("deflate")) method = "deflate";
3155
3387
  if (!method) return next();
3388
+ if (!allowedAlgorithms.has(method)) {
3389
+ return next();
3390
+ }
3156
3391
  let response = await next();
3157
3392
  if (!(response instanceof Response) && ctx._finalResponse instanceof Response) {
3158
3393
  response = ctx._finalResponse;
@@ -3707,77 +3942,6 @@ function enableOpenApiValidation(app) {
3707
3942
  precompileValidators(app, spec);
3708
3943
  });
3709
3944
  }
3710
- const eta = new Eta();
3711
- class ScalarPlugin extends ShokupanRouter {
3712
- constructor(pluginOptions = {}) {
3713
- pluginOptions.config ??= {};
3714
- super();
3715
- this.pluginOptions = pluginOptions;
3716
- this.init();
3717
- }
3718
- init() {
3719
- this.get("/", (ctx) => {
3720
- let path = ctx.url.toString();
3721
- if (!path.endsWith("/")) path += "/";
3722
- return ctx.html(eta.renderString(`<!doctype html>
3723
- <html>
3724
- <head>
3725
- <title>API Reference</title>
3726
- <meta charset = "utf-8" />
3727
- <meta name="viewport" content = "width=device-width, initial-scale=1" />
3728
- </head>
3729
-
3730
- <body>
3731
- <div id="app"></div>
3732
- <script src="https://cdn.jsdelivr.net/npm/@scalar/api-reference"><\/script>
3733
- <script>
3734
- Scalar.createApiReference('#app', [{ ...<%~ JSON.stringify(it.config.baseDocument) %>,
3735
- url: "<%= it.path %>openapi.json",
3736
- }
3737
- ])
3738
- <\/script>
3739
- </body>
3740
-
3741
- </html>`, { path, config: this.pluginOptions }));
3742
- });
3743
- this.get("/openapi.json", async (ctx) => {
3744
- let spec;
3745
- if (this.root.openApiSpec) {
3746
- try {
3747
- spec = structuredClone(this.root.openApiSpec);
3748
- } catch (e) {
3749
- spec = Object.assign({}, this.root.openApiSpec);
3750
- }
3751
- } else {
3752
- spec = await (this.root || this).generateApiSpec();
3753
- }
3754
- if (this.pluginOptions.baseDocument) {
3755
- deepMerge(spec, this.pluginOptions.baseDocument);
3756
- }
3757
- return ctx.json(spec);
3758
- });
3759
- }
3760
- // New lifecycle method to be called by router.mount
3761
- onMount(parent) {
3762
- if (parent.onStart) {
3763
- parent.onStart(async () => {
3764
- if (this.pluginOptions.enableStaticAnalysis) {
3765
- try {
3766
- const entrypoint = process.argv[1];
3767
- console.log(`[ScalarPlugin] Running eager static analysis on entrypoint: ${entrypoint}`);
3768
- const analyzer = new OpenAPIAnalyzer(process.cwd(), entrypoint);
3769
- let staticSpec = await analyzer.analyze();
3770
- if (!this.pluginOptions.baseDocument) this.pluginOptions.baseDocument = {};
3771
- deepMerge(this.pluginOptions.baseDocument, staticSpec);
3772
- console.log("[ScalarPlugin] Static analysis completed successfully.");
3773
- } catch (err) {
3774
- console.error("[ScalarPlugin] Failed to run static analysis:", err);
3775
- }
3776
- }
3777
- });
3778
- }
3779
- }
3780
- }
3781
3945
  function SecurityHeaders(options = {}) {
3782
3946
  const securityHeadersMiddleware = async function SecurityHeadersMiddleware(ctx, next) {
3783
3947
  const headers = {};
@@ -3901,18 +4065,18 @@ class MemoryStore extends EventEmitter {
3901
4065
  }
3902
4066
  set(sid, sess, cb) {
3903
4067
  this.sessions[sid] = JSON.stringify(sess);
3904
- cb && cb();
4068
+ cb?.();
3905
4069
  }
3906
4070
  destroy(sid, cb) {
3907
4071
  delete this.sessions[sid];
3908
- cb && cb();
4072
+ cb?.();
3909
4073
  }
3910
4074
  touch(sid, sess, cb) {
3911
4075
  const current = this.sessions[sid];
3912
4076
  if (current) {
3913
4077
  this.sessions[sid] = JSON.stringify(sess);
3914
4078
  }
3915
- cb && cb();
4079
+ cb?.();
3916
4080
  }
3917
4081
  all(cb) {
3918
4082
  const result = {};
@@ -3928,7 +4092,7 @@ class MemoryStore extends EventEmitter {
3928
4092
  }
3929
4093
  clear(cb) {
3930
4094
  this.sessions = {};
3931
- cb && cb();
4095
+ cb?.();
3932
4096
  }
3933
4097
  }
3934
4098
  function sign(val, secret) {
@@ -4124,6 +4288,7 @@ export {
4124
4288
  All,
4125
4289
  AuthPlugin,
4126
4290
  Body,
4291
+ ClusterPlugin,
4127
4292
  Compression,
4128
4293
  Container,
4129
4294
  Controller,
@@ -4147,16 +4312,13 @@ export {
4147
4312
  RateLimitMiddleware,
4148
4313
  Req,
4149
4314
  RouteParamType,
4150
- RouterRegistry,
4151
4315
  ScalarPlugin,
4152
4316
  SecurityHeaders,
4153
4317
  Session,
4154
4318
  Shokupan,
4155
- ShokupanApplicationTree,
4156
4319
  ShokupanContext,
4157
4320
  ShokupanRequest,
4158
4321
  ShokupanResponse,
4159
- ShokupanRouter,
4160
4322
  Spec,
4161
4323
  Use,
4162
4324
  ValidationError,