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,121 @@
1
+ // OML JSX runtime — produces an OmlNode tree instead of an HTML string.
2
+ // Use via `jsxImportSource` in tsconfig, or import jsx directly for programmatic use.
3
+ // Mirrors the API of src/jsx-runtime.ts; async components are fully supported.
4
+
5
+ import type {
6
+ OmlComponent,
7
+ OmlElement,
8
+ OmlFragment,
9
+ OmlNode,
10
+ OmlProps,
11
+ OmlRaw,
12
+ OmlText,
13
+ } from './types.js'
14
+
15
+ type Primitive = string | number | boolean | null | undefined
16
+ // Promise<string> is included so standard-runtime components (e.g. @davaux/ui) can be used
17
+ // as children in OML pages without TypeScript errors. The runtime handles both at render time.
18
+ type Child = Primitive | Promise<OmlNode | string> | Child[]
19
+
20
+ export type Props = {
21
+ children?: Child | Child[]
22
+ key?: string | number
23
+ ref?: unknown
24
+ dangerouslySetInnerHTML?: { __html: string }
25
+ style?: string | Record<string, string | number>
26
+ className?: string
27
+ htmlFor?: string
28
+ tabIndex?: number
29
+ [prop: string]: unknown
30
+ }
31
+
32
+ export type ComponentType<P extends Props = Props> = (props: P) => Promise<OmlNode> | OmlNode
33
+
34
+ async function resolveChildren(children: Child | Child[] | undefined): Promise<OmlNode[]> {
35
+ if (children == null || children === false) return []
36
+ if (Array.isArray(children)) {
37
+ const parts = await Promise.all(children.map((c) => resolveChildren(c)))
38
+ return parts.flat()
39
+ }
40
+ if (children instanceof Promise) {
41
+ const resolved = await children
42
+ if (resolved === null) return []
43
+ // A Promise<string> carries raw HTML (e.g. layout children passed from renderPage).
44
+ // Wrap as #raw so it passes through unescaped.
45
+ if (typeof resolved === 'string') return [{ type: '#raw', value: resolved } satisfies OmlRaw]
46
+ return [resolved as OmlNode]
47
+ }
48
+ if (typeof children === 'boolean') return []
49
+ const text: OmlText = { type: '#text', value: String(children) }
50
+ return [text]
51
+ }
52
+
53
+ // ─── JSX factory ───────────────────────────────────────────────────────────────
54
+
55
+ export async function jsx(
56
+ type: string | ComponentType,
57
+ props: Props,
58
+ key?: string | number,
59
+ ): Promise<OmlNode> {
60
+ const { children, key: _key, ref: _ref, ...restProps } = props
61
+ const id = key != null ? String(key) : undefined
62
+
63
+ if (typeof type === 'function') {
64
+ const result = type(props)
65
+ const rawResult = result instanceof Promise ? await result : result
66
+ // Components may return raw HTML strings (e.g. island server stubs). Wrap in #raw so
67
+ // renderToHtml passes the content through without escaping.
68
+ const resolved: OmlNode =
69
+ typeof rawResult === 'string'
70
+ ? ({ type: '#raw', value: rawResult } satisfies OmlRaw)
71
+ : (rawResult as OmlNode)
72
+ const resolvedChildren = await resolveChildren(children)
73
+ const node: OmlComponent = {
74
+ type: '#component',
75
+ name: type.name || 'Anonymous',
76
+ id,
77
+ props: restProps as OmlProps,
78
+ ...(resolvedChildren.length > 0 ? { children: resolvedChildren } : {}),
79
+ output: resolved,
80
+ }
81
+ return node
82
+ }
83
+
84
+ if (type === Fragment) {
85
+ const nodes = await resolveChildren(children)
86
+ const node: OmlFragment = { type: '#fragment', children: nodes }
87
+ return node
88
+ }
89
+
90
+ const nodes = await resolveChildren(children)
91
+ const node: OmlElement = {
92
+ type: 'element',
93
+ tag: type as string,
94
+ id,
95
+ props: restProps as OmlProps,
96
+ children: nodes,
97
+ }
98
+ return node
99
+ }
100
+
101
+ export const jsxs = jsx
102
+ export const jsxDEV = jsx
103
+
104
+ export const Fragment = '__OmlFragment__'
105
+
106
+ // ─── JSX namespace — required for `jsxImportSource: "davaux/oml"` in tsconfig ──
107
+ // Re-uses IntrinsicElements from the main runtime; only JSX.Element differs.
108
+ import type { JSX as BaseJSX } from '../jsx-runtime.js'
109
+
110
+ export namespace JSX {
111
+ // Promise<string> is included so standard-runtime components are valid JSX elements
112
+ // in OML pages (e.g. when jsxImportSource: "davaux/oml" is set globally in tsconfig).
113
+ export type Element = Promise<OmlNode | string>
114
+ export interface ElementChildrenAttribute {
115
+ children: object
116
+ }
117
+ export interface IntrinsicAttributes {
118
+ key?: string | number
119
+ }
120
+ export type IntrinsicElements = BaseJSX.IntrinsicElements
121
+ }
package/src/oml/jsx.ts ADDED
@@ -0,0 +1,151 @@
1
+ // OML → JSX serializer.
2
+ // Converts an OmlNode tree back to JSX source text, and an OmlBlueprint to a
3
+ // complete .tsx component file. Enables round-trip export from Blueprint JSON.
4
+
5
+ import type { OmlBlueprint, OmlNode, OmlPropType } from './types.js'
6
+
7
+ const VOID_TAGS = new Set([
8
+ 'area',
9
+ 'base',
10
+ 'br',
11
+ 'col',
12
+ 'embed',
13
+ 'hr',
14
+ 'img',
15
+ 'input',
16
+ 'link',
17
+ 'meta',
18
+ 'param',
19
+ 'source',
20
+ 'track',
21
+ 'wbr',
22
+ ])
23
+
24
+ const PROP_TS_TYPE: Record<OmlPropType, string> = {
25
+ string: 'string',
26
+ number: 'number',
27
+ boolean: 'boolean',
28
+ function: '() => void',
29
+ node: 'string',
30
+ array: 'unknown[]',
31
+ }
32
+
33
+ // ─── Prop serialization ────────────────────────────────────────────────────────
34
+
35
+ function escapeAttr(s: string): string {
36
+ return s.replace(/&/g, '&amp;').replace(/"/g, '&quot;')
37
+ }
38
+
39
+ function propToJsx(key: string, value: unknown): string {
40
+ if (value === null || value === undefined) return ''
41
+ if (value === true) return ` ${key}`
42
+ if (value === false) return ` ${key}={false}`
43
+ if (typeof value === 'string') return ` ${key}="${escapeAttr(value)}"`
44
+ if (typeof value === 'number') return ` ${key}={${value}}`
45
+ return ` ${key}={${JSON.stringify(value)}}`
46
+ }
47
+
48
+ function propsToJsx(props: Record<string, unknown>): string {
49
+ return Object.entries(props)
50
+ .filter(([k]) => k !== 'dangerouslySetInnerHTML' && k !== 'key' && k !== 'ref')
51
+ .map(([k, v]) => propToJsx(k, v))
52
+ .join('')
53
+ }
54
+
55
+ // ─── Node serialization ────────────────────────────────────────────────────────
56
+
57
+ function nodeToJsx(node: OmlNode, depth: number): string {
58
+ if (node === null) return ''
59
+
60
+ const pad = ' '.repeat(depth)
61
+ const childPad = ' '.repeat(depth + 1)
62
+
63
+ if (node.type === '#text') return node.value
64
+
65
+ if (node.type === '#fragment') {
66
+ if (node.children.length === 0) return '<></>'
67
+ const children = node.children
68
+ .map((c) => nodeToJsx(c, depth + 1))
69
+ .filter(Boolean)
70
+ .map((c) => `${childPad}${c}`)
71
+ .join('\n')
72
+ return `<>\n${children}\n${pad}</>`
73
+ }
74
+
75
+ if (node.type === '#component') {
76
+ const props = propsToJsx(node.props)
77
+ return `<${node.name}${props} />`
78
+ }
79
+
80
+ if (node.type === '#raw') return `{/* raw html */}`
81
+
82
+ // element
83
+ const { tag, props, children } = node
84
+ const attrs = propsToJsx(props)
85
+
86
+ if (VOID_TAGS.has(tag)) return `<${tag}${attrs} />`
87
+ if (children.length === 0) return `<${tag}${attrs}></${tag}>`
88
+
89
+ // Single text child: keep inline
90
+ if (children.length === 1 && children[0]?.type === '#text') {
91
+ const { dangerouslySetInnerHTML } = props as { dangerouslySetInnerHTML?: { __html: string } }
92
+ if (dangerouslySetInnerHTML) {
93
+ return `<${tag}${attrs} dangerouslySetInnerHTML={{ __html: ${JSON.stringify(dangerouslySetInnerHTML.__html)} }} />`
94
+ }
95
+ return `<${tag}${attrs}>${children[0].value}</${tag}>`
96
+ }
97
+
98
+ const inner = children
99
+ .map((c) => nodeToJsx(c, depth + 1))
100
+ .filter(Boolean)
101
+ .map((c) => `${childPad}${c}`)
102
+ .join('\n')
103
+ return `<${tag}${attrs}>\n${inner}\n${pad}</${tag}>`
104
+ }
105
+
106
+ /**
107
+ * Render an OmlNode tree to a JSX expression string.
108
+ * `#component` nodes render as component invocations (`<Name prop="val" />`),
109
+ * not as their resolved output — preserving the component boundary.
110
+ */
111
+ export function renderToJsx(node: OmlNode): string {
112
+ return nodeToJsx(node, 0)
113
+ }
114
+
115
+ // ─── Blueprint → .tsx file ─────────────────────────────────────────────────────
116
+
117
+ /**
118
+ * Convert an OmlBlueprint to a complete `.tsx` component file.
119
+ * Import paths are sourced from `blueprint.imports`; missing entries are
120
+ * left as bare identifiers for the developer to resolve.
121
+ */
122
+ export function blueprintToFile(blueprint: OmlBlueprint): string {
123
+ const lines: string[] = []
124
+
125
+ // Import statements
126
+ for (const [name, path] of Object.entries(blueprint.imports ?? {})) {
127
+ lines.push(`import ${name} from '${path}'`)
128
+ }
129
+ if (lines.length > 0) lines.push('')
130
+
131
+ // Props type
132
+ const propEntries = Object.entries(blueprint.props)
133
+ const propsType =
134
+ propEntries.length > 0
135
+ ? `{ ${propEntries.map(([n, s]) => `${n}${s.required ? '' : '?'}: ${PROP_TS_TYPE[s.type]}`).join(', ')} }`
136
+ : ''
137
+
138
+ const paramList =
139
+ propEntries.length > 0 ? `{ ${propEntries.map(([n]) => n).join(', ')} }: ${propsType}` : ''
140
+
141
+ lines.push(`export default function ${blueprint.name}(${paramList}) {`)
142
+ lines.push(' return (')
143
+
144
+ const body = nodeToJsx(blueprint.output, 2)
145
+ lines.push(` ${body}`)
146
+
147
+ lines.push(' )')
148
+ lines.push('}')
149
+
150
+ return lines.join('\n')
151
+ }
@@ -0,0 +1,13 @@
1
+ import type { PageHandler, RequestContext } from '../types.js'
2
+ import { definePage } from '../types.js'
3
+ import type { OmlNode } from './types.js'
4
+
5
+ /** Narrower alias for handlers that explicitly return OmlNode. Equivalent to PageHandler at runtime. */
6
+ export type OmlPageHandler<P extends Record<string, string> = Record<string, string>> = (
7
+ ctx: RequestContext<P>,
8
+ ) => OmlNode | Promise<OmlNode>
9
+
10
+ /** Alias for definePage — kept for clarity when a handler is intentionally OML-only. */
11
+ export const defineOmlPage: <P extends Record<string, string> = Record<string, string>>(
12
+ fn: OmlPageHandler<P>,
13
+ ) => PageHandler<P> = definePage
@@ -0,0 +1,181 @@
1
+ // OML renderer — converts an OmlNode tree to an HTML string.
2
+ // Separating rendering from tree construction enables caching, diffing, and
3
+ // multi-target output (HTML, email, etc.) from the same tree.
4
+
5
+ import type { OmlNode, OmlProps } from './types.js'
6
+
7
+ const VOID_ELEMENTS = new Set([
8
+ 'area',
9
+ 'base',
10
+ 'br',
11
+ 'col',
12
+ 'embed',
13
+ 'hr',
14
+ 'img',
15
+ 'input',
16
+ 'link',
17
+ 'meta',
18
+ 'param',
19
+ 'source',
20
+ 'track',
21
+ 'wbr',
22
+ ])
23
+
24
+ function escapeHtml(s: string): string {
25
+ return s
26
+ .replace(/&/g, '&amp;')
27
+ .replace(/</g, '&lt;')
28
+ .replace(/>/g, '&gt;')
29
+ .replace(/"/g, '&quot;')
30
+ .replace(/'/g, '&#39;')
31
+ }
32
+
33
+ function styleToString(style: Record<string, string | number>): string {
34
+ return Object.entries(style)
35
+ .map(([k, v]) => {
36
+ const prop = k.replace(/[A-Z]/g, (c) => `-${c.toLowerCase()}`)
37
+ return `${prop}:${v}`
38
+ })
39
+ .join(';')
40
+ }
41
+
42
+ function renderProps(props: OmlProps | null | undefined): string {
43
+ if (!props) return ''
44
+ const parts: string[] = []
45
+
46
+ for (const [key, val] of Object.entries(props)) {
47
+ if (key === 'key' || key === 'ref' || key === 'dangerouslySetInnerHTML') continue
48
+ if (val === false || val == null) continue
49
+
50
+ const attr =
51
+ key === 'className'
52
+ ? 'class'
53
+ : key === 'htmlFor'
54
+ ? 'for'
55
+ : key === 'tabIndex'
56
+ ? 'tabindex'
57
+ : key
58
+
59
+ if (attr.startsWith('on') && attr[2] === attr[2]?.toUpperCase()) continue
60
+
61
+ if (val === true) {
62
+ parts.push(attr)
63
+ continue
64
+ }
65
+
66
+ if (attr === 'style') {
67
+ const css =
68
+ typeof val === 'string' ? val : styleToString(val as Record<string, string | number>)
69
+ parts.push(`style="${escapeHtml(css)}"`)
70
+ continue
71
+ }
72
+
73
+ parts.push(`${attr}="${escapeHtml(String(val))}"`)
74
+ }
75
+
76
+ return parts.length ? ` ${parts.join(' ')}` : ''
77
+ }
78
+
79
+ /** Render an OmlNode tree to an HTML string. */
80
+ export function renderToHtml(node: OmlNode): string {
81
+ if (node === null) return ''
82
+ if (typeof (node as unknown) === 'string') return node as unknown as string
83
+
84
+ if (node.type === '#text') return escapeHtml(node.value)
85
+
86
+ if (node.type === '#fragment') return node.children.map(renderToHtml).join('')
87
+
88
+ if (node.type === '#component') return renderToHtml(node.output)
89
+
90
+ if (node.type === '#raw') return node.value
91
+
92
+ // OmlElement
93
+ const { tag, props, children } = node
94
+ const attrs = renderProps(props)
95
+
96
+ if (VOID_ELEMENTS.has(tag)) return `<${tag}${attrs}>`
97
+
98
+ const dsi = (props as { dangerouslySetInnerHTML?: { __html: string } } | null | undefined)
99
+ ?.dangerouslySetInnerHTML
100
+ if (dsi) {
101
+ return `<${tag}${attrs}>${dsi.__html}</${tag}>`
102
+ }
103
+
104
+ return `<${tag}${attrs}>${children.map(renderToHtml).join('')}</${tag}>`
105
+ }
106
+
107
+ function injectOmlAttrs(html: string, name: string, inst: number): string {
108
+ return html.replace(/(<[a-zA-Z][a-zA-Z0-9-]*)(\s|>|\/)/, (_, tag, after) => {
109
+ return `${tag} data-oml-comp="${name}" data-oml-inst="${inst}"${after}`
110
+ })
111
+ }
112
+
113
+ type DevCounters = Map<string, number>
114
+
115
+ /**
116
+ * For #raw-returning components whose JSX children are stored on the node, inject
117
+ * child markers into the parent's raw HTML via string replacement. Each child's
118
+ * original raw HTML is found in the parent string and replaced with the marked version.
119
+ * This works because library components include their children's HTML verbatim.
120
+ */
121
+ function injectChildMarkers(parentHtml: string, children: OmlNode[], counters: DevCounters): string {
122
+ let html = parentHtml
123
+ for (const child of children) {
124
+ if (!child) continue
125
+ if (child.type === '#component' && child.output?.type === '#raw') {
126
+ const childInst = counters.get(child.name) ?? 0
127
+ counters.set(child.name, childInst + 1)
128
+ let childHtml = child.output.value
129
+ if (child.children && child.children.length > 0) {
130
+ childHtml = injectChildMarkers(childHtml, child.children, counters)
131
+ }
132
+ const markedChildHtml = injectOmlAttrs(childHtml, child.name, childInst)
133
+ html = html.replace(child.output.value, markedChildHtml)
134
+ } else if (child.type === '#fragment') {
135
+ html = injectChildMarkers(html, child.children, counters)
136
+ }
137
+ }
138
+ return html
139
+ }
140
+
141
+ /**
142
+ * Dev-mode renderer — identical to renderToHtml but injects `data-oml-comp` and
143
+ * `data-oml-inst` attributes into the first HTML tag of #raw-returning components
144
+ * and recursively into their JSX children. This lets the visual editor map clicked
145
+ * DOM elements back to their OML #component nodes.
146
+ */
147
+ export function renderToHtmlDev(node: OmlNode, counters: DevCounters = new Map()): string {
148
+ if (node === null) return ''
149
+ if (typeof (node as unknown) === 'string') return node as unknown as string
150
+
151
+ if (node.type === '#text') return escapeHtml(node.value)
152
+
153
+ if (node.type === '#fragment')
154
+ return node.children.map((c) => renderToHtmlDev(c, counters)).join('')
155
+
156
+ if (node.type === '#raw') return node.value
157
+
158
+ if (node.type === '#component') {
159
+ const inst = counters.get(node.name) ?? 0
160
+ counters.set(node.name, inst + 1)
161
+ if (node.output?.type === '#raw') {
162
+ let html = node.output.value
163
+ if (node.children && node.children.length > 0) {
164
+ html = injectChildMarkers(html, node.children, counters)
165
+ }
166
+ return injectOmlAttrs(html, node.name, inst)
167
+ }
168
+ return renderToHtmlDev(node.output, counters)
169
+ }
170
+
171
+ const { tag, props, children } = node
172
+ const attrs = renderProps(props)
173
+
174
+ if (VOID_ELEMENTS.has(tag)) return `<${tag}${attrs}>`
175
+
176
+ const dsi = (props as { dangerouslySetInnerHTML?: { __html: string } } | null | undefined)
177
+ ?.dangerouslySetInnerHTML
178
+ if (dsi) return `<${tag}${attrs}>${dsi.__html}</${tag}>`
179
+
180
+ return `<${tag}${attrs}>${children.map((c) => renderToHtmlDev(c, counters)).join('')}</${tag}>`
181
+ }
@@ -0,0 +1,159 @@
1
+ // OML — Object Markup Language
2
+ // A stable, serializable intermediate representation of a rendered component tree.
3
+ // Produced by the OML JSX runtime; consumed by the OML renderer and visual editor.
4
+
5
+ export type OmlProps = Record<string, unknown>
6
+
7
+ /** A plain text node. */
8
+ export type OmlText = {
9
+ type: '#text'
10
+ value: string
11
+ }
12
+
13
+ /** A raw HTML passthrough node — not escaped; used for island server stubs and similar. */
14
+ export type OmlRaw = {
15
+ type: '#raw'
16
+ value: string
17
+ }
18
+
19
+ /** A rendered HTML element. */
20
+ export type OmlElement = {
21
+ type: 'element'
22
+ /** The HTML tag name (div, span, button, etc.). */
23
+ tag: string
24
+ /** Stable identity from the JSX `key` prop — used for diffing and caching. */
25
+ id?: string
26
+ props: OmlProps
27
+ children: OmlNode[]
28
+ }
29
+
30
+ /**
31
+ * A rendered component call — preserves component boundaries for the visual
32
+ * editor and inspector. The `return` field is what the component rendered.
33
+ */
34
+ export type OmlComponent = {
35
+ type: '#component'
36
+ /** Component function name — shown in the inspector. */
37
+ name: string
38
+ id?: string
39
+ /** Props passed to the component (children excluded). */
40
+ props: OmlProps
41
+ /**
42
+ * Resolved JSX children passed to the component — stored separately from `output`
43
+ * so the visual editor can show the JSX hierarchy even when `output` is #raw
44
+ * (i.e. when a library component renders to an HTML string).
45
+ */
46
+ children?: OmlNode[]
47
+ /** The component's rendered output. */
48
+ output: OmlNode
49
+ }
50
+
51
+ /** A JSX Fragment — a grouping with no wrapper element. */
52
+ export type OmlFragment = {
53
+ type: '#fragment'
54
+ children: OmlNode[]
55
+ }
56
+
57
+ export type OmlNode = OmlElement | OmlComponent | OmlFragment | OmlText | OmlRaw | null
58
+
59
+ // ─── Blueprint — JSON component definition ─────────────────────────────────────
60
+ // Stored in .oml.json files; naturally maps to SurrealDB records.
61
+
62
+ export type OmlPropType = 'string' | 'number' | 'boolean' | 'function' | 'node' | 'array'
63
+
64
+ export type OmlPropSchema = {
65
+ type: OmlPropType
66
+ default?: unknown
67
+ required?: boolean
68
+ description?: string
69
+ }
70
+
71
+ /**
72
+ * A component definition stored in a Blueprint JSON file.
73
+ * The `return` field is the root OmlNode of the component's render tree.
74
+ * Wires (signal connections) between nodes are stored as id references.
75
+ * `imports` maps component names used in the tree to their module paths,
76
+ * enabling round-trip export back to .tsx without losing import information.
77
+ */
78
+ export type OmlBlueprint = {
79
+ id: string
80
+ name: string
81
+ props: Record<string, OmlPropSchema>
82
+ output: OmlNode
83
+ imports?: Record<string, string>
84
+ }
85
+
86
+ // ─── JSON parse / validate ─────────────────────────────────────────────────────
87
+
88
+ function isObject(v: unknown): v is Record<string, unknown> {
89
+ return typeof v === 'object' && v !== null && !Array.isArray(v)
90
+ }
91
+
92
+ function parseNode(v: unknown): OmlNode {
93
+ if (v === null) return null
94
+ if (!isObject(v)) throw new Error(`OML: expected node object, got ${typeof v}`)
95
+
96
+ const { type } = v
97
+ if (type === '#text') {
98
+ if (typeof v.value !== 'string') throw new Error('OML: #text node missing string value')
99
+ return { type: '#text', value: v.value }
100
+ }
101
+ if (type === '#fragment') {
102
+ return { type: '#fragment', children: parseChildren(v.children) }
103
+ }
104
+ if (type === '#component') {
105
+ if (typeof v.name !== 'string') throw new Error('OML: #component node missing name')
106
+ const parsed: OmlComponent = {
107
+ type: '#component',
108
+ name: v.name,
109
+ id: typeof v.id === 'string' ? v.id : undefined,
110
+ props: isObject(v.props) ? (v.props as OmlProps) : {},
111
+ output: parseNode(v.output),
112
+ }
113
+ if (Array.isArray(v.children) && v.children.length > 0) {
114
+ parsed.children = parseChildren(v.children)
115
+ }
116
+ return parsed
117
+ }
118
+ if (type === 'element') {
119
+ if (typeof v.tag !== 'string') throw new Error('OML: element node missing tag')
120
+ return {
121
+ type: 'element',
122
+ tag: v.tag,
123
+ id: typeof v.id === 'string' ? v.id : undefined,
124
+ props: isObject(v.props) ? (v.props as OmlProps) : {},
125
+ children: parseChildren(v.children),
126
+ }
127
+ }
128
+ throw new Error(`OML: unknown node type "${type}"`)
129
+ }
130
+
131
+ function parseChildren(v: unknown): OmlNode[] {
132
+ if (!Array.isArray(v)) return []
133
+ return v.map(parseNode)
134
+ }
135
+
136
+ /**
137
+ * Parse and validate an `OmlNode` from untrusted JSON (e.g. a Blueprint file).
138
+ * Throws a descriptive error if the shape is invalid.
139
+ */
140
+ export function parseOml(json: unknown): OmlNode {
141
+ return parseNode(json)
142
+ }
143
+
144
+ /**
145
+ * Parse and validate an `OmlBlueprint` from untrusted JSON.
146
+ * Throws a descriptive error if the shape is invalid.
147
+ */
148
+ export function parseOmlBlueprint(json: unknown): OmlBlueprint {
149
+ if (!isObject(json)) throw new Error('OML: blueprint must be an object')
150
+ if (typeof json.id !== 'string') throw new Error('OML: blueprint missing id')
151
+ if (typeof json.name !== 'string') throw new Error('OML: blueprint missing name')
152
+ return {
153
+ id: json.id,
154
+ name: json.name,
155
+ props: isObject(json.props) ? (json.props as Record<string, OmlPropSchema>) : {},
156
+ output: parseNode(json.output),
157
+ imports: isObject(json.imports) ? (json.imports as Record<string, string>) : undefined,
158
+ }
159
+ }