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
package/src/types.ts ADDED
@@ -0,0 +1,670 @@
1
+ import type { IncomingMessage, ServerResponse } from 'node:http'
2
+ import type { OmlNode } from './oml/types.js'
3
+
4
+ // ─── Cookie helpers ────────────────────────────────────────────────────────────
5
+
6
+ /** Options for the `Set-Cookie` header written by `ctx.cookies.set()`. */
7
+ export interface CookieOptions {
8
+ httpOnly?: boolean
9
+ secure?: boolean
10
+ sameSite?: 'Strict' | 'Lax' | 'None'
11
+ /** Max age in seconds. */
12
+ maxAge?: number
13
+ expires?: Date
14
+ /** Defaults to `'/'` when not specified. */
15
+ path?: string
16
+ domain?: string
17
+ }
18
+
19
+ function sanitizeCookieAttr(val: string): string {
20
+ return val.replace(/[;\r\n]/g, '')
21
+ }
22
+
23
+ /** Reads incoming cookies and writes `Set-Cookie` response headers.
24
+ * Available as `ctx.cookies` in every handler, layout, and middleware. */
25
+ export class CookieJar {
26
+ private readonly incoming: Record<string, string>
27
+ private readonly res: ServerResponse
28
+
29
+ constructor(req: IncomingMessage, res: ServerResponse) {
30
+ this.res = res
31
+ this.incoming = CookieJar.parse(req.headers.cookie ?? '')
32
+ }
33
+
34
+ private static parse(header: string): Record<string, string> {
35
+ const out: Record<string, string> = {}
36
+ for (const pair of header.split(';')) {
37
+ const eq = pair.indexOf('=')
38
+ if (eq === -1) continue
39
+ const name = pair.slice(0, eq).trim()
40
+ const value = pair.slice(eq + 1).trim()
41
+ if (name) out[name] = decodeURIComponent(value)
42
+ }
43
+ return out
44
+ }
45
+
46
+ /** Read a cookie sent by the browser. Returns `undefined` if absent. */
47
+ get(name: string): string | undefined {
48
+ return this.incoming[name]
49
+ }
50
+
51
+ /** Return all incoming cookies as a plain object. */
52
+ getAll(): Record<string, string> {
53
+ return { ...this.incoming }
54
+ }
55
+
56
+ /** Write a `Set-Cookie` header on the response. */
57
+ set(name: string, value: string, options: CookieOptions = {}): void {
58
+ const encoded = `${encodeURIComponent(name)}=${encodeURIComponent(value)}`
59
+ const parts = [encoded]
60
+ const path = sanitizeCookieAttr(options.path ?? '/')
61
+
62
+ if (options.maxAge != null) parts.push(`Max-Age=${options.maxAge}`)
63
+ if (options.expires) parts.push(`Expires=${options.expires.toUTCString()}`)
64
+ parts.push(`Path=${path}`)
65
+ if (options.domain) parts.push(`Domain=${sanitizeCookieAttr(options.domain)}`)
66
+ if (options.httpOnly) parts.push('HttpOnly')
67
+ if (options.secure) parts.push('Secure')
68
+ if (options.sameSite) parts.push(`SameSite=${options.sameSite}`)
69
+
70
+ const existing = this.res.getHeader('Set-Cookie')
71
+ const prev: string[] = existing
72
+ ? Array.isArray(existing)
73
+ ? (existing as string[])
74
+ : [String(existing)]
75
+ : []
76
+ this.res.setHeader('Set-Cookie', [...prev, parts.join('; ')])
77
+ }
78
+
79
+ /** Expire a cookie by setting `Max-Age=0`. */
80
+ delete(name: string, options?: Pick<CookieOptions, 'path' | 'domain'>): void {
81
+ this.set(name, '', { ...options, maxAge: 0, expires: new Date(0) })
82
+ }
83
+ }
84
+
85
+ // ─── Validation ───────────────────────────────────────────────────────────────
86
+
87
+ /** Schema interface satisfied by zod, valibot, arktype, and any library whose
88
+ * `.parse()` throws on invalid input. Pass a schema to `ctx.json()` or
89
+ * `ctx.form()` to validate and type the parsed body in one call. */
90
+ export interface Validator<T> {
91
+ parse(data: unknown): T
92
+ }
93
+
94
+ /** Thrown when a body validator rejects the request data.
95
+ * Caught by the framework and sent as `400 { error, issues }`. */
96
+ export class ValidationError extends Error {
97
+ readonly isValidation = true
98
+ constructor(cause: unknown) {
99
+ super(cause instanceof Error ? cause.message : 'Validation failed', { cause })
100
+ this.name = 'ValidationError'
101
+ }
102
+ }
103
+
104
+ /** Thrown when a request body exceeds the configured size limit.
105
+ * Caught by the framework and sent as `413 Payload Too Large`. */
106
+ export class PayloadTooLargeError extends Error {
107
+ readonly isPayloadTooLarge = true
108
+ readonly status = 413
109
+ constructor(limitBytes: number) {
110
+ super(`Payload Too Large — body exceeds ${limitBytes.toLocaleString()} bytes`)
111
+ this.name = 'PayloadTooLargeError'
112
+ }
113
+ }
114
+
115
+ // ─── Multipart ────────────────────────────────────────────────────────────────
116
+
117
+ /** An uploaded file parsed from a `multipart/form-data` body. */
118
+ export interface MultipartFile {
119
+ filename: string
120
+ mimetype: string
121
+ data: Buffer
122
+ }
123
+
124
+ /** Parsed result of `ctx.multipart()`. */
125
+ export interface MultipartResult {
126
+ fields: Record<string, string>
127
+ files: Record<string, MultipartFile>
128
+ }
129
+
130
+ // ─── Redirect ─────────────────────────────────────────────────────────────────
131
+
132
+ /** Thrown by `redirect()`. Never construct directly. */
133
+ export class RedirectError extends Error {
134
+ readonly isRedirect = true
135
+ constructor(
136
+ public readonly url: string,
137
+ public readonly status: 301 | 302 | 307 | 308 = 302,
138
+ ) {
139
+ super(`Redirect → ${url}`)
140
+ this.name = 'RedirectError'
141
+ }
142
+ }
143
+
144
+ /**
145
+ * Throw a redirect to the given URL.
146
+ *
147
+ * **Security note:** If `url` comes from user input (e.g. `ctx.query.get('next')`),
148
+ * validate it before passing it here to prevent open-redirect attacks. At minimum,
149
+ * ensure the value starts with `/` and does not contain `//` or a protocol.
150
+ */
151
+ export function redirect(url: string, status: 301 | 302 | 307 | 308 = 302): never {
152
+ throw new RedirectError(url, status)
153
+ }
154
+
155
+ // ─── Head context ─────────────────────────────────────────────────────────────
156
+
157
+ /** Per-request `<head>` metadata. Set values in pages and layouts; the root
158
+ * layout renders them. Pages render before their layouts, so all values are
159
+ * populated by the time the root layout runs. */
160
+ export interface HeadContext {
161
+ title?: string
162
+ description?: string
163
+ /** Arbitrary `<meta name="..." content="...">` entries. */
164
+ meta: Record<string, string>
165
+ /** CSS file URLs injected as `<link rel="stylesheet">`. */
166
+ stylesheets: string[]
167
+ /** JS module URLs injected as `<script type="module">`. */
168
+ scripts: string[]
169
+ }
170
+
171
+ // ─── Route param type inference ───────────────────────────────────────────────
172
+
173
+ // Recursively collect bracket-param names from a path pattern string.
174
+ type RouteParamNames<T extends string> = T extends `${string}[${infer Param}]${infer Rest}`
175
+ ? Param | RouteParamNames<Rest>
176
+ : never
177
+
178
+ /**
179
+ * Infer typed route params from a file path pattern (relative to your routes
180
+ * directory, without the method suffix or extension).
181
+ *
182
+ * @example
183
+ * // src/routes/blog/[slug].page.tsx
184
+ * export default definePage<ExtractParams<'blog/[slug]'>>(async (ctx) => {
185
+ * ctx.params.slug // string ✓ — TypeScript error if you typo the key
186
+ * })
187
+ *
188
+ * // src/routes/users/[id]/posts/[postId].get.ts
189
+ * export default defineHandler<unknown, ExtractParams<'users/[id]/posts/[postId]'>>(async (ctx) => {
190
+ * ctx.params.id // string ✓
191
+ * ctx.params.postId // string ✓
192
+ * })
193
+ */
194
+ export type ExtractParams<T extends string> = { [K in RouteParamNames<T>]: string }
195
+
196
+ // ─── Request context ──────────────────────────────────────────────────────────
197
+
198
+ /**
199
+ * Open interface for middleware-to-handler pass-through data.
200
+ * Ecosystem packages augment this via declaration merging to add typed properties:
201
+ *
202
+ * @example
203
+ * declare module 'davaux' {
204
+ * interface State {
205
+ * session: Session
206
+ * }
207
+ * }
208
+ */
209
+ export interface State extends Record<string, unknown> {
210
+ /** Result returned by `defineAction` before the page re-renders on form POST. */
211
+ actionResult?: unknown
212
+ }
213
+
214
+ /**
215
+ * Context object passed to every page, API, layout, middleware, and error handler.
216
+ * @typeParam P - Typed URL params. Use `ExtractParams<'path/[param]'>` for compile-time safety.
217
+ */
218
+ export interface RequestContext<P extends Record<string, string> = Record<string, string>> {
219
+ req: IncomingMessage
220
+ res: ServerResponse
221
+ params: P
222
+ query: URLSearchParams
223
+ url: URL
224
+ /** Base path for subdirectory deployments (e.g. '/docs'). Empty string in dev/SSR. */
225
+ basePath: string
226
+ cookies: CookieJar
227
+ /** Mutable head metadata — set in pages/layouts, rendered by the root layout. */
228
+ head: HeadContext
229
+ /** Pass-through bag for middleware → handler data. Augment via `State` interface merging. */
230
+ state: State
231
+ /** Parse the request body as JSON. Pass a validator (zod schema, etc.) to
232
+ * validate and type the result; throws `ValidationError` (→ 400) on failure.
233
+ * Default body size limit: 4 MB. Override with `options.maxBytes`. */
234
+ json<T = Record<string, unknown>>(
235
+ validator?: Validator<T>,
236
+ options?: { maxBytes?: number },
237
+ ): Promise<T>
238
+ /** Parse the request body as URL-encoded form data. Pass a validator to
239
+ * validate and type the result; throws `ValidationError` (→ 400) on failure.
240
+ * Default body size limit: 1 MB. Override with `options.maxBytes`.
241
+ * When a field name appears more than once, the last value wins — use
242
+ * `ctx.formAll()` to collect all values for multi-select / checkbox groups. */
243
+ form<T = Record<string, string>>(
244
+ validator?: Validator<T>,
245
+ options?: { maxBytes?: number },
246
+ ): Promise<T>
247
+ /** Like `ctx.form()`, but returns every value for every key as a `string[]`.
248
+ * Use this for `<select multiple>` and repeated checkbox fields where
249
+ * `ctx.form()` would silently drop all but the last value. */
250
+ formAll<T = Record<string, string[]>>(
251
+ validator?: Validator<T>,
252
+ options?: { maxBytes?: number },
253
+ ): Promise<T>
254
+ /** Parse a multipart/form-data request body. Returns parsed text fields and
255
+ * uploaded files. Default body size limit: 10 MB. Override with `options.maxBytes`. */
256
+ multipart(options?: { maxBytes?: number }): Promise<MultipartResult>
257
+ /** Write a raw response with a custom content-type. The response is immediately
258
+ * sent; no further framework serialization occurs. */
259
+ send(body: string | Buffer, contentType: string, status?: number): void
260
+ /** Write a one-time message that survives a single redirect. Requires @davaux/session. */
261
+ flash(key: string, value: string): void
262
+ /** Read and consume a flash message set by a previous request. Returns undefined if absent. */
263
+ flash(key: string): string | undefined
264
+ /**
265
+ * Register a named deferred HTML slot. Returns a `<?marker name="...">` placeholder node
266
+ * that renders inline; when the promise resolves the framework streams a
267
+ * `<template for="name">` element into the response to fill the slot.
268
+ * Polyfill scripts are injected automatically for browsers without native support.
269
+ */
270
+ defer(name: string, content: Promise<OmlNode | string>): Promise<OmlNode>
271
+ /** @internal Pending deferred slots registered via `ctx.defer()`. */
272
+ _deferredSlots: Map<string, Promise<OmlNode | string>>
273
+ }
274
+
275
+ interface FlashSession {
276
+ get(key: string): unknown
277
+ set(key: string, value: unknown): void
278
+ delete(key: string): void
279
+ }
280
+
281
+ const DEFAULT_FORM_LIMIT = 1_000_000 // 1 MB
282
+ const DEFAULT_JSON_LIMIT = 4_000_000 // 4 MB
283
+ const DEFAULT_MULTIPART_LIMIT = 10_000_000 // 10 MB
284
+
285
+ function readRawBuffer(req: IncomingMessage, maxBytes: number): Promise<Buffer> {
286
+ return new Promise((resolve, reject) => {
287
+ const chunks: Buffer[] = []
288
+ let size = 0
289
+ req.on('data', (chunk: Buffer) => {
290
+ size += chunk.length
291
+ if (size > maxBytes) {
292
+ req.destroy()
293
+ reject(new PayloadTooLargeError(maxBytes))
294
+ return
295
+ }
296
+ chunks.push(chunk)
297
+ })
298
+ req.on('end', () => resolve(Buffer.concat(chunks)))
299
+ req.on('error', reject)
300
+ })
301
+ }
302
+
303
+ function splitBuffer(buf: Buffer, sep: Buffer): Buffer[] {
304
+ const parts: Buffer[] = []
305
+ let start = 0
306
+ while (true) {
307
+ const idx = buf.indexOf(sep, start)
308
+ if (idx === -1) break
309
+ parts.push(buf.subarray(start, idx))
310
+ start = idx + sep.length
311
+ }
312
+ parts.push(buf.subarray(start))
313
+ return parts
314
+ }
315
+
316
+ function extractMultipartParam(header: string, param: string): string | undefined {
317
+ const rq = new RegExp(`(?:^|;)\\s*${param}="([^"]*)"`, 'i')
318
+ const mq = header.match(rq)
319
+ if (mq) return mq[1]
320
+ const ru = new RegExp(`(?:^|;)\\s*${param}=([^;\\s]*)`, 'i')
321
+ const mu = header.match(ru)
322
+ return mu ? mu[1] : undefined
323
+ }
324
+
325
+ function parseMultipart(body: Buffer, boundary: string): MultipartResult {
326
+ const fields: Record<string, string> = {}
327
+ const files: Record<string, MultipartFile> = {}
328
+
329
+ // Prepend \r\n so every boundary (including the first) matches `\r\n--boundary`
330
+ const sep = Buffer.from(`\r\n--${boundary}`)
331
+ const parts = splitBuffer(Buffer.concat([Buffer.from('\r\n'), body]), sep)
332
+
333
+ for (const part of parts) {
334
+ if (part.length < 2) continue
335
+ const prefix = part.subarray(0, 2).toString('ascii')
336
+ if (prefix === '--') break // final `--boundary--` terminator
337
+ if (prefix !== '\r\n') continue // preamble before first boundary
338
+
339
+ const content = part.subarray(2) // skip leading \r\n
340
+ const headerEnd = content.indexOf('\r\n\r\n')
341
+ if (headerEnd === -1) continue
342
+
343
+ const headerStr = content.subarray(0, headerEnd).toString('utf-8')
344
+ const partBody = content.subarray(headerEnd + 4)
345
+
346
+ const headers: Record<string, string> = {}
347
+ for (const line of headerStr.split('\r\n')) {
348
+ const colon = line.indexOf(':')
349
+ if (colon === -1) continue
350
+ headers[line.slice(0, colon).trim().toLowerCase()] = line.slice(colon + 1).trim()
351
+ }
352
+
353
+ const disposition = headers['content-disposition'] ?? ''
354
+ const name = extractMultipartParam(disposition, 'name')
355
+ if (!name) continue
356
+
357
+ const filename = extractMultipartParam(disposition, 'filename')
358
+ if (filename !== undefined) {
359
+ files[name] = {
360
+ filename,
361
+ mimetype: headers['content-type'] ?? 'application/octet-stream',
362
+ data: partBody,
363
+ }
364
+ } else {
365
+ fields[name] = partBody.toString('utf-8')
366
+ }
367
+ }
368
+
369
+ return { fields, files }
370
+ }
371
+
372
+ /** @internal Construct a `RequestContext` for a given request. Used by the router and SSG. */
373
+ export function makeContext(
374
+ req: IncomingMessage,
375
+ res: ServerResponse,
376
+ params: Record<string, string>,
377
+ url: URL,
378
+ basePath = '',
379
+ ): RequestContext {
380
+ let bodyCache: Promise<Buffer> | undefined
381
+ function rawBuffer(maxBytes: number): Promise<Buffer> {
382
+ if (bodyCache === undefined) bodyCache = readRawBuffer(req, maxBytes)
383
+ return bodyCache
384
+ }
385
+
386
+ return {
387
+ req,
388
+ res,
389
+ params,
390
+ query: url.searchParams,
391
+ url,
392
+ basePath,
393
+ cookies: new CookieJar(req, res),
394
+ head: { meta: {}, stylesheets: [], scripts: [] },
395
+ state: {},
396
+ async json<T = Record<string, unknown>>(
397
+ validator?: Validator<T>,
398
+ options?: { maxBytes?: number },
399
+ ) {
400
+ const raw: unknown = JSON.parse(
401
+ (await rawBuffer(options?.maxBytes ?? DEFAULT_JSON_LIMIT)).toString('utf-8'),
402
+ )
403
+ if (!validator) return raw as T
404
+ try {
405
+ return validator.parse(raw)
406
+ } catch (err) {
407
+ throw new ValidationError(err)
408
+ }
409
+ },
410
+ async form<T = Record<string, string>>(
411
+ validator?: Validator<T>,
412
+ options?: { maxBytes?: number },
413
+ ) {
414
+ const text = (await rawBuffer(options?.maxBytes ?? DEFAULT_FORM_LIMIT)).toString('utf-8')
415
+ const out: Record<string, string> = {}
416
+ for (const [k, v] of new URLSearchParams(text)) out[k] = v
417
+ if (!validator) return out as unknown as T
418
+ try {
419
+ return validator.parse(out)
420
+ } catch (err) {
421
+ throw new ValidationError(err)
422
+ }
423
+ },
424
+ async formAll<T = Record<string, string[]>>(
425
+ validator?: Validator<T>,
426
+ options?: { maxBytes?: number },
427
+ ) {
428
+ const text = (await rawBuffer(options?.maxBytes ?? DEFAULT_FORM_LIMIT)).toString('utf-8')
429
+ const out: Record<string, string[]> = {}
430
+ for (const [k, v] of new URLSearchParams(text)) {
431
+ if (out[k] === undefined) out[k] = []
432
+ out[k].push(v)
433
+ }
434
+ if (!validator) return out as unknown as T
435
+ try {
436
+ return validator.parse(out)
437
+ } catch (err) {
438
+ throw new ValidationError(err)
439
+ }
440
+ },
441
+ async multipart(options?: { maxBytes?: number }): Promise<MultipartResult> {
442
+ const ct = req.headers['content-type'] ?? ''
443
+ const boundary = extractMultipartParam(ct, 'boundary')
444
+ if (!boundary)
445
+ throw new Error(
446
+ '[davaux] ctx.multipart() requires a multipart/form-data request with a boundary',
447
+ )
448
+ const buf = await rawBuffer(options?.maxBytes ?? DEFAULT_MULTIPART_LIMIT)
449
+ return parseMultipart(buf, boundary)
450
+ },
451
+ send(body: string | Buffer, contentType: string, status = 200): void {
452
+ if (!res.headersSent) {
453
+ res.writeHead(status, { 'Content-Type': contentType })
454
+ res.end(body)
455
+ }
456
+ },
457
+ flash(key: string, value?: string): string | undefined {
458
+ const session = this.state.session as FlashSession | undefined
459
+ if (!session)
460
+ throw new Error(
461
+ '[davaux] ctx.flash() requires @davaux/session — add sessionMiddleware to your middleware config',
462
+ )
463
+ const NS = '_flash'
464
+ if (value !== undefined) {
465
+ const bag = (session.get(NS) as Record<string, string> | undefined) ?? {}
466
+ session.set(NS, { ...bag, [key]: value })
467
+ return
468
+ }
469
+ const bag = (session.get(NS) as Record<string, string> | undefined) ?? {}
470
+ const msg = bag[key]
471
+ if (msg !== undefined) {
472
+ const { [key as string]: _, ...rest } = bag
473
+ Object.keys(rest).length > 0 ? session.set(NS, rest) : session.delete(NS)
474
+ }
475
+ return msg
476
+ },
477
+ _deferredSlots: new Map<string, Promise<OmlNode | string>>(),
478
+ defer(name: string, content: Promise<OmlNode | string>): Promise<OmlNode> {
479
+ if (this._deferredSlots.size === 0) {
480
+ this.head.scripts.push('/_davaux/partial-updates.js')
481
+ }
482
+ this._deferredSlots.set(name, content)
483
+ return Promise.resolve({ type: '#raw' as const, value: `<?marker name="${name}">` })
484
+ },
485
+ }
486
+ }
487
+
488
+ // ─── Route file types ─────────────────────────────────────────────────────────
489
+
490
+ /** The HTTP method (or `'page'` for full-page SSR routes) a route file handles. */
491
+ export type RouteType = 'page' | 'get' | 'post' | 'put' | 'patch' | 'delete' | 'head' | 'options'
492
+
493
+ /** A discovered route file and its compiled URL metadata. */
494
+ export interface RouteFile {
495
+ filePath: string
496
+ urlPattern: string
497
+ type: RouteType
498
+ params: string[]
499
+ }
500
+
501
+ /** A discovered `_layout` file and the directory it covers. */
502
+ export interface LayoutFile {
503
+ /** Absolute path to the _layout.tsx file */
504
+ filePath: string
505
+ /** Absolute path to the directory this layout covers */
506
+ dirPath: string
507
+ }
508
+
509
+ /** A discovered `_middleware` file and the directory it covers. */
510
+ export interface MiddlewareFile {
511
+ /** Absolute path to the _middleware.ts file */
512
+ filePath: string
513
+ /** Absolute path to the directory this middleware covers */
514
+ dirPath: string
515
+ }
516
+
517
+ /** A discovered island component file and its derived hydration ID. */
518
+ export interface IslandFile {
519
+ /** Absolute path to the island source file */
520
+ filePath: string
521
+ /** Island ID derived from filename — used as the key in the hydration manifest */
522
+ id: string
523
+ }
524
+
525
+ /** The full result of scanning a routes directory. */
526
+ export interface ScanResult {
527
+ routes: RouteFile[]
528
+ layouts: LayoutFile[]
529
+ middlewares: MiddlewareFile[]
530
+ /** Absolute path to _error.tsx if one exists in the routes directory */
531
+ errorPage?: string
532
+ }
533
+
534
+ // ─── Handler types ────────────────────────────────────────────────────────────
535
+
536
+ /** Function signature for a `.page.*` route handler — receives context and returns HTML or an OmlNode. */
537
+ export type PageHandler<P extends Record<string, string> = Record<string, string>> = (
538
+ ctx: RequestContext<P>,
539
+ ) => string | OmlNode | Promise<string | OmlNode>
540
+
541
+ /** Function signature for an API route handler — receives context and returns a JSON-serialisable value. */
542
+ export type ApiHandler<T = unknown, P extends Record<string, string> = Record<string, string>> = (
543
+ ctx: RequestContext<P>,
544
+ ) => T | Promise<T>
545
+
546
+ /** Function signature for a `_layout.*` handler — receives rendered children and returns HTML or an OmlNode. */
547
+ export type LayoutHandler = (props: LayoutProps) => string | OmlNode | Promise<string | OmlNode>
548
+
549
+ /** Props passed to every layout handler. */
550
+ export interface LayoutProps {
551
+ /** Already-rendered inner HTML. Use as a JSX child or template literal interpolation — it will not be escaped. */
552
+ children: Promise<string>
553
+ ctx: RequestContext
554
+ }
555
+
556
+ // ─── Error page ───────────────────────────────────────────────────────────────
557
+
558
+ /** Props passed to the `_error.tsx` error page handler. */
559
+ export interface ErrorPageProps {
560
+ status: number
561
+ message: string
562
+ ctx: RequestContext
563
+ }
564
+
565
+ /** Function signature for the `_error.*` handler — receives status/message and returns HTML or an OmlNode. */
566
+ export type ErrorPageHandler = (
567
+ props: ErrorPageProps,
568
+ ) => string | OmlNode | Promise<string | OmlNode>
569
+
570
+ // ─── Developer ergonomics helpers ─────────────────────────────────────────────
571
+
572
+ /**
573
+ * Wraps a page handler so TypeScript infers the correct return type.
574
+ * Handlers may return an HTML string or an OmlNode — both are handled transparently.
575
+ *
576
+ * export default definePage(async (ctx) => <Layout>...</Layout>)
577
+ */
578
+ export function definePage<P extends Record<string, string> = Record<string, string>>(
579
+ fn: PageHandler<P>,
580
+ ): PageHandler<P> {
581
+ return fn
582
+ }
583
+
584
+ /**
585
+ * Wraps an API route handler.
586
+ *
587
+ * export default defineHandler(async (ctx) => ({ ok: true }))
588
+ */
589
+ export function defineHandler<
590
+ T = unknown,
591
+ P extends Record<string, string> = Record<string, string>,
592
+ >(fn: ApiHandler<T, P>): ApiHandler<T, P> {
593
+ return fn
594
+ }
595
+
596
+ /**
597
+ * Wraps a layout component.
598
+ *
599
+ * export default defineLayout(({ children, ctx }) => <html>...</html>)
600
+ */
601
+ export function defineLayout(fn: LayoutHandler): LayoutHandler {
602
+ return fn
603
+ }
604
+
605
+ /**
606
+ * Wraps the `_error.tsx` handler for type inference.
607
+ *
608
+ * export default defineError(({ status, message, ctx }) => (
609
+ * <div><h1>{status}</h1><p>{message}</p></div>
610
+ * ))
611
+ */
612
+ export function defineError(fn: ErrorPageHandler): ErrorPageHandler {
613
+ return fn
614
+ }
615
+
616
+ /** A middleware function: runs code before and/or after the next handler in the chain. */
617
+ export type MiddlewareFn = (ctx: RequestContext, next: () => Promise<void>) => Promise<void>
618
+
619
+ /**
620
+ * Wraps a middleware function for type inference.
621
+ *
622
+ * export default defineMiddleware(async (ctx, next) => {
623
+ * // run before the route handler
624
+ * await next()
625
+ * // run after
626
+ * })
627
+ */
628
+ export function defineMiddleware(fn: MiddlewareFn): MiddlewareFn {
629
+ return fn
630
+ }
631
+
632
+ /**
633
+ * Function signature for a form action handler.
634
+ * The return value (if any) is set as `ctx.state.actionResult` before the
635
+ * page re-renders. Throw `redirect()` to skip re-rendering entirely.
636
+ */
637
+ export type ActionFn<T = unknown> = (ctx: RequestContext) => T | Promise<T>
638
+
639
+ /**
640
+ * Wraps a form action handler co-located with a page file.
641
+ *
642
+ * export const action = defineAction(async (ctx) => {
643
+ * const { email, password } = await ctx.form()
644
+ * const user = await db.authenticate(email, password)
645
+ * if (!user) return { error: 'Invalid credentials' }
646
+ * redirect('/dashboard')
647
+ * })
648
+ */
649
+ export function defineAction<T = unknown>(fn: ActionFn<T>): ActionFn<T> {
650
+ return fn
651
+ }
652
+
653
+ // ─── Static generation ────────────────────────────────────────────────────────
654
+
655
+ /** A single set of params for a statically pre-rendered dynamic route. */
656
+ export type StaticPath = { params: Record<string, string> }
657
+
658
+ /** Function signature for `getStaticPaths` — returns all param sets to render at build time. */
659
+ export type GetStaticPathsFn = () => StaticPath[] | Promise<StaticPath[]>
660
+
661
+ /**
662
+ * Wraps a `getStaticPaths` export for type inference.
663
+ *
664
+ * export const getStaticPaths = defineStaticPaths(async () => [
665
+ * { params: { slug: 'hello-world' } },
666
+ * ])
667
+ */
668
+ export function defineStaticPaths(fn: GetStaticPathsFn): GetStaticPathsFn {
669
+ return fn
670
+ }
@@ -0,0 +1,17 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ESNext",
4
+ "module": "ESNext",
5
+ "moduleResolution": "Bundler",
6
+ "jsx": "react-jsx",
7
+ "jsxImportSource": "davaux/client",
8
+ "strict": true,
9
+ "outDir": "dist/client",
10
+ "rootDir": "client",
11
+ "declaration": true,
12
+ "declarationMap": true,
13
+ "lib": ["ESNext", "DOM"],
14
+ "skipLibCheck": true
15
+ },
16
+ "include": ["client/**/*"]
17
+ }