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,272 @@
1
+ import { existsSync } from 'node:fs'
2
+ import { readdir } from 'node:fs/promises'
3
+ import { basename, dirname, join, relative } from 'node:path'
4
+ import type {
5
+ IslandFile,
6
+ LayoutFile,
7
+ MiddlewareFile,
8
+ RouteFile,
9
+ RouteType,
10
+ ScanResult,
11
+ } from '../types.js'
12
+
13
+ // Maps file suffix → HTTP route type. Longer suffixes first (more specific).
14
+ const SUFFIX_MAP: [suffix: string, type: RouteType][] = [
15
+ ['.page.tsx', 'page'],
16
+ ['.page.ts', 'page'],
17
+ ['.page.jsx', 'page'],
18
+ ['.page.js', 'page'],
19
+ ['.get.tsx', 'get'],
20
+ ['.get.ts', 'get'],
21
+ ['.get.jsx', 'get'],
22
+ ['.get.js', 'get'],
23
+ ['.post.tsx', 'post'],
24
+ ['.post.ts', 'post'],
25
+ ['.post.jsx', 'post'],
26
+ ['.post.js', 'post'],
27
+ ['.put.tsx', 'put'],
28
+ ['.put.ts', 'put'],
29
+ ['.put.jsx', 'put'],
30
+ ['.put.js', 'put'],
31
+ ['.patch.tsx', 'patch'],
32
+ ['.patch.ts', 'patch'],
33
+ ['.patch.jsx', 'patch'],
34
+ ['.patch.js', 'patch'],
35
+ ['.delete.tsx', 'delete'],
36
+ ['.delete.ts', 'delete'],
37
+ ['.delete.jsx', 'delete'],
38
+ ['.delete.js', 'delete'],
39
+ ['.head.ts', 'head'],
40
+ ['.head.js', 'head'],
41
+ ['.options.ts', 'options'],
42
+ ['.options.js', 'options'],
43
+ ]
44
+
45
+ // Files beginning with _ are framework-reserved (layouts, middleware, etc.)
46
+ const LAYOUT_NAMES = new Set(['_layout.tsx', '_layout.ts', '_layout.jsx', '_layout.js'])
47
+
48
+ const ERROR_PAGE_NAMES = new Set(['_error.tsx', '_error.ts', '_error.jsx', '_error.js'])
49
+
50
+ const MIDDLEWARE_NAMES = new Set([
51
+ '_middleware.tsx',
52
+ '_middleware.ts',
53
+ '_middleware.jsx',
54
+ '_middleware.js',
55
+ ])
56
+
57
+ function resolveRouteSuffix(
58
+ filename: string,
59
+ suffixMap: [string, RouteType][],
60
+ ): [stripped: string, type: RouteType] | null {
61
+ for (const [suffix, type] of suffixMap) {
62
+ if (filename.endsWith(suffix)) return [filename.slice(0, -suffix.length), type]
63
+ }
64
+ return null
65
+ }
66
+
67
+ function segmentToParam(segment: string): {
68
+ pattern: string
69
+ param: string | null
70
+ } {
71
+ const catchAll = segment.match(/^\[\.\.\.([^\]]+)\]$/)
72
+ if (catchAll) return { pattern: `*${catchAll[1]}`, param: catchAll[1] }
73
+ const m = segment.match(/^\[([^\]]+)\]$/)
74
+ if (m) return { pattern: `:${m[1]}`, param: m[1] }
75
+ return { pattern: segment, param: null }
76
+ }
77
+
78
+ function fileToUrlPattern(rel: string): {
79
+ urlPattern: string
80
+ params: string[]
81
+ } {
82
+ const params: string[] = []
83
+ const segments = rel.split('/').map((seg) => {
84
+ const { pattern, param } = segmentToParam(seg)
85
+ if (param) params.push(param)
86
+ return pattern
87
+ })
88
+
89
+ if (segments.at(-1) === 'index') segments.pop()
90
+
91
+ const raw = `/${segments.join('/')}`
92
+ return { urlPattern: raw.replace(/\/+$/, '') || '/', params }
93
+ }
94
+
95
+ async function walk(dir: string, files: string[] = []): Promise<string[]> {
96
+ const entries = await readdir(dir, { withFileTypes: true })
97
+ for (const entry of entries) {
98
+ const full = join(dir, entry.name)
99
+ entry.isDirectory() ? await walk(full, files) : files.push(full)
100
+ }
101
+ return files
102
+ }
103
+
104
+ /**
105
+ * Recursively scan `islandsDir` for client island component files.
106
+ * Files beginning with `_` are ignored. Returns an empty array when the
107
+ * directory does not exist.
108
+ */
109
+ export async function scanIslands(islandsDir: string): Promise<IslandFile[]> {
110
+ if (!existsSync(islandsDir)) return []
111
+
112
+ const islands: IslandFile[] = []
113
+
114
+ async function walkIslands(dir: string): Promise<void> {
115
+ const entries = await readdir(dir, { withFileTypes: true })
116
+ for (const entry of entries) {
117
+ const full = join(dir, entry.name)
118
+ if (entry.isDirectory()) {
119
+ await walkIslands(full)
120
+ } else if (/\.(tsx?|jsx?)$/.test(entry.name) && !entry.name.startsWith('_')) {
121
+ const id = entry.name.replace(/\.(tsx?|jsx?)$/, '')
122
+ islands.push({ filePath: full, id })
123
+ }
124
+ }
125
+ }
126
+
127
+ await walkIslands(islandsDir)
128
+ return islands
129
+ }
130
+
131
+ // Which HTTP methods does each route type respond to?
132
+ // Mirrors the METHOD_TYPES map in handler.ts, inverted per type.
133
+ const TYPE_METHODS: Record<RouteType, string[]> = {
134
+ page: ['GET', 'HEAD', 'POST'], // POST only when an `action` export is present, but we warn conservatively
135
+ get: ['GET', 'HEAD'], // HEAD checks 'get' in its allowed list
136
+ post: ['POST'],
137
+ put: ['PUT'],
138
+ patch: ['PATCH'],
139
+ delete: ['DELETE'],
140
+ head: ['HEAD'],
141
+ options: ['OPTIONS'],
142
+ }
143
+
144
+ function checkConflicts(routes: RouteFile[], routesDir: string): void {
145
+ // Group routes by URL pattern — conflicts only possible within the same pattern.
146
+ const byPattern = new Map<string, RouteFile[]>()
147
+ for (const route of routes) {
148
+ const group = byPattern.get(route.urlPattern) ?? []
149
+ group.push(route)
150
+ byPattern.set(route.urlPattern, group)
151
+ }
152
+
153
+ for (const [pattern, group] of byPattern) {
154
+ if (group.length < 2) continue
155
+
156
+ for (let i = 0; i < group.length; i++) {
157
+ for (let j = i + 1; j < group.length; j++) {
158
+ const a = group[i]
159
+ const b = group[j]
160
+ const overlap = TYPE_METHODS[a.type].filter((m) => TYPE_METHODS[b.type].includes(m))
161
+ if (overlap.length === 0) continue
162
+
163
+ const aRel = relative(routesDir, a.filePath)
164
+ const bRel = relative(routesDir, b.filePath)
165
+
166
+ if (a.type === b.type) {
167
+ console.warn(
168
+ `[davaux] warning: duplicate ${a.type} route at ${pattern}\n` +
169
+ ` ${aRel}\n` +
170
+ ` ${bRel}\n` +
171
+ ` Only the first will be used.`,
172
+ )
173
+ } else {
174
+ // Explicit method routes sort before page routes, so they always win.
175
+ const winner = a.type !== 'page' ? aRel : bRel
176
+ console.warn(
177
+ `[davaux] warning: ${overlap.join(' + ')} ${pattern} matched by both ${aRel} (${a.type}) and ${bRel} (${b.type}) — ${winner} takes priority`,
178
+ )
179
+ }
180
+ }
181
+ }
182
+ }
183
+ }
184
+
185
+ /**
186
+ * Recursively scan `routesDir` for route, layout, middleware, and error-page files.
187
+ * Routes are sorted so static segments beat dynamic segments, and explicit
188
+ * HTTP-method routes beat page routes at the same URL pattern.
189
+ *
190
+ * @param extraSuffixes - Additional `[suffix, RouteType]` pairs contributed by
191
+ * plugins (e.g. `[['.page.md', 'page']]` from `@davaux/markdown`).
192
+ */
193
+ export async function scanRoutes(
194
+ routesDir: string,
195
+ extraSuffixes: [string, RouteType][] = [],
196
+ ): Promise<ScanResult> {
197
+ const suffixMap: [string, RouteType][] = [...SUFFIX_MAP, ...extraSuffixes]
198
+ const allFiles = await walk(routesDir)
199
+ const routes: RouteFile[] = []
200
+ const layouts: LayoutFile[] = []
201
+ const middlewares: MiddlewareFile[] = []
202
+ let errorPage: string | undefined
203
+
204
+ for (const filePath of allFiles) {
205
+ const rel = relative(routesDir, filePath)
206
+ const name = basename(rel)
207
+
208
+ // Framework-reserved file: layout
209
+ if (LAYOUT_NAMES.has(name)) {
210
+ layouts.push({ filePath, dirPath: dirname(filePath) })
211
+ continue
212
+ }
213
+
214
+ // Framework-reserved file: error page (first one wins)
215
+ if (ERROR_PAGE_NAMES.has(name)) {
216
+ errorPage ??= filePath
217
+ continue
218
+ }
219
+
220
+ // Framework-reserved file: scoped middleware
221
+ if (MIDDLEWARE_NAMES.has(name)) {
222
+ middlewares.push({ filePath, dirPath: dirname(filePath) })
223
+ continue
224
+ }
225
+
226
+ // Skip other underscore-prefixed files silently
227
+ if (name.startsWith('_')) continue
228
+
229
+ const resolved = resolveRouteSuffix(rel, suffixMap)
230
+ if (!resolved) {
231
+ // Warn about .tsx files sitting directly in the routes root — they look like
232
+ // components that belong in src/components/ rather than src/routes/.
233
+ if (name.endsWith('.tsx') && dirname(filePath) === routesDir) {
234
+ console.warn(
235
+ `[davaux] warning: ${rel} is not a route file and will be ignored.\n` +
236
+ ` If this is a shared component, move it to src/components/ or co-locate it\n` +
237
+ ` in a subdirectory alongside the routes that use it.`,
238
+ )
239
+ }
240
+ continue
241
+ }
242
+
243
+ const [stripped, type] = resolved
244
+ const { urlPattern, params } = fileToUrlPattern(stripped)
245
+ routes.push({ filePath, urlPattern, type, params })
246
+ }
247
+
248
+ checkConflicts(routes, routesDir)
249
+
250
+ // Explicit method routes (post, get, put, …) beat page routes at the same URL.
251
+ // This matters for POST: both `login.post.ts` and `login.page.tsx` (with defineAction)
252
+ // resolve to `/login`, but the explicit file should always win.
253
+ const TYPE_PRIORITY: Record<string, number> = { page: 1 } // all explicit methods default to 0
254
+
255
+ // Static segments before dynamic before catch-all; shorter paths before longer; explicit methods before pages.
256
+ const dynamicWeight = (pattern: string) => {
257
+ const segs = pattern.split('/')
258
+ if (segs.some((s) => s.startsWith('*'))) return 1000
259
+ return segs.filter((s) => s.startsWith(':')).length
260
+ }
261
+
262
+ routes.sort((a, b) => {
263
+ const ad = dynamicWeight(a.urlPattern)
264
+ const bd = dynamicWeight(b.urlPattern)
265
+ if (ad !== bd) return ad - bd
266
+ if (a.urlPattern.length !== b.urlPattern.length)
267
+ return a.urlPattern.length - b.urlPattern.length
268
+ return (TYPE_PRIORITY[a.type] ?? 0) - (TYPE_PRIORITY[b.type] ?? 0)
269
+ })
270
+
271
+ return { routes, layouts, middlewares, errorPage }
272
+ }
@@ -0,0 +1,49 @@
1
+ import { createServer, type IncomingMessage, type ServerResponse } from 'node:http'
2
+ import type { CompiledApp } from '../router/handler.js'
3
+ import { dispatch } from '../router/handler.js'
4
+
5
+ /** Options for `startServer()`. */
6
+ export interface ServerOptions {
7
+ /** TCP port to listen on. Default: `3000` */
8
+ port?: number
9
+ /** Hostname or IP address to bind to. Default: `'localhost'` */
10
+ hostname?: string
11
+ /**
12
+ * Called before the Davaux dispatcher on every request.
13
+ * Return `true` to signal that the request was fully handled (the dispatcher is skipped).
14
+ */
15
+ onRequest?: (req: IncomingMessage, res: ServerResponse) => boolean | Promise<boolean>
16
+ }
17
+
18
+ /**
19
+ * Start the HTTP server and begin accepting requests.
20
+ *
21
+ * @param app - A compiled app from `buildApp()`, or a factory function that returns one
22
+ * (useful in development when the app is rebuilt between requests).
23
+ * @returns The underlying `http.Server` instance.
24
+ */
25
+ export function startServer(app: CompiledApp | (() => CompiledApp), options: ServerOptions = {}) {
26
+ const { port = 3000, hostname = 'localhost', onRequest } = options
27
+ const getApp = typeof app === 'function' ? app : () => app
28
+
29
+ const server = createServer(async (req, res) => {
30
+ if (onRequest) {
31
+ const handled = await onRequest(req, res)
32
+ if (handled) return
33
+ }
34
+
35
+ dispatch(req, res, getApp()).catch((err) => {
36
+ console.error('[davaux] Unhandled dispatch error:', err)
37
+ if (!res.headersSent) {
38
+ res.writeHead(500, { 'Content-Type': 'text/plain' })
39
+ res.end('Internal Server Error')
40
+ }
41
+ })
42
+ })
43
+
44
+ server.listen(port, hostname, () => {
45
+ console.log(`\n davaux http://${hostname}:${port}\n`)
46
+ })
47
+
48
+ return server
49
+ }
package/src/signal.ts ADDED
@@ -0,0 +1,39 @@
1
+ // Server-side stubs — synchronous values, no reactive graph.
2
+ // The reactive implementations in davaux/client are used inside islands.
3
+
4
+ /** A reactive signal: a `[getter, setter]` tuple. On the server this is backed by a plain variable. */
5
+ export type Signal<T> = [get: () => T, set: (value: T) => void]
6
+
7
+ /** A read-only derived value: a zero-argument getter that returns the current result. */
8
+ export type ReadonlySignal<T> = () => T
9
+
10
+ /** Server-side stub — returns a getter/setter pair backed by a plain variable. */
11
+ export function createSignal<T>(value: T): Signal<T> {
12
+ return [
13
+ () => value,
14
+ (v: T) => {
15
+ value = v
16
+ },
17
+ ]
18
+ }
19
+
20
+ /** Server-side stub — runs `fn` once synchronously, no reactive tracking. */
21
+ export function createEffect(fn: () => void): void {
22
+ fn()
23
+ }
24
+
25
+ /** Server-side stub — evaluates `fn` immediately and returns a static getter. */
26
+ export function createMemo<T>(fn: () => T): ReadonlySignal<T> {
27
+ const result = fn()
28
+ return () => result
29
+ }
30
+
31
+ /** Server-side stub — calls `fn` directly with no tracking context. */
32
+ export function untrack<T>(fn: () => T): T {
33
+ return fn()
34
+ }
35
+
36
+ /** Server-side stub — calls `fn` directly with no batching. */
37
+ export function batch<T>(fn: () => T): T {
38
+ return fn()
39
+ }
package/src/ssg.ts ADDED
@@ -0,0 +1,253 @@
1
+ import { existsSync } from 'node:fs'
2
+ import { cp, mkdir, writeFile } from 'node:fs/promises'
3
+ import type { IncomingMessage, ServerResponse } from 'node:http'
4
+ import { dirname, join, relative } from 'node:path'
5
+ import { frameworkWarn } from './errors.js'
6
+ import { buildApp, dispatch } from './router/handler.js'
7
+ import type { GetStaticPathsFn, ScanResult } from './types.js'
8
+
9
+ /** Options for `generateStatic()`. All directory paths should be absolute. */
10
+ export interface SsgOptions {
11
+ /** Output directory for generated HTML files. */
12
+ outDir: string
13
+ /** Build output directory — used to locate `_davaux` assets to copy across. */
14
+ distDir: string
15
+ /** Public directory to copy into `outDir` after generation. */
16
+ publicDir: string
17
+ /** Scanned route manifest from `scanRoutes()`. */
18
+ scan: ScanResult
19
+ /** Client JS bundle URLs injected into every generated page. */
20
+ clientScripts: string[]
21
+ /** Client CSS URLs injected into every generated page. */
22
+ clientStylesheets: string[]
23
+ /** Compiled path to `src/middleware.ts` — applied to every SSG render request. */
24
+ appMiddlewarePath?: string
25
+ /**
26
+ * Controls the URL-to-filename mapping.
27
+ * - `'always'` — `/about` → `about/index.html` (default)
28
+ * - `'never'` — `/about` → `about.html`
29
+ */
30
+ trailingSlash?: 'always' | 'never'
31
+ /** Base path for subdirectory deployments (e.g. `'/docs'`). */
32
+ basePath?: string
33
+ /** Generate a `404.html` from `_error.tsx`. Default: `true` when `_error.tsx` is present. */
34
+ notFound?: boolean
35
+ /**
36
+ * Generate a `sitemap.xml`. Pass `false` to disable, or `{ baseUrl }` to enable.
37
+ * `baseUrl` is prepended to every URL (e.g. `'https://example.com'`).
38
+ */
39
+ sitemap?: false | { baseUrl: string }
40
+ }
41
+
42
+ function makeMockReq(pathname: string): IncomingMessage {
43
+ return {
44
+ method: 'GET',
45
+ url: pathname,
46
+ headers: { host: 'localhost', cookie: '' },
47
+ on(event: string, listener: (...args: unknown[]) => void) {
48
+ // Immediately signal end of body so ctx.json()/ctx.form() resolve empty
49
+ if (event === 'end') setImmediate(() => listener())
50
+ return this
51
+ },
52
+ removeListener() {
53
+ return this
54
+ },
55
+ } as unknown as IncomingMessage
56
+ }
57
+
58
+ function makeMockRes(): [ServerResponse, () => { status: number; html: string }] {
59
+ let status = 200
60
+ let body = ''
61
+ let sent = false
62
+ let ended = false
63
+ const hdrs: Record<string, string | string[]> = {}
64
+
65
+ const res = {
66
+ get statusCode() {
67
+ return status
68
+ },
69
+ set statusCode(v: number) {
70
+ status = v
71
+ },
72
+ get headersSent() {
73
+ return sent
74
+ },
75
+ get writableEnded() {
76
+ return ended
77
+ },
78
+ writeHead(code: number) {
79
+ status = code
80
+ sent = true
81
+ },
82
+ end(data?: string | Buffer) {
83
+ if (data != null) body = typeof data === 'string' ? data : data.toString('utf-8')
84
+ ended = true
85
+ },
86
+ getHeader: (name: string) => hdrs[name.toLowerCase()],
87
+ setHeader(name: string, value: string | string[]) {
88
+ hdrs[name.toLowerCase()] = value
89
+ },
90
+ } as unknown as ServerResponse
91
+
92
+ return [res, () => ({ status, html: body })]
93
+ }
94
+
95
+ function urlToFile(
96
+ outDir: string,
97
+ url: string,
98
+ trailingSlash: 'always' | 'never' = 'always',
99
+ ): string {
100
+ const trimmed = url.replace(/^\//, '').replace(/\/$/, '')
101
+ if (!trimmed) return join(outDir, 'index.html')
102
+ if (trailingSlash === 'never') return join(outDir, `${trimmed}.html`)
103
+ return join(outDir, trimmed, 'index.html')
104
+ }
105
+
106
+ function toSitemapUrl(
107
+ baseUrl: string,
108
+ basePath: string,
109
+ urlPath: string,
110
+ trailingSlash: 'always' | 'never',
111
+ ): string {
112
+ const base = baseUrl.replace(/\/$/, '')
113
+ const bp = basePath.replace(/\/$/, '')
114
+ const trimmed = urlPath.replace(/\/$/, '')
115
+ const path = !trimmed ? '/' : trailingSlash === 'never' ? trimmed : `${trimmed}/`
116
+ return `${base}${bp}${path}`
117
+ }
118
+
119
+ /**
120
+ * Pre-render all static pages in the route manifest and write them to `outDir`.
121
+ * Pages with `export const prerender = false` are skipped. Dynamic routes must
122
+ * export `getStaticPaths` to supply the parameter sets to render.
123
+ *
124
+ * Also generates `404.html` (from `_error.tsx`) and `sitemap.xml` when
125
+ * configured, and copies `_davaux` assets and `public/` into `outDir`.
126
+ */
127
+ export async function generateStatic(opts: SsgOptions): Promise<void> {
128
+ const {
129
+ outDir,
130
+ distDir,
131
+ publicDir,
132
+ scan,
133
+ appMiddlewarePath,
134
+ trailingSlash = 'always',
135
+ basePath = '',
136
+ notFound,
137
+ sitemap,
138
+ } = opts
139
+
140
+ // Prepend basePath to injected asset URLs so the rendered HTML references the correct paths
141
+ const clientScripts = opts.clientScripts.map((s) => `${basePath}${s}`)
142
+ const clientStylesheets = opts.clientStylesheets.map((s) => `${basePath}${s}`)
143
+
144
+ const app = buildApp(scan, false, clientScripts, clientStylesheets, appMiddlewarePath, basePath)
145
+
146
+ // Collect all URL paths to render
147
+ const toRender: string[] = []
148
+
149
+ for (const route of scan.routes) {
150
+ if (route.type !== 'page') continue
151
+
152
+ const mod = (await import(route.filePath)) as Record<string, unknown>
153
+ if (mod.prerender === false) continue
154
+
155
+ if (route.params.length === 0) {
156
+ toRender.push(route.urlPattern)
157
+ } else {
158
+ const fn = mod.getStaticPaths as GetStaticPathsFn | undefined
159
+ if (typeof fn !== 'function') {
160
+ frameworkWarn(
161
+ `[ssg] skipping ${route.urlPattern} — dynamic route has no getStaticPaths export`,
162
+ `Add a getStaticPaths export to ${relative(process.cwd(), route.filePath)}:\n\n` +
163
+ ` export const getStaticPaths = async () => [\n` +
164
+ ` { params: { /* param: value */ } },\n` +
165
+ ` ]`,
166
+ )
167
+ continue
168
+ }
169
+ const paths = await fn()
170
+ for (const sp of paths) {
171
+ let url = route.urlPattern
172
+ for (const [k, v] of Object.entries(sp.params)) url = url.replace(`:${k}`, String(v))
173
+ toRender.push(url)
174
+ }
175
+ }
176
+ }
177
+
178
+ await mkdir(outDir, { recursive: true })
179
+
180
+ let count = 0
181
+ const renderedUrls: string[] = []
182
+
183
+ for (const url of toRender) {
184
+ try {
185
+ const req = makeMockReq(url)
186
+ const [res, result] = makeMockRes()
187
+ await dispatch(req, res, app)
188
+ const { status, html } = result()
189
+
190
+ if (status >= 300) {
191
+ frameworkWarn(`[ssg] ${url} → HTTP ${status} — skipping`)
192
+ continue
193
+ }
194
+
195
+ const file = urlToFile(outDir, url, trailingSlash)
196
+ await mkdir(dirname(file), { recursive: true })
197
+ const full = html.trimStart().startsWith('<!') ? html : `<!DOCTYPE html>${html}`
198
+ await writeFile(file, full, 'utf-8')
199
+ renderedUrls.push(url)
200
+ count++
201
+ } catch (err) {
202
+ const detail = err instanceof Error ? err.message : String(err)
203
+ frameworkWarn(`[ssg] ${url} failed to render — skipping\n ${detail}`)
204
+ }
205
+ }
206
+
207
+ // Generate 404.html for static hosts (Netlify, GitHub Pages, etc.)
208
+ const shouldGenerateNotFound = notFound !== false && scan.errorPage != null
209
+ if (shouldGenerateNotFound) {
210
+ try {
211
+ const req = makeMockReq('/__davaux_404__')
212
+ const [res, result] = makeMockRes()
213
+ await dispatch(req, res, app)
214
+ const { html } = result()
215
+ const full = html.trimStart().startsWith('<!') ? html : `<!DOCTYPE html>${html}`
216
+ await writeFile(join(outDir, '404.html'), full, 'utf-8')
217
+ console.log('[davaux] Generated 404.html')
218
+ } catch (err) {
219
+ const detail = err instanceof Error ? err.message : String(err)
220
+ frameworkWarn(`[ssg] Failed to render 404.html — skipping\n ${detail}`)
221
+ }
222
+ }
223
+
224
+ // Generate sitemap.xml
225
+ if (sitemap !== false && sitemap != null) {
226
+ const entries = renderedUrls
227
+ .map(
228
+ (url) =>
229
+ ` <url><loc>${toSitemapUrl(sitemap.baseUrl, basePath, url, trailingSlash)}</loc></url>`,
230
+ )
231
+ .join('\n')
232
+ const xml =
233
+ `<?xml version="1.0" encoding="UTF-8"?>\n` +
234
+ `<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n` +
235
+ `${entries}\n` +
236
+ `</urlset>\n`
237
+ await writeFile(join(outDir, 'sitemap.xml'), xml, 'utf-8')
238
+ console.log(`[davaux] Generated sitemap.xml (${renderedUrls.length} URL(s))`)
239
+ }
240
+
241
+ // Copy _davaux assets (islands, client bundle, CSS)
242
+ const assetsDir = join(distDir, '_davaux')
243
+ if (existsSync(assetsDir)) {
244
+ await cp(assetsDir, join(outDir, '_davaux'), { recursive: true })
245
+ }
246
+
247
+ // Copy public/ directory if present
248
+ if (existsSync(publicDir)) {
249
+ await cp(publicDir, outDir, { recursive: true })
250
+ }
251
+
252
+ console.log(`[davaux] Generated ${count} static page(s) → ${relative(process.cwd(), outDir)}/`)
253
+ }
@@ -0,0 +1,40 @@
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('form actions', () => {
6
+ test('GET renders the page normally', async () => {
7
+ const app = makeApp({ routes: [route('action.page.ts', '/')] })
8
+ const { status, body } = await request(app, { url: '/' })
9
+ assert.equal(status, 200)
10
+ assert.ok(body.includes('form'))
11
+ assert.ok(!body.includes('submitted:'))
12
+ })
13
+
14
+ test('POST with action rerenders page with actionResult', async () => {
15
+ const app = makeApp({ routes: [route('action.page.ts', '/')] })
16
+ const { status, body } = await request(app, {
17
+ method: 'POST',
18
+ url: '/',
19
+ body: 'name=Alice',
20
+ })
21
+ assert.equal(status, 200)
22
+ assert.ok(body.includes('submitted:Alice'))
23
+ })
24
+
25
+ test('POST to page without action returns 405', async () => {
26
+ const app = makeApp({ routes: [route('index.page.ts', '/')] })
27
+ const { status } = await request(app, { method: 'POST', url: '/' })
28
+ assert.equal(status, 405)
29
+ })
30
+
31
+ test('action can parse multiple form fields', async () => {
32
+ const app = makeApp({ routes: [route('action.page.ts', '/')] })
33
+ const { body } = await request(app, {
34
+ method: 'POST',
35
+ url: '/',
36
+ body: 'name=Bob&extra=ignored',
37
+ })
38
+ assert.ok(body.includes('submitted:Bob'))
39
+ })
40
+ })