@stacksjs/bun-router 0.0.17 → 0.0.18

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 (146) hide show
  1. package/dist/cli.js +1 -1
  2. package/dist/container/service-provider.d.ts +3 -3
  3. package/dist/development/performance-profiler.d.ts +3 -3
  4. package/dist/development/route-debugger.d.ts +1 -1
  5. package/dist/development/route-inspector.d.ts +2 -2
  6. package/dist/observability/integration.d.ts +1 -1
  7. package/dist/response/macros.d.ts +1 -1
  8. package/dist/router/index.d.ts +7 -7
  9. package/dist/router/router.d.ts +0 -32
  10. package/dist/testing/performance-testing.d.ts +2 -2
  11. package/dist/testing/test-client.d.ts +1 -1
  12. package/dist/testing/websocket-testing.d.ts +1 -1
  13. package/package.json +2 -3
  14. package/src/auth.ts +0 -489
  15. package/src/cache/lru-cache.ts +0 -457
  16. package/src/cache/middleware-memoization.ts +0 -531
  17. package/src/cache/route-cache-warmer.ts +0 -486
  18. package/src/cache/sqlite-cache.ts +0 -783
  19. package/src/cache/streaming-cache.ts +0 -572
  20. package/src/cli/colors.ts +0 -29
  21. package/src/cli/index.ts +0 -287
  22. package/src/cli/middleware.ts +0 -291
  23. package/src/cli/openapi.ts +0 -407
  24. package/src/cli/router.ts +0 -188
  25. package/src/cli/routes.ts +0 -265
  26. package/src/cli/utils.ts +0 -531
  27. package/src/cli.ts +0 -5
  28. package/src/config.ts +0 -322
  29. package/src/container/container.ts +0 -786
  30. package/src/container/contextual-binding.ts +0 -606
  31. package/src/container/decorators.ts +0 -368
  32. package/src/container/index.ts +0 -49
  33. package/src/container/service-provider.ts +0 -596
  34. package/src/development/hot-reload.ts +0 -673
  35. package/src/development/index.ts +0 -499
  36. package/src/development/performance-profiler.ts +0 -717
  37. package/src/development/route-debugger.ts +0 -527
  38. package/src/development/route-inspector.ts +0 -749
  39. package/src/development/typescript-utilities.ts +0 -682
  40. package/src/docs.ts +0 -397
  41. package/src/errors/circuit-breaker.ts +0 -733
  42. package/src/errors/error-handler.ts +0 -569
  43. package/src/errors/error-reporting.ts +0 -672
  44. package/src/errors/exceptions.ts +0 -536
  45. package/src/errors/graceful-degradation.ts +0 -623
  46. package/src/errors/index.ts +0 -21
  47. package/src/errors/router-errors.ts +0 -632
  48. package/src/file-serving/static-files.ts +0 -718
  49. package/src/index.ts +0 -66
  50. package/src/middleware/auth.ts +0 -220
  51. package/src/middleware/content_security_policy.ts +0 -215
  52. package/src/middleware/cors.ts +0 -142
  53. package/src/middleware/csrf.ts +0 -166
  54. package/src/middleware/ddos_protection.ts +0 -279
  55. package/src/middleware/file_security.ts +0 -191
  56. package/src/middleware/file_upload.ts +0 -275
  57. package/src/middleware/helmet.ts +0 -268
  58. package/src/middleware/index.ts +0 -117
  59. package/src/middleware/input_validation.ts +0 -453
  60. package/src/middleware/json_body.ts +0 -37
  61. package/src/middleware/performance_alerting.ts +0 -540
  62. package/src/middleware/performance_dashboard.ts +0 -661
  63. package/src/middleware/performance_monitor.ts +0 -945
  64. package/src/middleware/pipeline.ts +0 -489
  65. package/src/middleware/rate_limit.ts +0 -251
  66. package/src/middleware/request_id.ts +0 -36
  67. package/src/middleware/request_signing.ts +0 -638
  68. package/src/middleware/request_tracer.ts +0 -653
  69. package/src/middleware/response_cache.ts +0 -778
  70. package/src/middleware/security.ts +0 -482
  71. package/src/middleware/security_suite.ts +0 -257
  72. package/src/middleware/session.ts +0 -91
  73. package/src/model-binding/index.ts +0 -18
  74. package/src/model-binding/model-middleware.ts +0 -425
  75. package/src/model-binding/model-registry.ts +0 -550
  76. package/src/model-binding.ts +0 -370
  77. package/src/model-resolver-factory.ts +0 -106
  78. package/src/observability/correlation.ts +0 -691
  79. package/src/observability/health-checks.ts +0 -729
  80. package/src/observability/index.ts +0 -184
  81. package/src/observability/integration.ts +0 -548
  82. package/src/observability/metrics.ts +0 -753
  83. package/src/observability/tracing.ts +0 -638
  84. package/src/optimization/bun-utilities.ts +0 -778
  85. package/src/query-builder-integration.ts +0 -137
  86. package/src/request/context.ts +0 -109
  87. package/src/request/enhanced-request.ts +0 -876
  88. package/src/request/macros.ts +0 -822
  89. package/src/response/macros.ts +0 -665
  90. package/src/response/response-factory.ts +0 -596
  91. package/src/router/api-routes.ts +0 -243
  92. package/src/router/file-based-routing.ts +0 -697
  93. package/src/router/file-streaming.ts +0 -383
  94. package/src/router/fluent-routing.ts +0 -866
  95. package/src/router/group-organization.ts +0 -229
  96. package/src/router/handler-resolver.ts +0 -327
  97. package/src/router/http-methods.ts +0 -407
  98. package/src/router/index.ts +0 -193
  99. package/src/router/middleware-groups.ts +0 -222
  100. package/src/router/middleware-integration.ts +0 -399
  101. package/src/router/middleware.ts +0 -234
  102. package/src/router/model-binding.ts +0 -215
  103. package/src/router/optimized-route-matching.ts +0 -302
  104. package/src/router/route-building.ts +0 -221
  105. package/src/router/route-compiler.ts +0 -691
  106. package/src/router/route-matching.ts +0 -368
  107. package/src/router/route-trie.ts +0 -450
  108. package/src/router/router.ts +0 -1616
  109. package/src/router/server.ts +0 -684
  110. package/src/router/validation-integration.ts +0 -449
  111. package/src/router/view-rendering.ts +0 -233
  112. package/src/router/websocket.ts +0 -100
  113. package/src/routing/route-caching.ts +0 -402
  114. package/src/routing/route-throttling.ts +0 -469
  115. package/src/routing/subdomain-routing.ts +0 -492
  116. package/src/session/database-store.ts +0 -109
  117. package/src/session/file-store.ts +0 -148
  118. package/src/session/index.ts +0 -244
  119. package/src/session/memory-store.ts +0 -88
  120. package/src/session/redis-store.ts +0 -93
  121. package/src/streaming/index.ts +0 -17
  122. package/src/streaming/sse-handler.ts +0 -482
  123. package/src/streaming/stream-handler.ts +0 -552
  124. package/src/testing/auth-testing.ts +0 -448
  125. package/src/testing/file-upload-testing.ts +0 -543
  126. package/src/testing/index.ts +0 -10
  127. package/src/testing/middleware-testing.ts +0 -322
  128. package/src/testing/model-binding-testing.ts +0 -645
  129. package/src/testing/performance-testing.ts +0 -738
  130. package/src/testing/test-client.ts +0 -310
  131. package/src/testing/test-request.ts +0 -315
  132. package/src/testing/test-response.ts +0 -332
  133. package/src/testing/types.ts +0 -224
  134. package/src/testing/websocket-testing.ts +0 -590
  135. package/src/types/controller-types.ts +0 -385
  136. package/src/types/core.ts +0 -683
  137. package/src/types/middleware-types.ts +0 -422
  138. package/src/types/request-response-augmentation.ts +0 -489
  139. package/src/types/route-inference.ts +0 -372
  140. package/src/types.ts +0 -2123
  141. package/src/url.ts +0 -131
  142. package/src/utils/index.ts +0 -1
  143. package/src/utils/query-preservation.ts +0 -201
  144. package/src/utils.ts +0 -395
  145. package/src/validation/validator.ts +0 -688
  146. package/src/websocket/clustering.ts +0 -769
