@stacksjs/bun-router 0.0.13 → 0.0.14

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