@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,697 +0,0 @@
1
- import type { ActionHandler, EnhancedRequest, QueryPreservationConfig } from '../types'
2
- import type { Router } from './router'
3
- import { join, relative, resolve, basename, dirname } from 'node:path'
4
- import { readdirSync, statSync, existsSync } from 'node:fs'
5
- import { injectQueryPreservationScript } from '../utils/query-preservation'
6
-
7
- /**
8
- * File-based routing configuration
9
- */
10
- export interface FileBasedRoutingConfig {
11
- /**
12
- * Whether automatic file-based routing is enabled
13
- * Default: true (auto-enabled when views directory is detected)
14
- */
15
- enabled?: boolean
16
-
17
- /**
18
- * Directory containing view files for automatic routing
19
- * Default: auto-detected from 'src/views', 'views', 'resources/views'
20
- */
21
- viewsPath?: string
22
-
23
- /**
24
- * File extensions to treat as routable pages
25
- * Default: ['.stx', '.html']
26
- */
27
- extensions?: string[]
28
-
29
- /**
30
- * Files/directories to exclude from routing
31
- * Default: ['_', 'components', 'layouts', 'partials', 'scripts', 'styles']
32
- */
33
- exclude?: string[]
34
-
35
- /**
36
- * Directory containing component files for STX rendering
37
- * Default: join(viewsPath, 'components')
38
- */
39
- componentsDir?: string
40
-
41
- /**
42
- * Directory containing layout files for STX rendering
43
- * Default: join(viewsPath, 'layouts')
44
- */
45
- layoutsDir?: string
46
-
47
- /**
48
- * Directory containing partial files for STX rendering
49
- * Default: join(viewsPath, 'partials')
50
- */
51
- partialsDir?: string
52
-
53
- /**
54
- * Custom render function for view files
55
- * If not provided, attempts to use STX renderer or serves raw content
56
- */
57
- render?: (filePath: string, data: Record<string, unknown>, request: EnhancedRequest) => Promise<Response>
58
- }
59
-
60
- interface DiscoveredRoute {
61
- filePath: string
62
- routePath: string
63
- params: string[]
64
- isIndex: boolean
65
- isDynamic: boolean
66
- }
67
-
68
- /**
69
- * Default directories to scan for views (in order of priority)
70
- */
71
- const DEFAULT_VIEW_DIRECTORIES: string[] = [
72
- 'src/views',
73
- 'views',
74
- 'resources/views',
75
- 'app/views',
76
- ]
77
-
78
- /**
79
- * Default exclusion patterns for non-routable files/directories
80
- */
81
- const DEFAULT_EXCLUDES: string[] = [
82
- '_', // Underscore-prefixed files are private
83
- 'components', // Component partials
84
- 'layouts', // Layout templates
85
- 'partials', // Partial templates
86
- 'scripts', // Script files
87
- 'styles', // Style files
88
- ]
89
-
90
- /**
91
- * Default routable file extensions
92
- */
93
- const DEFAULT_EXTENSIONS: string[] = ['.stx', '.html']
94
-
95
- /**
96
- * Auto-detect the views directory from common conventions
97
- */
98
- function detectViewsDirectory(cwd: string = process.cwd()): string | null {
99
- for (const dir of DEFAULT_VIEW_DIRECTORIES) {
100
- const fullPath = resolve(cwd, dir)
101
- if (existsSync(fullPath) && statSync(fullPath).isDirectory()) {
102
- return fullPath
103
- }
104
- }
105
- return null
106
- }
107
-
108
- /**
109
- * Check if a directory contains any routable files
110
- */
111
- function hasRoutableFiles(dir: string, extensions: string[], exclude: string[]): boolean {
112
- if (!existsSync(dir)) return false
113
-
114
- try {
115
- const entries = readdirSync(dir, { withFileTypes: true })
116
- for (const entry of entries) {
117
- if (exclude.some(pattern => entry.name.startsWith(pattern) || entry.name === pattern)) {
118
- continue
119
- }
120
-
121
- if (entry.isFile() && extensions.some(ext => entry.name.endsWith(ext))) {
122
- return true
123
- }
124
-
125
- if (entry.isDirectory()) {
126
- const subDir = join(dir, entry.name)
127
- if (hasRoutableFiles(subDir, extensions, exclude)) {
128
- return true
129
- }
130
- }
131
- }
132
- }
133
- catch {
134
- // Directory not readable
135
- }
136
-
137
- return false
138
- }
139
-
140
- /**
141
- * Converts a file path to a route path
142
- * Examples:
143
- * views/index.stx -> /
144
- * views/about.stx -> /about
145
- * views/dashboard/index.stx -> /dashboard
146
- * views/dashboard/errors.stx -> /dashboard/errors
147
- * views/users/[id].stx -> /users/{id}
148
- * views/posts/[...slug].stx -> /posts/{slug}*
149
- */
150
- function filePathToRoutePath(filePath: string, viewsDir: string, extensions: string[]): { routePath: string; params: string[] } {
151
- // Get relative path from views directory
152
- let routePath = relative(viewsDir, filePath)
153
-
154
- // Remove file extension
155
- for (const ext of extensions) {
156
- if (routePath.endsWith(ext)) {
157
- routePath = routePath.slice(0, -ext.length)
158
- break
159
- }
160
- }
161
-
162
- // Convert directory separators to forward slashes
163
- routePath = routePath.replace(/\\/g, '/')
164
-
165
- // Handle index files
166
- if (routePath === 'index' || routePath.endsWith('/index')) {
167
- routePath = routePath.replace(/\/?index$/, '')
168
- }
169
-
170
- // Extract dynamic parameters
171
- const params: string[] = []
172
-
173
- // Convert [...param] to {param}* (catch-all) syntax
174
- routePath = routePath.replace(/\[\.\.\.([^\]]+)\]/g, (_match, param) => {
175
- params.push(param)
176
- return `{${param}}*`
177
- })
178
-
179
- // Convert [param] to {param} syntax
180
- routePath = routePath.replace(/\[([^\]]+)\]/g, (_match, param) => {
181
- params.push(param)
182
- return `{${param}}`
183
- })
184
-
185
- // Ensure leading slash
186
- if (!routePath.startsWith('/')) {
187
- routePath = `/${routePath}`
188
- }
189
-
190
- // Handle root path
191
- if (routePath === '/') {
192
- return { routePath: '/', params }
193
- }
194
-
195
- return { routePath, params }
196
- }
197
-
198
- /**
199
- * Recursively discovers all routable files in a directory
200
- */
201
- function discoverRoutes(
202
- dir: string,
203
- viewsDir: string,
204
- extensions: string[],
205
- exclude: string[],
206
- ): DiscoveredRoute[] {
207
- const routes: DiscoveredRoute[] = []
208
-
209
- if (!existsSync(dir)) {
210
- return routes
211
- }
212
-
213
- const entries = readdirSync(dir, { withFileTypes: true })
214
-
215
- for (const entry of entries) {
216
- const fullPath = join(dir, entry.name)
217
-
218
- // Skip excluded files/directories
219
- if (exclude.some(pattern => entry.name.startsWith(pattern) || entry.name === pattern)) {
220
- continue
221
- }
222
-
223
- if (entry.isDirectory()) {
224
- // Recursively discover routes in subdirectories
225
- routes.push(...discoverRoutes(fullPath, viewsDir, extensions, exclude))
226
- }
227
- else if (entry.isFile()) {
228
- // Check if file has a routable extension
229
- const hasRoutableExtension = extensions.some(ext => entry.name.endsWith(ext))
230
-
231
- if (hasRoutableExtension) {
232
- const { routePath, params } = filePathToRoutePath(fullPath, viewsDir, extensions)
233
-
234
- routes.push({
235
- filePath: fullPath,
236
- routePath,
237
- params,
238
- isIndex: entry.name.startsWith('index.'),
239
- isDynamic: params.length > 0,
240
- })
241
- }
242
- }
243
- }
244
-
245
- // Sort routes: static routes before dynamic, more specific before less specific
246
- routes.sort((a, b) => {
247
- // Static routes come first
248
- if (a.isDynamic !== b.isDynamic) {
249
- return a.isDynamic ? 1 : -1
250
- }
251
-
252
- // More segments = more specific
253
- const aSegments = a.routePath.split('/').length
254
- const bSegments = b.routePath.split('/').length
255
- if (aSegments !== bSegments) {
256
- return bSegments - aSegments
257
- }
258
-
259
- // Alphabetical for consistency
260
- return a.routePath.localeCompare(b.routePath)
261
- })
262
-
263
- // Check for duplicate routes and warn
264
- const seen = new Map<string, DiscoveredRoute>()
265
- for (const route of routes) {
266
- const existing = seen.get(route.routePath)
267
- if (existing) {
268
- console.warn(
269
- `[bun-router] Warning: Duplicate route "${route.routePath}" found:\n` +
270
- ` - ${existing.filePath}\n` +
271
- ` - ${route.filePath}\n` +
272
- ` Using: ${existing.filePath}`
273
- )
274
- }
275
- else {
276
- seen.set(route.routePath, route)
277
- }
278
- }
279
-
280
- return Array.from(seen.values())
281
- }
282
-
283
- /**
284
- * Check for pre-built HTML version of a view file
285
- * Looks in dist/views/ for production builds
286
- */
287
- function findPrebuiltView(stxFilePath: string, viewsDir: string): string | null {
288
- // Get relative path from views directory
289
- const relativePath = relative(viewsDir, stxFilePath)
290
-
291
- // Check common pre-built locations
292
- const prebuiltLocations = [
293
- resolve(process.cwd(), 'dist/views', relativePath.replace(/\.stx$/, '.html')),
294
- resolve(process.cwd(), 'views', relativePath.replace(/\.stx$/, '.html')),
295
- resolve('/var/task/views', relativePath.replace(/\.stx$/, '.html')), // Lambda
296
- ]
297
-
298
- for (const location of prebuiltLocations) {
299
- if (existsSync(location)) {
300
- return location
301
- }
302
- }
303
-
304
- return null
305
- }
306
-
307
- /**
308
- * Render an STX file using the @stacksjs/stx library
309
- */
310
- async function renderStxFile(
311
- filePath: string,
312
- viewsDir: string,
313
- data: Record<string, unknown>,
314
- routingConfig?: FileBasedRoutingConfig,
315
- ): Promise<string> {
316
- // Dynamic import to avoid build-time resolution
317
- const stxModule = '@stacksjs/stx'
318
- const stx = await import(/* @vite-ignore */ stxModule)
319
-
320
- if (!stx.processDirectives || !stx.extractVariables) {
321
- throw new Error('STX library not properly loaded. Install with: bun add @stacksjs/stx')
322
- }
323
-
324
- const content = await Bun.file(filePath).text()
325
-
326
- // Extract script content and template
327
- const scriptMatch = content.match(/<script\s+server\s*>([\s\S]*?)<\/script>/i)
328
- const scriptContent = scriptMatch ? scriptMatch[1] : ''
329
- let templateContent = scriptMatch
330
- ? content.replace(/<script\s+server\s*>[\s\S]*?<\/script>/i, '')
331
- : content
332
-
333
- // Replace <script client> with regular <script>
334
- templateContent = templateContent.replace(/<script\s+client\s*>/gi, '<script>')
335
-
336
- // Build context with data
337
- const context: Record<string, unknown> = {
338
- __filename: filePath,
339
- __dirname: dirname(filePath),
340
- props: data,
341
- ...data,
342
- }
343
-
344
- // Extract variables from server script
345
- if (scriptContent) {
346
- await stx.extractVariables(scriptContent, context, filePath)
347
- }
348
-
349
- // Resolve componentsDir: user config > viewsDir/components fallback
350
- const resolvedComponentsDir = routingConfig?.componentsDir
351
- ? resolve(routingConfig.componentsDir)
352
- : join(viewsDir, 'components')
353
-
354
- const resolvedLayoutsDir = routingConfig?.layoutsDir
355
- ? resolve(routingConfig.layoutsDir)
356
- : join(viewsDir, 'layouts')
357
-
358
- const resolvedPartialsDir = routingConfig?.partialsDir
359
- ? resolve(routingConfig.partialsDir)
360
- : join(viewsDir, 'partials')
361
-
362
- // Configure STX with proper paths
363
- const config = {
364
- ...stx.defaultConfig,
365
- componentsDir: resolvedComponentsDir,
366
- layoutsDir: resolvedLayoutsDir,
367
- partialsDir: resolvedPartialsDir,
368
- }
369
-
370
- return stx.processDirectives(templateContent, context, filePath, config, new Set())
371
- }
372
-
373
- /**
374
- * Creates a handler for rendering a view file
375
- */
376
- function createViewHandler(
377
- filePath: string,
378
- viewsDir: string,
379
- config: FileBasedRoutingConfig,
380
- queryPreservationConfig?: QueryPreservationConfig,
381
- ): ActionHandler {
382
- return async (req: EnhancedRequest): Promise<Response> => {
383
- const url = new URL(req.url)
384
-
385
- // Build request data for template context
386
- const data = {
387
- params: req.params || {},
388
- query: Object.fromEntries(url.searchParams),
389
- url: req.url,
390
- path: url.pathname,
391
- method: req.method,
392
- headers: Object.fromEntries(req.headers.entries()),
393
- }
394
-
395
- /**
396
- * Apply query preservation script to HTML if configured
397
- */
398
- function applyQueryPreservation(html: string): string {
399
- if (queryPreservationConfig?.enabled !== false && queryPreservationConfig?.preserve?.length) {
400
- return injectQueryPreservationScript(html, queryPreservationConfig)
401
- }
402
- return html
403
- }
404
-
405
- try {
406
- // 1. Check for pre-built HTML (production optimization)
407
- const prebuiltPath = findPrebuiltView(filePath, viewsDir)
408
- if (prebuiltPath) {
409
- let html = await Bun.file(prebuiltPath).text()
410
- html = applyQueryPreservation(html)
411
- return new Response(html, {
412
- status: 200,
413
- headers: { 'Content-Type': 'text/html; charset=utf-8' },
414
- })
415
- }
416
-
417
- // 2. Render STX file
418
- if (filePath.endsWith('.stx')) {
419
- let html = await renderStxFile(filePath, viewsDir, data, config)
420
- html = applyQueryPreservation(html)
421
- return new Response(html, {
422
- status: 200,
423
- headers: { 'Content-Type': 'text/html; charset=utf-8' },
424
- })
425
- }
426
-
427
- // 3. Serve raw HTML
428
- let content = await Bun.file(filePath).text()
429
- content = applyQueryPreservation(content)
430
- return new Response(content, {
431
- status: 200,
432
- headers: { 'Content-Type': 'text/html; charset=utf-8' },
433
- })
434
- }
435
- catch (error) {
436
- console.error(`[bun-router] Error rendering ${filePath}:`, error)
437
- return new Response('Internal Server Error', { status: 500 })
438
- }
439
- }
440
- }
441
-
442
- /**
443
- * Registers file-based routing extension on the Router class
444
- */
445
- export function registerFileBasedRouting(RouterClass: typeof Router): void {
446
- Object.defineProperties(RouterClass.prototype, {
447
- /**
448
- * Internal: detected views directory
449
- */
450
- _viewsDir: {
451
- value: null as string | null,
452
- writable: true,
453
- configurable: true,
454
- },
455
-
456
- /**
457
- * Internal: file-based routing config
458
- */
459
- _fileRoutingConfig: {
460
- value: null as FileBasedRoutingConfig | null,
461
- writable: true,
462
- configurable: true,
463
- },
464
-
465
- /**
466
- * Internal: discovered file-based routes
467
- */
468
- _fileBasedRoutes: {
469
- value: [] as DiscoveredRoute[],
470
- writable: true,
471
- configurable: true,
472
- },
473
-
474
- /**
475
- * Internal: whether file routes have been initialized
476
- */
477
- _fileRoutesInitialized: {
478
- value: false,
479
- writable: true,
480
- configurable: true,
481
- },
482
-
483
- /**
484
- * Initialize automatic file-based routing
485
- * Called automatically when serve() is invoked
486
- */
487
- _initFileRoutes: {
488
- async value(): Promise<void> {
489
- if (this._fileRoutesInitialized) return
490
-
491
- // Ensure each Router instance gets its OWN routes array. The
492
- // `_fileBasedRoutes: { value: [] }` declaration on the prototype
493
- // means every instance shares a single array reference until
494
- // someone reassigns. Without this lazy-init, calls like
495
- // `instanceA.getFileRoutes()` leak routes registered by
496
- // `instanceB._initFileRoutes()` — cross-instance contamination.
497
- if (!Object.prototype.hasOwnProperty.call(this, '_fileBasedRoutes')) {
498
- this._fileBasedRoutes = []
499
- }
500
-
501
- // Auto-detect views directory if not configured
502
- const viewsDir = this._viewsDir || detectViewsDirectory()
503
- if (!viewsDir) {
504
- this._fileRoutesInitialized = true
505
- return
506
- }
507
-
508
- const config = this._fileRoutingConfig || {}
509
-
510
- // Honour the opt-out flag set by `router.disableFileRouting()` and by
511
- // anyone passing `{ enabled: false }` to `router.views(...)`. Before
512
- // this check the flag was stored but never read, so the public
513
- // `disableFileRouting()` API silently did nothing.
514
- if (config.enabled === false) {
515
- this._fileRoutesInitialized = true
516
- return
517
- }
518
-
519
- const extensions = config.extensions || DEFAULT_EXTENSIONS
520
- const exclude = config.exclude || DEFAULT_EXCLUDES
521
-
522
- // Check if there are any routable files
523
- if (!hasRoutableFiles(viewsDir, extensions, exclude)) {
524
- this._fileRoutesInitialized = true
525
- return
526
- }
527
-
528
- // Discover and register routes
529
- const routes = discoverRoutes(viewsDir, viewsDir, extensions, exclude)
530
-
531
- if (this.config.verbose) {
532
- console.log(`[bun-router] Auto-discovered ${routes.length} file-based routes from ${viewsDir}:`)
533
- for (const route of routes) {
534
- console.log(` ${route.routePath} -> ${relative(viewsDir, route.filePath)}`)
535
- }
536
- }
537
-
538
- // Store viewsDir for use in handlers
539
- this._viewsDir = viewsDir
540
-
541
- // Register each discovered route. The existing GET paths are
542
- // collected once — the previous per-route `.find()` scan made
543
- // startup O(routes²) for large route tables.
544
- const existingGetPaths = new Set<string>()
545
- for (const r of this.routes as Array<{ path: string, method: string }>) {
546
- if (r.method === 'GET') {
547
- existingGetPaths.add(r.path)
548
- }
549
- }
550
-
551
- for (const route of routes) {
552
- // Only register if no explicit route exists for this path
553
- if (existingGetPaths.has(route.routePath)) {
554
- continue
555
- }
556
-
557
- const handler = createViewHandler(route.filePath, viewsDir, config, this.config.queryPreservation)
558
- this.get(route.routePath, handler, 'web')
559
- this._fileBasedRoutes.push(route)
560
- existingGetPaths.add(route.routePath)
561
- }
562
-
563
- this._fileRoutesInitialized = true
564
- },
565
- writable: true,
566
- configurable: true,
567
- },
568
-
569
- /**
570
- * Configure file-based routing
571
- * Call this before serve() to customize behavior
572
- */
573
- views: {
574
- value(config: FileBasedRoutingConfig | string): Router {
575
- if (typeof config === 'string') {
576
- // Just a path
577
- this._viewsDir = resolve(config)
578
- this._fileRoutingConfig = {}
579
- }
580
- else {
581
- // Full config
582
- if (config.viewsPath) {
583
- this._viewsDir = resolve(config.viewsPath)
584
- }
585
- this._fileRoutingConfig = config
586
- }
587
- return this
588
- },
589
- writable: true,
590
- configurable: true,
591
- },
592
-
593
- /**
594
- * Explicitly disable file-based routing
595
- */
596
- disableFileRouting: {
597
- value(): Router {
598
- this._fileRoutingConfig = { enabled: false }
599
- return this
600
- },
601
- writable: true,
602
- configurable: true,
603
- },
604
-
605
- /**
606
- * Get list of discovered file-based routes
607
- */
608
- getFileRoutes: {
609
- value(): DiscoveredRoute[] {
610
- return this._fileBasedRoutes || []
611
- },
612
- writable: true,
613
- configurable: true,
614
- },
615
-
616
- /**
617
- * Legacy method for manual file routes loading
618
- * Kept for backwards compatibility
619
- */
620
- fileRoutes: {
621
- async value(viewsPath?: string, options?: Omit<FileBasedRoutingConfig, 'viewsPath'>): Promise<Router> {
622
- if (viewsPath) {
623
- this._viewsDir = resolve(viewsPath)
624
- }
625
- if (options) {
626
- this._fileRoutingConfig = { ...this._fileRoutingConfig, ...options }
627
- }
628
- // Force re-initialization
629
- this._fileRoutesInitialized = false
630
- await this._initFileRoutes()
631
- return this
632
- },
633
- writable: true,
634
- configurable: true,
635
- },
636
-
637
- /**
638
- * Legacy method alias
639
- */
640
- loadFileRoutes: {
641
- async value(config: FileBasedRoutingConfig = {}): Promise<Router> {
642
- return this.fileRoutes(config.viewsPath, config)
643
- },
644
- writable: true,
645
- configurable: true,
646
- },
647
- })
648
- }
649
-
650
- // Type augmentation for Router
651
- declare module './router' {
652
- interface Router {
653
- _viewsDir?: string | null
654
- _fileRoutingConfig?: FileBasedRoutingConfig | null
655
- _fileBasedRoutes?: DiscoveredRoute[]
656
- _fileRoutesInitialized?: boolean
657
-
658
- /**
659
- * Internal: Initialize file-based routing (called by serve())
660
- */
661
- _initFileRoutes(): Promise<void>
662
-
663
- /**
664
- * Configure file-based routing
665
- * @param config - Path to views directory or full configuration object
666
- * @example
667
- * router.views('src/views')
668
- * router.views({ viewsPath: 'src/views', extensions: ['.stx'] })
669
- */
670
- views(config: FileBasedRoutingConfig | string): Router
671
-
672
- /**
673
- * Disable automatic file-based routing
674
- */
675
- disableFileRouting(): Router
676
-
677
- /**
678
- * Get the list of discovered file-based routes
679
- */
680
- getFileRoutes(): DiscoveredRoute[]
681
-
682
- /**
683
- * Manually trigger file-based route discovery
684
- * @param viewsPath - Optional path to views directory
685
- * @param options - Additional configuration options
686
- */
687
- fileRoutes(viewsPath?: string, options?: Omit<FileBasedRoutingConfig, 'viewsPath'>): Promise<Router>
688
-
689
- /**
690
- * Legacy: Load file-based routes with configuration
691
- */
692
- loadFileRoutes(config?: FileBasedRoutingConfig): Promise<Router>
693
- }
694
- }
695
-
696
- export type { DiscoveredRoute }
697
- export { detectViewsDirectory, discoverRoutes, DEFAULT_VIEW_DIRECTORIES, DEFAULT_EXCLUDES, DEFAULT_EXTENSIONS }