@stacksjs/bun-router 0.0.4 → 0.0.6

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