@stacksjs/bun-router 0.0.14 → 0.0.15

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 (273) hide show
  1. package/README.md +261 -584
  2. package/bin/cli.ts +9 -0
  3. package/dist/auth.d.ts +134 -0
  4. package/dist/cache/lru-cache.d.ts +139 -0
  5. package/dist/cache/middleware-memoization.d.ts +151 -0
  6. package/dist/cache/route-cache-warmer.d.ts +161 -0
  7. package/dist/cache/sqlite-cache.d.ts +209 -0
  8. package/dist/cache/streaming-cache.d.ts +132 -0
  9. package/dist/chunk-j0e7z7hd.js +18053 -0
  10. package/dist/cli/colors.d.ts +15 -0
  11. package/dist/cli/index.d.ts +10 -0
  12. package/dist/cli/middleware.d.ts +31 -0
  13. package/dist/cli/openapi.d.ts +17 -0
  14. package/dist/cli/router.d.ts +15 -0
  15. package/dist/cli/routes.d.ts +29 -0
  16. package/dist/cli/utils.d.ts +74 -0
  17. package/dist/cli.d.ts +1 -0
  18. package/dist/cli.js +3360 -0
  19. package/dist/config.d.ts +7 -0
  20. package/dist/container/container.d.ts +273 -0
  21. package/dist/container/contextual-binding.d.ts +240 -0
  22. package/dist/container/decorators.d.ts +141 -0
  23. package/dist/container/service-provider.d.ts +285 -0
  24. package/dist/development/hot-reload.d.ts +187 -0
  25. package/dist/development/index.d.ts +167 -0
  26. package/dist/development/performance-profiler.d.ts +217 -0
  27. package/dist/development/route-debugger.d.ts +154 -0
  28. package/dist/development/route-inspector.d.ts +211 -0
  29. package/dist/development/typescript-utilities.d.ts +209 -0
  30. package/dist/docs.d.ts +11 -0
  31. package/dist/errors/circuit-breaker.d.ts +195 -0
  32. package/dist/errors/error-handler.d.ts +88 -0
  33. package/dist/errors/error-reporting.d.ts +144 -0
  34. package/dist/errors/exceptions.d.ts +184 -0
  35. package/dist/errors/graceful-degradation.d.ts +154 -0
  36. package/dist/errors/index.d.ts +9 -0
  37. package/dist/errors/router-errors.d.ts +110 -0
  38. package/dist/file-serving/static-files.d.ts +143 -0
  39. package/dist/index.d.ts +14 -0
  40. package/dist/index.js +13968 -0
  41. package/dist/middleware/auth.d.ts +63 -0
  42. package/dist/middleware/content_security_policy.d.ts +53 -0
  43. package/dist/middleware/cors.d.ts +5 -0
  44. package/dist/middleware/csrf.d.ts +7 -0
  45. package/dist/middleware/ddos_protection.d.ts +39 -0
  46. package/dist/middleware/file_security.d.ts +26 -0
  47. package/dist/middleware/file_upload.d.ts +38 -0
  48. package/dist/middleware/helmet.d.ts +54 -0
  49. package/dist/middleware/index.d.ts +44 -0
  50. package/dist/middleware/input_validation.d.ts +45 -0
  51. package/dist/middleware/json_body.d.ts +4 -0
  52. package/dist/middleware/performance_alerting.d.ts +87 -0
  53. package/dist/middleware/performance_dashboard.d.ts +87 -0
  54. package/dist/middleware/performance_monitor.d.ts +209 -0
  55. package/dist/middleware/pipeline.d.ts +131 -0
  56. package/dist/middleware/rate_limit.d.ts +36 -0
  57. package/dist/middleware/request_id.d.ts +4 -0
  58. package/dist/middleware/request_signing.d.ts +153 -0
  59. package/dist/middleware/request_tracer.d.ts +72 -0
  60. package/dist/middleware/response_cache.d.ts +97 -0
  61. package/dist/middleware/security.d.ts +69 -0
  62. package/dist/middleware/security_suite.d.ts +46 -0
  63. package/dist/middleware/session.d.ts +23 -0
  64. package/dist/model-binding/index.d.ts +2 -0
  65. package/dist/model-binding/model-middleware.d.ts +118 -0
  66. package/dist/model-binding/model-registry.d.ts +164 -0
  67. package/dist/model-binding.d.ts +185 -0
  68. package/dist/model-resolver-factory.d.ts +31 -0
  69. package/dist/observability/correlation.d.ts +187 -0
  70. package/dist/observability/health-checks.d.ts +186 -0
  71. package/dist/observability/index.d.ts +60 -0
  72. package/dist/observability/integration.d.ts +147 -0
  73. package/dist/observability/metrics.d.ts +184 -0
  74. package/dist/observability/tracing.d.ts +187 -0
  75. package/dist/optimization/bun-utilities.d.ts +224 -0
  76. package/dist/query-builder-integration.d.ts +36 -0
  77. package/dist/request/context.d.ts +34 -0
  78. package/dist/request/enhanced-request.d.ts +213 -0
  79. package/dist/request/macros.d.ts +300 -0
  80. package/dist/response/macros.d.ts +256 -0
  81. package/dist/response/response-factory.d.ts +133 -0
  82. package/dist/router/api-routes.d.ts +5 -0
  83. package/dist/router/file-based-routing.d.ts +118 -0
  84. package/dist/router/file-streaming.d.ts +5 -0
  85. package/dist/router/fluent-router.d.ts +315 -0
  86. package/dist/router/fluent-routing.d.ts +271 -0
  87. package/dist/router/group-organization.d.ts +5 -0
  88. package/dist/router/handler-resolver.d.ts +24 -0
  89. package/dist/router/http-methods.d.ts +10 -0
  90. package/dist/router/index.d.ts +114 -0
  91. package/dist/router/middleware-groups.d.ts +94 -0
  92. package/dist/router/middleware-integration.d.ts +112 -0
  93. package/dist/router/middleware.d.ts +5 -0
  94. package/dist/router/model-binding.d.ts +5 -0
  95. package/dist/router/optimized-route-matching.d.ts +6 -0
  96. package/dist/router/route-building.d.ts +5 -0
  97. package/dist/router/route-compiler.d.ts +153 -0
  98. package/dist/router/route-matching.d.ts +5 -0
  99. package/dist/router/route-trie.d.ts +114 -0
  100. package/dist/router/router.d.ts +317 -0
  101. package/dist/router/server.d.ts +5 -0
  102. package/dist/router/validation-integration.d.ts +170 -0
  103. package/dist/router/view-rendering.d.ts +5 -0
  104. package/dist/router/websocket.d.ts +5 -0
  105. package/dist/routing/route-caching.d.ts +129 -0
  106. package/dist/routing/route-throttling.d.ts +150 -0
  107. package/dist/routing/subdomain-routing.d.ts +206 -0
  108. package/dist/session/database-store.d.ts +51 -0
  109. package/dist/session/file-store.d.ts +22 -0
  110. package/dist/session/index.d.ts +106 -0
  111. package/dist/session/memory-store.d.ts +21 -0
  112. package/dist/session/redis-store.d.ts +31 -0
  113. package/dist/streaming/index.d.ts +3 -0
  114. package/dist/streaming/sse-handler.d.ts +138 -0
  115. package/dist/streaming/stream-handler.d.ts +114 -0
  116. package/dist/testing/auth-testing.d.ts +156 -0
  117. package/dist/testing/file-upload-testing.d.ts +190 -0
  118. package/dist/testing/index.d.ts +10 -0
  119. package/dist/testing/middleware-testing.d.ts +138 -0
  120. package/dist/testing/model-binding-testing.d.ts +186 -0
  121. package/dist/testing/performance-testing.d.ts +235 -0
  122. package/dist/testing/test-client.d.ts +117 -0
  123. package/dist/testing/test-request.d.ts +85 -0
  124. package/dist/testing/test-response.d.ts +90 -0
  125. package/dist/testing/types.d.ts +207 -0
  126. package/dist/testing/websocket-testing.d.ts +227 -0
  127. package/dist/types/controller-types.d.ts +208 -0
  128. package/dist/types/core.d.ts +543 -0
  129. package/dist/types/middleware-types.d.ts +227 -0
  130. package/dist/types/request-response-augmentation.d.ts +261 -0
  131. package/dist/types/route-inference.d.ts +168 -0
  132. package/dist/types.d.ts +1794 -0
  133. package/dist/url.d.ts +56 -0
  134. package/dist/utils/index.d.ts +1 -0
  135. package/dist/utils/query-preservation.d.ts +48 -0
  136. package/dist/utils.d.ts +69 -0
  137. package/dist/validation/validator.d.ts +140 -0
  138. package/dist/websocket/clustering.d.ts +185 -0
  139. package/package.json +45 -44
  140. package/src/auth.ts +469 -0
  141. package/src/cache/lru-cache.ts +457 -0
  142. package/src/cache/middleware-memoization.ts +531 -0
  143. package/src/cache/route-cache-warmer.ts +486 -0
  144. package/src/cache/sqlite-cache.ts +783 -0
  145. package/src/cache/streaming-cache.ts +572 -0
  146. package/src/cli/colors.ts +29 -0
  147. package/src/cli/index.ts +287 -0
  148. package/src/cli/middleware.ts +291 -0
  149. package/src/cli/openapi.ts +407 -0
  150. package/src/cli/router.ts +188 -0
  151. package/src/cli/routes.ts +265 -0
  152. package/src/cli/utils.ts +531 -0
  153. package/src/cli.ts +5 -0
  154. package/src/config.ts +322 -0
  155. package/src/container/container.ts +740 -0
  156. package/src/container/contextual-binding.ts +603 -0
  157. package/src/container/decorators.ts +359 -0
  158. package/src/container/service-provider.ts +596 -0
  159. package/src/development/hot-reload.ts +673 -0
  160. package/src/development/index.ts +499 -0
  161. package/src/development/performance-profiler.ts +717 -0
  162. package/src/development/route-debugger.ts +527 -0
  163. package/src/development/route-inspector.ts +749 -0
  164. package/src/development/typescript-utilities.ts +682 -0
  165. package/src/docs.ts +397 -0
  166. package/src/errors/circuit-breaker.ts +732 -0
  167. package/src/errors/error-handler.ts +569 -0
  168. package/src/errors/error-reporting.ts +672 -0
  169. package/src/errors/exceptions.ts +536 -0
  170. package/src/errors/graceful-degradation.ts +621 -0
  171. package/src/errors/index.ts +21 -0
  172. package/src/errors/router-errors.ts +632 -0
  173. package/src/file-serving/static-files.ts +581 -0
  174. package/src/index.ts +14 -0
  175. package/src/middleware/auth.ts +220 -0
  176. package/src/middleware/content_security_policy.ts +215 -0
  177. package/src/middleware/cors.ts +74 -0
  178. package/src/middleware/csrf.ts +108 -0
  179. package/src/middleware/ddos_protection.ts +255 -0
  180. package/src/middleware/file_security.ts +191 -0
  181. package/src/middleware/file_upload.ts +275 -0
  182. package/src/middleware/helmet.ts +268 -0
  183. package/src/middleware/index.ts +117 -0
  184. package/src/middleware/input_validation.ts +449 -0
  185. package/src/middleware/json_body.ts +37 -0
  186. package/src/middleware/performance_alerting.ts +538 -0
  187. package/src/middleware/performance_dashboard.ts +661 -0
  188. package/src/middleware/performance_monitor.ts +943 -0
  189. package/src/middleware/pipeline.ts +489 -0
  190. package/src/middleware/rate_limit.ts +245 -0
  191. package/src/middleware/request_id.ts +36 -0
  192. package/src/middleware/request_signing.ts +636 -0
  193. package/src/middleware/request_tracer.ts +636 -0
  194. package/src/middleware/response_cache.ts +743 -0
  195. package/src/middleware/security.ts +482 -0
  196. package/src/middleware/security_suite.ts +257 -0
  197. package/src/middleware/session.ts +91 -0
  198. package/src/model-binding/index.ts +18 -0
  199. package/src/model-binding/model-middleware.ts +425 -0
  200. package/src/model-binding/model-registry.ts +550 -0
  201. package/src/model-binding.ts +370 -0
  202. package/src/model-resolver-factory.ts +106 -0
  203. package/src/observability/correlation.ts +691 -0
  204. package/src/observability/health-checks.ts +729 -0
  205. package/src/observability/index.ts +184 -0
  206. package/src/observability/integration.ts +548 -0
  207. package/src/observability/metrics.ts +753 -0
  208. package/src/observability/tracing.ts +638 -0
  209. package/src/optimization/bun-utilities.ts +778 -0
  210. package/src/query-builder-integration.ts +137 -0
  211. package/src/request/context.ts +62 -0
  212. package/src/request/enhanced-request.ts +857 -0
  213. package/src/request/macros.ts +688 -0
  214. package/src/response/macros.ts +665 -0
  215. package/src/response/response-factory.ts +596 -0
  216. package/src/router/api-routes.ts +243 -0
  217. package/src/router/file-based-routing.ts +690 -0
  218. package/src/router/file-streaming.ts +383 -0
  219. package/src/router/fluent-router.ts +927 -0
  220. package/src/router/fluent-routing.ts +797 -0
  221. package/src/router/group-organization.ts +213 -0
  222. package/src/router/handler-resolver.ts +291 -0
  223. package/src/router/http-methods.ts +377 -0
  224. package/src/router/index.ts +187 -0
  225. package/src/router/middleware-groups.ts +222 -0
  226. package/src/router/middleware-integration.ts +399 -0
  227. package/src/router/middleware.ts +231 -0
  228. package/src/router/model-binding.ts +215 -0
  229. package/src/router/optimized-route-matching.ts +253 -0
  230. package/src/router/route-building.ts +221 -0
  231. package/src/router/route-compiler.ts +702 -0
  232. package/src/router/route-matching.ts +349 -0
  233. package/src/router/route-trie.ts +464 -0
  234. package/src/router/router.ts +1604 -0
  235. package/src/router/server.ts +462 -0
  236. package/src/router/validation-integration.ts +443 -0
  237. package/src/router/view-rendering.ts +233 -0
  238. package/src/router/websocket.ts +100 -0
  239. package/src/routing/route-caching.ts +402 -0
  240. package/src/routing/route-throttling.ts +469 -0
  241. package/src/routing/subdomain-routing.ts +492 -0
  242. package/src/session/database-store.ts +109 -0
  243. package/src/session/file-store.ts +148 -0
  244. package/src/session/index.ts +244 -0
  245. package/src/session/memory-store.ts +88 -0
  246. package/src/session/redis-store.ts +93 -0
  247. package/src/streaming/index.ts +17 -0
  248. package/src/streaming/sse-handler.ts +482 -0
  249. package/src/streaming/stream-handler.ts +552 -0
  250. package/src/testing/auth-testing.ts +446 -0
  251. package/src/testing/file-upload-testing.ts +543 -0
  252. package/src/testing/index.ts +10 -0
  253. package/src/testing/middleware-testing.ts +322 -0
  254. package/src/testing/model-binding-testing.ts +645 -0
  255. package/src/testing/performance-testing.ts +738 -0
  256. package/src/testing/test-client.ts +310 -0
  257. package/src/testing/test-request.ts +315 -0
  258. package/src/testing/test-response.ts +332 -0
  259. package/src/testing/types.ts +224 -0
  260. package/src/testing/websocket-testing.ts +590 -0
  261. package/src/types/controller-types.ts +385 -0
  262. package/src/types/core.ts +683 -0
  263. package/src/types/middleware-types.ts +419 -0
  264. package/src/types/request-response-augmentation.ts +489 -0
  265. package/src/types/route-inference.ts +357 -0
  266. package/src/types.ts +2067 -0
  267. package/src/url.ts +126 -0
  268. package/src/utils/index.ts +1 -0
  269. package/src/utils/query-preservation.ts +201 -0
  270. package/src/utils.ts +327 -0
  271. package/src/validation/validator.ts +685 -0
  272. package/src/websocket/clustering.ts +762 -0
  273. package/CHANGELOG.md +0 -770
