davaux 0.8.0

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 (333) hide show
  1. package/BASELINE.md +169 -0
  2. package/CLAUDE.md +518 -0
  3. package/LICENSE +21 -0
  4. package/README.md +36 -0
  5. package/ROADMAP.md +198 -0
  6. package/build.mjs +101 -0
  7. package/client/control.ts +247 -0
  8. package/client/hydrate.ts +37 -0
  9. package/client/index.ts +19 -0
  10. package/client/jsx-runtime.ts +209 -0
  11. package/client/resource.ts +122 -0
  12. package/client/signal.ts +211 -0
  13. package/client/store.ts +110 -0
  14. package/client/useHead.ts +63 -0
  15. package/dist/build/config.d.ts +3 -0
  16. package/dist/build/config.d.ts.map +1 -0
  17. package/dist/build/config.js +38 -0
  18. package/dist/build/config.js.map +7 -0
  19. package/dist/build/index.d.ts +2 -0
  20. package/dist/build/index.d.ts.map +1 -0
  21. package/dist/build/index.js +13 -0
  22. package/dist/build/index.js.map +7 -0
  23. package/dist/build/plugins.d.ts +7 -0
  24. package/dist/build/plugins.d.ts.map +1 -0
  25. package/dist/build/plugins.js +85 -0
  26. package/dist/build/plugins.js.map +7 -0
  27. package/dist/cli.d.ts +2 -0
  28. package/dist/cli.d.ts.map +1 -0
  29. package/dist/cli.js +427 -0
  30. package/dist/cli.js.map +7 -0
  31. package/dist/client/control.d.ts +49 -0
  32. package/dist/client/control.d.ts.map +1 -0
  33. package/dist/client/control.js +154 -0
  34. package/dist/client/control.js.map +7 -0
  35. package/dist/client/hydrate.d.ts +7 -0
  36. package/dist/client/hydrate.d.ts.map +1 -0
  37. package/dist/client/hydrate.js +23 -0
  38. package/dist/client/hydrate.js.map +7 -0
  39. package/dist/client/index.d.ts +12 -0
  40. package/dist/client/index.d.ts.map +1 -0
  41. package/dist/client/index.js +32 -0
  42. package/dist/client/index.js.map +7 -0
  43. package/dist/client/jsx-runtime.d.ts +40 -0
  44. package/dist/client/jsx-runtime.d.ts.map +1 -0
  45. package/dist/client/jsx-runtime.js +139 -0
  46. package/dist/client/jsx-runtime.js.map +7 -0
  47. package/dist/client/resource.d.ts +31 -0
  48. package/dist/client/resource.d.ts.map +1 -0
  49. package/dist/client/resource.js +64 -0
  50. package/dist/client/resource.js.map +7 -0
  51. package/dist/client/signal.d.ts +90 -0
  52. package/dist/client/signal.d.ts.map +1 -0
  53. package/dist/client/signal.js +115 -0
  54. package/dist/client/signal.js.map +7 -0
  55. package/dist/client/store.d.ts +26 -0
  56. package/dist/client/store.d.ts.map +1 -0
  57. package/dist/client/store.js +63 -0
  58. package/dist/client/store.js.map +7 -0
  59. package/dist/client/useHead.d.ts +28 -0
  60. package/dist/client/useHead.d.ts.map +1 -0
  61. package/dist/client/useHead.js +33 -0
  62. package/dist/client/useHead.js.map +7 -0
  63. package/dist/config.d.ts +182 -0
  64. package/dist/config.d.ts.map +1 -0
  65. package/dist/config.js +21 -0
  66. package/dist/config.js.map +7 -0
  67. package/dist/create-multisite.d.ts +2 -0
  68. package/dist/create-multisite.d.ts.map +1 -0
  69. package/dist/create-multisite.js +291 -0
  70. package/dist/create-multisite.js.map +7 -0
  71. package/dist/create.d.ts +2 -0
  72. package/dist/create.d.ts.map +1 -0
  73. package/dist/create.js +179 -0
  74. package/dist/create.js.map +7 -0
  75. package/dist/dev/blueprints.d.ts +11 -0
  76. package/dist/dev/blueprints.d.ts.map +1 -0
  77. package/dist/dev/blueprints.js +65 -0
  78. package/dist/dev/blueprints.js.map +7 -0
  79. package/dist/dev/components.d.ts +19 -0
  80. package/dist/dev/components.d.ts.map +1 -0
  81. package/dist/dev/components.js +87 -0
  82. package/dist/dev/components.js.map +7 -0
  83. package/dist/dev/insert.d.ts +11 -0
  84. package/dist/dev/insert.d.ts.map +1 -0
  85. package/dist/dev/insert.js +160 -0
  86. package/dist/dev/insert.js.map +7 -0
  87. package/dist/dev/remove.d.ts +53 -0
  88. package/dist/dev/remove.d.ts.map +1 -0
  89. package/dist/dev/remove.js +518 -0
  90. package/dist/dev/remove.js.map +7 -0
  91. package/dist/dev/watch.d.ts +26 -0
  92. package/dist/dev/watch.d.ts.map +1 -0
  93. package/dist/dev/watch.js +2905 -0
  94. package/dist/dev/watch.js.map +7 -0
  95. package/dist/errors.d.ts +6 -0
  96. package/dist/errors.d.ts.map +1 -0
  97. package/dist/errors.js +63 -0
  98. package/dist/errors.js.map +7 -0
  99. package/dist/generate.d.ts +2 -0
  100. package/dist/generate.d.ts.map +1 -0
  101. package/dist/generate.js +191 -0
  102. package/dist/generate.js.map +7 -0
  103. package/dist/index.d.ts +9 -0
  104. package/dist/index.d.ts.map +1 -0
  105. package/dist/index.js +57 -0
  106. package/dist/index.js.map +7 -0
  107. package/dist/island.d.ts +24 -0
  108. package/dist/island.d.ts.map +1 -0
  109. package/dist/island.js +15 -0
  110. package/dist/island.js.map +7 -0
  111. package/dist/jsx-runtime.d.ts +406 -0
  112. package/dist/jsx-runtime.d.ts.map +1 -0
  113. package/dist/jsx-runtime.js +90 -0
  114. package/dist/jsx-runtime.js.map +7 -0
  115. package/dist/link.d.ts +27 -0
  116. package/dist/link.d.ts.map +1 -0
  117. package/dist/link.js +29 -0
  118. package/dist/link.js.map +7 -0
  119. package/dist/oml/fragment.d.ts +16 -0
  120. package/dist/oml/fragment.d.ts.map +1 -0
  121. package/dist/oml/fragment.js +26 -0
  122. package/dist/oml/fragment.js.map +7 -0
  123. package/dist/oml/index.d.ts +11 -0
  124. package/dist/oml/index.d.ts.map +1 -0
  125. package/dist/oml/index.js +21 -0
  126. package/dist/oml/index.js.map +7 -0
  127. package/dist/oml/jsx-runtime.d.ts +34 -0
  128. package/dist/oml/jsx-runtime.d.ts.map +1 -0
  129. package/dist/oml/jsx-runtime.js +59 -0
  130. package/dist/oml/jsx-runtime.js.map +7 -0
  131. package/dist/oml/jsx.d.ts +14 -0
  132. package/dist/oml/jsx.d.ts.map +1 -0
  133. package/dist/oml/jsx.js +96 -0
  134. package/dist/oml/jsx.js.map +7 -0
  135. package/dist/oml/page.d.ts +7 -0
  136. package/dist/oml/page.d.ts.map +1 -0
  137. package/dist/oml/page.js +6 -0
  138. package/dist/oml/page.js.map +7 -0
  139. package/dist/oml/render.d.ts +13 -0
  140. package/dist/oml/render.d.ts.map +1 -0
  141. package/dist/oml/render.js +117 -0
  142. package/dist/oml/render.js.map +7 -0
  143. package/dist/oml/types.d.ts +79 -0
  144. package/dist/oml/types.d.ts.map +1 -0
  145. package/dist/oml/types.js +64 -0
  146. package/dist/oml/types.js.map +7 -0
  147. package/dist/router/handler.d.ts +53 -0
  148. package/dist/router/handler.d.ts.map +1 -0
  149. package/dist/router/handler.js +342 -0
  150. package/dist/router/handler.js.map +7 -0
  151. package/dist/router/matcher.d.ts +21 -0
  152. package/dist/router/matcher.d.ts.map +1 -0
  153. package/dist/router/matcher.js +28 -0
  154. package/dist/router/matcher.js.map +7 -0
  155. package/dist/router/scanner.d.ts +17 -0
  156. package/dist/router/scanner.d.ts.map +1 -0
  157. package/dist/router/scanner.js +197 -0
  158. package/dist/router/scanner.js.map +7 -0
  159. package/dist/server/index.d.ts +23 -0
  160. package/dist/server/index.d.ts.map +1 -0
  161. package/dist/server/index.js +29 -0
  162. package/dist/server/index.js.map +7 -0
  163. package/dist/signal.d.ts +15 -0
  164. package/dist/signal.d.ts.map +1 -0
  165. package/dist/signal.js +29 -0
  166. package/dist/signal.js.map +7 -0
  167. package/dist/ssg.d.ts +45 -0
  168. package/dist/ssg.d.ts.map +1 -0
  169. package/dist/ssg.js +175 -0
  170. package/dist/ssg.js.map +7 -0
  171. package/dist/test/actions.test.d.ts +2 -0
  172. package/dist/test/actions.test.d.ts.map +1 -0
  173. package/dist/test/body-limits.test.d.ts +2 -0
  174. package/dist/test/body-limits.test.d.ts.map +1 -0
  175. package/dist/test/errors.test.d.ts +2 -0
  176. package/dist/test/errors.test.d.ts.map +1 -0
  177. package/dist/test/fixtures/routes/[id].page.d.ts +4 -0
  178. package/dist/test/fixtures/routes/[id].page.d.ts.map +1 -0
  179. package/dist/test/fixtures/routes/_error.d.ts +3 -0
  180. package/dist/test/fixtures/routes/_error.d.ts.map +1 -0
  181. package/dist/test/fixtures/routes/_global.d.ts +3 -0
  182. package/dist/test/fixtures/routes/_global.d.ts.map +1 -0
  183. package/dist/test/fixtures/routes/_layout-template.d.ts +3 -0
  184. package/dist/test/fixtures/routes/_layout-template.d.ts.map +1 -0
  185. package/dist/test/fixtures/routes/_layout.d.ts +3 -0
  186. package/dist/test/fixtures/routes/_layout.d.ts.map +1 -0
  187. package/dist/test/fixtures/routes/_layout_scripts.d.ts +3 -0
  188. package/dist/test/fixtures/routes/_layout_scripts.d.ts.map +1 -0
  189. package/dist/test/fixtures/routes/_middleware.d.ts +3 -0
  190. package/dist/test/fixtures/routes/_middleware.d.ts.map +1 -0
  191. package/dist/test/fixtures/routes/_redirect301_mw.d.ts +3 -0
  192. package/dist/test/fixtures/routes/_redirect301_mw.d.ts.map +1 -0
  193. package/dist/test/fixtures/routes/_redirect_mw.d.ts +3 -0
  194. package/dist/test/fixtures/routes/_redirect_mw.d.ts.map +1 -0
  195. package/dist/test/fixtures/routes/about.page.d.ts +3 -0
  196. package/dist/test/fixtures/routes/about.page.d.ts.map +1 -0
  197. package/dist/test/fixtures/routes/action.page.d.ts +6 -0
  198. package/dist/test/fixtures/routes/action.page.d.ts.map +1 -0
  199. package/dist/test/fixtures/routes/api/form-all.post.d.ts +3 -0
  200. package/dist/test/fixtures/routes/api/form-all.post.d.ts.map +1 -0
  201. package/dist/test/fixtures/routes/api/form-limited.post.d.ts +6 -0
  202. package/dist/test/fixtures/routes/api/form-limited.post.d.ts.map +1 -0
  203. package/dist/test/fixtures/routes/api/response-obj.get.d.ts +3 -0
  204. package/dist/test/fixtures/routes/api/response-obj.get.d.ts.map +1 -0
  205. package/dist/test/fixtures/routes/api/upload.post.d.ts +12 -0
  206. package/dist/test/fixtures/routes/api/upload.post.d.ts.map +1 -0
  207. package/dist/test/fixtures/routes/api/users.get.d.ts +6 -0
  208. package/dist/test/fixtures/routes/api/users.get.d.ts.map +1 -0
  209. package/dist/test/fixtures/routes/api/xml.get.d.ts +3 -0
  210. package/dist/test/fixtures/routes/api/xml.get.d.ts.map +1 -0
  211. package/dist/test/fixtures/routes/auth/_middleware.d.ts +3 -0
  212. package/dist/test/fixtures/routes/auth/_middleware.d.ts.map +1 -0
  213. package/dist/test/fixtures/routes/auth/protected.page.d.ts +3 -0
  214. package/dist/test/fixtures/routes/auth/protected.page.d.ts.map +1 -0
  215. package/dist/test/fixtures/routes/index.page.d.ts +3 -0
  216. package/dist/test/fixtures/routes/index.page.d.ts.map +1 -0
  217. package/dist/test/fixtures/routes/oml.page.d.ts +3 -0
  218. package/dist/test/fixtures/routes/oml.page.d.ts.map +1 -0
  219. package/dist/test/fixtures/routes/redirect.page.d.ts +3 -0
  220. package/dist/test/fixtures/routes/redirect.page.d.ts.map +1 -0
  221. package/dist/test/fixtures/routes/ssg/[slug].page.d.ts +5 -0
  222. package/dist/test/fixtures/routes/ssg/[slug].page.d.ts.map +1 -0
  223. package/dist/test/fixtures/routes/ssg/server.page.d.ts +4 -0
  224. package/dist/test/fixtures/routes/ssg/server.page.d.ts.map +1 -0
  225. package/dist/test/fixtures/routes/state.page.d.ts +3 -0
  226. package/dist/test/fixtures/routes/state.page.d.ts.map +1 -0
  227. package/dist/test/fixtures/routes/throw.page.d.ts +3 -0
  228. package/dist/test/fixtures/routes/throw.page.d.ts.map +1 -0
  229. package/dist/test/fixtures/routes/wiki/[...slug].page.d.ts +3 -0
  230. package/dist/test/fixtures/routes/wiki/[...slug].page.d.ts.map +1 -0
  231. package/dist/test/helpers.d.ts +37 -0
  232. package/dist/test/helpers.d.ts.map +1 -0
  233. package/dist/test/layouts.test.d.ts +2 -0
  234. package/dist/test/layouts.test.d.ts.map +1 -0
  235. package/dist/test/middleware.test.d.ts +2 -0
  236. package/dist/test/middleware.test.d.ts.map +1 -0
  237. package/dist/test/multipart.test.d.ts +2 -0
  238. package/dist/test/multipart.test.d.ts.map +1 -0
  239. package/dist/test/oml-routing.test.d.ts +2 -0
  240. package/dist/test/oml-routing.test.d.ts.map +1 -0
  241. package/dist/test/oml.test.d.ts +2 -0
  242. package/dist/test/oml.test.d.ts.map +1 -0
  243. package/dist/test/redirects.test.d.ts +2 -0
  244. package/dist/test/redirects.test.d.ts.map +1 -0
  245. package/dist/test/routing.test.d.ts +2 -0
  246. package/dist/test/routing.test.d.ts.map +1 -0
  247. package/dist/test/ssg.test.d.ts +2 -0
  248. package/dist/test/ssg.test.d.ts.map +1 -0
  249. package/dist/test/web-response.test.d.ts +2 -0
  250. package/dist/test/web-response.test.d.ts.map +1 -0
  251. package/dist/types.d.ts +314 -0
  252. package/dist/types.d.ts.map +1 -0
  253. package/dist/types.js +292 -0
  254. package/dist/types.js.map +7 -0
  255. package/package.json +103 -0
  256. package/pka.config.json +32 -0
  257. package/src/build/config.ts +42 -0
  258. package/src/build/index.ts +6 -0
  259. package/src/build/plugins.ts +118 -0
  260. package/src/cli.ts +502 -0
  261. package/src/config.ts +197 -0
  262. package/src/create-multisite.ts +310 -0
  263. package/src/create.ts +194 -0
  264. package/src/dev/blueprints.ts +75 -0
  265. package/src/dev/components.ts +108 -0
  266. package/src/dev/insert.ts +221 -0
  267. package/src/dev/remove.ts +677 -0
  268. package/src/dev/watch.ts +3098 -0
  269. package/src/env.d.ts +5 -0
  270. package/src/errors.ts +64 -0
  271. package/src/generate.ts +228 -0
  272. package/src/index.ts +67 -0
  273. package/src/island.ts +47 -0
  274. package/src/jsx-runtime.d.ts +408 -0
  275. package/src/jsx-runtime.d.ts.map +1 -0
  276. package/src/jsx-runtime.ts +536 -0
  277. package/src/link.ts +49 -0
  278. package/src/oml/fragment.ts +54 -0
  279. package/src/oml/index.ts +21 -0
  280. package/src/oml/jsx-runtime.ts +121 -0
  281. package/src/oml/jsx.ts +151 -0
  282. package/src/oml/page.ts +13 -0
  283. package/src/oml/render.ts +181 -0
  284. package/src/oml/types.ts +159 -0
  285. package/src/router/handler.ts +515 -0
  286. package/src/router/matcher.ts +52 -0
  287. package/src/router/scanner.ts +272 -0
  288. package/src/server/index.ts +49 -0
  289. package/src/signal.ts +39 -0
  290. package/src/ssg.ts +253 -0
  291. package/src/test/actions.test.ts +40 -0
  292. package/src/test/body-limits.test.ts +83 -0
  293. package/src/test/errors.test.ts +53 -0
  294. package/src/test/fixtures/routes/[id].page.ts +3 -0
  295. package/src/test/fixtures/routes/_error.ts +6 -0
  296. package/src/test/fixtures/routes/_global.ts +8 -0
  297. package/src/test/fixtures/routes/_layout-template.ts +7 -0
  298. package/src/test/fixtures/routes/_layout.ts +7 -0
  299. package/src/test/fixtures/routes/_layout_scripts.ts +8 -0
  300. package/src/test/fixtures/routes/_middleware.ts +8 -0
  301. package/src/test/fixtures/routes/_redirect301_mw.ts +5 -0
  302. package/src/test/fixtures/routes/_redirect_mw.ts +5 -0
  303. package/src/test/fixtures/routes/about.page.ts +6 -0
  304. package/src/test/fixtures/routes/action.page.ts +11 -0
  305. package/src/test/fixtures/routes/api/form-all.post.ts +5 -0
  306. package/src/test/fixtures/routes/api/form-limited.post.ts +6 -0
  307. package/src/test/fixtures/routes/api/response-obj.get.ts +17 -0
  308. package/src/test/fixtures/routes/api/upload.post.ts +14 -0
  309. package/src/test/fixtures/routes/api/users.get.ts +3 -0
  310. package/src/test/fixtures/routes/api/xml.get.ts +5 -0
  311. package/src/test/fixtures/routes/auth/_middleware.ts +11 -0
  312. package/src/test/fixtures/routes/auth/protected.page.ts +3 -0
  313. package/src/test/fixtures/routes/index.page.ts +3 -0
  314. package/src/test/fixtures/routes/oml.page.ts +7 -0
  315. package/src/test/fixtures/routes/redirect.page.ts +3 -0
  316. package/src/test/fixtures/routes/ssg/[slug].page.ts +8 -0
  317. package/src/test/fixtures/routes/ssg/server.page.ts +5 -0
  318. package/src/test/fixtures/routes/state.page.ts +4 -0
  319. package/src/test/fixtures/routes/throw.page.ts +5 -0
  320. package/src/test/fixtures/routes/wiki/[...slug].page.ts +3 -0
  321. package/src/test/helpers.ts +132 -0
  322. package/src/test/layouts.test.ts +76 -0
  323. package/src/test/middleware.test.ts +69 -0
  324. package/src/test/multipart.test.ts +91 -0
  325. package/src/test/oml-routing.test.ts +59 -0
  326. package/src/test/oml.test.ts +429 -0
  327. package/src/test/redirects.test.ts +32 -0
  328. package/src/test/routing.test.ts +118 -0
  329. package/src/test/ssg.test.ts +273 -0
  330. package/src/test/web-response.test.ts +33 -0
  331. package/src/types.ts +670 -0
  332. package/tsconfig.client.json +17 -0
  333. package/tsconfig.json +20 -0
