@stacksjs/bun-router 0.0.14 → 0.0.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (273) hide show
  1. package/README.md +261 -584
  2. package/bin/cli.ts +9 -0
  3. package/dist/auth.d.ts +134 -0
  4. package/dist/cache/lru-cache.d.ts +139 -0
  5. package/dist/cache/middleware-memoization.d.ts +151 -0
  6. package/dist/cache/route-cache-warmer.d.ts +161 -0
  7. package/dist/cache/sqlite-cache.d.ts +209 -0
  8. package/dist/cache/streaming-cache.d.ts +132 -0
  9. package/dist/chunk-j0e7z7hd.js +18053 -0
  10. package/dist/cli/colors.d.ts +15 -0
  11. package/dist/cli/index.d.ts +10 -0
  12. package/dist/cli/middleware.d.ts +31 -0
  13. package/dist/cli/openapi.d.ts +17 -0
  14. package/dist/cli/router.d.ts +15 -0
  15. package/dist/cli/routes.d.ts +29 -0
  16. package/dist/cli/utils.d.ts +74 -0
  17. package/dist/cli.d.ts +1 -0
  18. package/dist/cli.js +3360 -0
  19. package/dist/config.d.ts +7 -0
  20. package/dist/container/container.d.ts +273 -0
  21. package/dist/container/contextual-binding.d.ts +240 -0
  22. package/dist/container/decorators.d.ts +141 -0
  23. package/dist/container/service-provider.d.ts +285 -0
  24. package/dist/development/hot-reload.d.ts +187 -0
  25. package/dist/development/index.d.ts +167 -0
  26. package/dist/development/performance-profiler.d.ts +217 -0
  27. package/dist/development/route-debugger.d.ts +154 -0
  28. package/dist/development/route-inspector.d.ts +211 -0
  29. package/dist/development/typescript-utilities.d.ts +209 -0
  30. package/dist/docs.d.ts +11 -0
  31. package/dist/errors/circuit-breaker.d.ts +195 -0
  32. package/dist/errors/error-handler.d.ts +88 -0
  33. package/dist/errors/error-reporting.d.ts +144 -0
  34. package/dist/errors/exceptions.d.ts +184 -0
  35. package/dist/errors/graceful-degradation.d.ts +154 -0
  36. package/dist/errors/index.d.ts +9 -0
  37. package/dist/errors/router-errors.d.ts +110 -0
  38. package/dist/file-serving/static-files.d.ts +143 -0
  39. package/dist/index.d.ts +14 -0
  40. package/dist/index.js +13968 -0
  41. package/dist/middleware/auth.d.ts +63 -0
  42. package/dist/middleware/content_security_policy.d.ts +53 -0
  43. package/dist/middleware/cors.d.ts +5 -0
  44. package/dist/middleware/csrf.d.ts +7 -0
  45. package/dist/middleware/ddos_protection.d.ts +39 -0
  46. package/dist/middleware/file_security.d.ts +26 -0
  47. package/dist/middleware/file_upload.d.ts +38 -0
  48. package/dist/middleware/helmet.d.ts +54 -0
  49. package/dist/middleware/index.d.ts +44 -0
  50. package/dist/middleware/input_validation.d.ts +45 -0
  51. package/dist/middleware/json_body.d.ts +4 -0
  52. package/dist/middleware/performance_alerting.d.ts +87 -0
  53. package/dist/middleware/performance_dashboard.d.ts +87 -0
  54. package/dist/middleware/performance_monitor.d.ts +209 -0
  55. package/dist/middleware/pipeline.d.ts +131 -0
  56. package/dist/middleware/rate_limit.d.ts +36 -0
  57. package/dist/middleware/request_id.d.ts +4 -0
  58. package/dist/middleware/request_signing.d.ts +153 -0
  59. package/dist/middleware/request_tracer.d.ts +72 -0
  60. package/dist/middleware/response_cache.d.ts +97 -0
  61. package/dist/middleware/security.d.ts +69 -0
  62. package/dist/middleware/security_suite.d.ts +46 -0
  63. package/dist/middleware/session.d.ts +23 -0
  64. package/dist/model-binding/index.d.ts +2 -0
  65. package/dist/model-binding/model-middleware.d.ts +118 -0
  66. package/dist/model-binding/model-registry.d.ts +164 -0
  67. package/dist/model-binding.d.ts +185 -0
  68. package/dist/model-resolver-factory.d.ts +31 -0
  69. package/dist/observability/correlation.d.ts +187 -0
  70. package/dist/observability/health-checks.d.ts +186 -0
  71. package/dist/observability/index.d.ts +60 -0
  72. package/dist/observability/integration.d.ts +147 -0
  73. package/dist/observability/metrics.d.ts +184 -0
  74. package/dist/observability/tracing.d.ts +187 -0
  75. package/dist/optimization/bun-utilities.d.ts +224 -0
  76. package/dist/query-builder-integration.d.ts +36 -0
  77. package/dist/request/context.d.ts +34 -0
  78. package/dist/request/enhanced-request.d.ts +213 -0
  79. package/dist/request/macros.d.ts +300 -0
  80. package/dist/response/macros.d.ts +256 -0
  81. package/dist/response/response-factory.d.ts +133 -0
  82. package/dist/router/api-routes.d.ts +5 -0
  83. package/dist/router/file-based-routing.d.ts +118 -0
  84. package/dist/router/file-streaming.d.ts +5 -0
  85. package/dist/router/fluent-router.d.ts +315 -0
  86. package/dist/router/fluent-routing.d.ts +271 -0
  87. package/dist/router/group-organization.d.ts +5 -0
  88. package/dist/router/handler-resolver.d.ts +24 -0
  89. package/dist/router/http-methods.d.ts +10 -0
  90. package/dist/router/index.d.ts +114 -0
  91. package/dist/router/middleware-groups.d.ts +94 -0
  92. package/dist/router/middleware-integration.d.ts +112 -0
  93. package/dist/router/middleware.d.ts +5 -0
  94. package/dist/router/model-binding.d.ts +5 -0
  95. package/dist/router/optimized-route-matching.d.ts +6 -0
  96. package/dist/router/route-building.d.ts +5 -0
  97. package/dist/router/route-compiler.d.ts +153 -0
  98. package/dist/router/route-matching.d.ts +5 -0
  99. package/dist/router/route-trie.d.ts +114 -0
  100. package/dist/router/router.d.ts +317 -0
  101. package/dist/router/server.d.ts +5 -0
  102. package/dist/router/validation-integration.d.ts +170 -0
  103. package/dist/router/view-rendering.d.ts +5 -0
  104. package/dist/router/websocket.d.ts +5 -0
  105. package/dist/routing/route-caching.d.ts +129 -0
  106. package/dist/routing/route-throttling.d.ts +150 -0
  107. package/dist/routing/subdomain-routing.d.ts +206 -0
  108. package/dist/session/database-store.d.ts +51 -0
  109. package/dist/session/file-store.d.ts +22 -0
  110. package/dist/session/index.d.ts +106 -0
  111. package/dist/session/memory-store.d.ts +21 -0
  112. package/dist/session/redis-store.d.ts +31 -0
  113. package/dist/streaming/index.d.ts +3 -0
  114. package/dist/streaming/sse-handler.d.ts +138 -0
  115. package/dist/streaming/stream-handler.d.ts +114 -0
  116. package/dist/testing/auth-testing.d.ts +156 -0
  117. package/dist/testing/file-upload-testing.d.ts +190 -0
  118. package/dist/testing/index.d.ts +10 -0
  119. package/dist/testing/middleware-testing.d.ts +138 -0
  120. package/dist/testing/model-binding-testing.d.ts +186 -0
  121. package/dist/testing/performance-testing.d.ts +235 -0
  122. package/dist/testing/test-client.d.ts +117 -0
  123. package/dist/testing/test-request.d.ts +85 -0
  124. package/dist/testing/test-response.d.ts +90 -0
  125. package/dist/testing/types.d.ts +207 -0
  126. package/dist/testing/websocket-testing.d.ts +227 -0
  127. package/dist/types/controller-types.d.ts +208 -0
  128. package/dist/types/core.d.ts +543 -0
  129. package/dist/types/middleware-types.d.ts +227 -0
  130. package/dist/types/request-response-augmentation.d.ts +261 -0
  131. package/dist/types/route-inference.d.ts +168 -0
  132. package/dist/types.d.ts +1794 -0
  133. package/dist/url.d.ts +56 -0
  134. package/dist/utils/index.d.ts +1 -0
  135. package/dist/utils/query-preservation.d.ts +48 -0
  136. package/dist/utils.d.ts +69 -0
  137. package/dist/validation/validator.d.ts +140 -0
  138. package/dist/websocket/clustering.d.ts +185 -0
  139. package/package.json +45 -44
  140. package/src/auth.ts +469 -0
  141. package/src/cache/lru-cache.ts +457 -0
  142. package/src/cache/middleware-memoization.ts +531 -0
  143. package/src/cache/route-cache-warmer.ts +486 -0
  144. package/src/cache/sqlite-cache.ts +783 -0
  145. package/src/cache/streaming-cache.ts +572 -0
  146. package/src/cli/colors.ts +29 -0
  147. package/src/cli/index.ts +287 -0
  148. package/src/cli/middleware.ts +291 -0
  149. package/src/cli/openapi.ts +407 -0
  150. package/src/cli/router.ts +188 -0
  151. package/src/cli/routes.ts +265 -0
  152. package/src/cli/utils.ts +531 -0
  153. package/src/cli.ts +5 -0
  154. package/src/config.ts +322 -0
  155. package/src/container/container.ts +740 -0
  156. package/src/container/contextual-binding.ts +603 -0
  157. package/src/container/decorators.ts +359 -0
  158. package/src/container/service-provider.ts +596 -0
  159. package/src/development/hot-reload.ts +673 -0
  160. package/src/development/index.ts +499 -0
  161. package/src/development/performance-profiler.ts +717 -0
  162. package/src/development/route-debugger.ts +527 -0
  163. package/src/development/route-inspector.ts +749 -0
  164. package/src/development/typescript-utilities.ts +682 -0
  165. package/src/docs.ts +397 -0
  166. package/src/errors/circuit-breaker.ts +732 -0
  167. package/src/errors/error-handler.ts +569 -0
  168. package/src/errors/error-reporting.ts +672 -0
  169. package/src/errors/exceptions.ts +536 -0
  170. package/src/errors/graceful-degradation.ts +621 -0
  171. package/src/errors/index.ts +21 -0
  172. package/src/errors/router-errors.ts +632 -0
  173. package/src/file-serving/static-files.ts +581 -0
  174. package/src/index.ts +14 -0
  175. package/src/middleware/auth.ts +220 -0
  176. package/src/middleware/content_security_policy.ts +215 -0
  177. package/src/middleware/cors.ts +74 -0
  178. package/src/middleware/csrf.ts +108 -0
  179. package/src/middleware/ddos_protection.ts +255 -0
  180. package/src/middleware/file_security.ts +191 -0
  181. package/src/middleware/file_upload.ts +275 -0
  182. package/src/middleware/helmet.ts +268 -0
  183. package/src/middleware/index.ts +117 -0
  184. package/src/middleware/input_validation.ts +449 -0
  185. package/src/middleware/json_body.ts +37 -0
  186. package/src/middleware/performance_alerting.ts +538 -0
  187. package/src/middleware/performance_dashboard.ts +661 -0
  188. package/src/middleware/performance_monitor.ts +943 -0
  189. package/src/middleware/pipeline.ts +489 -0
  190. package/src/middleware/rate_limit.ts +245 -0
  191. package/src/middleware/request_id.ts +36 -0
  192. package/src/middleware/request_signing.ts +636 -0
  193. package/src/middleware/request_tracer.ts +636 -0
  194. package/src/middleware/response_cache.ts +743 -0
  195. package/src/middleware/security.ts +482 -0
  196. package/src/middleware/security_suite.ts +257 -0
  197. package/src/middleware/session.ts +91 -0
  198. package/src/model-binding/index.ts +18 -0
  199. package/src/model-binding/model-middleware.ts +425 -0
  200. package/src/model-binding/model-registry.ts +550 -0
  201. package/src/model-binding.ts +370 -0
  202. package/src/model-resolver-factory.ts +106 -0
  203. package/src/observability/correlation.ts +691 -0
  204. package/src/observability/health-checks.ts +729 -0
  205. package/src/observability/index.ts +184 -0
  206. package/src/observability/integration.ts +548 -0
  207. package/src/observability/metrics.ts +753 -0
  208. package/src/observability/tracing.ts +638 -0
  209. package/src/optimization/bun-utilities.ts +778 -0
  210. package/src/query-builder-integration.ts +137 -0
  211. package/src/request/context.ts +62 -0
  212. package/src/request/enhanced-request.ts +857 -0
  213. package/src/request/macros.ts +688 -0
  214. package/src/response/macros.ts +665 -0
  215. package/src/response/response-factory.ts +596 -0
  216. package/src/router/api-routes.ts +243 -0
  217. package/src/router/file-based-routing.ts +690 -0
  218. package/src/router/file-streaming.ts +383 -0
  219. package/src/router/fluent-router.ts +927 -0
  220. package/src/router/fluent-routing.ts +797 -0
  221. package/src/router/group-organization.ts +213 -0
  222. package/src/router/handler-resolver.ts +291 -0
  223. package/src/router/http-methods.ts +377 -0
  224. package/src/router/index.ts +187 -0
  225. package/src/router/middleware-groups.ts +222 -0
  226. package/src/router/middleware-integration.ts +399 -0
  227. package/src/router/middleware.ts +231 -0
  228. package/src/router/model-binding.ts +215 -0
  229. package/src/router/optimized-route-matching.ts +253 -0
  230. package/src/router/route-building.ts +221 -0
  231. package/src/router/route-compiler.ts +702 -0
  232. package/src/router/route-matching.ts +349 -0
  233. package/src/router/route-trie.ts +464 -0
  234. package/src/router/router.ts +1604 -0
  235. package/src/router/server.ts +462 -0
  236. package/src/router/validation-integration.ts +443 -0
  237. package/src/router/view-rendering.ts +233 -0
  238. package/src/router/websocket.ts +100 -0
  239. package/src/routing/route-caching.ts +402 -0
  240. package/src/routing/route-throttling.ts +469 -0
  241. package/src/routing/subdomain-routing.ts +492 -0
  242. package/src/session/database-store.ts +109 -0
  243. package/src/session/file-store.ts +148 -0
  244. package/src/session/index.ts +244 -0
  245. package/src/session/memory-store.ts +88 -0
  246. package/src/session/redis-store.ts +93 -0
  247. package/src/streaming/index.ts +17 -0
  248. package/src/streaming/sse-handler.ts +482 -0
  249. package/src/streaming/stream-handler.ts +552 -0
  250. package/src/testing/auth-testing.ts +446 -0
  251. package/src/testing/file-upload-testing.ts +543 -0
  252. package/src/testing/index.ts +10 -0
  253. package/src/testing/middleware-testing.ts +322 -0
  254. package/src/testing/model-binding-testing.ts +645 -0
  255. package/src/testing/performance-testing.ts +738 -0
  256. package/src/testing/test-client.ts +310 -0
  257. package/src/testing/test-request.ts +315 -0
  258. package/src/testing/test-response.ts +332 -0
  259. package/src/testing/types.ts +224 -0
  260. package/src/testing/websocket-testing.ts +590 -0
  261. package/src/types/controller-types.ts +385 -0
  262. package/src/types/core.ts +683 -0
  263. package/src/types/middleware-types.ts +419 -0
  264. package/src/types/request-response-augmentation.ts +489 -0
  265. package/src/types/route-inference.ts +357 -0
  266. package/src/types.ts +2067 -0
  267. package/src/url.ts +126 -0
  268. package/src/utils/index.ts +1 -0
  269. package/src/utils/query-preservation.ts +201 -0
  270. package/src/utils.ts +327 -0
  271. package/src/validation/validator.ts +685 -0
  272. package/src/websocket/clustering.ts +762 -0
  273. package/CHANGELOG.md +0 -770
