@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,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 }