@@ -0,0 +1,515 @@
1
+ import type { IncomingMessage, ServerResponse } from 'node:http'
2
+ import { dirname } from 'node:path'
3
+ import type { OmlCacheConfig } from '../config.js'
4
+ import { renderToHtml, renderToHtmlDev } from '../oml/render.js'
5
+ import type { OmlNode } from '../oml/types.js'
6
+ import type {
7
+ ActionFn,
8
+ ErrorPageHandler,
9
+ ErrorPageProps,
10
+ LayoutFile,
11
+ LayoutProps,
12
+ MiddlewareFile,
13
+ MiddlewareFn,
14
+ RequestContext,
15
+ RouteFile,
16
+ ScanResult,
17
+ } from '../types.js'
18
+ import {
19
+ makeContext,
20
+ type PayloadTooLargeError,
21
+ type RedirectError,
22
+ type ValidationError,
23
+ } from '../types.js'
24
+ import { createMatcher, type MatchFn } from './matcher.js'
25
+
26
+ const METHOD_TYPES: Record<string, string[]> = {
27
+ GET: ['page', 'get'],
28
+ POST: ['post', 'page'], // 'page' to support defineAction exports
29
+ PUT: ['put'],
30
+ PATCH: ['patch'],
31
+ DELETE: ['delete'],
32
+ HEAD: ['head', 'page', 'get'],
33
+ OPTIONS: ['options'],
34
+ }
35
+
36
+ /** A route file paired with its compiled URL matcher, ready for dispatch. */
37
+ export interface CompiledRoute {
38
+ route: RouteFile
39
+ match: MatchFn
40
+ }
41
+
42
+ /** The compiled representation of a Davaux application, ready to dispatch requests. */
43
+ export interface CompiledApp {
44
+ routes: CompiledRoute[]
45
+ layouts: LayoutFile[]
46
+ middlewares: MiddlewareFile[]
47
+ /** Compiled path to `src/middleware.ts` — runs on every request before route matching. */
48
+ appMiddlewarePath?: string
49
+ isDev: boolean
50
+ /** Whether the visual editor and inspector overlay are active. Requires `editor.enabled: true` in config. */
51
+ editorEnabled: boolean
52
+ clientScripts: string[]
53
+ clientStylesheets: string[]
54
+ errorPage?: string
55
+ basePath: string
56
+ /**
57
+ * In-process OML cache. Keyed by filePath::pathname+search. Only populated in production
58
+ * for routes matched by `omlCacheConfig`. Stores the OmlNode and pre-rendered HTML string
59
+ * so cache hits require zero renderToHtml traversal.
60
+ */
61
+ omlCache: Map<string, { node: OmlNode; html: string }>
62
+ /** In dev mode, stores the last rendered OmlNode per URL for the inspector. Not populated in production. */
63
+ devOmlStore?: Map<string, OmlNode>
64
+ /**
65
+ * OML production cache configuration from `davaux.config.ts` `oml.cache`.
66
+ * When absent, no routes are cached in production.
67
+ */
68
+ omlCacheConfig?: OmlCacheConfig
69
+ }
70
+
71
+ /**
72
+ * Compile a scan result into a `CompiledApp` ready to dispatch requests.
73
+ * Typically called once at startup (or on each rebuild in dev mode).
74
+ */
75
+ export function buildApp(
76
+ result: ScanResult,
77
+ isDev = false,
78
+ clientScripts: string[] = [],
79
+ clientStylesheets: string[] = [],
80
+ appMiddlewarePath?: string,
81
+ basePath = '',
82
+ editorEnabled = false,
83
+ omlCacheConfig?: OmlCacheConfig,
84
+ ): CompiledApp {
85
+ return {
86
+ routes: result.routes.map((route) => ({
87
+ route,
88
+ match: createMatcher(route.urlPattern),
89
+ })),
90
+ layouts: result.layouts,
91
+ middlewares: result.middlewares,
92
+ appMiddlewarePath,
93
+ isDev,
94
+ editorEnabled,
95
+ clientScripts,
96
+ clientStylesheets,
97
+ errorPage: result.errorPage,
98
+ basePath,
99
+ omlCache: new Map(),
100
+ devOmlStore: isDev && editorEnabled ? new Map() : undefined,
101
+ omlCacheConfig,
102
+ }
103
+ }
104
+
105
+ // ─── OML cache helpers ────────────────────────────────────────────────────────
106
+
107
+ function matchesCachePattern(pathname: string, pattern: string): boolean {
108
+ const regexStr = pattern
109
+ .replace(/[.+?^${}()|[\]\\]/g, '\\$&') // escape regex special chars (not * or :)
110
+ .replace(/:[^/]+/g, '[^/]+') // :param → any non-slash segment
111
+ .replace(/\*/g, '.*') // * → anything
112
+ return new RegExp(`^${regexStr}$`).test(pathname)
113
+ }
114
+
115
+ function isOmlCacheable(pathname: string, config: OmlCacheConfig | undefined): boolean {
116
+ if (!config) return false
117
+ if ('include' in config) return config.include.some((p) => matchesCachePattern(pathname, p))
118
+ return !config.exclude.some((p) => matchesCachePattern(pathname, p))
119
+ }
120
+
121
+ function isOmlNode(value: unknown): value is OmlNode {
122
+ if (value === null || typeof value !== 'object' || Array.isArray(value)) return false
123
+ const t = (value as Record<string, unknown>).type
124
+ return t === 'element' || t === '#component' || t === '#fragment' || t === '#text' || t === '#raw'
125
+ }
126
+
127
+ async function importModule(filePath: string, isDev: boolean): Promise<Record<string, unknown>> {
128
+ const url = isDev ? `${filePath}?t=${Date.now()}` : filePath
129
+ try {
130
+ return await (import(url) as Promise<Record<string, unknown>>)
131
+ } catch (err) {
132
+ const detail = err instanceof Error ? err.message : String(err)
133
+ throw new Error(`[davaux] Failed to load module: ${filePath}\n ${detail}`, { cause: err })
134
+ }
135
+ }
136
+
137
+ // Find layouts that apply to a given route file, ordered innermost → outermost.
138
+ function findLayouts(routeFilePath: string, layouts: LayoutFile[]): LayoutFile[] {
139
+ const routeDir = dirname(routeFilePath)
140
+ return layouts
141
+ .filter((l) => routeDir === l.dirPath || routeDir.startsWith(`${l.dirPath}/`))
142
+ .sort((a, b) => b.dirPath.length - a.dirPath.length) // deepest first
143
+ }
144
+
145
+ // Find middlewares that apply to a given route file, ordered outermost → innermost.
146
+ function findMiddlewares(routeFilePath: string, middlewares: MiddlewareFile[]): MiddlewareFile[] {
147
+ const routeDir = dirname(routeFilePath)
148
+ return middlewares
149
+ .filter((m) => routeDir === m.dirPath || routeDir.startsWith(`${m.dirPath}/`))
150
+ .sort((a, b) => a.dirPath.length - b.dirPath.length) // shallowest first
151
+ }
152
+
153
+ async function runWithMiddleware(
154
+ middlewares: MiddlewareFile[],
155
+ ctx: RequestContext,
156
+ isDev: boolean,
157
+ handler: () => Promise<void>,
158
+ ): Promise<void> {
159
+ const step = async (i: number): Promise<void> => {
160
+ if (i < middlewares.length) {
161
+ const mod = await importModule(middlewares[i].filePath, isDev)
162
+ const fn = mod.default as MiddlewareFn
163
+ await fn(ctx, () => step(i + 1))
164
+ } else {
165
+ await handler()
166
+ }
167
+ }
168
+ await step(0)
169
+ }
170
+
171
+ function sendPage(html: string, res: ServerResponse): void {
172
+ if (!res.headersSent) {
173
+ const trimmed = html.trimStart()
174
+ const full =
175
+ trimmed.startsWith('<') && !trimmed.startsWith('<!DOCTYPE') ? `<!DOCTYPE html>${html}` : html
176
+ res.writeHead(res.statusCode, {
177
+ 'Content-Type': 'text/html; charset=utf-8',
178
+ })
179
+ res.end(full)
180
+ } else if (!res.writableEnded) {
181
+ res.end(html)
182
+ }
183
+ }
184
+
185
+ async function sendPageWithDeferred(
186
+ html: string,
187
+ ctx: import('../types.js').RequestContext,
188
+ res: ServerResponse,
189
+ ): Promise<void> {
190
+ if (ctx._deferredSlots.size === 0) {
191
+ sendPage(html, res)
192
+ return
193
+ }
194
+ if (res.headersSent) return
195
+ const trimmed = html.trimStart()
196
+ const shell =
197
+ trimmed.startsWith('<') && !trimmed.startsWith('<!DOCTYPE') ? `<!DOCTYPE html>${html}` : html
198
+ res.writeHead(res.statusCode, { 'Content-Type': 'text/html; charset=utf-8' })
199
+ res.write(shell)
200
+ for (const [name, contentPromise] of ctx._deferredSlots) {
201
+ const content = await contentPromise
202
+ const slotHtml = isOmlNode(content) ? renderToHtml(content) : (content as string)
203
+ res.write(`<template for="${name}">${slotHtml}</template>`)
204
+ }
205
+ res.end()
206
+ }
207
+
208
+ async function renderPage(
209
+ route: RouteFile,
210
+ ctx: RequestContext,
211
+ layouts: LayoutFile[],
212
+ isDev: boolean,
213
+ omlCache: Map<string, { node: OmlNode; html: string }>,
214
+ opts?: {
215
+ preloadedMod?: Record<string, unknown>
216
+ devOmlStore?: Map<string, OmlNode>
217
+ omlCacheConfig?: OmlCacheConfig
218
+ },
219
+ ): Promise<string> {
220
+ const { preloadedMod, devOmlStore, omlCacheConfig } = opts ?? {}
221
+ const mod = preloadedMod ?? (await importModule(route.filePath, isDev))
222
+ const handler = mod.default as (
223
+ ctx: RequestContext,
224
+ ) => string | OmlNode | Promise<string | OmlNode>
225
+
226
+ const cacheKey =
227
+ !isDev && isOmlCacheable(ctx.url.pathname, omlCacheConfig)
228
+ ? `${route.filePath}::${ctx.url.pathname}${ctx.url.search}`
229
+ : null
230
+ const cached = cacheKey ? omlCache.get(cacheKey) : undefined
231
+
232
+ let html: string
233
+ if (cached !== undefined) {
234
+ html = cached.html
235
+ } else {
236
+ const result = await handler(ctx)
237
+ if (isOmlNode(result)) {
238
+ html = isDev && devOmlStore ? renderToHtmlDev(result) : renderToHtml(result)
239
+ if (cacheKey) omlCache.set(cacheKey, { node: result, html })
240
+ if (devOmlStore) {
241
+ const sp = new URLSearchParams(ctx.url.search)
242
+ sp.delete('_editor')
243
+ const search = sp.toString()
244
+ devOmlStore.set(ctx.url.pathname + (search ? `?${search}` : ''), result)
245
+ }
246
+ } else {
247
+ html = result as string
248
+ }
249
+ }
250
+
251
+ // Wrap in applicable layouts from innermost to outermost
252
+ const applicable = findLayouts(route.filePath, layouts)
253
+ for (const layout of applicable) {
254
+ const lmod = await importModule(layout.filePath, isDev)
255
+ const lhandler = lmod.default as (
256
+ props: LayoutProps,
257
+ ) => string | OmlNode | Promise<string | OmlNode>
258
+ // Pass children as Promise<string> so the JSX runtime renders it without escaping
259
+ const lresult = await lhandler({
260
+ children: Object.assign(Promise.resolve(html), { toString: () => html }),
261
+ ctx,
262
+ })
263
+ html = isOmlNode(lresult) ? renderToHtml(lresult) : (lresult as string)
264
+ }
265
+
266
+ return html
267
+ }
268
+
269
+ function initCtx(
270
+ req: IncomingMessage,
271
+ res: ServerResponse,
272
+ params: Record<string, string>,
273
+ url: URL,
274
+ app: CompiledApp,
275
+ ): RequestContext {
276
+ const ctx = makeContext(req, res, params, url, app.basePath)
277
+ ctx.head.stylesheets.push(...app.clientStylesheets)
278
+ ctx.head.scripts.push(...app.clientScripts)
279
+ if (app.isDev) {
280
+ ctx.head.scripts.push('/_davaux/livereload.js')
281
+ if (app.editorEnabled && !url.searchParams.has('_editor')) {
282
+ ctx.head.scripts.push('/_davaux/inspector.js')
283
+ }
284
+ }
285
+ return ctx
286
+ }
287
+
288
+ async function sendErrorPage(
289
+ status: number,
290
+ message: string,
291
+ req: IncomingMessage,
292
+ res: ServerResponse,
293
+ url: URL,
294
+ app: CompiledApp,
295
+ existingCtx?: RequestContext,
296
+ ): Promise<void> {
297
+ if (res.headersSent) return
298
+ if (!app.errorPage) {
299
+ res.writeHead(status, { 'Content-Type': 'text/plain' })
300
+ res.end(message)
301
+ return
302
+ }
303
+ try {
304
+ const ctx = existingCtx ?? initCtx(req, res, {}, url, app)
305
+ const mod = await importModule(app.errorPage, app.isDev)
306
+ const handler = mod.default as ErrorPageHandler
307
+ const rawErrorResult = await handler({ status, message, ctx } satisfies ErrorPageProps)
308
+ let html = isOmlNode(rawErrorResult) ? renderToHtml(rawErrorResult) : (rawErrorResult as string)
309
+
310
+ // Apply layouts so error pages inherit site chrome
311
+ const applicable = findLayouts(app.errorPage, app.layouts)
312
+ for (const layout of applicable) {
313
+ const lmod = await importModule(layout.filePath, app.isDev)
314
+ const lhandler = lmod.default as (
315
+ props: LayoutProps,
316
+ ) => string | OmlNode | Promise<string | OmlNode>
317
+ const lresult = await lhandler({
318
+ children: Object.assign(Promise.resolve(html), { toString: () => html }),
319
+ ctx,
320
+ })
321
+ html = isOmlNode(lresult) ? renderToHtml(lresult) : (lresult as string)
322
+ }
323
+
324
+ const trimmed = html.trimStart()
325
+ const full =
326
+ trimmed.startsWith('<') && !trimmed.startsWith('<!DOCTYPE') ? `<!DOCTYPE html>${html}` : html
327
+ res.writeHead(status, { 'Content-Type': 'text/html; charset=utf-8' })
328
+ res.end(full)
329
+ } catch {
330
+ res.writeHead(status, { 'Content-Type': 'text/plain' })
331
+ res.end(message)
332
+ }
333
+ }
334
+
335
+ /**
336
+ * Dispatch a single HTTP request through the compiled app.
337
+ * Runs app middleware → route matching → scoped middleware → route handler.
338
+ * Handles redirects, validation errors, payload-too-large, and 404s internally.
339
+ */
340
+ export async function dispatch(
341
+ req: IncomingMessage,
342
+ res: ServerResponse,
343
+ app: CompiledApp,
344
+ ): Promise<void> {
345
+ const method = (req.method ?? 'GET').toUpperCase()
346
+ const url = new URL(req.url ?? '/', `http://${req.headers.host ?? 'localhost'}`)
347
+ const allowed = METHOD_TYPES[method] ?? []
348
+
349
+ // ctx is created once and shared between global middleware and route handlers.
350
+ // params starts empty and is populated in-place after a route match.
351
+ const ctx = initCtx(req, res, {}, url, app)
352
+
353
+ // Inner handler: route matching → scoped middleware → route handler → 404.
354
+ // Never throws — all errors are caught and turned into responses here.
355
+ const handleRoutes = async (): Promise<void> => {
356
+ for (const { route, match } of app.routes) {
357
+ if (!allowed.includes(route.type)) continue
358
+
359
+ const result = match(url.pathname)
360
+ if (!result) continue
361
+
362
+ // Populate params into the shared ctx now that we have a match
363
+ Object.assign(ctx.params, result.params)
364
+
365
+ const applicable = findMiddlewares(route.filePath, app.middlewares)
366
+
367
+ try {
368
+ await runWithMiddleware(applicable, ctx, app.isDev, async () => {
369
+ if (route.type === 'page') {
370
+ if (method === 'POST') {
371
+ const mod = await importModule(route.filePath, app.isDev)
372
+ const actionFn = mod.action as ActionFn | undefined
373
+ if (typeof actionFn !== 'function') {
374
+ if (!res.headersSent) {
375
+ res.writeHead(405, {
376
+ Allow: 'GET, HEAD',
377
+ 'Content-Type': 'text/plain',
378
+ })
379
+ res.end('Method Not Allowed')
380
+ }
381
+ return
382
+ }
383
+ const result = await actionFn(ctx)
384
+ if (result !== undefined) ctx.state.actionResult = result
385
+ await sendPageWithDeferred(
386
+ await renderPage(route, ctx, app.layouts, app.isDev, app.omlCache, {
387
+ preloadedMod: mod,
388
+ devOmlStore: app.devOmlStore,
389
+ omlCacheConfig: app.omlCacheConfig,
390
+ }),
391
+ ctx,
392
+ res,
393
+ )
394
+ } else {
395
+ await sendPageWithDeferred(
396
+ await renderPage(route, ctx, app.layouts, app.isDev, app.omlCache, {
397
+ devOmlStore: app.devOmlStore,
398
+ omlCacheConfig: app.omlCacheConfig,
399
+ }),
400
+ ctx,
401
+ res,
402
+ )
403
+ }
404
+ } else {
405
+ // API route
406
+ const mod = await importModule(route.filePath, app.isDev)
407
+ const handler = mod.default as (ctx: RequestContext) => unknown
408
+ const value = await handler(ctx)
409
+ if (!res.headersSent && value !== undefined) {
410
+ if (value instanceof Response) {
411
+ await writeWebResponse(value, res)
412
+ } else {
413
+ res.writeHead(res.statusCode, {
414
+ 'Content-Type': 'application/json; charset=utf-8',
415
+ })
416
+ res.end(JSON.stringify(value))
417
+ }
418
+ }
419
+ }
420
+ })
421
+ } catch (err) {
422
+ if (isRedirect(err)) throw err
423
+ if (isValidation(err)) throw err
424
+ if (isPayloadTooLarge(err)) throw err
425
+ console.error(`[davaux] Error in ${route.filePath}:`, err)
426
+ await sendErrorPage(
427
+ 500,
428
+ err instanceof Error ? err.message : 'Internal Server Error',
429
+ req,
430
+ res,
431
+ url,
432
+ app,
433
+ ctx,
434
+ )
435
+ }
436
+ return // route matched — stop iterating
437
+ }
438
+
439
+ await sendErrorPage(404, 'Not Found', req, res, url, app, ctx)
440
+ }
441
+
442
+ // App middleware (src/middleware.ts) → routes.
443
+ // Errors thrown anywhere in the chain are caught here.
444
+ try {
445
+ if (app.appMiddlewarePath) {
446
+ const mod = await importModule(app.appMiddlewarePath, app.isDev)
447
+ const fn = mod.default as MiddlewareFn
448
+ await fn(ctx, handleRoutes)
449
+ } else {
450
+ await handleRoutes()
451
+ }
452
+ } catch (err) {
453
+ if (isRedirect(err)) {
454
+ if (!res.headersSent) {
455
+ res.writeHead(err.status, { Location: err.url })
456
+ res.end()
457
+ }
458
+ return
459
+ }
460
+ if (isValidation(err)) {
461
+ const body: Record<string, unknown> = { error: err.message }
462
+ const cause = err.cause
463
+ if (
464
+ cause != null &&
465
+ typeof cause === 'object' &&
466
+ Array.isArray((cause as { issues?: unknown }).issues)
467
+ ) {
468
+ body.issues = (cause as { issues: unknown }).issues
469
+ }
470
+ if (!res.headersSent) {
471
+ res.writeHead(400, { 'Content-Type': 'application/json; charset=utf-8' })
472
+ res.end(JSON.stringify(body))
473
+ }
474
+ return
475
+ }
476
+ if (isPayloadTooLarge(err)) {
477
+ if (!res.headersSent) {
478
+ res.writeHead(413, { 'Content-Type': 'text/plain' })
479
+ res.end(err.message)
480
+ }
481
+ return
482
+ }
483
+ console.error('[davaux] Error in global middleware:', err)
484
+ await sendErrorPage(
485
+ 500,
486
+ err instanceof Error ? err.message : 'Internal Server Error',
487
+ req,
488
+ res,
489
+ url,
490
+ app,
491
+ ctx,
492
+ )
493
+ }
494
+ }
495
+
496
+ async function writeWebResponse(webRes: Response, res: ServerResponse): Promise<void> {
497
+ const headers: Record<string, string> = {}
498
+ webRes.headers.forEach((value, key) => {
499
+ headers[key] = value
500
+ })
501
+ res.writeHead(webRes.status, headers)
502
+ res.end(Buffer.from(await webRes.arrayBuffer()))
503
+ }
504
+
505
+ function isRedirect(err: unknown): err is RedirectError {
506
+ return (err as RedirectError)?.isRedirect === true
507
+ }
508
+
509
+ function isValidation(err: unknown): err is ValidationError {
510
+ return (err as ValidationError)?.isValidation === true
511
+ }
512
+
513
+ function isPayloadTooLarge(err: unknown): err is PayloadTooLargeError {
514
+ return (err as PayloadTooLargeError)?.isPayloadTooLarge === true
515
+ }
@@ -0,0 +1,52 @@
1
+ /** Result of a successful URL pattern match, containing the extracted named params. */
2
+ export interface MatchResult {
3
+ params: Record<string, string>
4
+ }
5
+
6
+ /** A compiled URL matcher. Returns a `MatchResult` on match, `null` on miss. */
7
+ export type MatchFn = (pathname: string) => MatchResult | null
8
+
9
+ /**
10
+ * Compile a URL pattern string into a fast matcher function.
11
+ *
12
+ * Pattern segments:
13
+ * - `:param` — matches a single path segment and captures it by name.
14
+ * - `*param` — catch-all; matches the rest of the path including slashes.
15
+ * - anything else — matched literally.
16
+ *
17
+ * @example
18
+ * const match = createMatcher('/blog/:slug')
19
+ * match('/blog/hello') // → { params: { slug: 'hello' } }
20
+ * match('/about') // → null
21
+ */
22
+ export function createMatcher(urlPattern: string): MatchFn {
23
+ const paramNames: string[] = []
24
+
25
+ // Build regex from pattern segments
26
+ const regexParts = urlPattern.split('/').map((segment) => {
27
+ if (segment.startsWith('*')) {
28
+ paramNames.push(segment.slice(1))
29
+ return '(.+)' // catch-all: matches multiple path segments including slashes
30
+ }
31
+ if (segment.startsWith(':')) {
32
+ paramNames.push(segment.slice(1))
33
+ return '([^/]+)'
34
+ }
35
+ // Escape regex special chars in static segments
36
+ return segment.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
37
+ })
38
+
39
+ const pattern = new RegExp(`^${regexParts.join('\\/')}$`)
40
+
41
+ return (pathname: string): MatchResult | null => {
42
+ const match = pathname.match(pattern)
43
+ if (!match) return null
44
+
45
+ const params: Record<string, string> = {}
46
+ paramNames.forEach((name, i) => {
47
+ params[name] = decodeURIComponent(match[i + 1])
48
+ })
49
+
50
+ return { params }
51
+ }
52
+ }