@@ -0,0 +1,690 @@
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
542
+ for (const route of routes) {
543
+ const handler = createViewHandler(route.filePath, viewsDir, config, this.config.queryPreservation)
544
+
545
+ // Only register if no explicit route exists for this path
546
+ const existingRoute = this.routes.find(
547
+ (r: { path: string; method: string }) => r.path === route.routePath && r.method === 'GET'
548
+ )
549
+
550
+ if (!existingRoute) {
551
+ this.get(route.routePath, handler, 'web')
552
+ this._fileBasedRoutes.push(route)
553
+ }
554
+ }
555
+
556
+ this._fileRoutesInitialized = true
557
+ },
558
+ writable: true,
559
+ configurable: true,
560
+ },
561
+
562
+ /**
563
+ * Configure file-based routing
564
+ * Call this before serve() to customize behavior
565
+ */
566
+ views: {
567
+ value(config: FileBasedRoutingConfig | string): Router {
568
+ if (typeof config === 'string') {
569
+ // Just a path
570
+ this._viewsDir = resolve(config)
571
+ this._fileRoutingConfig = {}
572
+ }
573
+ else {
574
+ // Full config
575
+ if (config.viewsPath) {
576
+ this._viewsDir = resolve(config.viewsPath)
577
+ }
578
+ this._fileRoutingConfig = config
579
+ }
580
+ return this
581
+ },
582
+ writable: true,
583
+ configurable: true,
584
+ },
585
+
586
+ /**
587
+ * Explicitly disable file-based routing
588
+ */
589
+ disableFileRouting: {
590
+ value(): Router {
591
+ this._fileRoutingConfig = { enabled: false }
592
+ return this
593
+ },
594
+ writable: true,
595
+ configurable: true,
596
+ },
597
+
598
+ /**
599
+ * Get list of discovered file-based routes
600
+ */
601
+ getFileRoutes: {
602
+ value(): DiscoveredRoute[] {
603
+ return this._fileBasedRoutes || []
604
+ },
605
+ writable: true,
606
+ configurable: true,
607
+ },
608
+
609
+ /**
610
+ * Legacy method for manual file routes loading
611
+ * Kept for backwards compatibility
612
+ */
613
+ fileRoutes: {
614
+ async value(viewsPath?: string, options?: Omit<FileBasedRoutingConfig, 'viewsPath'>): Promise<Router> {
615
+ if (viewsPath) {
616
+ this._viewsDir = resolve(viewsPath)
617
+ }
618
+ if (options) {
619
+ this._fileRoutingConfig = { ...this._fileRoutingConfig, ...options }
620
+ }
621
+ // Force re-initialization
622
+ this._fileRoutesInitialized = false
623
+ await this._initFileRoutes()
624
+ return this
625
+ },
626
+ writable: true,
627
+ configurable: true,
628
+ },
629
+
630
+ /**
631
+ * Legacy method alias
632
+ */
633
+ loadFileRoutes: {
634
+ async value(config: FileBasedRoutingConfig = {}): Promise<Router> {
635
+ return this.fileRoutes(config.viewsPath, config)
636
+ },
637
+ writable: true,
638
+ configurable: true,
639
+ },
640
+ })
641
+ }
642
+
643
+ // Type augmentation for Router
644
+ declare module './router' {
645
+ interface Router {
646
+ _viewsDir?: string | null
647
+ _fileRoutingConfig?: FileBasedRoutingConfig | null
648
+ _fileBasedRoutes?: DiscoveredRoute[]
649
+ _fileRoutesInitialized?: boolean
650
+
651
+ /**
652
+ * Internal: Initialize file-based routing (called by serve())
653
+ */
654
+ _initFileRoutes(): Promise<void>
655
+
656
+ /**
657
+ * Configure file-based routing
658
+ * @param config - Path to views directory or full configuration object
659
+ * @example
660
+ * router.views('src/views')
661
+ * router.views({ viewsPath: 'src/views', extensions: ['.stx'] })
662
+ */
663
+ views(config: FileBasedRoutingConfig | string): Router
664
+
665
+ /**
666
+ * Disable automatic file-based routing
667
+ */
668
+ disableFileRouting(): Router
669
+
670
+ /**
671
+ * Get the list of discovered file-based routes
672
+ */
673
+ getFileRoutes(): DiscoveredRoute[]
674
+
675
+ /**
676
+ * Manually trigger file-based route discovery
677
+ * @param viewsPath - Optional path to views directory
678
+ * @param options - Additional configuration options
679
+ */
680
+ fileRoutes(viewsPath?: string, options?: Omit<FileBasedRoutingConfig, 'viewsPath'>): Promise<Router>
681
+
682
+ /**
683
+ * Legacy: Load file-based routes with configuration
684
+ */
685
+ loadFileRoutes(config?: FileBasedRoutingConfig): Promise<Router>
686
+ }
687
+ }
688
+
689
+ export type { DiscoveredRoute }
690
+ export { detectViewsDirectory, discoverRoutes, DEFAULT_VIEW_DIRECTORIES, DEFAULT_EXCLUDES, DEFAULT_EXTENSIONS }