@@ -0,0 +1,1604 @@
1
+ import type { Server } from 'bun'
2
+ import type { MiddlewareDependency, MiddlewarePipeline, MiddlewarePipelineStats, MiddlewareSkipCondition } from '../middleware/pipeline'
3
+ import type {
4
+ ActionHandler,
5
+ CookieOptions,
6
+ CookieToSet,
7
+ EnhancedRequest,
8
+ MiddlewareHandler,
9
+ NextFunction,
10
+ Route,
11
+ RouteGroup,
12
+ RouteHandler,
13
+ RouterConfig,
14
+ ThrottlePattern,
15
+ WebSocketConfig,
16
+ WebSocketData,
17
+ } from '../types'
18
+ import { createRateLimitMiddleware, parseThrottleString } from '../routing/route-throttling'
19
+ import { registerNamedRoute } from '../url'
20
+ import { extractParamNames, joinPaths, matchPath } from '../utils'
21
+
22
+ /**
23
+ * Route compiler interface for pattern matching
24
+ */
25
+ export interface RouteCompiler {
26
+ compile: (path: string) => RegExp
27
+ match: (path: string, pattern: RegExp) => Record<string, string> | null
28
+ }
29
+
30
+ /**
31
+ * Middleware cache info interface
32
+ */
33
+ export interface MiddlewareCacheInfo {
34
+ size: number
35
+ routes: string[]
36
+ hitRate: number
37
+ }
38
+
39
+ // Re-export types for module augmentation
40
+ export type {
41
+ ActionHandler,
42
+ MiddlewareHandler,
43
+ Route,
44
+ RouteGroup,
45
+ RouteHandler,
46
+ RouterConfig,
47
+ WebSocketConfig,
48
+ }
49
+
50
+ // Middleware condition type
51
+ export type MiddlewareCondition = (_req: EnhancedRequest) => boolean
52
+
53
+ /**
54
+ * Unified Router class with advanced middleware patterns
55
+ */
56
+ export class Router {
57
+ routes: Route[] = []
58
+ currentGroup: RouteGroup | null = null
59
+ globalMiddleware: MiddlewareHandler[] = []
60
+ namedRoutes: Map<string, Route> = new Map()
61
+ fallbackHandler: ActionHandler | null = null
62
+ patterns: Map<string, string> = new Map()
63
+ currentDomain: string | null = null
64
+ domains: Record<string, Route[]> = {}
65
+ serverInstance: Server<WebSocketData> | null = null
66
+ wsConfig: WebSocketConfig | null = null
67
+ errorHandler: ((_error: Error) => Response | Promise<Response>) | null = null
68
+ templateCache: Map<string, string> = new Map<string, string>()
69
+ routeCache: Map<string, { route: Route, params: Record<string, string> }> = new Map()
70
+ staticRoutes: Map<string, Map<string, Route>> = new Map()
71
+ staticResponses: Map<string, Response> = new Map()
72
+ precompiledPatterns: Map<string, RegExp> = new Map()
73
+ domainPatternCache: Map<string, RegExp> = new Map()
74
+ routeCompiler: RouteCompiler | null = null
75
+
76
+ // Advanced middleware features
77
+ private middlewareGroups: Map<string, MiddlewareHandler[]> = new Map()
78
+ private namedMiddleware: Map<string, (params?: string) => MiddlewareHandler> = new Map()
79
+ private conditionalMiddleware: Array<{ condition: MiddlewareCondition, middleware: MiddlewareHandler[] }> = []
80
+
81
+ // Middleware pipeline for advanced features
82
+ _middlewarePipeline?: MiddlewarePipeline
83
+
84
+ config: RouterConfig = {
85
+ verbose: false,
86
+ routesPath: 'routes',
87
+ apiRoutesPath: 'routes/api.ts',
88
+ webRoutesPath: 'routes/web.ts',
89
+ apiPrefix: '/api',
90
+ webPrefix: '',
91
+ actionsPath: 'actions',
92
+ controllersPath: 'controllers',
93
+ defaultMiddleware: {
94
+ api: [],
95
+ web: [],
96
+ },
97
+ }
98
+
99
+ constructor(config: Partial<RouterConfig> = {}) {
100
+ this.routes = []
101
+ this.config = { ...this.config, ...config }
102
+ this.initializeDefaultMiddleware()
103
+ }
104
+
105
+ /**
106
+ * Initialize default named middleware
107
+ */
108
+ private initializeDefaultMiddleware(): void {
109
+ // Auth middleware
110
+ this.namedMiddleware.set('auth', () => async (req: EnhancedRequest, next: NextFunction) => {
111
+ const token = req.headers.get('authorization')?.replace('Bearer ', '')
112
+ if (!token) {
113
+ return new Response('Unauthorized', { status: 401 })
114
+ }
115
+ return await next()
116
+ })
117
+
118
+ // Throttle middleware with parameters
119
+ this.namedMiddleware.set('throttle', (params?: string): MiddlewareHandler => {
120
+ const config = params ? parseThrottleString(params as ThrottlePattern) : { maxAttempts: 60, windowMs: 60000 }
121
+ return createRateLimitMiddleware({
122
+ maxAttempts: config.maxAttempts || 60,
123
+ windowMs: config.windowMs,
124
+ keyGenerator: (req: EnhancedRequest) => req.headers.get('x-forwarded-for') || 'anonymous',
125
+ })
126
+ })
127
+
128
+ // CORS middleware
129
+ this.namedMiddleware.set('cors', () => async (_req: EnhancedRequest, next: NextFunction) => {
130
+ const response = await next()
131
+ if (response) {
132
+ response.headers.set('Access-Control-Allow-Origin', '*')
133
+ response.headers.set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS')
134
+ response.headers.set('Access-Control-Allow-Headers', 'Content-Type, Authorization')
135
+ }
136
+ return response
137
+ })
138
+ }
139
+
140
+ /**
141
+ * Register a middleware group
142
+ */
143
+ middlewareGroup(name: string, middlewareNames: string[]): this {
144
+ const middlewareHandlers: MiddlewareHandler[] = []
145
+
146
+ for (const middlewareName of middlewareNames) {
147
+ const [name, params] = middlewareName.split(':')
148
+ const middlewareFactory = this.namedMiddleware.get(name)
149
+
150
+ if (middlewareFactory) {
151
+ middlewareHandlers.push(middlewareFactory(params))
152
+ }
153
+ }
154
+
155
+ this.middlewareGroups.set(name, middlewareHandlers)
156
+ return this
157
+ }
158
+
159
+ /**
160
+ * Apply middleware group to routes
161
+ */
162
+ middlewareGroupRoutes(name: string): RouteGroupBuilder {
163
+ const middlewareHandlers = this.middlewareGroups.get(name) || []
164
+ return new RouteGroupBuilder(this, middlewareHandlers)
165
+ }
166
+
167
+ /**
168
+ * Conditional middleware execution
169
+ */
170
+ when(condition: MiddlewareCondition): ConditionalBuilder {
171
+ return new ConditionalBuilder(this, condition)
172
+ }
173
+
174
+ /**
175
+ * Apply middleware with parameters
176
+ */
177
+ middleware(middlewareName: string): MiddlewareBuilder {
178
+ const [name, params] = middlewareName.split(':')
179
+ const middlewareFactory = this.namedMiddleware.get(name)
180
+
181
+ if (!middlewareFactory) {
182
+ throw new Error(`Unknown middleware: ${name}`)
183
+ }
184
+
185
+ const middlewareHandler = middlewareFactory(params)
186
+ return new MiddlewareBuilder(this, [middlewareHandler])
187
+ }
188
+
189
+ /**
190
+ * Get the server instance
191
+ */
192
+ getServer(): Server<WebSocketData> | null {
193
+ return this.serverInstance
194
+ }
195
+
196
+ /**
197
+ * Extend the router with custom methods
198
+ */
199
+ extend(methods: Record<string, (...args: unknown[]) => unknown>): Router {
200
+ for (const [name, method] of Object.entries(methods)) {
201
+ if (typeof method === 'function') {
202
+ // @ts-expect-error - dynamically extending the object
203
+ this[name] = method.bind(this)
204
+ }
205
+ }
206
+ return this
207
+ }
208
+
209
+ /**
210
+ * Register routes from a package or module file.
211
+ * Loads the route file within an optional group (prefix + middleware).
212
+ */
213
+ async register(routePath: string, options?: { prefix?: string, middleware?: MiddlewareHandler[] }): Promise<Router> {
214
+ const callback = async () => {
215
+ await import(routePath)
216
+ }
217
+
218
+ if (options?.prefix || (options?.middleware && options.middleware.length > 0)) {
219
+ await this.group({
220
+ prefix: options.prefix,
221
+ middleware: options.middleware,
222
+ }, callback)
223
+ }
224
+ else {
225
+ await callback()
226
+ }
227
+
228
+ return this
229
+ }
230
+
231
+ /**
232
+ * Invalidate route caches
233
+ */
234
+ invalidateCache(): void {
235
+ this.routeCache.clear()
236
+ }
237
+
238
+ /**
239
+ * Internal method to add a route with full HTTP method support
240
+ * This is the core route registration method used by get/post/put/patch/delete
241
+ */
242
+ registerRoute(
243
+ method: string,
244
+ path: string,
245
+ handler: ActionHandler,
246
+ type?: 'api' | 'web',
247
+ name?: string,
248
+ middleware?: (string | MiddlewareHandler)[],
249
+ ): Router {
250
+ // Apply current group settings if in a group
251
+ let routePath = path
252
+ let routeMiddleware: MiddlewareHandler[] = []
253
+ const routeType = type || 'web'
254
+
255
+ if (this.currentGroup) {
256
+ // Apply prefix if it exists
257
+ if (this.currentGroup.prefix) {
258
+ routePath = joinPaths(this.currentGroup.prefix, path)
259
+ }
260
+
261
+ // Apply middleware if it exists
262
+ if (this.currentGroup.middleware && this.currentGroup.middleware.length > 0) {
263
+ routeMiddleware = [...this.currentGroup.middleware] as MiddlewareHandler[]
264
+ }
265
+ }
266
+
267
+ // Apply route-specific middleware if provided
268
+ if (middleware && middleware.length > 0) {
269
+ for (const middlewareItem of middleware) {
270
+ const resolved = this.resolveMiddleware(middlewareItem)
271
+ if (resolved) {
272
+ routeMiddleware.push(resolved)
273
+ }
274
+ }
275
+ }
276
+
277
+ // Apply API/Web path prefixes
278
+ if (routeType === 'api' && this.config.apiPrefix) {
279
+ routePath = joinPaths(this.config.apiPrefix, routePath)
280
+ }
281
+ else if (routeType === 'web' && this.config.webPrefix) {
282
+ routePath = joinPaths(this.config.webPrefix, routePath)
283
+ }
284
+
285
+ // Apply domain if in a domain group
286
+ let domain: string | undefined
287
+ if (this.currentDomain) {
288
+ domain = this.currentDomain
289
+ }
290
+
291
+ // Create the route
292
+ const route: Route = {
293
+ method: method.toUpperCase(),
294
+ path: routePath,
295
+ handler,
296
+ domain,
297
+ params: {},
298
+ middleware: routeMiddleware,
299
+ }
300
+
301
+ // Apply constraints from patterns map
302
+ const paramNames = extractParamNames(routePath)
303
+ const constraints: Record<string, string> = {}
304
+
305
+ paramNames.forEach((param: string) => {
306
+ // Remove optional marker for constraint lookup
307
+ const baseParam = param.replace('?', '')
308
+ if (this.patterns.has(baseParam)) {
309
+ constraints[baseParam] = this.patterns.get(baseParam)!
310
+ }
311
+ })
312
+
313
+ if (Object.keys(constraints).length > 0) {
314
+ route.constraints = constraints
315
+ }
316
+
317
+ // Add pattern property for route matching
318
+ route.pattern = {
319
+ exec: (url: URL): { pathname: { groups: Record<string, string> } } | null => {
320
+ const params: Record<string, string> = {}
321
+ // Pass constraints directly to matchPath for more efficient matching
322
+ const constraintsRecord = route.constraints && !Array.isArray(route.constraints)
323
+ ? route.constraints as Record<string, string>
324
+ : undefined
325
+
326
+ const isMatch = matchPath(routePath, url.pathname, params, constraintsRecord)
327
+
328
+ if (!isMatch) {
329
+ return null
330
+ }
331
+
332
+ return {
333
+ pathname: {
334
+ groups: params,
335
+ },
336
+ }
337
+ },
338
+ }
339
+
340
+ // Add to the appropriate collection
341
+ if (domain) {
342
+ if (!this.domains[domain]) {
343
+ this.domains[domain] = []
344
+ }
345
+ this.domains[domain].push(route)
346
+ }
347
+ else {
348
+ this.routes.push(route)
349
+ }
350
+
351
+ // Add to static routes map for fast lookup if it's a static route
352
+ if (!routePath.includes('{') && !routePath.includes('*')) {
353
+ if (!this.staticRoutes.has(method.toUpperCase())) {
354
+ this.staticRoutes.set(method.toUpperCase(), new Map())
355
+ }
356
+ this.staticRoutes.get(method.toUpperCase())!.set(routePath, route)
357
+ }
358
+
359
+ // Add to named routes if name is provided
360
+ if (name) {
361
+ route.name = name
362
+ this.namedRoutes.set(name, route)
363
+ registerNamedRoute(name, route.path)
364
+ }
365
+
366
+ // Clear route cache when new routes are added
367
+ this.routeCache.clear()
368
+
369
+ return this
370
+ }
371
+
372
+ /**
373
+ * Resolve middleware from string or handler (synchronous)
374
+ */
375
+ resolveMiddleware(middleware: string | MiddlewareHandler): MiddlewareHandler | null {
376
+ if (typeof middleware === 'function') {
377
+ return middleware
378
+ }
379
+
380
+ // Parse middleware string like "auth:api" or "throttle:60,1"
381
+ const [name, params] = middleware.split(':')
382
+ const middlewareFactory = this.namedMiddleware.get(name)
383
+
384
+ if (middlewareFactory) {
385
+ return middlewareFactory(params)
386
+ }
387
+
388
+ return null
389
+ }
390
+
391
+ /**
392
+ * HTTP GET method.
393
+ *
394
+ * `TPath` is inferred from the path literal at the call site, so an
395
+ * inline handler's `request.params` narrows to the extracted keyset:
396
+ *
397
+ * ```ts
398
+ * router.get('/api/users/{id}', (req) => {
399
+ * req.params.id // typed `string`, no `as any`
400
+ * // @ts-expect-error wrong key
401
+ * req.params.bogus
402
+ * })
403
+ * ```
404
+ *
405
+ * The string-action / class-handler forms (`'Actions/Foo/BarAction'`)
406
+ * keep working — `ActionHandler<TPath>` is a union that accepts them.
407
+ * See stacksjs/stacks#1851 for the broader typed-request work.
408
+ */
409
+ get<TPath extends string>(path: TPath, handler: ActionHandler<TPath>, type?: 'api' | 'web', name?: string, middleware?: (string | MiddlewareHandler)[]): Router {
410
+ return this.registerRoute('GET', path, handler, type, name, middleware)
411
+ }
412
+
413
+ /**
414
+ * HTTP POST method. See {@link get} for `TPath`-driven param
415
+ * narrowing on inline handlers.
416
+ */
417
+ post<TPath extends string>(path: TPath, handler: ActionHandler<TPath>, type?: 'api' | 'web', name?: string, middleware?: (string | MiddlewareHandler)[]): Router {
418
+ return this.registerRoute('POST', path, handler, type, name, middleware)
419
+ }
420
+
421
+ /**
422
+ * HTTP PUT method. See {@link get} for `TPath`-driven param
423
+ * narrowing on inline handlers.
424
+ */
425
+ put<TPath extends string>(path: TPath, handler: ActionHandler<TPath>, type?: 'api' | 'web', name?: string, middleware?: (string | MiddlewareHandler)[]): Router {
426
+ return this.registerRoute('PUT', path, handler, type, name, middleware)
427
+ }
428
+
429
+ /**
430
+ * HTTP PATCH method. See {@link get} for `TPath`-driven param
431
+ * narrowing on inline handlers.
432
+ */
433
+ patch<TPath extends string>(path: TPath, handler: ActionHandler<TPath>, type?: 'api' | 'web', name?: string, middleware?: (string | MiddlewareHandler)[]): Router {
434
+ return this.registerRoute('PATCH', path, handler, type, name, middleware)
435
+ }
436
+
437
+ /**
438
+ * HTTP DELETE method. See {@link get} for `TPath`-driven param
439
+ * narrowing on inline handlers.
440
+ */
441
+ delete<TPath extends string>(path: TPath, handler: ActionHandler<TPath>, type?: 'api' | 'web', name?: string, middleware?: (string | MiddlewareHandler)[]): Router {
442
+ return this.registerRoute('DELETE', path, handler, type, name, middleware)
443
+ }
444
+
445
+ /**
446
+ * HTTP OPTIONS method. See {@link get} for `TPath`-driven param
447
+ * narrowing on inline handlers.
448
+ */
449
+ options<TPath extends string>(path: TPath, handler: ActionHandler<TPath>, type?: 'api' | 'web', name?: string, middleware?: (string | MiddlewareHandler)[]): Router {
450
+ return this.registerRoute('OPTIONS', path, handler, type, name, middleware)
451
+ }
452
+
453
+ /**
454
+ * Register the same handler against multiple HTTP methods. The
455
+ * handler is type-narrowed via {@link ActionHandler}'s `TPath`
456
+ * generic, same as the single-method overloads.
457
+ */
458
+ match<TPath extends string>(methods: string[], path: TPath, handler: ActionHandler<TPath>, type?: 'api' | 'web', name?: string, middleware?: (string | MiddlewareHandler)[]): Router {
459
+ for (const method of methods) {
460
+ this.registerRoute(method, path, handler, type, name, middleware)
461
+ }
462
+ return this
463
+ }
464
+
465
+ /**
466
+ * Register the handler against every HTTP method. See {@link get}
467
+ * for `TPath`-driven param narrowing on inline handlers.
468
+ */
469
+ any<TPath extends string>(path: TPath, handler: ActionHandler<TPath>, type?: 'api' | 'web', name?: string, middleware?: (string | MiddlewareHandler)[]): Router {
470
+ const methods = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS', 'HEAD']
471
+ return this.match(methods, path, handler, type, name, middleware)
472
+ }
473
+
474
+ /**
475
+ * Set fallback handler for unmatched routes
476
+ */
477
+ fallback(handler: ActionHandler): Router {
478
+ this.fallbackHandler = handler
479
+ return this
480
+ }
481
+
482
+ /**
483
+ * Generate URL for a named route
484
+ */
485
+ route(name: string, params: Record<string, string> = {}): string {
486
+ const route = this.namedRoutes.get(name)
487
+ if (!route) {
488
+ throw new Error(`Route with name "${name}" not found`)
489
+ }
490
+
491
+ let url = route.path
492
+ // Replace path parameters
493
+ for (const [param, value] of Object.entries(params)) {
494
+ url = url.replace(`{${param}}`, encodeURIComponent(value))
495
+ url = url.replace(`{${param}?}`, encodeURIComponent(value))
496
+ }
497
+
498
+ return url
499
+ }
500
+
501
+ /**
502
+ * Set error handler
503
+ */
504
+ onError(handler: (error: Error) => Response | Promise<Response>): Router {
505
+ this.errorHandler = handler
506
+ return this
507
+ }
508
+
509
+ /**
510
+ * Create redirect response
511
+ */
512
+ redirect(url: string, status: 301 | 302 | 303 | 307 | 308 = 302): Response {
513
+ const headers = new Headers()
514
+ headers.set('Location', url)
515
+ return new Response(null, {
516
+ status,
517
+ headers,
518
+ })
519
+ }
520
+
521
+ /**
522
+ * Create permanent redirect response
523
+ */
524
+ permanentRedirect(url: string): Response {
525
+ return this.redirect(url, 301)
526
+ }
527
+
528
+ /**
529
+ * Register redirect route
530
+ */
531
+ redirectRoute(from: string, to: string, status: 301 | 302 | 303 | 307 | 308 = 302): Router {
532
+ this.get(from, (_req: EnhancedRequest) => {
533
+ return this.redirect(to, status)
534
+ })
535
+ return this
536
+ }
537
+
538
+ /**
539
+ * Start the HTTP server
540
+ */
541
+ async serve(options?: { port?: number, hostname?: string }): Promise<Server<WebSocketData>> {
542
+ // Invalidate route cache before starting server
543
+ this.invalidateCache()
544
+
545
+ // Create server options - use type assertion for Bun.serve compatibility
546
+ const serverOptions = {
547
+ ...options,
548
+ fetch: this.handleRequest.bind(this),
549
+ websocket: this.wsConfig,
550
+ } as Parameters<typeof Bun.serve>[0]
551
+
552
+ // Start the server
553
+ this.serverInstance = Bun.serve(serverOptions) as Server<WebSocketData>
554
+
555
+ if (this.config.verbose) {
556
+ const port = this.serverInstance.port
557
+ const hostname = this.serverInstance.hostname
558
+ console.log(`\n🚀 Server running at http://${hostname}:${port}\n`)
559
+ }
560
+
561
+ return this.serverInstance
562
+ }
563
+
564
+ /**
565
+ * Handle an HTTP request
566
+ */
567
+ async handleRequest(req: Request): Promise<Response> {
568
+ try {
569
+ // Create URL for route matching
570
+ const url = new URL(req.url)
571
+
572
+ if (this.config.verbose) {
573
+ console.log(`${req.method} ${url.pathname}`)
574
+ }
575
+
576
+ // Handle CORS preflight OPTIONS requests - but check for registered OPTIONS routes first
577
+ // This ensures explicitly registered OPTIONS routes work while still providing CORS support
578
+ if (req.method === 'OPTIONS') {
579
+ const hostname = url.hostname || req.headers.get('host')?.split(':')[0] || 'localhost'
580
+ const optionsMatch = this.matchRoute(url.pathname, 'OPTIONS', hostname)
581
+ if (!optionsMatch) {
582
+ // No explicit OPTIONS route - return generic CORS preflight response
583
+ return new Response(null, {
584
+ status: 204,
585
+ headers: {
586
+ 'Access-Control-Allow-Origin': '*',
587
+ 'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, PATCH, OPTIONS',
588
+ 'Access-Control-Allow-Headers': 'Content-Type, Authorization, X-Requested-With',
589
+ 'Access-Control-Max-Age': '86400',
590
+ },
591
+ })
592
+ }
593
+ // Let the registered OPTIONS route handle it (fall through to normal route matching)
594
+ }
595
+
596
+ // Get domain from the host header
597
+ const hostname = url.hostname || req.headers.get('host')?.split(':')[0] || 'localhost'
598
+
599
+ // Find a matching route
600
+ const match = this.matchRoute(url.pathname, req.method as any, hostname)
601
+
602
+ // Enhance the request with params and other utilities
603
+ const enhancedReq = this.enhanceRequest(req, match?.params || {})
604
+
605
+ if (match) {
606
+ // Add the matched route to the request
607
+ enhancedReq.route = match.route
608
+
609
+ // Collect all middleware to run
610
+ const middlewareStack = [...this.globalMiddleware]
611
+
612
+ // Add route-specific middleware
613
+ if (match.route.middleware && match.route.middleware.length > 0) {
614
+ middlewareStack.push(...match.route.middleware)
615
+ }
616
+
617
+ // Create a final middleware that executes the route handler
618
+ const routeHandlerMiddleware = async (req: EnhancedRequest, _next: NextFunction) => {
619
+ return await this.resolveHandler(match.route.handler, req)
620
+ }
621
+
622
+ // Add the route handler as the final middleware
623
+ middlewareStack.push(routeHandlerMiddleware)
624
+
625
+ // Run middleware stack with the route handler at the end
626
+ const response = await this.runMiddleware(enhancedReq, middlewareStack)
627
+
628
+ // Apply modified cookies to the response
629
+ if (response) {
630
+ return this.applyModifiedCookies(response, enhancedReq)
631
+ }
632
+
633
+ // This should not happen since we're always returning a response now
634
+ return new Response('No response from middleware chain', { status: 500 })
635
+ }
636
+
637
+ // No route found - check if the path exists with a different method (405 vs 404).
638
+ // 404/405 bodies now include path + method so client-side debugging (typo'd
639
+ // endpoint, stale SPA cache, missing route registration) is one grep away.
640
+ // Both responses still flow through globalMiddleware so user middleware
641
+ // (X-Request-ID, audit, custom CORS) sees them.
642
+ const allowedMethods = this.getAllowedMethods(url.pathname, hostname)
643
+
644
+ if (allowedMethods.length > 0) {
645
+ const methodNotAllowedHandler = async (_req: EnhancedRequest, _next: NextFunction) => {
646
+ return new Response(JSON.stringify({
647
+ error: 'Method Not Allowed',
648
+ path: url.pathname,
649
+ method: req.method,
650
+ allowed: allowedMethods,
651
+ }), {
652
+ status: 405,
653
+ headers: {
654
+ 'Content-Type': 'application/json',
655
+ 'Allow': allowedMethods.join(', '),
656
+ },
657
+ })
658
+ }
659
+
660
+ if (this.globalMiddleware.length > 0) {
661
+ const middlewareStack = [...this.globalMiddleware, methodNotAllowedHandler]
662
+ const response = await this.runMiddleware(enhancedReq, middlewareStack)
663
+ if (response) {
664
+ return this.applyModifiedCookies(response, enhancedReq)
665
+ }
666
+ }
667
+
668
+ return methodNotAllowedHandler(enhancedReq, async () => new Response(null))
669
+ }
670
+
671
+ // No route found with any method - still run global middleware for CORS headers on 404
672
+ if (this.globalMiddleware.length > 0) {
673
+ const notFoundHandler = async (_req: EnhancedRequest, _next: NextFunction) => {
674
+ if (this.fallbackHandler) {
675
+ return await this.resolveHandler(this.fallbackHandler, enhancedReq)
676
+ }
677
+ return new Response(JSON.stringify({
678
+ error: 'Not Found',
679
+ path: url.pathname,
680
+ method: req.method,
681
+ }), {
682
+ status: 404,
683
+ headers: { 'Content-Type': 'application/json' },
684
+ })
685
+ }
686
+ const middlewareStack = [...this.globalMiddleware, notFoundHandler]
687
+ const response = await this.runMiddleware(enhancedReq, middlewareStack)
688
+ if (response) {
689
+ return this.applyModifiedCookies(response, enhancedReq)
690
+ }
691
+ }
692
+
693
+ // No global middleware, try the fallback handler directly
694
+ if (this.fallbackHandler) {
695
+ const response = await this.resolveHandler(this.fallbackHandler, enhancedReq)
696
+ return this.applyModifiedCookies(response, enhancedReq)
697
+ }
698
+
699
+ // No fallback handler, return a 404 with path context
700
+ return new Response(JSON.stringify({
701
+ error: 'Not Found',
702
+ path: url.pathname,
703
+ method: req.method,
704
+ }), {
705
+ status: 404,
706
+ headers: { 'Content-Type': 'application/json' },
707
+ })
708
+ }
709
+ catch (error) {
710
+ console.error('Error handling request:', error)
711
+
712
+ // Still run global middleware for CORS headers on errors
713
+ if (this.globalMiddleware.length > 0) {
714
+ const enhancedReq = this.enhanceRequest(req, {})
715
+ const errorHandler = async (_req: EnhancedRequest, _next: NextFunction) => {
716
+ if (this.errorHandler) {
717
+ return this.errorHandler(error as Error)
718
+ }
719
+ return new Response(JSON.stringify({
720
+ error: 'Internal Server Error',
721
+ message: error instanceof Error ? error.message : String(error),
722
+ }), {
723
+ status: 500,
724
+ headers: { 'Content-Type': 'application/json' },
725
+ })
726
+ }
727
+ const middlewareStack = [...this.globalMiddleware, errorHandler]
728
+ try {
729
+ const response = await this.runMiddleware(enhancedReq, middlewareStack)
730
+ if (response) {
731
+ return response
732
+ }
733
+ }
734
+ catch {
735
+ // Middleware itself failed, fall through to default error
736
+ }
737
+ }
738
+
739
+ // Use custom error handler if available
740
+ if (this.errorHandler) {
741
+ return this.errorHandler(error as Error)
742
+ }
743
+
744
+ // Default error response
745
+ return new Response(JSON.stringify({
746
+ error: 'Internal Server Error',
747
+ message: error instanceof Error ? error.message : String(error),
748
+ }), {
749
+ status: 500,
750
+ headers: { 'Content-Type': 'application/json' },
751
+ })
752
+ }
753
+ }
754
+
755
+ /**
756
+ * Get all allowed HTTP methods for a given path
757
+ * Used to determine if a 405 Method Not Allowed should be returned instead of 404
758
+ */
759
+ getAllowedMethods(path: string, domain?: string): string[] {
760
+ const url = new URL(path, 'http://localhost')
761
+ const methods: Set<string> = new Set()
762
+ const allMethods = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS', 'HEAD']
763
+
764
+ for (const method of allMethods) {
765
+ // Check static routes
766
+ if (this.staticRoutes.has(method)) {
767
+ const staticRoute = this.staticRoutes.get(method)!.get(url.pathname)
768
+ if (staticRoute && (!domain || !staticRoute.domain || staticRoute.domain === domain)) {
769
+ methods.add(method)
770
+ continue
771
+ }
772
+ }
773
+
774
+ // Get potential routes
775
+ const potentialRoutes: Route[] = domain && this.domains[domain]
776
+ ? this.domains[domain]
777
+ : this.routes
778
+
779
+ // Filter routes to only those matching the HTTP method
780
+ const methodRoutes = potentialRoutes.filter((route: Route) => route.method === method)
781
+
782
+ // Check exact match
783
+ for (const route of methodRoutes) {
784
+ if (route.path === url.pathname) {
785
+ methods.add(method)
786
+ break
787
+ }
788
+ }
789
+
790
+ // Check pattern match if not already found
791
+ if (!methods.has(method)) {
792
+ for (const route of methodRoutes) {
793
+ if (route.pattern) {
794
+ const match = route.pattern.exec(url)
795
+ if (match) {
796
+ methods.add(method)
797
+ break
798
+ }
799
+ }
800
+ }
801
+ }
802
+
803
+ // Check wildcard routes if not already found
804
+ if (!methods.has(method)) {
805
+ const wildcardRoutes = potentialRoutes.filter((route: Route) =>
806
+ route.method === method && route.path.endsWith('*'),
807
+ )
808
+
809
+ for (const route of wildcardRoutes) {
810
+ const basePath = route.path.slice(0, -1)
811
+ if (url.pathname.startsWith(basePath)) {
812
+ methods.add(method)
813
+ break
814
+ }
815
+ }
816
+ }
817
+ }
818
+
819
+ // If GET is allowed, HEAD is implicitly allowed too
820
+ if (methods.has('GET')) {
821
+ methods.add('HEAD')
822
+ }
823
+
824
+ return Array.from(methods)
825
+ }
826
+
827
+ /**
828
+ * Match a route based on the path, method, and domain
829
+ */
830
+ matchRoute(path: string, method: string, domain?: string): { route: Route, params: Record<string, string> } | undefined {
831
+ const url = new URL(path, 'http://localhost')
832
+
833
+ // Generate cache key
834
+ const cacheKey = `${domain || ''}:${method}:${url.pathname}`
835
+
836
+ // Check cache first
837
+ if (this.routeCache.has(cacheKey)) {
838
+ return this.routeCache.get(cacheKey)
839
+ }
840
+
841
+ // Fast path for static routes
842
+ if (this.staticRoutes.has(method)) {
843
+ const staticRoute = this.staticRoutes.get(method)!.get(url.pathname)
844
+ if (staticRoute && (!domain || !staticRoute.domain || staticRoute.domain === domain)) {
845
+ const result = {
846
+ route: staticRoute,
847
+ params: {},
848
+ }
849
+ this.routeCache.set(cacheKey, result)
850
+ return result
851
+ }
852
+ }
853
+
854
+ // Get potential routes - either all routes or domain-specific routes
855
+ const potentialRoutes: Route[] = domain && this.domains[domain]
856
+ ? this.domains[domain]
857
+ : this.routes
858
+
859
+ // Filter routes to only those matching the HTTP method
860
+ const methodRoutes = potentialRoutes.filter((route: Route) => route.method === method)
861
+
862
+ // First, try to find an exact match
863
+ for (const route of methodRoutes) {
864
+ if (route.path === url.pathname) {
865
+ const result = {
866
+ route,
867
+ params: {},
868
+ }
869
+ this.routeCache.set(cacheKey, result)
870
+ return result
871
+ }
872
+ }
873
+
874
+ // If no exact match, try matching patterns
875
+ for (const route of methodRoutes) {
876
+ if (route.pattern) {
877
+ const match = route.pattern.exec(url)
878
+ if (match) {
879
+ const result = {
880
+ route,
881
+ params: match.pathname.groups,
882
+ }
883
+ this.routeCache.set(cacheKey, result)
884
+ return result
885
+ }
886
+ }
887
+ }
888
+
889
+ // If still no match, try domain-specific * (wildcard) routes
890
+ if (domain && this.domains[domain]) {
891
+ const wildcardRoutes = this.domains[domain].filter((route: Route) =>
892
+ route.method === method && route.path.endsWith('*'),
893
+ )
894
+
895
+ for (const route of wildcardRoutes) {
896
+ const basePath = route.path.slice(0, -1) // Remove the '*'
897
+ if (url.pathname.startsWith(basePath)) {
898
+ const result = {
899
+ route,
900
+ params: {
901
+ wildcard: url.pathname.slice(basePath.length),
902
+ },
903
+ }
904
+ this.routeCache.set(cacheKey, result)
905
+ return result
906
+ }
907
+ }
908
+ }
909
+
910
+ // If no match in domain-specific routes, try global wildcard routes
911
+ const globalWildcardRoutes = this.routes.filter((route: Route) =>
912
+ route.method === method && route.path.endsWith('*') && (!domain || !route.domain),
913
+ )
914
+
915
+ for (const route of globalWildcardRoutes) {
916
+ const basePath = route.path.slice(0, -1) // Remove the '*'
917
+ if (url.pathname.startsWith(basePath)) {
918
+ const result = {
919
+ route,
920
+ params: {
921
+ wildcard: url.pathname.slice(basePath.length),
922
+ },
923
+ }
924
+ this.routeCache.set(cacheKey, result)
925
+ return result
926
+ }
927
+ }
928
+
929
+ // If no match for specific method, try HEAD for GET requests
930
+ if (method === 'HEAD') {
931
+ return this.matchRoute(path, 'GET', domain)
932
+ }
933
+
934
+ // No matching route found
935
+ return undefined
936
+ }
937
+
938
+ /**
939
+ * Enhance a request with params and other utilities
940
+ */
941
+ enhanceRequest(req: Request, params: Record<string, string> = {}): EnhancedRequest {
942
+ // Lazy cookie parsing
943
+ let parsedCookies: Record<string, string> | null = null
944
+
945
+ const getCookies = () => {
946
+ if (parsedCookies === null) {
947
+ parsedCookies = {}
948
+ const cookieHeader = req.headers.get('cookie') || ''
949
+
950
+ cookieHeader.split(';').forEach((cookie) => {
951
+ const parts = cookie.trim().split('=')
952
+ if (parts.length >= 2) {
953
+ const name = parts[0].trim()
954
+ const value = parts.slice(1).join('=').trim()
955
+ parsedCookies![name] = decodeURIComponent(value)
956
+ }
957
+ })
958
+ }
959
+ return parsedCookies
960
+ }
961
+
962
+ // Parse query string
963
+ const url = new URL(req.url)
964
+ const query: Record<string, string> = {}
965
+ url.searchParams.forEach((value, key) => {
966
+ query[key] = value
967
+ })
968
+
969
+ // Create a wrapper object that proxies to the original request
970
+ // This is necessary because native Request objects don't allow property assignment in Bun
971
+ const enhancedReq = {
972
+ // Proxy the native Request properties/methods
973
+ get url() { return req.url },
974
+ get method() { return req.method },
975
+ get headers() { return req.headers },
976
+ get body() { return req.body },
977
+ get bodyUsed() { return req.bodyUsed },
978
+ get cache() { return req.cache },
979
+ get credentials() { return req.credentials },
980
+ get destination() { return req.destination },
981
+ get integrity() { return req.integrity },
982
+ get keepalive() { return req.keepalive },
983
+ get mode() { return req.mode },
984
+ get redirect() { return req.redirect },
985
+ get referrer() { return req.referrer },
986
+ get referrerPolicy() { return req.referrerPolicy },
987
+ get signal() { return req.signal },
988
+ arrayBuffer: () => req.arrayBuffer(),
989
+ blob: () => req.blob(),
990
+ clone: () => req.clone(),
991
+ formData: () => req.formData(),
992
+ json: () => req.json(),
993
+ text: () => req.text(),
994
+
995
+ // Enhanced properties
996
+ params,
997
+ query,
998
+ jsonBody: null as any,
999
+ formBody: null as any,
1000
+ _cookiesToSet: [] as CookieToSet[],
1001
+ _cookiesToDelete: [] as { name: string, options: CookieOptions }[],
1002
+
1003
+ } as EnhancedRequest
1004
+
1005
+ // Helper to get all input data - needs to be defined before adding to object
1006
+ const getAllInput = (): Record<string, any> => {
1007
+ const input: Record<string, any> = {}
1008
+
1009
+ // Query parameters
1010
+ for (const [key, value] of Object.entries(query)) {
1011
+ input[key] = value
1012
+ }
1013
+
1014
+ // JSON body
1015
+ if (enhancedReq.jsonBody && typeof enhancedReq.jsonBody === 'object') {
1016
+ for (const [key, value] of Object.entries(enhancedReq.jsonBody)) {
1017
+ input[key] = value
1018
+ }
1019
+ }
1020
+
1021
+ // Form body
1022
+ if (enhancedReq.formBody && typeof enhancedReq.formBody === 'object') {
1023
+ for (const [key, value] of Object.entries(enhancedReq.formBody)) {
1024
+ input[key] = value
1025
+ }
1026
+ }
1027
+
1028
+ // Route params
1029
+ for (const [key, value] of Object.entries(params)) {
1030
+ input[key] = value
1031
+ }
1032
+
1033
+ return input
1034
+ }
1035
+
1036
+ // Add cookie utilities
1037
+ enhancedReq.cookies = {
1038
+ get: (name: string) => getCookies()[name],
1039
+ set: (name: string, value: string, options: CookieOptions = {}) => {
1040
+ enhancedReq._cookiesToSet!.push({ name, value, options })
1041
+ },
1042
+ delete: (name: string, options: CookieOptions = {}) => {
1043
+ enhancedReq._cookiesToDelete!.push({ name, options })
1044
+ },
1045
+ getAll: () => ({ ...getCookies() }),
1046
+ }
1047
+
1048
+ // Add Laravel-style request methods directly to the wrapper object
1049
+ // These are the methods that provide Laravel-like request handling
1050
+ ;(enhancedReq as any).get = <T = any>(key: string, defaultValue?: T): T => {
1051
+ const input = getAllInput()
1052
+ const value = input[key]
1053
+ return (value !== undefined ? value : defaultValue) as T
1054
+ }
1055
+
1056
+ ;(enhancedReq as any).input = <T = any>(key: string, defaultValue?: T): T => {
1057
+ const input = getAllInput()
1058
+ const value = input[key]
1059
+ return (value !== undefined ? value : defaultValue) as T
1060
+ }
1061
+
1062
+ ;(enhancedReq as any).all = (): Record<string, any> => getAllInput()
1063
+
1064
+ ;(enhancedReq as any).only = <T extends Record<string, unknown>>(keys: string[]): T => {
1065
+ const input = getAllInput()
1066
+ const result = {} as T
1067
+ for (const key of keys) {
1068
+ if (key in input) {
1069
+ (result as any)[key] = input[key]
1070
+ }
1071
+ }
1072
+ return result
1073
+ }
1074
+
1075
+ ;(enhancedReq as any).except = <T extends Record<string, unknown>>(keys: string[]): T => {
1076
+ const input = getAllInput()
1077
+ const result = { ...input } as T
1078
+ for (const key of keys) {
1079
+ delete (result as any)[key]
1080
+ }
1081
+ return result
1082
+ }
1083
+
1084
+ ;(enhancedReq as any).has = (key: string | string[]): boolean => {
1085
+ const input = getAllInput()
1086
+ if (Array.isArray(key)) {
1087
+ return key.every(k => k in input && input[k] !== undefined)
1088
+ }
1089
+ return key in input && input[key] !== undefined
1090
+ }
1091
+
1092
+ ;(enhancedReq as any).hasAny = (keys: string[]): boolean => {
1093
+ const input = getAllInput()
1094
+ return keys.some(k => k in input && input[k] !== undefined)
1095
+ }
1096
+
1097
+ ;(enhancedReq as any).filled = (key: string | string[]): boolean => {
1098
+ const input = getAllInput()
1099
+ const isFilled = (k: string): boolean => {
1100
+ const value = input[k]
1101
+ return value !== undefined && value !== null && value !== '' && !(Array.isArray(value) && value.length === 0)
1102
+ }
1103
+ if (Array.isArray(key)) {
1104
+ return key.every(isFilled)
1105
+ }
1106
+ return isFilled(key)
1107
+ }
1108
+
1109
+ ;(enhancedReq as any).missing = (key: string | string[]): boolean => {
1110
+ const input = getAllInput()
1111
+ if (Array.isArray(key)) {
1112
+ return key.every(k => !(k in input) || input[k] === undefined)
1113
+ }
1114
+ return !(key in input) || input[key] === undefined
1115
+ }
1116
+
1117
+ ;(enhancedReq as any).string = (key: string, defaultValue: string = ''): string => {
1118
+ const input = getAllInput()
1119
+ const value = input[key]
1120
+ return value !== undefined && value !== null ? String(value) : defaultValue
1121
+ }
1122
+
1123
+ ;(enhancedReq as any).integer = (key: string, defaultValue: number = 0): number => {
1124
+ const input = getAllInput()
1125
+ const value = input[key]
1126
+ const parsed = Number.parseInt(String(value), 10)
1127
+ return Number.isNaN(parsed) ? defaultValue : parsed
1128
+ }
1129
+
1130
+ ;(enhancedReq as any).float = (key: string, defaultValue: number = 0): number => {
1131
+ const input = getAllInput()
1132
+ const value = input[key]
1133
+ const parsed = Number.parseFloat(String(value))
1134
+ return Number.isNaN(parsed) ? defaultValue : parsed
1135
+ }
1136
+
1137
+ ;(enhancedReq as any).boolean = (key: string, defaultValue: boolean = false): boolean => {
1138
+ const input = getAllInput()
1139
+ const value = input[key]
1140
+ if (value === undefined || value === null)
1141
+ return defaultValue
1142
+ if (typeof value === 'boolean')
1143
+ return value
1144
+ if (value === 'true' || value === '1' || value === 1)
1145
+ return true
1146
+ if (value === 'false' || value === '0' || value === 0)
1147
+ return false
1148
+ return defaultValue
1149
+ }
1150
+
1151
+ ;(enhancedReq as any).array = <T = unknown>(key: string): T[] => {
1152
+ const input = getAllInput()
1153
+ const value = input[key]
1154
+ if (Array.isArray(value))
1155
+ return value as T[]
1156
+ return value !== undefined && value !== null ? [value as T] : []
1157
+ }
1158
+
1159
+ ;(enhancedReq as any).bearerToken = (): string | null => {
1160
+ const authHeader
1161
+ = req.headers.get('authorization')
1162
+ || req.headers.get('Authorization')
1163
+ || ''
1164
+ if (authHeader.startsWith('Bearer '))
1165
+ return authHeader.substring(7)
1166
+ return null
1167
+ }
1168
+
1169
+ ;(enhancedReq as any).header = (name: string): string | null => {
1170
+ return req.headers.get(name) || req.headers.get(name.toLowerCase()) || null
1171
+ }
1172
+
1173
+ // Convenience cookie reader. The full `enhancedReq.cookies.get(name)` API
1174
+ // is also available below — `cookie(name)` is the shorter form callers
1175
+ // (and the Laravel-style macros) reach for first, so it deserves a
1176
+ // direct method on the enhanced request, not just on the macros class.
1177
+ ;(enhancedReq as any).cookie = (name: string, defaultValue?: string): string | null => {
1178
+ const value = getCookies()[name]
1179
+ return value !== undefined ? value : (defaultValue ?? null)
1180
+ }
1181
+
1182
+ ;(enhancedReq as any).getParam = <T = string>(name: string, defaultValue?: T): T | undefined => {
1183
+ const value = params?.[name] as T | undefined
1184
+ return value !== undefined ? value : defaultValue
1185
+ }
1186
+
1187
+ ;(enhancedReq as any).params = params || {}
1188
+
1189
+ return enhancedReq as EnhancedRequest
1190
+ }
1191
+
1192
+ /**
1193
+ * Apply modified cookies to a response
1194
+ */
1195
+ applyModifiedCookies(response: Response, req: EnhancedRequest): Response {
1196
+ // Clone the response to modify headers
1197
+ const newResponse = new Response(response.body, {
1198
+ status: response.status,
1199
+ statusText: response.statusText,
1200
+ headers: response.headers,
1201
+ })
1202
+
1203
+ // Apply cookies to set
1204
+ if (req._cookiesToSet && req._cookiesToSet.length > 0) {
1205
+ for (const { name, value, options } of req._cookiesToSet) {
1206
+ const cookieString = this.serializeCookie(name, value, options)
1207
+ newResponse.headers.append('Set-Cookie', cookieString)
1208
+ }
1209
+ }
1210
+
1211
+ // Apply cookies to delete
1212
+ if (req._cookiesToDelete && req._cookiesToDelete.length > 0) {
1213
+ for (const { name, options } of req._cookiesToDelete) {
1214
+ const deletionOptions = {
1215
+ ...options,
1216
+ expires: new Date(0), // Set expiration to past date
1217
+ maxAge: 0,
1218
+ }
1219
+ const cookieString = this.serializeCookie(name, '', deletionOptions)
1220
+ newResponse.headers.append('Set-Cookie', cookieString)
1221
+ }
1222
+ }
1223
+
1224
+ return newResponse
1225
+ }
1226
+
1227
+ /**
1228
+ * Serialize a cookie for the Set-Cookie header
1229
+ */
1230
+ serializeCookie(name: string, value: string, options: CookieOptions = {}): string {
1231
+ let cookie = `${encodeURIComponent(name)}=${encodeURIComponent(value)}`
1232
+
1233
+ if (options.maxAge !== undefined) {
1234
+ cookie += `; Max-Age=${options.maxAge}`
1235
+ }
1236
+
1237
+ if (options.expires && options.expires instanceof Date) {
1238
+ cookie += `; Expires=${options.expires.toUTCString()}`
1239
+ }
1240
+
1241
+ if (options.path) {
1242
+ cookie += `; Path=${options.path}`
1243
+ }
1244
+ else {
1245
+ cookie += '; Path=/'
1246
+ }
1247
+
1248
+ if (options.domain) {
1249
+ cookie += `; Domain=${options.domain}`
1250
+ }
1251
+
1252
+ if (options.secure) {
1253
+ cookie += '; Secure'
1254
+ }
1255
+
1256
+ if (options.httpOnly) {
1257
+ cookie += '; HttpOnly'
1258
+ }
1259
+
1260
+ if (options.sameSite) {
1261
+ const sameSite = options.sameSite.toLowerCase()
1262
+ cookie += `; SameSite=${sameSite.charAt(0).toUpperCase() + sameSite.slice(1)}`
1263
+ }
1264
+
1265
+ return cookie
1266
+ }
1267
+
1268
+ /**
1269
+ * Build an optimized middleware chain
1270
+ */
1271
+ buildMiddlewareChain(middlewares: MiddlewareHandler[]): (req: EnhancedRequest) => Promise<Response | null> {
1272
+ if (middlewares.length === 0) {
1273
+ return async (_req: EnhancedRequest) => null
1274
+ }
1275
+
1276
+ // Build the chain from the end to start for better performance
1277
+ let chain = async (_req: EnhancedRequest): Promise<Response | null> => null
1278
+
1279
+ for (let i = middlewares.length - 1; i >= 0; i--) {
1280
+ const middleware = middlewares[i]
1281
+ const nextChain = chain
1282
+ chain = async (req: EnhancedRequest): Promise<Response | null> => {
1283
+ const next = async (): Promise<Response> => {
1284
+ const result = await nextChain(req)
1285
+ return result || new Response(null, { status: 200 })
1286
+ }
1287
+ return middleware(req, next)
1288
+ }
1289
+ }
1290
+
1291
+ return chain
1292
+ }
1293
+
1294
+ /**
1295
+ * Run middleware stack for a request
1296
+ */
1297
+ async runMiddleware(req: EnhancedRequest, middlewareStack: MiddlewareHandler[]): Promise<Response | null> {
1298
+ if (middlewareStack.length === 0) {
1299
+ return null // No middleware to run
1300
+ }
1301
+
1302
+ try {
1303
+ // Build and execute optimized middleware chain
1304
+ const chain = this.buildMiddlewareChain(middlewareStack)
1305
+ return await chain(req)
1306
+ }
1307
+ catch (error) {
1308
+ if (this.errorHandler) {
1309
+ return this.errorHandler(error as Error)
1310
+ }
1311
+ throw error
1312
+ }
1313
+ }
1314
+
1315
+ /**
1316
+ * Resolve an action handler
1317
+ */
1318
+ async resolveHandler(handler: ActionHandler, req: EnhancedRequest): Promise<Response> {
1319
+ // If it's a function, call it with the request
1320
+ if (typeof handler === 'function' && !(handler as { prototype?: { handle?: unknown } }).prototype?.handle) {
1321
+ return await (handler as (req: EnhancedRequest) => Response | Promise<Response>)(req)
1322
+ }
1323
+
1324
+ // If it's a class constructor, instantiate it and call handle
1325
+ if (typeof handler === 'function' && (handler as { prototype?: { handle?: unknown } }).prototype?.handle) {
1326
+ const HandlerClass = handler as new () => { handle: (req: EnhancedRequest) => Response | Promise<Response> }
1327
+ const handlerInstance = new HandlerClass()
1328
+ return await handlerInstance.handle(req)
1329
+ }
1330
+
1331
+ // If it's an object with a handle method
1332
+ if (handler && typeof (handler as unknown as { handle?: unknown }).handle === 'function') {
1333
+ return await (handler as unknown as { handle: (req: EnhancedRequest) => Response | Promise<Response> }).handle(req)
1334
+ }
1335
+
1336
+ throw new Error(`Invalid action handler: ${typeof handler}`)
1337
+ }
1338
+
1339
+ /**
1340
+ * Add middleware to the router
1341
+ */
1342
+ use(...middleware: (string | MiddlewareHandler)[]): Router {
1343
+ for (const mw of middleware) {
1344
+ const resolvedMiddleware = this.resolveMiddleware(mw)
1345
+ if (resolvedMiddleware) {
1346
+ this.globalMiddleware.push(resolvedMiddleware)
1347
+ }
1348
+ }
1349
+ return this
1350
+ }
1351
+
1352
+ /**
1353
+ * Create a route group with prefix and middleware
1354
+ */
1355
+ group(options: { prefix?: string, middleware?: (string | MiddlewareHandler)[] }, callback: () => void | Promise<void>): Router {
1356
+ // Save current group state
1357
+ const previousGroup = this.currentGroup
1358
+
1359
+ // Create new group
1360
+ this.currentGroup = {
1361
+ prefix: options.prefix || '',
1362
+ middleware: [],
1363
+ }
1364
+
1365
+ // Resolve middleware if provided
1366
+ if (options.middleware) {
1367
+ for (const mw of options.middleware) {
1368
+ if (typeof mw === 'function') {
1369
+ this.currentGroup.middleware!.push(mw)
1370
+ }
1371
+ }
1372
+ }
1373
+
1374
+ // Execute callback
1375
+ const result = callback()
1376
+
1377
+ // Handle async callbacks
1378
+ if (result instanceof Promise) {
1379
+ result.then(() => {
1380
+ this.currentGroup = previousGroup
1381
+ })
1382
+ }
1383
+ else {
1384
+ // Restore previous group state
1385
+ this.currentGroup = previousGroup
1386
+ }
1387
+
1388
+ return this
1389
+ }
1390
+
1391
+ /**
1392
+ * Add route to the router
1393
+ */
1394
+ addRoute(route: Route): this {
1395
+ this.routes.push(route)
1396
+ if (route.name) {
1397
+ this.namedRoutes.set(route.name, route)
1398
+ registerNamedRoute(route.name, route.path)
1399
+ }
1400
+ return this
1401
+ }
1402
+
1403
+ /**
1404
+ * Register middleware dependency
1405
+ */
1406
+ registerMiddlewareDependency(dependency: MiddlewareDependency): this {
1407
+ if (this._middlewarePipeline) {
1408
+ this._middlewarePipeline.registerDependency(dependency)
1409
+ }
1410
+ return this
1411
+ }
1412
+
1413
+ /**
1414
+ * Register middleware skip conditions
1415
+ */
1416
+ registerMiddlewareSkipConditions(_middlewareName: string, _conditions: MiddlewareSkipCondition[]): this {
1417
+ // Implementation would register skip conditions with the pipeline
1418
+ return this
1419
+ }
1420
+
1421
+ /**
1422
+ * Get middleware statistics
1423
+ */
1424
+ getMiddlewareStats(): MiddlewarePipelineStats | Record<string, never> {
1425
+ if (this._middlewarePipeline) {
1426
+ return this._middlewarePipeline.getStats()
1427
+ }
1428
+ return {}
1429
+ }
1430
+
1431
+ /**
1432
+ * Get middleware cache info
1433
+ */
1434
+ getMiddlewareCacheInfo(): MiddlewareCacheInfo {
1435
+ // Return cache info based on compiled pipelines
1436
+ return {
1437
+ size: 0,
1438
+ routes: [],
1439
+ hitRate: 0,
1440
+ }
1441
+ }
1442
+
1443
+ /**
1444
+ * Clear middleware cache
1445
+ */
1446
+ clearMiddlewareCache(): this {
1447
+ if (this._middlewarePipeline) {
1448
+ this._middlewarePipeline.clear()
1449
+ }
1450
+ return this
1451
+ }
1452
+
1453
+ /**
1454
+ * Execute middleware pipeline
1455
+ */
1456
+ async executeMiddleware(middleware: MiddlewareHandler[], request: EnhancedRequest, handler: () => Promise<Response>): Promise<Response> {
1457
+ if (middleware.length === 0) {
1458
+ return handler()
1459
+ }
1460
+
1461
+ let currentIndex = 0
1462
+
1463
+ const next = async (): Promise<Response> => {
1464
+ if (currentIndex >= middleware.length) {
1465
+ return handler()
1466
+ }
1467
+
1468
+ const mw = middleware[currentIndex++]
1469
+ const result = await mw(request, next)
1470
+ return result || new Response('No response from middleware', { status: 500 })
1471
+ }
1472
+
1473
+ return next()
1474
+ }
1475
+ }
1476
+
1477
+ /**
1478
+ * Builder for conditional middleware
1479
+ */
1480
+ export class ConditionalBuilder {
1481
+ constructor(
1482
+ private router: Router,
1483
+ private condition: MiddlewareCondition,
1484
+ ) {}
1485
+
1486
+ middleware(middlewareName: string): this {
1487
+ const [name, params] = middlewareName.split(':')
1488
+ const middlewareFactory = (this.router as any).namedMiddleware.get(name)
1489
+
1490
+ if (!middlewareFactory) {
1491
+ throw new Error(`Unknown middleware: ${name}`)
1492
+ }
1493
+
1494
+ const middlewareHandler = middlewareFactory(params);
1495
+ (this.router as any).conditionalMiddleware.push({
1496
+ condition: this.condition,
1497
+ middleware: [middlewareHandler],
1498
+ })
1499
+
1500
+ return this
1501
+ }
1502
+ }
1503
+
1504
+ /**
1505
+ * Builder for middleware with parameters
1506
+ */
1507
+ export class MiddlewareBuilder {
1508
+ constructor(
1509
+ private router: Router,
1510
+ private middleware: MiddlewareHandler[],
1511
+ ) {}
1512
+
1513
+ get(_path: string, _handler: RouteHandler): this {
1514
+ // This would integrate with the router's route registration
1515
+ return this
1516
+ }
1517
+
1518
+ post(_path: string, _handler: RouteHandler): this {
1519
+ return this
1520
+ }
1521
+
1522
+ put(_path: string, _handler: RouteHandler): this {
1523
+ return this
1524
+ }
1525
+
1526
+ delete(_path: string, _handler: RouteHandler): this {
1527
+ return this
1528
+ }
1529
+ }
1530
+
1531
+ /**
1532
+ * Builder for route groups with middleware
1533
+ */
1534
+ export class RouteGroupBuilder {
1535
+ constructor(
1536
+ private router: Router,
1537
+ private middleware: MiddlewareHandler[],
1538
+ ) {}
1539
+
1540
+ get(_path: string, _handler: RouteHandler): this {
1541
+ // This would integrate with the router's route registration
1542
+ return this
1543
+ }
1544
+
1545
+ post(_path: string, _handler: RouteHandler): this {
1546
+ return this
1547
+ }
1548
+
1549
+ put(_path: string, _handler: RouteHandler): this {
1550
+ return this
1551
+ }
1552
+
1553
+ delete(_path: string, _handler: RouteHandler): this {
1554
+ return this
1555
+ }
1556
+ }
1557
+
1558
+ // ---------------------------------------------------------------------------
1559
+ // Standalone enhancement helper
1560
+ // ---------------------------------------------------------------------------
1561
+
1562
+ // Singleton scratch Router used to expose `enhanceRequest` as a standalone
1563
+ // function. Downstream consumers (frameworks layered on bun-router) often
1564
+ // need to attach the request macros to a request that was created outside
1565
+ // of `route.serve()` — for instance, when a higher-level router wraps
1566
+ // each handler with its own middleware chain. Without an exported function,
1567
+ // those consumers either spin up their own `new Router()` per call or
1568
+ // duplicate the attachment logic in user code.
1569
+ //
1570
+ // We share one instance because `enhanceRequest` does not depend on any
1571
+ // per-router state (route table, middleware groups, etc.) — it only reads
1572
+ // from the request and the supplied params.
1573
+ let _enhancementHost: Router | null = null
1574
+
1575
+ function getEnhancementHost(): Router {
1576
+ if (_enhancementHost === null) _enhancementHost = new Router()
1577
+ return _enhancementHost
1578
+ }
1579
+
1580
+ /**
1581
+ * Attach bun-router's request macros (`bearerToken`, `getParam`, `cookie`,
1582
+ * `cookies`, `header`, `params`, plus the Laravel-style input helpers
1583
+ * `get`, `input`, `string`, `integer`, `float`, `boolean`, `array`, `has`,
1584
+ * `filled`, etc.) to a request.
1585
+ *
1586
+ * Idempotent: calling on an already-enhanced request returns it unchanged.
1587
+ * We sniff `req.bearerToken` as the marker — cheap and reliable since no
1588
+ * native Request has it.
1589
+ *
1590
+ * @example
1591
+ * import { applyRequestEnhancements } from '@stacksjs/bun-router'
1592
+ *
1593
+ * const enhanced = applyRequestEnhancements(req, { id: '42' })
1594
+ * enhanced.bearerToken() // → string | null
1595
+ * enhanced.getParam('id') // → '42'
1596
+ */
1597
+ export function applyRequestEnhancements(
1598
+ req: Request | EnhancedRequest,
1599
+ params: Record<string, string> = {},
1600
+ ): EnhancedRequest {
1601
+ if (typeof (req as any).bearerToken === 'function')
1602
+ return req as EnhancedRequest
1603
+ return getEnhancementHost().enhanceRequest(req as Request, params)
1604
+ }