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.
- package/BASELINE.md +169 -0
- package/CLAUDE.md +518 -0
- package/LICENSE +21 -0
- package/README.md +36 -0
- package/ROADMAP.md +198 -0
- package/build.mjs +101 -0
- package/client/control.ts +247 -0
- package/client/hydrate.ts +37 -0
- package/client/index.ts +19 -0
- package/client/jsx-runtime.ts +209 -0
- package/client/resource.ts +122 -0
- package/client/signal.ts +211 -0
- package/client/store.ts +110 -0
- package/client/useHead.ts +63 -0
- package/dist/build/config.d.ts +3 -0
- package/dist/build/config.d.ts.map +1 -0
- package/dist/build/config.js +38 -0
- package/dist/build/config.js.map +7 -0
- package/dist/build/index.d.ts +2 -0
- package/dist/build/index.d.ts.map +1 -0
- package/dist/build/index.js +13 -0
- package/dist/build/index.js.map +7 -0
- package/dist/build/plugins.d.ts +7 -0
- package/dist/build/plugins.d.ts.map +1 -0
- package/dist/build/plugins.js +85 -0
- package/dist/build/plugins.js.map +7 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +427 -0
- package/dist/cli.js.map +7 -0
- package/dist/client/control.d.ts +49 -0
- package/dist/client/control.d.ts.map +1 -0
- package/dist/client/control.js +154 -0
- package/dist/client/control.js.map +7 -0
- package/dist/client/hydrate.d.ts +7 -0
- package/dist/client/hydrate.d.ts.map +1 -0
- package/dist/client/hydrate.js +23 -0
- package/dist/client/hydrate.js.map +7 -0
- package/dist/client/index.d.ts +12 -0
- package/dist/client/index.d.ts.map +1 -0
- package/dist/client/index.js +32 -0
- package/dist/client/index.js.map +7 -0
- package/dist/client/jsx-runtime.d.ts +40 -0
- package/dist/client/jsx-runtime.d.ts.map +1 -0
- package/dist/client/jsx-runtime.js +139 -0
- package/dist/client/jsx-runtime.js.map +7 -0
- package/dist/client/resource.d.ts +31 -0
- package/dist/client/resource.d.ts.map +1 -0
- package/dist/client/resource.js +64 -0
- package/dist/client/resource.js.map +7 -0
- package/dist/client/signal.d.ts +90 -0
- package/dist/client/signal.d.ts.map +1 -0
- package/dist/client/signal.js +115 -0
- package/dist/client/signal.js.map +7 -0
- package/dist/client/store.d.ts +26 -0
- package/dist/client/store.d.ts.map +1 -0
- package/dist/client/store.js +63 -0
- package/dist/client/store.js.map +7 -0
- package/dist/client/useHead.d.ts +28 -0
- package/dist/client/useHead.d.ts.map +1 -0
- package/dist/client/useHead.js +33 -0
- package/dist/client/useHead.js.map +7 -0
- package/dist/config.d.ts +182 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +21 -0
- package/dist/config.js.map +7 -0
- package/dist/create-multisite.d.ts +2 -0
- package/dist/create-multisite.d.ts.map +1 -0
- package/dist/create-multisite.js +291 -0
- package/dist/create-multisite.js.map +7 -0
- package/dist/create.d.ts +2 -0
- package/dist/create.d.ts.map +1 -0
- package/dist/create.js +179 -0
- package/dist/create.js.map +7 -0
- package/dist/dev/blueprints.d.ts +11 -0
- package/dist/dev/blueprints.d.ts.map +1 -0
- package/dist/dev/blueprints.js +65 -0
- package/dist/dev/blueprints.js.map +7 -0
- package/dist/dev/components.d.ts +19 -0
- package/dist/dev/components.d.ts.map +1 -0
- package/dist/dev/components.js +87 -0
- package/dist/dev/components.js.map +7 -0
- package/dist/dev/insert.d.ts +11 -0
- package/dist/dev/insert.d.ts.map +1 -0
- package/dist/dev/insert.js +160 -0
- package/dist/dev/insert.js.map +7 -0
- package/dist/dev/remove.d.ts +53 -0
- package/dist/dev/remove.d.ts.map +1 -0
- package/dist/dev/remove.js +518 -0
- package/dist/dev/remove.js.map +7 -0
- package/dist/dev/watch.d.ts +26 -0
- package/dist/dev/watch.d.ts.map +1 -0
- package/dist/dev/watch.js +2905 -0
- package/dist/dev/watch.js.map +7 -0
- package/dist/errors.d.ts +6 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +63 -0
- package/dist/errors.js.map +7 -0
- package/dist/generate.d.ts +2 -0
- package/dist/generate.d.ts.map +1 -0
- package/dist/generate.js +191 -0
- package/dist/generate.js.map +7 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +57 -0
- package/dist/index.js.map +7 -0
- package/dist/island.d.ts +24 -0
- package/dist/island.d.ts.map +1 -0
- package/dist/island.js +15 -0
- package/dist/island.js.map +7 -0
- package/dist/jsx-runtime.d.ts +406 -0
- package/dist/jsx-runtime.d.ts.map +1 -0
- package/dist/jsx-runtime.js +90 -0
- package/dist/jsx-runtime.js.map +7 -0
- package/dist/link.d.ts +27 -0
- package/dist/link.d.ts.map +1 -0
- package/dist/link.js +29 -0
- package/dist/link.js.map +7 -0
- package/dist/oml/fragment.d.ts +16 -0
- package/dist/oml/fragment.d.ts.map +1 -0
- package/dist/oml/fragment.js +26 -0
- package/dist/oml/fragment.js.map +7 -0
- package/dist/oml/index.d.ts +11 -0
- package/dist/oml/index.d.ts.map +1 -0
- package/dist/oml/index.js +21 -0
- package/dist/oml/index.js.map +7 -0
- package/dist/oml/jsx-runtime.d.ts +34 -0
- package/dist/oml/jsx-runtime.d.ts.map +1 -0
- package/dist/oml/jsx-runtime.js +59 -0
- package/dist/oml/jsx-runtime.js.map +7 -0
- package/dist/oml/jsx.d.ts +14 -0
- package/dist/oml/jsx.d.ts.map +1 -0
- package/dist/oml/jsx.js +96 -0
- package/dist/oml/jsx.js.map +7 -0
- package/dist/oml/page.d.ts +7 -0
- package/dist/oml/page.d.ts.map +1 -0
- package/dist/oml/page.js +6 -0
- package/dist/oml/page.js.map +7 -0
- package/dist/oml/render.d.ts +13 -0
- package/dist/oml/render.d.ts.map +1 -0
- package/dist/oml/render.js +117 -0
- package/dist/oml/render.js.map +7 -0
- package/dist/oml/types.d.ts +79 -0
- package/dist/oml/types.d.ts.map +1 -0
- package/dist/oml/types.js +64 -0
- package/dist/oml/types.js.map +7 -0
- package/dist/router/handler.d.ts +53 -0
- package/dist/router/handler.d.ts.map +1 -0
- package/dist/router/handler.js +342 -0
- package/dist/router/handler.js.map +7 -0
- package/dist/router/matcher.d.ts +21 -0
- package/dist/router/matcher.d.ts.map +1 -0
- package/dist/router/matcher.js +28 -0
- package/dist/router/matcher.js.map +7 -0
- package/dist/router/scanner.d.ts +17 -0
- package/dist/router/scanner.d.ts.map +1 -0
- package/dist/router/scanner.js +197 -0
- package/dist/router/scanner.js.map +7 -0
- package/dist/server/index.d.ts +23 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/index.js +29 -0
- package/dist/server/index.js.map +7 -0
- package/dist/signal.d.ts +15 -0
- package/dist/signal.d.ts.map +1 -0
- package/dist/signal.js +29 -0
- package/dist/signal.js.map +7 -0
- package/dist/ssg.d.ts +45 -0
- package/dist/ssg.d.ts.map +1 -0
- package/dist/ssg.js +175 -0
- package/dist/ssg.js.map +7 -0
- package/dist/test/actions.test.d.ts +2 -0
- package/dist/test/actions.test.d.ts.map +1 -0
- package/dist/test/body-limits.test.d.ts +2 -0
- package/dist/test/body-limits.test.d.ts.map +1 -0
- package/dist/test/errors.test.d.ts +2 -0
- package/dist/test/errors.test.d.ts.map +1 -0
- package/dist/test/fixtures/routes/[id].page.d.ts +4 -0
- package/dist/test/fixtures/routes/[id].page.d.ts.map +1 -0
- package/dist/test/fixtures/routes/_error.d.ts +3 -0
- package/dist/test/fixtures/routes/_error.d.ts.map +1 -0
- package/dist/test/fixtures/routes/_global.d.ts +3 -0
- package/dist/test/fixtures/routes/_global.d.ts.map +1 -0
- package/dist/test/fixtures/routes/_layout-template.d.ts +3 -0
- package/dist/test/fixtures/routes/_layout-template.d.ts.map +1 -0
- package/dist/test/fixtures/routes/_layout.d.ts +3 -0
- package/dist/test/fixtures/routes/_layout.d.ts.map +1 -0
- package/dist/test/fixtures/routes/_layout_scripts.d.ts +3 -0
- package/dist/test/fixtures/routes/_layout_scripts.d.ts.map +1 -0
- package/dist/test/fixtures/routes/_middleware.d.ts +3 -0
- package/dist/test/fixtures/routes/_middleware.d.ts.map +1 -0
- package/dist/test/fixtures/routes/_redirect301_mw.d.ts +3 -0
- package/dist/test/fixtures/routes/_redirect301_mw.d.ts.map +1 -0
- package/dist/test/fixtures/routes/_redirect_mw.d.ts +3 -0
- package/dist/test/fixtures/routes/_redirect_mw.d.ts.map +1 -0
- package/dist/test/fixtures/routes/about.page.d.ts +3 -0
- package/dist/test/fixtures/routes/about.page.d.ts.map +1 -0
- package/dist/test/fixtures/routes/action.page.d.ts +6 -0
- package/dist/test/fixtures/routes/action.page.d.ts.map +1 -0
- package/dist/test/fixtures/routes/api/form-all.post.d.ts +3 -0
- package/dist/test/fixtures/routes/api/form-all.post.d.ts.map +1 -0
- package/dist/test/fixtures/routes/api/form-limited.post.d.ts +6 -0
- package/dist/test/fixtures/routes/api/form-limited.post.d.ts.map +1 -0
- package/dist/test/fixtures/routes/api/response-obj.get.d.ts +3 -0
- package/dist/test/fixtures/routes/api/response-obj.get.d.ts.map +1 -0
- package/dist/test/fixtures/routes/api/upload.post.d.ts +12 -0
- package/dist/test/fixtures/routes/api/upload.post.d.ts.map +1 -0
- package/dist/test/fixtures/routes/api/users.get.d.ts +6 -0
- package/dist/test/fixtures/routes/api/users.get.d.ts.map +1 -0
- package/dist/test/fixtures/routes/api/xml.get.d.ts +3 -0
- package/dist/test/fixtures/routes/api/xml.get.d.ts.map +1 -0
- package/dist/test/fixtures/routes/auth/_middleware.d.ts +3 -0
- package/dist/test/fixtures/routes/auth/_middleware.d.ts.map +1 -0
- package/dist/test/fixtures/routes/auth/protected.page.d.ts +3 -0
- package/dist/test/fixtures/routes/auth/protected.page.d.ts.map +1 -0
- package/dist/test/fixtures/routes/index.page.d.ts +3 -0
- package/dist/test/fixtures/routes/index.page.d.ts.map +1 -0
- package/dist/test/fixtures/routes/oml.page.d.ts +3 -0
- package/dist/test/fixtures/routes/oml.page.d.ts.map +1 -0
- package/dist/test/fixtures/routes/redirect.page.d.ts +3 -0
- package/dist/test/fixtures/routes/redirect.page.d.ts.map +1 -0
- package/dist/test/fixtures/routes/ssg/[slug].page.d.ts +5 -0
- package/dist/test/fixtures/routes/ssg/[slug].page.d.ts.map +1 -0
- package/dist/test/fixtures/routes/ssg/server.page.d.ts +4 -0
- package/dist/test/fixtures/routes/ssg/server.page.d.ts.map +1 -0
- package/dist/test/fixtures/routes/state.page.d.ts +3 -0
- package/dist/test/fixtures/routes/state.page.d.ts.map +1 -0
- package/dist/test/fixtures/routes/throw.page.d.ts +3 -0
- package/dist/test/fixtures/routes/throw.page.d.ts.map +1 -0
- package/dist/test/fixtures/routes/wiki/[...slug].page.d.ts +3 -0
- package/dist/test/fixtures/routes/wiki/[...slug].page.d.ts.map +1 -0
- package/dist/test/helpers.d.ts +37 -0
- package/dist/test/helpers.d.ts.map +1 -0
- package/dist/test/layouts.test.d.ts +2 -0
- package/dist/test/layouts.test.d.ts.map +1 -0
- package/dist/test/middleware.test.d.ts +2 -0
- package/dist/test/middleware.test.d.ts.map +1 -0
- package/dist/test/multipart.test.d.ts +2 -0
- package/dist/test/multipart.test.d.ts.map +1 -0
- package/dist/test/oml-routing.test.d.ts +2 -0
- package/dist/test/oml-routing.test.d.ts.map +1 -0
- package/dist/test/oml.test.d.ts +2 -0
- package/dist/test/oml.test.d.ts.map +1 -0
- package/dist/test/redirects.test.d.ts +2 -0
- package/dist/test/redirects.test.d.ts.map +1 -0
- package/dist/test/routing.test.d.ts +2 -0
- package/dist/test/routing.test.d.ts.map +1 -0
- package/dist/test/ssg.test.d.ts +2 -0
- package/dist/test/ssg.test.d.ts.map +1 -0
- package/dist/test/web-response.test.d.ts +2 -0
- package/dist/test/web-response.test.d.ts.map +1 -0
- package/dist/types.d.ts +314 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +292 -0
- package/dist/types.js.map +7 -0
- package/package.json +103 -0
- package/pka.config.json +32 -0
- package/src/build/config.ts +42 -0
- package/src/build/index.ts +6 -0
- package/src/build/plugins.ts +118 -0
- package/src/cli.ts +502 -0
- package/src/config.ts +197 -0
- package/src/create-multisite.ts +310 -0
- package/src/create.ts +194 -0
- package/src/dev/blueprints.ts +75 -0
- package/src/dev/components.ts +108 -0
- package/src/dev/insert.ts +221 -0
- package/src/dev/remove.ts +677 -0
- package/src/dev/watch.ts +3098 -0
- package/src/env.d.ts +5 -0
- package/src/errors.ts +64 -0
- package/src/generate.ts +228 -0
- package/src/index.ts +67 -0
- package/src/island.ts +47 -0
- package/src/jsx-runtime.d.ts +408 -0
- package/src/jsx-runtime.d.ts.map +1 -0
- package/src/jsx-runtime.ts +536 -0
- package/src/link.ts +49 -0
- package/src/oml/fragment.ts +54 -0
- package/src/oml/index.ts +21 -0
- package/src/oml/jsx-runtime.ts +121 -0
- package/src/oml/jsx.ts +151 -0
- package/src/oml/page.ts +13 -0
- package/src/oml/render.ts +181 -0
- package/src/oml/types.ts +159 -0
- package/src/router/handler.ts +515 -0
- package/src/router/matcher.ts +52 -0
- package/src/router/scanner.ts +272 -0
- package/src/server/index.ts +49 -0
- package/src/signal.ts +39 -0
- package/src/ssg.ts +253 -0
- package/src/test/actions.test.ts +40 -0
- package/src/test/body-limits.test.ts +83 -0
- package/src/test/errors.test.ts +53 -0
- package/src/test/fixtures/routes/[id].page.ts +3 -0
- package/src/test/fixtures/routes/_error.ts +6 -0
- package/src/test/fixtures/routes/_global.ts +8 -0
- package/src/test/fixtures/routes/_layout-template.ts +7 -0
- package/src/test/fixtures/routes/_layout.ts +7 -0
- package/src/test/fixtures/routes/_layout_scripts.ts +8 -0
- package/src/test/fixtures/routes/_middleware.ts +8 -0
- package/src/test/fixtures/routes/_redirect301_mw.ts +5 -0
- package/src/test/fixtures/routes/_redirect_mw.ts +5 -0
- package/src/test/fixtures/routes/about.page.ts +6 -0
- package/src/test/fixtures/routes/action.page.ts +11 -0
- package/src/test/fixtures/routes/api/form-all.post.ts +5 -0
- package/src/test/fixtures/routes/api/form-limited.post.ts +6 -0
- package/src/test/fixtures/routes/api/response-obj.get.ts +17 -0
- package/src/test/fixtures/routes/api/upload.post.ts +14 -0
- package/src/test/fixtures/routes/api/users.get.ts +3 -0
- package/src/test/fixtures/routes/api/xml.get.ts +5 -0
- package/src/test/fixtures/routes/auth/_middleware.ts +11 -0
- package/src/test/fixtures/routes/auth/protected.page.ts +3 -0
- package/src/test/fixtures/routes/index.page.ts +3 -0
- package/src/test/fixtures/routes/oml.page.ts +7 -0
- package/src/test/fixtures/routes/redirect.page.ts +3 -0
- package/src/test/fixtures/routes/ssg/[slug].page.ts +8 -0
- package/src/test/fixtures/routes/ssg/server.page.ts +5 -0
- package/src/test/fixtures/routes/state.page.ts +4 -0
- package/src/test/fixtures/routes/throw.page.ts +5 -0
- package/src/test/fixtures/routes/wiki/[...slug].page.ts +3 -0
- package/src/test/helpers.ts +132 -0
- package/src/test/layouts.test.ts +76 -0
- package/src/test/middleware.test.ts +69 -0
- package/src/test/multipart.test.ts +91 -0
- package/src/test/oml-routing.test.ts +59 -0
- package/src/test/oml.test.ts +429 -0
- package/src/test/redirects.test.ts +32 -0
- package/src/test/routing.test.ts +118 -0
- package/src/test/ssg.test.ts +273 -0
- package/src/test/web-response.test.ts +33 -0
- package/src/types.ts +670 -0
- package/tsconfig.client.json +17 -0
- 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, '&').replace(/"/g, '"')
|
|
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
|
+
}
|
package/src/oml/page.ts
ADDED
|
@@ -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, '&')
|
|
27
|
+
.replace(/</g, '<')
|
|
28
|
+
.replace(/>/g, '>')
|
|
29
|
+
.replace(/"/g, '"')
|
|
30
|
+
.replace(/'/g, ''')
|
|
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
|
+
}
|
package/src/oml/types.ts
ADDED
|
@@ -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
|
+
}
|