@@ -1,691 +0,0 @@
1
- import type { HTTPMethod, MatchResult, PatternMatchResult, Route } from '../types'
2
- import type { CompiledRoute } from './route-trie'
3
- import { RouteTrie } from './route-trie'
4
-
5
- /**
6
- * Route compilation options
7
- */
8
- export interface RouteCompilerOptions {
9
- enableCaching: boolean
10
- enableMethodGrouping: boolean
11
- enablePriorityOptimization: boolean
12
- cacheSize: number
13
- precompilePatterns: boolean
14
- /**
15
- * Record per-match timing via `performance.now()`. Off by default —
16
- * timing every request costs two clock reads per match on the hot path.
17
- * Match/hit/miss counters are always collected (they're just integers).
18
- */
19
- enableProfiling: boolean
20
- }
21
-
22
- /**
23
- * Route matching statistics for performance monitoring
24
- */
25
- export interface RouteMatchStats {
26
- totalMatches: number
27
- cacheHits: number
28
- cacheMisses: number
29
- averageMatchTime: number
30
- trieDepth: number
31
- methodDistribution: Record<string, number>
32
- }
33
-
34
- /**
35
- * High-performance route compiler with pre-compilation and optimization
36
- */
37
- export class RouteCompiler {
38
- private trie: RouteTrie
39
- // Bounded match cache with FIFO eviction (oldest key in Map iteration
40
- // order). Cache hits are pure reads — refreshing recency on every hit
41
- // (true LRU) costs a delete+set per request and buys almost nothing
42
- // for route tables, where the hot set is small and stable. The old
43
- // implementation kept a `cacheKeys` array whose indexOf/splice
44
- // bookkeeping cost O(cacheSize) on every request.
45
- private matchCache: Map<string, MatchResult | null> = new Map()
46
- // O(1) duplicate detection by `method:path` (the old approach scanned
47
- // every compiled route on each addRoute call — O(n²) registration).
48
- private routeKeys: Set<string> = new Set()
49
- private stats: RouteMatchStats = {
50
- totalMatches: 0,
51
- cacheHits: 0,
52
- cacheMisses: 0,
53
- averageMatchTime: 0,
54
- trieDepth: 0,
55
- methodDistribution: {},
56
- }
57
-
58
- private options: RouteCompilerOptions
59
-
60
- constructor(options: Partial<RouteCompilerOptions> = {}) {
61
- this.options = {
62
- enableCaching: true,
63
- enableMethodGrouping: true,
64
- enablePriorityOptimization: true,
65
- cacheSize: 1000,
66
- precompilePatterns: true,
67
- enableProfiling: false,
68
- ...options,
69
- }
70
-
71
- this.trie = new RouteTrie()
72
- }
73
-
74
- /**
75
- * Add a route to the compiler
76
- * @returns true if route was added, false if it's a duplicate
77
- */
78
- addRoute(route: Route): boolean {
79
- // Pre-compile the route pattern if enabled
80
- if (this.options.precompilePatterns) {
81
- this.precompileRoutePattern(route)
82
- }
83
-
84
- // Check for exact duplicates before adding — O(1) set lookup
85
- const routeKey = `${route.method}:${route.path}`
86
- if (this.routeKeys.has(routeKey)) {
87
- return false
88
- }
89
- this.routeKeys.add(routeKey)
90
-
91
- // Add to trie for fast matching
92
- this.trie.addRoute(route)
93
-
94
- // Clear cache when routes change
95
- if (this.options.enableCaching) {
96
- this.matchCache.clear()
97
- }
98
-
99
- // Update method distribution stats
100
- const method = route.method
101
- this.stats.methodDistribution[method] = (this.stats.methodDistribution[method] || 0) + 1
102
-
103
- return true
104
- }
105
-
106
- /**
107
- * Pre-compile route patterns for faster matching
108
- */
109
- // Pattern cache for memory optimization
110
- private patternCache: Map<string, { exec: (url: URL) => PatternMatchResult | null }> = new Map()
111
-
112
- /**
113
- * Extract parameter names from a route path
114
- */
115
- private extractParamNames(path: string): string[] {
116
- const paramNames: string[] = []
117
- const regex = /\{([^}:]+)(?::[^}]+)?\}/g
118
-
119
- // Using a safe approach to avoid lint error with assignment in while condition
120
- let tempMatch: RegExpExecArray | null
121
- // eslint-disable-next-line no-cond-assign
122
- while (tempMatch = regex.exec(path)) {
123
- paramNames.push(tempMatch[1].replace('?', ''))
124
- }
125
-
126
- return paramNames
127
- }
128
-
129
- /**
130
- * Pre-compile route patterns for faster matching
131
- */
132
- private precompileRoutePattern(route: Route): void {
133
- if (!route.pattern && route.path.includes('{')) {
134
- // Generate a cache key that includes constraints if present
135
- let cacheKey = route.path
136
- if (route.constraints && !Array.isArray(route.constraints)) {
137
- // Sort constraint keys for consistent cache key generation
138
- // Type assertion to handle the Record<string, string> case
139
- const constraintsRecord = route.constraints as Record<string, string>
140
- const constraintKeys = Object.keys(constraintsRecord).sort()
141
- const constraintString = constraintKeys
142
- .map(key => `${key}:${constraintsRecord[key]}`)
143
- .join('|')
144
- cacheKey = `${route.path}#${constraintString}`
145
- }
146
-
147
- // Check if we already have this pattern cached
148
- if (this.patternCache.has(cacheKey)) {
149
- route.pattern = this.patternCache.get(cacheKey)!
150
- return
151
- }
152
-
153
- // Convert Laravel-style parameters to URLPattern with constraints
154
- const pattern = this.convertToURLPattern(
155
- route.path,
156
- route.constraints && !Array.isArray(route.constraints) ? route.constraints : undefined,
157
- )
158
-
159
- // Create a URL pattern adapter that matches the expected interface
160
- const urlPatternAdapter = {
161
- exec: (url: URL): PatternMatchResult | null => {
162
- const pathname = url.pathname
163
- const result = pattern.exec(pathname)
164
- if (!result)
165
- return null
166
-
167
- return {
168
- pathname: {
169
- groups: result.groups || {},
170
- },
171
- }
172
- },
173
- }
174
-
175
- // Cache the compiled pattern
176
- this.patternCache.set(cacheKey, urlPatternAdapter)
177
-
178
- // Assign the pattern to the route
179
- route.pattern = urlPatternAdapter
180
- }
181
- }
182
-
183
- /**
184
- * Convert Laravel-style route to a simple regex pattern
185
- * @param path The route path with Laravel-style parameters
186
- * @param constraints Optional constraints for route parameters
187
- */
188
- private convertToURLPattern(
189
- path: string,
190
- constraints?: Record<string, string>,
191
- ): { exec: (pathname: string) => { groups?: Record<string, string> } | null } {
192
- // Pull params out as placeholder tokens so the static text can be
193
- // regex-escaped safely — without this, a literal dot in a path like
194
- // `/api/v1.0/users` would match any character.
195
- const paramTokens: string[] = []
196
- const withTokens = path.replace(/\{([^}]+)\}/g, (_m, inner: string) => {
197
- paramTokens.push(inner)
198
- return `\u0000${paramTokens.length - 1}\u0000`
199
- })
200
-
201
- let regexPattern = withTokens.replace(/[.+*?^${}()|[\]\\]/g, '\\$&')
202
-
203
- // Convert placeholder tokens to named capture groups. Optional params
204
- // (`{id?}`) make both the parameter and its leading slash optional.
205
- regexPattern = regexPattern.replace(/\/?\u0000(\d+)\u0000/g, (token, idx: string) => {
206
- const inner = paramTokens[Number(idx)]
207
- const isOptional = inner.endsWith('?')
208
- const core = isOptional ? inner.slice(0, -1) : inner
209
- const colonIndex = core.indexOf(':')
210
- const name = colonIndex === -1 ? core : core.slice(0, colonIndex)
211
- const inlinePattern = colonIndex === -1 ? undefined : core.slice(colonIndex + 1)
212
- const pattern = constraints?.[name] ?? inlinePattern ?? '[^/]+'
213
- const group = `(?<${name}>${pattern})`
214
- const leadingSlash = token.startsWith('/') ? '/' : ''
215
- return isOptional ? `(?:${leadingSlash}${group})?` : `${leadingSlash}${group}`
216
- })
217
-
218
- const regex = new RegExp(`^${regexPattern}$`)
219
-
220
- return {
221
- exec: (pathname: string) => {
222
- const match = pathname.match(regex)
223
- if (!match)
224
- return null
225
-
226
- return {
227
- groups: match.groups || {},
228
- }
229
- },
230
- }
231
- }
232
-
233
- /**
234
- * Match a request path and method to a route
235
- */
236
- match(path: string, method: HTTPMethod): MatchResult | null {
237
- const startTime = this.options.enableProfiling ? performance.now() : 0
238
- this.stats.totalMatches++
239
-
240
- // Check cache first if enabled
241
- if (this.options.enableCaching) {
242
- const cacheKey = `${method}:${path}`
243
- const cached = this.matchCache.get(cacheKey)
244
- if (cached !== undefined || this.matchCache.has(cacheKey)) {
245
- this.stats.cacheHits++
246
- if (startTime)
247
- this.updateMatchTime(startTime)
248
- // Hand each request its own params object — cached results are
249
- // shared, and handlers are allowed to mutate `req.params`.
250
- return cached ? { route: cached.route, params: { ...cached.params } } : null
251
- }
252
- this.stats.cacheMisses++
253
- }
254
-
255
- // Use trie for fast matching
256
- const trieMatch = this.trie.match(path, method)
257
- let result: MatchResult | null = null
258
-
259
- if (trieMatch) {
260
- result = {
261
- route: trieMatch.route,
262
- params: trieMatch.params,
263
- }
264
- }
265
-
266
- // Cache the result if enabled
267
- if (this.options.enableCaching) {
268
- const cacheKey = `${method}:${path}`
269
- this.addToCache(cacheKey, result)
270
- }
271
-
272
- if (startTime)
273
- this.updateMatchTime(startTime)
274
- return result ? { route: result.route, params: { ...result.params } } : null
275
- }
276
-
277
- /**
278
- * Add an entry to the cache with LRU eviction if needed
279
- */
280
- private addToCache(key: string, value: MatchResult | null): void {
281
- // If cache is full, evict the least recently used entry (oldest key
282
- // in Map iteration order)
283
- if (this.matchCache.size >= this.options.cacheSize && !this.matchCache.has(key)) {
284
- const lruKey = this.matchCache.keys().next().value
285
- if (lruKey !== undefined) {
286
- this.matchCache.delete(lruKey)
287
- }
288
- }
289
-
290
- this.matchCache.set(key, value)
291
- }
292
-
293
- /**
294
- * Update average match time statistics (only when profiling is enabled)
295
- */
296
- private updateMatchTime(startTime: number): void {
297
- const matchTime = performance.now() - startTime
298
- this.stats.averageMatchTime = (
299
- (this.stats.averageMatchTime * (this.stats.totalMatches - 1) + matchTime)
300
- / this.stats.totalMatches
301
- )
302
- }
303
-
304
- /**
305
- * Get all compiled routes sorted by priority
306
- */
307
- getCompiledRoutes(): CompiledRoute[] {
308
- const routes = this.trie.getAllRoutes()
309
-
310
- if (this.options.enablePriorityOptimization) {
311
- return routes.sort((a, b) => b.priority - a.priority)
312
- }
313
-
314
- return routes
315
- }
316
-
317
- /**
318
- * Get routes grouped by HTTP method
319
- */
320
- getRoutesByMethod(): Map<HTTPMethod, CompiledRoute[]> {
321
- const routesByMethod = new Map<HTTPMethod, CompiledRoute[]>()
322
-
323
- for (const compiled of this.trie.getAllRoutes()) {
324
- const method = compiled.route.method as HTTPMethod
325
- if (!routesByMethod.has(method)) {
326
- routesByMethod.set(method, [])
327
- }
328
- routesByMethod.get(method)!.push(compiled)
329
- }
330
-
331
- // Sort each method's routes by priority
332
- if (this.options.enablePriorityOptimization) {
333
- for (const [method, routes] of routesByMethod) {
334
- routes.sort((a, b) => b.priority - a.priority)
335
- routesByMethod.set(method, routes)
336
- }
337
- }
338
-
339
- return routesByMethod
340
- }
341
-
342
- /**
343
- * Warm up the cache with common routes
344
- * @param commonPaths Array of paths and methods to pre-cache
345
- * @param options Optional warming options
346
- * @param options.force If true, enables caching temporarily even if disabled in compiler options
347
- * @param options.precompile If true, performs a second match to ensure cache hits are recorded
348
- * @returns Statistics about the warming operation
349
- */
350
- warmCache(
351
- commonPaths: Array<{ path: string, method: HTTPMethod }>,
352
- options: { force?: boolean, precompile?: boolean } = {},
353
- ): { warmedPaths: number, cacheSize: number, hitRate: number } {
354
- if (!this.options.enableCaching && !options.force) {
355
- return { warmedPaths: 0, cacheSize: 0, hitRate: 0 }
356
- }
357
-
358
- // Enable caching temporarily if force is true
359
- const originalCachingState = this.options.enableCaching
360
- if (options.force && !this.options.enableCaching) {
361
- this.options.enableCaching = true
362
- }
363
-
364
- // Clear existing cache entries for these paths to ensure fresh warming
365
- if (options.precompile) {
366
- for (const { path, method } of commonPaths) {
367
- const cacheKey = `${method}:${path}`
368
- this.matchCache.delete(cacheKey)
369
- }
370
- }
371
-
372
- // Warm the cache by matching each path
373
- const initialCacheSize = this.matchCache.size
374
- const initialHits = this.stats.cacheHits
375
- const initialTotal = this.stats.totalMatches
376
-
377
- for (const { path, method } of commonPaths) {
378
- // First match will cache the result
379
- this.match(path, method)
380
-
381
- // Second match should be a cache hit
382
- if (options.precompile) {
383
- this.match(path, method)
384
- }
385
- }
386
-
387
- // Calculate statistics
388
- const newCacheEntries = this.matchCache.size - initialCacheSize
389
- const newHits = this.stats.cacheHits - initialHits
390
- const newTotal = this.stats.totalMatches - initialTotal
391
- const hitRate = newTotal > 0 ? newHits / newTotal : 0
392
-
393
- // Restore original caching state if needed
394
- if (options.force && !originalCachingState) {
395
- this.options.enableCaching = originalCachingState
396
- }
397
-
398
- return {
399
- warmedPaths: newCacheEntries,
400
- cacheSize: this.matchCache.size,
401
- hitRate,
402
- }
403
- }
404
-
405
- /**
406
- * Clear all caches and reset statistics
407
- */
408
- clear(): void {
409
- this.trie.clear()
410
- this.matchCache.clear()
411
- this.routeKeys.clear()
412
- this.stats = {
413
- totalMatches: 0,
414
- cacheHits: 0,
415
- cacheMisses: 0,
416
- averageMatchTime: 0,
417
- trieDepth: 0,
418
- methodDistribution: {},
419
- }
420
- }
421
-
422
- /**
423
- * Get performance statistics
424
- */
425
- getStats(): RouteMatchStats {
426
- const trieStats = this.trie.getStats()
427
- return {
428
- ...this.stats,
429
- trieDepth: trieStats.averageDepth,
430
- methodDistribution: trieStats.methodDistribution,
431
- }
432
- }
433
-
434
- /**
435
- * Get cache statistics
436
- */
437
- getCacheStats(): {
438
- size: number
439
- maxSize: number
440
- hitRate: number
441
- enabled: boolean
442
- } {
443
- const hitRate = this.stats.totalMatches > 0
444
- ? this.stats.cacheHits / this.stats.totalMatches
445
- : 0
446
-
447
- return {
448
- size: this.matchCache.size,
449
- maxSize: this.options.cacheSize,
450
- hitRate,
451
- enabled: this.options.enableCaching,
452
- }
453
- }
454
-
455
- /**
456
- * Optimize route order based on usage patterns
457
- */
458
- optimizeRouteOrder(_usageStats: Record<string, number>): void {
459
- if (!this.options.enablePriorityOptimization)
460
- return
461
-
462
- // This would reorder routes based on actual usage patterns
463
- // For now, we'll just clear the cache to force recompilation
464
- this.matchCache.clear()
465
- }
466
-
467
- /**
468
- * Get route conflicts (overlapping patterns)
469
- */
470
- getRouteConflicts(): Array<{
471
- routes: CompiledRoute[]
472
- conflictType: 'overlap' | 'duplicate' | 'ambiguous'
473
- severity: 'low' | 'medium' | 'high'
474
- }> {
475
- const conflicts: Array<{
476
- routes: CompiledRoute[]
477
- conflictType: 'overlap' | 'duplicate' | 'ambiguous'
478
- severity: 'low' | 'medium' | 'high'
479
- }> = []
480
-
481
- const routes = this.trie.getAllRoutes()
482
- const routesByMethod = new Map<string, CompiledRoute[]>()
483
-
484
- // Group routes by method
485
- for (const route of routes) {
486
- const method = route.route.method
487
- if (!routesByMethod.has(method)) {
488
- routesByMethod.set(method, [])
489
- }
490
- routesByMethod.get(method)!.push(route)
491
- }
492
-
493
- // Check for conflicts within each method
494
- for (const [_method, methodRoutes] of routesByMethod) {
495
- for (let i = 0; i < methodRoutes.length; i++) {
496
- for (let j = i + 1; j < methodRoutes.length; j++) {
497
- const route1 = methodRoutes[i]
498
- const route2 = methodRoutes[j]
499
-
500
- if (this.routesConflict(route1, route2)) {
501
- const severity = this.getConflictSeverity(route1, route2)
502
- const conflictType = this.getConflictType(route1, route2)
503
-
504
- conflicts.push({
505
- routes: [route1, route2],
506
- conflictType,
507
- severity,
508
- })
509
- }
510
- }
511
- }
512
- }
513
-
514
- return conflicts
515
- }
516
-
517
- /**
518
- * Check if two routes conflict
519
- */
520
- private routesConflict(route1: CompiledRoute, route2: CompiledRoute): boolean {
521
- // Exact path match
522
- if (route1.route.path === route2.route.path) {
523
- return true
524
- }
525
-
526
- // If routes have different methods, they don't conflict
527
- if (route1.route.method !== route2.route.method) {
528
- return false
529
- }
530
-
531
- // Check if patterns could match the same paths
532
- return this.patternsOverlap(route1.segments, route2.segments)
533
- }
534
-
535
- /**
536
- * Check if two route patterns overlap
537
- */
538
- private patternsOverlap(segments1: any[], segments2: any[]): boolean {
539
- // If segment lengths differ and neither has optional parameters or wildcards,
540
- // they can't overlap
541
- const hasOptionalOrWildcard1 = segments1.some(seg => seg.optional || seg.type === 'wildcard')
542
- const hasOptionalOrWildcard2 = segments2.some(seg => seg.optional || seg.type === 'wildcard')
543
-
544
- if (segments1.length !== segments2.length && !hasOptionalOrWildcard1 && !hasOptionalOrWildcard2) {
545
- return false
546
- }
547
-
548
- const maxLength = Math.max(segments1.length, segments2.length)
549
- let potentialOverlap = true
550
-
551
- for (let i = 0; i < maxLength; i++) {
552
- const seg1 = i < segments1.length ? segments1[i] : null
553
- const seg2 = i < segments2.length ? segments2[i] : null
554
-
555
- // If one segment is missing and the other isn't optional, they can't overlap
556
- if (!seg1 && seg2 && !seg2.optional) {
557
- return false
558
- }
559
- if (!seg2 && seg1 && !seg1.optional) {
560
- return false
561
- }
562
-
563
- // If both segments are missing, continue
564
- if (!seg1 || !seg2) {
565
- continue
566
- }
567
-
568
- // Static segments must match exactly
569
- if (seg1.type === 'static' && seg2.type === 'static') {
570
- if (seg1.value !== seg2.value) {
571
- return false
572
- }
573
- }
574
- // Check for parameter constraints
575
- else if (seg1.type === 'parameter' && seg2.type === 'parameter') {
576
- // If both have patterns/constraints, check if they're mutually exclusive
577
- if (seg1.pattern && seg2.pattern) {
578
- // Simple check: if patterns are different, assume they might overlap
579
- // A more sophisticated check would analyze the regex patterns
580
- if (seg1.pattern.toString() === seg2.pattern.toString()) {
581
- potentialOverlap = true
582
- }
583
- }
584
-
585
- // If parameter names are the same, they likely represent the same entity
586
- // This is a heuristic that helps identify conflicts
587
- if (seg1.paramName === seg2.paramName) {
588
- potentialOverlap = true
589
- }
590
- }
591
- // One static, one parameter
592
- else if (
593
- (seg1.type === 'static' && seg2.type === 'parameter')
594
- || (seg1.type === 'parameter' && seg2.type === 'static')
595
- ) {
596
- const staticSeg = seg1.type === 'static' ? seg1 : seg2
597
- const paramSeg = seg1.type === 'parameter' ? seg1 : seg2
598
-
599
- // If parameter has a pattern, check if static value matches the pattern
600
- if (paramSeg.pattern) {
601
- if (!paramSeg.pattern.test(staticSeg.value)) {
602
- return false
603
- }
604
- }
605
- }
606
- // Wildcard matches anything
607
- else if (seg1.type === 'wildcard' || seg2.type === 'wildcard') {
608
- return true
609
- }
610
- }
611
-
612
- return potentialOverlap
613
- }
614
-
615
- /**
616
- * Get conflict severity
617
- */
618
- private getConflictSeverity(route1: CompiledRoute, route2: CompiledRoute): 'low' | 'medium' | 'high' {
619
- if (route1.route.path === route2.route.path) {
620
- return 'high' // Exact duplicate
621
- }
622
-
623
- if (route1.staticScore === route2.staticScore) {
624
- return 'medium' // Same specificity
625
- }
626
-
627
- return 'low' // Different specificity, priority will resolve
628
- }
629
-
630
- /**
631
- * Get conflict type
632
- */
633
- private getConflictType(route1: CompiledRoute, route2: CompiledRoute): 'overlap' | 'duplicate' | 'ambiguous' {
634
- if (route1.route.path === route2.route.path) {
635
- return 'duplicate'
636
- }
637
-
638
- // Check for ambiguous parameter patterns
639
- const hasAmbiguousParams = this.hasAmbiguousParameters(route1, route2)
640
- if (hasAmbiguousParams) {
641
- return 'ambiguous'
642
- }
643
-
644
- // Check for similar priority
645
- if (Math.abs(route1.priority - route2.priority) < 10) {
646
- return 'ambiguous'
647
- }
648
-
649
- return 'overlap'
650
- }
651
-
652
- /**
653
- * Check if routes have ambiguous parameters
654
- */
655
- private hasAmbiguousParameters(route1: CompiledRoute, route2: CompiledRoute): boolean {
656
- // If routes have different segment counts, they're less likely to be ambiguous
657
- if (Math.abs(route1.segments.length - route2.segments.length) > 1) {
658
- return false
659
- }
660
-
661
- // Count parameter segments in each route
662
- const paramCount1 = route1.segments.filter(s => s.type === 'parameter').length
663
- const paramCount2 = route2.segments.filter(s => s.type === 'parameter').length
664
-
665
- // If both routes have parameters in the same positions, they might be ambiguous
666
- if (paramCount1 > 0 && paramCount2 > 0) {
667
- const minLength = Math.min(route1.segments.length, route2.segments.length)
668
- let samePositionParams = 0
669
-
670
- for (let i = 0; i < minLength; i++) {
671
- if (route1.segments[i].type === 'parameter' && route2.segments[i].type === 'parameter') {
672
- samePositionParams++
673
-
674
- // If parameters have different constraints, they might still be distinct
675
- const hasPattern1 = !!route1.segments[i].pattern
676
- const hasPattern2 = !!route2.segments[i].pattern
677
-
678
- if (hasPattern1 !== hasPattern2) {
679
- return true // One has constraint, one doesn't = ambiguous
680
- }
681
- }
682
- }
683
-
684
- // If more than half of parameters are in the same positions, likely ambiguous
685
- return samePositionParams > 0
686
- && samePositionParams >= Math.min(paramCount1, paramCount2) / 2
687
- }
688
-
689
- return false
690
- }
691
- }