@stacksjs/bun-router 0.0.17 → 0.0.19

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