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,83 @@
1
+ import assert from 'node:assert/strict'
2
+ import { describe, test } from 'node:test'
3
+ import { makeApp, request, route } from './helpers.js'
4
+
5
+ describe('ctx.send()', () => {
6
+ test('responds with custom content-type and body', async () => {
7
+ const app = makeApp({ routes: [route('api/xml.get.ts', '/api/xml', 'get')] })
8
+ const { status, body, headers } = await request(app, { url: '/api/xml' })
9
+ assert.equal(status, 200)
10
+ assert.equal(body, '<root><item>hello</item></root>')
11
+ assert.equal(headers['content-type'], 'application/xml; charset=utf-8')
12
+ })
13
+
14
+ test('ctx.send() with custom status code', async () => {
15
+ const app = makeApp({ routes: [route('api/xml.get.ts', '/api/xml', 'get')] })
16
+ const { status } = await request(app, { url: '/api/xml' })
17
+ assert.equal(status, 200)
18
+ })
19
+ })
20
+
21
+ describe('ctx.formAll()', () => {
22
+ const app = makeApp({ routes: [route('api/form-all.post.ts', '/form-all', 'post')] })
23
+
24
+ test('single-value fields are wrapped in arrays', async () => {
25
+ const { status, body } = await request(app, {
26
+ method: 'POST',
27
+ url: '/form-all',
28
+ body: 'title=Hello&author=Alice',
29
+ })
30
+ assert.equal(status, 200)
31
+ const data = JSON.parse(body)
32
+ assert.deepEqual(data.title, ['Hello'])
33
+ assert.deepEqual(data.author, ['Alice'])
34
+ })
35
+
36
+ test('multi-value fields collect all values', async () => {
37
+ const { status, body } = await request(app, {
38
+ method: 'POST',
39
+ url: '/form-all',
40
+ body: 'tag=tech&tag=news&tag=coding',
41
+ })
42
+ assert.equal(status, 200)
43
+ const data = JSON.parse(body)
44
+ assert.deepEqual(data.tag, ['tech', 'news', 'coding'])
45
+ })
46
+
47
+ test('mixed single and multi-value fields', async () => {
48
+ const { status, body } = await request(app, {
49
+ method: 'POST',
50
+ url: '/form-all',
51
+ body: 'title=Post&tag=a&tag=b',
52
+ })
53
+ assert.equal(status, 200)
54
+ const data = JSON.parse(body)
55
+ assert.deepEqual(data.title, ['Post'])
56
+ assert.deepEqual(data.tag, ['a', 'b'])
57
+ })
58
+ })
59
+
60
+ describe('body size limits', () => {
61
+ test('form body within limit succeeds', async () => {
62
+ const app = makeApp({ routes: [route('api/form-limited.post.ts', '/upload', 'post')] })
63
+ const { status, body } = await request(app, {
64
+ method: 'POST',
65
+ url: '/upload',
66
+ body: 'name=Alice',
67
+ })
68
+ assert.equal(status, 200)
69
+ const data = JSON.parse(body) as { ok: boolean; keys: number }
70
+ assert.equal(data.ok, true)
71
+ assert.equal(data.keys, 1)
72
+ })
73
+
74
+ test('form body exceeding limit returns 413', async () => {
75
+ const app = makeApp({ routes: [route('api/form-limited.post.ts', '/upload', 'post')] })
76
+ const { status } = await request(app, {
77
+ method: 'POST',
78
+ url: '/upload',
79
+ body: `name=${'x'.repeat(100)}`, // 105 bytes > 50 byte limit
80
+ })
81
+ assert.equal(status, 413)
82
+ })
83
+ })
@@ -0,0 +1,53 @@
1
+ import assert from 'node:assert/strict'
2
+ import { describe, test } from 'node:test'
3
+ import { fix, layout, makeApp, request, route } from './helpers.js'
4
+
5
+ const errorPage = fix('_error.ts')
6
+
7
+ describe('error pages', () => {
8
+ test('unmatched route returns 404 plain text when no error page is configured', async () => {
9
+ const app = makeApp({ routes: [] })
10
+ const { status, body } = await request(app, { url: '/missing' })
11
+ assert.equal(status, 404)
12
+ assert.ok(body.includes('Not Found'))
13
+ })
14
+
15
+ test('unmatched route renders custom error page with status 404', async () => {
16
+ const app = makeApp({ routes: [], errorPage })
17
+ const { status, body } = await request(app, { url: '/missing' })
18
+ assert.equal(status, 404)
19
+ const result = JSON.parse(body) as Record<string, unknown>
20
+ assert.equal(result.status, 404)
21
+ })
22
+
23
+ test('handler that throws renders custom error page with status 500', async () => {
24
+ const app = makeApp({
25
+ routes: [route('throw.page.ts', '/')],
26
+ errorPage,
27
+ })
28
+ const { status, body } = await request(app, { url: '/' })
29
+ assert.equal(status, 500)
30
+ const result = JSON.parse(body) as Record<string, unknown>
31
+ assert.equal(result.status, 500)
32
+ assert.ok(typeof result.message === 'string')
33
+ })
34
+
35
+ test('error page inherits layout when one is configured', async () => {
36
+ const app = makeApp({
37
+ routes: [],
38
+ layouts: [layout('_layout.ts')],
39
+ errorPage,
40
+ })
41
+ const { status, body } = await request(app, { url: '/missing' })
42
+ assert.equal(status, 404)
43
+ // The layout wraps the error page output
44
+ assert.ok(body.includes('<html>'))
45
+ })
46
+
47
+ test('error page receives ctx.state populated by app middleware', async () => {
48
+ const app = makeApp({ routes: [], errorPage }, fix('_global.ts'))
49
+ const { body } = await request(app, { url: '/missing' })
50
+ const result = JSON.parse(body) as Record<string, unknown>
51
+ assert.deepEqual(result.order, ['global'])
52
+ })
53
+ })
@@ -0,0 +1,3 @@
1
+ import { definePage, type ExtractParams } from '../../../index.js'
2
+
3
+ export default definePage<ExtractParams<'[id]'>>((ctx) => `<h1>Item: ${ctx.params.id}</h1>`)
@@ -0,0 +1,6 @@
1
+ import { defineError } from '../../../index.js'
2
+
3
+ // Outputs status + message + ctx.state as JSON so tests can inspect all three.
4
+ export default defineError(({ status, message, ctx }) =>
5
+ JSON.stringify({ status, message, ...ctx.state }),
6
+ )
@@ -0,0 +1,8 @@
1
+ import { defineMiddleware } from '../../../index.js'
2
+
3
+ // Pushes 'global' onto ctx.state.order so middleware ordering can be verified.
4
+ export default defineMiddleware(async (ctx, next) => {
5
+ if (!ctx.state.order) ctx.state.order = []
6
+ ;(ctx.state.order as string[]).push('global')
7
+ await next()
8
+ })
@@ -0,0 +1,7 @@
1
+ import { defineLayout } from '../../../index.js'
2
+
3
+ // Template literal layout using ${children} directly — no await needed.
4
+ export default defineLayout(({ children, ctx }) => {
5
+ const title = ctx.head.title ?? 'Test'
6
+ return `<html><head><title>${title}</title></head><body>${children}</body></html>`
7
+ })
@@ -0,0 +1,7 @@
1
+ import { defineLayout } from '../../../index.js'
2
+
3
+ export default defineLayout(async ({ children, ctx }) => {
4
+ const title = ctx.head.title ?? 'Test'
5
+ const content = await children
6
+ return `<html><head><title>${title}</title></head><body>${content}</body></html>`
7
+ })
@@ -0,0 +1,8 @@
1
+ import { defineLayout } from '../../../index.js'
2
+
3
+ export default defineLayout(async ({ children, ctx }) => {
4
+ const content = await children
5
+ const scripts = ctx.head.scripts.map((s) => `<script src="${s}"></script>`).join('')
6
+ const styles = ctx.head.stylesheets.map((s) => `<link rel="stylesheet" href="${s}">`).join('')
7
+ return `<html><head>${styles}${scripts}</head><body>${content}</body></html>`
8
+ })
@@ -0,0 +1,8 @@
1
+ import { defineMiddleware } from '../../../index.js'
2
+
3
+ // Pushes 'scoped' onto ctx.state.order so middleware ordering can be verified.
4
+ export default defineMiddleware(async (ctx, next) => {
5
+ if (!ctx.state.order) ctx.state.order = []
6
+ ;(ctx.state.order as string[]).push('scoped')
7
+ await next()
8
+ })
@@ -0,0 +1,5 @@
1
+ import { defineMiddleware, redirect } from '../../../index.js'
2
+
3
+ export default defineMiddleware(async (_ctx, _next) => {
4
+ redirect('/new-home', 301)
5
+ })
@@ -0,0 +1,5 @@
1
+ import { defineMiddleware, redirect } from '../../../index.js'
2
+
3
+ export default defineMiddleware(async (_ctx, _next) => {
4
+ redirect('/login')
5
+ })
@@ -0,0 +1,6 @@
1
+ import { definePage } from '../../../index.js'
2
+
3
+ export default definePage((ctx) => {
4
+ ctx.head.title = 'About'
5
+ return '<h1>About</h1>'
6
+ })
@@ -0,0 +1,11 @@
1
+ import { defineAction, definePage } from '../../../index.js'
2
+
3
+ export const action = defineAction(async (ctx) => {
4
+ const data = await ctx.form()
5
+ return { submitted: data.name ?? '(none)' }
6
+ })
7
+
8
+ export default definePage((ctx) => {
9
+ const result = ctx.state.actionResult as { submitted: string } | undefined
10
+ return result ? `submitted:${result.submitted}` : 'form'
11
+ })
@@ -0,0 +1,5 @@
1
+ import { defineHandler } from '../../../../index.js'
2
+
3
+ export default defineHandler(async (ctx) => {
4
+ return ctx.formAll()
5
+ })
@@ -0,0 +1,6 @@
1
+ import { defineHandler } from '../../../../index.js'
2
+
3
+ export default defineHandler(async (ctx) => {
4
+ const data = await ctx.form(undefined, { maxBytes: 50 })
5
+ return { ok: true, keys: Object.keys(data).length }
6
+ })
@@ -0,0 +1,17 @@
1
+ import { defineHandler } from '../../../../index.js'
2
+
3
+ export default defineHandler(async (ctx) => {
4
+ const shape = ctx.query.get('shape') ?? 'json'
5
+
6
+ if (shape === 'text') {
7
+ return new Response('hello plain', { status: 200, headers: { 'Content-Type': 'text/plain' } })
8
+ }
9
+ if (shape === 'created') {
10
+ return Response.json({ created: true }, { status: 201 })
11
+ }
12
+ if (shape === 'nocontent') {
13
+ return new Response(null, { status: 204 })
14
+ }
15
+
16
+ return Response.json({ ok: true })
17
+ })
@@ -0,0 +1,14 @@
1
+ import { defineHandler } from '../../../../index.js'
2
+
3
+ export default defineHandler(async (ctx) => {
4
+ const result = await ctx.multipart()
5
+ return {
6
+ fields: result.fields,
7
+ files: Object.fromEntries(
8
+ Object.entries(result.files).map(([k, f]) => [
9
+ k,
10
+ { filename: f.filename, mimetype: f.mimetype, size: f.data.length },
11
+ ]),
12
+ ),
13
+ }
14
+ })
@@ -0,0 +1,3 @@
1
+ import { defineHandler } from '../../../../index.js'
2
+
3
+ export default defineHandler(() => [{ id: 1, name: 'Alice' }])
@@ -0,0 +1,5 @@
1
+ import { defineHandler } from '../../../../index.js'
2
+
3
+ export default defineHandler((ctx) => {
4
+ ctx.send('<root><item>hello</item></root>', 'application/xml; charset=utf-8')
5
+ })
@@ -0,0 +1,11 @@
1
+ import { defineMiddleware } from '../../../../index.js'
2
+
3
+ // Returns 401 when no ?token query param is present.
4
+ export default defineMiddleware(async (ctx, next) => {
5
+ if (!ctx.query.get('token')) {
6
+ ctx.res.writeHead(401, { 'Content-Type': 'text/plain' })
7
+ ctx.res.end('Unauthorized')
8
+ return
9
+ }
10
+ await next()
11
+ })
@@ -0,0 +1,3 @@
1
+ import { definePage } from '../../../../index.js'
2
+
3
+ export default definePage(() => '<p>Protected content</p>')
@@ -0,0 +1,3 @@
1
+ import { definePage } from '../../../index.js'
2
+
3
+ export default definePage(() => '<h1>Home</h1>')
@@ -0,0 +1,7 @@
1
+ import { jsx } from '../../../oml/jsx-runtime.js'
2
+ import { definePage } from '../../../types.js'
3
+
4
+ export default definePage(async (ctx) => {
5
+ const name = ctx.query.get('name') ?? 'World'
6
+ return jsx('div', { class: 'oml-page', children: `Hello, ${name}!` })
7
+ })
@@ -0,0 +1,3 @@
1
+ import { definePage, redirect } from '../../../index.js'
2
+
3
+ export default definePage(() => redirect('/about'))
@@ -0,0 +1,8 @@
1
+ import { definePage, defineStaticPaths, type ExtractParams } from '../../../../index.js'
2
+
3
+ export const getStaticPaths = defineStaticPaths(() => [
4
+ { params: { slug: 'hello' } },
5
+ { params: { slug: 'world' } },
6
+ ])
7
+
8
+ export default definePage<ExtractParams<'ssg/[slug]'>>((ctx) => `<h1>${ctx.params.slug}</h1>`)
@@ -0,0 +1,5 @@
1
+ import { definePage } from '../../../../index.js'
2
+
3
+ export const prerender = false
4
+
5
+ export default definePage(() => '<h1>Server rendered</h1>')
@@ -0,0 +1,4 @@
1
+ import { definePage } from '../../../index.js'
2
+
3
+ // Outputs ctx.state as JSON so tests can verify middleware side-effects.
4
+ export default definePage((ctx) => JSON.stringify(ctx.state))
@@ -0,0 +1,5 @@
1
+ import { definePage } from '../../../index.js'
2
+
3
+ export default definePage(() => {
4
+ throw new Error('intentional test error')
5
+ })
@@ -0,0 +1,3 @@
1
+ import { definePage } from '../../../../index.js'
2
+
3
+ export default definePage((ctx) => `<h1>Wiki: ${ctx.params.slug}</h1>`)
@@ -0,0 +1,132 @@
1
+ import type { IncomingMessage, ServerResponse } from 'node:http'
2
+ import { dirname, join } from 'node:path'
3
+ import { fileURLToPath } from 'node:url'
4
+ import type { OmlCacheConfig } from '../config.js'
5
+ import type { CompiledApp } from '../router/handler.js'
6
+ import { buildApp, dispatch } from '../router/handler.js'
7
+ import type { LayoutFile, MiddlewareFile, RouteFile, RouteType, ScanResult } from '../types.js'
8
+
9
+ const FIXTURES = fileURLToPath(new URL('./fixtures/routes', import.meta.url))
10
+
11
+ /** Resolve a path relative to the fixtures/routes directory. */
12
+ export function fix(...parts: string[]): string {
13
+ return join(FIXTURES, ...parts)
14
+ }
15
+
16
+ /** Build a RouteFile pointing to a fixture, deriving params from the URL pattern. */
17
+ export function route(file: string, urlPattern: string, type: RouteType = 'page'): RouteFile {
18
+ const params = [
19
+ ...[...urlPattern.matchAll(/:(\w+)/g)].map((m) => m[1]),
20
+ ...[...urlPattern.matchAll(/\*(\w+)/g)].map((m) => m[1]),
21
+ ]
22
+ return { filePath: fix(file), urlPattern, type, params }
23
+ }
24
+
25
+ /** Build a LayoutFile pointing to a fixture. */
26
+ export function layout(file: string): LayoutFile {
27
+ const filePath = fix(file)
28
+ return { filePath, dirPath: dirname(filePath) }
29
+ }
30
+
31
+ /** Build a MiddlewareFile pointing to a fixture. */
32
+ export function middleware(file: string): MiddlewareFile {
33
+ const filePath = fix(file)
34
+ return { filePath, dirPath: dirname(filePath) }
35
+ }
36
+
37
+ /** Construct a minimal ScanResult, defaulting all collections to empty. */
38
+ export function makeScan(opts: Partial<ScanResult> = {}): ScanResult {
39
+ return {
40
+ routes: [],
41
+ layouts: [],
42
+ middlewares: [],
43
+ errorPage: undefined,
44
+ ...opts,
45
+ }
46
+ }
47
+
48
+ /** Build a CompiledApp from a partial ScanResult and an optional app middleware path. */
49
+ export function makeApp(
50
+ scan: Partial<ScanResult> = {},
51
+ appMiddlewarePath?: string,
52
+ omlCacheConfig?: OmlCacheConfig,
53
+ ): CompiledApp {
54
+ return buildApp(makeScan(scan), false, [], [], appMiddlewarePath, '', false, omlCacheConfig)
55
+ }
56
+
57
+ // ─── Mock request ─────────────────────────────────────────────────────────────
58
+
59
+ export function mockReq(options: {
60
+ method?: string
61
+ url: string
62
+ headers?: Record<string, string>
63
+ body?: string
64
+ }): IncomingMessage {
65
+ const { method = 'GET', url, headers = {}, body = '' } = options
66
+ return {
67
+ method,
68
+ url,
69
+ headers: { host: 'localhost', ...headers },
70
+ on(event: string, listener: (...args: unknown[]) => void) {
71
+ if (event === 'data' && body) setImmediate(() => listener(Buffer.from(body)))
72
+ if (event === 'end') setImmediate(() => listener())
73
+ return this
74
+ },
75
+ removeListener() {
76
+ return this
77
+ },
78
+ destroy() {
79
+ return this
80
+ },
81
+ } as unknown as IncomingMessage
82
+ }
83
+
84
+ // ─── Mock response ────────────────────────────────────────────────────────────
85
+
86
+ export function mockRes() {
87
+ let status = 200
88
+ let body = ''
89
+ let sent = false
90
+ let ended = false
91
+ const hdrs: Record<string, string | string[]> = {}
92
+
93
+ const res = {
94
+ get statusCode() {
95
+ return status
96
+ },
97
+ set statusCode(v: number) {
98
+ status = v
99
+ },
100
+ get headersSent() {
101
+ return sent
102
+ },
103
+ get writableEnded() {
104
+ return ended
105
+ },
106
+ writeHead(code: number, h?: Record<string, string>) {
107
+ status = code
108
+ sent = true
109
+ if (h) for (const [k, v] of Object.entries(h)) hdrs[k.toLowerCase()] = v
110
+ },
111
+ end(data?: string | Buffer) {
112
+ if (data != null) body = typeof data === 'string' ? data : data.toString('utf-8')
113
+ ended = true
114
+ },
115
+ getHeader: (name: string) => hdrs[name.toLowerCase()],
116
+ setHeader(name: string, value: string | string[]) {
117
+ hdrs[name.toLowerCase()] = value
118
+ },
119
+ } as unknown as ServerResponse
120
+
121
+ return { res, result: () => ({ status, body, headers: hdrs }) }
122
+ }
123
+
124
+ // ─── Convenience dispatch helper ──────────────────────────────────────────────
125
+
126
+ /** Run a request through an app and return the captured response. */
127
+ export async function request(app: CompiledApp, options: Parameters<typeof mockReq>[0]) {
128
+ const req = mockReq(options)
129
+ const { res, result } = mockRes()
130
+ await dispatch(req, res, app)
131
+ return result()
132
+ }
@@ -0,0 +1,76 @@
1
+ import assert from 'node:assert/strict'
2
+ import { describe, test } from 'node:test'
3
+ import { layout, makeApp, request, route } from './helpers.js'
4
+
5
+ describe('layouts', () => {
6
+ test('layout wraps page content with outer HTML shell', async () => {
7
+ const app = makeApp({
8
+ routes: [route('index.page.ts', '/')],
9
+ layouts: [layout('_layout.ts')],
10
+ })
11
+ const { status, body } = await request(app, { url: '/' })
12
+ assert.equal(status, 200)
13
+ assert.ok(body.includes('<html>'))
14
+ assert.ok(body.includes('</html>'))
15
+ assert.ok(body.includes('Home'))
16
+ })
17
+
18
+ test('page content appears inside layout body', async () => {
19
+ const app = makeApp({
20
+ routes: [route('about.page.ts', '/about')],
21
+ layouts: [layout('_layout.ts')],
22
+ })
23
+ const { body } = await request(app, { url: '/about' })
24
+ // The layout fixture wraps with <body>...</body>
25
+ assert.ok(body.includes('<body>'))
26
+ assert.ok(body.includes('<h1>About</h1>'))
27
+ assert.ok(body.includes('</body>'))
28
+ })
29
+
30
+ test('ctx.head properties set by page are readable in layout', async () => {
31
+ const app = makeApp({
32
+ routes: [route('about.page.ts', '/about')],
33
+ layouts: [layout('_layout.ts')],
34
+ })
35
+ const { body } = await request(app, { url: '/about' })
36
+ // The layout fixture renders ctx.head.title in <title>
37
+ assert.ok(body.includes('<title>About</title>'))
38
+ })
39
+
40
+ test('layout does not apply to routes outside its directory', async () => {
41
+ // layout('_layout.ts') covers the root fixtures dir
42
+ // If we only register it for /auth, it should not wrap the root route
43
+ // We test the inverse: root layout covers all descendant routes
44
+ const app = makeApp({
45
+ routes: [route('auth/protected.page.ts', '/auth/protected')],
46
+ layouts: [layout('_layout.ts')],
47
+ })
48
+ const { body } = await request(app, { url: '/auth/protected?token=x' })
49
+ // The root layout covers /auth/protected because it's a descendant
50
+ assert.ok(body.includes('<html>'))
51
+ assert.ok(body.includes('Protected'))
52
+ })
53
+
54
+ test('template literal layout renders children without await', async () => {
55
+ const app = makeApp({
56
+ routes: [route('about.page.ts', '/about')],
57
+ layouts: [layout('_layout-template.ts')],
58
+ })
59
+ const { status, body } = await request(app, { url: '/about' })
60
+ assert.equal(status, 200)
61
+ // Children must be injected as raw HTML, not "[object Promise]"
62
+ assert.ok(body.includes('<h1>About</h1>'), 'page content rendered')
63
+ assert.ok(!body.includes('[object'), 'no Promise leak into output')
64
+ assert.ok(body.includes('<title>About</title>'), 'ctx.head.title readable')
65
+ })
66
+
67
+ test('response includes DOCTYPE when layout provides full HTML', async () => {
68
+ const app = makeApp({
69
+ routes: [route('index.page.ts', '/')],
70
+ layouts: [layout('_layout.ts')],
71
+ })
72
+ const { body } = await request(app, { url: '/' })
73
+ // sendPage prepends DOCTYPE if not present; the layout starts with <html>
74
+ assert.ok(body.startsWith('<!DOCTYPE html>') || body.includes('<html>'))
75
+ })
76
+ })
@@ -0,0 +1,69 @@
1
+ import assert from 'node:assert/strict'
2
+ import { describe, test } from 'node:test'
3
+ import { fix, makeApp, middleware, request, route } from './helpers.js'
4
+
5
+ describe('middleware', () => {
6
+ test('scoped middleware runs before the route handler', async () => {
7
+ const app = makeApp({
8
+ routes: [route('state.page.ts', '/')],
9
+ middlewares: [middleware('_middleware.ts')],
10
+ })
11
+ const { body } = await request(app, { url: '/' })
12
+ const state = JSON.parse(body) as Record<string, unknown>
13
+ assert.deepEqual(state.order, ['scoped'])
14
+ })
15
+
16
+ test('scoped middleware does not run for routes in other directories', async () => {
17
+ // auth/_middleware.ts only covers auth/*, not root routes
18
+ const app = makeApp({
19
+ routes: [route('state.page.ts', '/'), route('auth/protected.page.ts', '/auth/protected')],
20
+ middlewares: [middleware('auth/_middleware.ts')],
21
+ })
22
+ // Root route should not be intercepted by the auth middleware
23
+ const { status } = await request(app, { url: '/' })
24
+ assert.equal(status, 200)
25
+ })
26
+
27
+ test('scoped middleware gates access to routes it covers', async () => {
28
+ const app = makeApp({
29
+ routes: [route('auth/protected.page.ts', '/auth/protected')],
30
+ middlewares: [middleware('auth/_middleware.ts')],
31
+ })
32
+ // Without token → 401
33
+ const denied = await request(app, { url: '/auth/protected' })
34
+ assert.equal(denied.status, 401)
35
+
36
+ // With token → 200
37
+ const allowed = await request(app, { url: '/auth/protected?token=secret' })
38
+ assert.equal(allowed.status, 200)
39
+ assert.ok(allowed.body.includes('Protected'))
40
+ })
41
+
42
+ test('app middleware runs on matched routes', async () => {
43
+ const app = makeApp({ routes: [route('state.page.ts', '/')] }, fix('_global.ts'))
44
+ const { body } = await request(app, { url: '/' })
45
+ const state = JSON.parse(body) as Record<string, unknown>
46
+ assert.deepEqual(state.order, ['global'])
47
+ })
48
+
49
+ test('app middleware runs on 404 responses', async () => {
50
+ const app = makeApp({ routes: [], errorPage: fix('_error.ts') }, fix('_global.ts'))
51
+ const { body } = await request(app, { url: '/missing' })
52
+ const result = JSON.parse(body) as Record<string, unknown>
53
+ assert.equal(result.status, 404)
54
+ assert.deepEqual(result.order, ['global'])
55
+ })
56
+
57
+ test('app middleware runs before scoped middleware', async () => {
58
+ const app = makeApp(
59
+ {
60
+ routes: [route('state.page.ts', '/')],
61
+ middlewares: [middleware('_middleware.ts')],
62
+ },
63
+ fix('_global.ts'),
64
+ )
65
+ const { body } = await request(app, { url: '/' })
66
+ const state = JSON.parse(body) as Record<string, unknown>
67
+ assert.deepEqual(state.order, ['global', 'scoped'])
68
+ })
69
+ })