@mindees/renderer 0.1.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 (43) hide show
  1. package/LICENSE +31 -0
  2. package/README.md +129 -0
  3. package/dist/backend.d.ts +68 -0
  4. package/dist/backend.d.ts.map +1 -0
  5. package/dist/backend.js +9 -0
  6. package/dist/backend.js.map +1 -0
  7. package/dist/dom.d.ts +39 -0
  8. package/dist/dom.d.ts.map +1 -0
  9. package/dist/dom.js +125 -0
  10. package/dist/dom.js.map +1 -0
  11. package/dist/headless.d.ts +25 -0
  12. package/dist/headless.d.ts.map +1 -0
  13. package/dist/headless.js +116 -0
  14. package/dist/headless.js.map +1 -0
  15. package/dist/index.d.ts +32 -0
  16. package/dist/index.d.ts.map +1 -0
  17. package/dist/index.js +36 -0
  18. package/dist/index.js.map +1 -0
  19. package/dist/native-command-backend.d.ts +71 -0
  20. package/dist/native-command-backend.d.ts.map +1 -0
  21. package/dist/native-command-backend.js +246 -0
  22. package/dist/native-command-backend.js.map +1 -0
  23. package/dist/native-host.d.ts +56 -0
  24. package/dist/native-host.d.ts.map +1 -0
  25. package/dist/native-host.js +125 -0
  26. package/dist/native-host.js.map +1 -0
  27. package/dist/native-protocol.d.ts +141 -0
  28. package/dist/native-protocol.d.ts.map +1 -0
  29. package/dist/native-protocol.js +135 -0
  30. package/dist/native-protocol.js.map +1 -0
  31. package/dist/native.d.ts +42 -0
  32. package/dist/native.d.ts.map +1 -0
  33. package/dist/native.js +59 -0
  34. package/dist/native.js.map +1 -0
  35. package/dist/render.d.ts +28 -0
  36. package/dist/render.d.ts.map +1 -0
  37. package/dist/render.js +145 -0
  38. package/dist/render.js.map +1 -0
  39. package/dist/ssr.d.ts +40 -0
  40. package/dist/ssr.d.ts.map +1 -0
  41. package/dist/ssr.js +31 -0
  42. package/dist/ssr.js.map +1 -0
  43. package/package.json +35 -0
package/LICENSE ADDED
@@ -0,0 +1,31 @@
1
+ # License
2
+
3
+ MindeesNative is dual-licensed under either of:
4
+
5
+ - **Apache License, Version 2.0** ([LICENSE-APACHE](./LICENSE-APACHE) or
6
+ <https://www.apache.org/licenses/LICENSE-2.0>)
7
+ - **MIT license** ([LICENSE-MIT](./LICENSE-MIT) or
8
+ <https://opensource.org/licenses/MIT>)
9
+
10
+ at your option.
11
+
12
+ This `MIT OR Apache-2.0` dual-license is the same model used by the Rust
13
+ ecosystem and many modern open-source projects. It gives downstream users
14
+ maximum flexibility: the MIT option is short and permissive, while the Apache
15
+ option adds an explicit patent grant.
16
+
17
+ ## SPDX identifier
18
+
19
+ ```
20
+ SPDX-License-Identifier: MIT OR Apache-2.0
21
+ ```
22
+
23
+ ## Contribution
24
+
25
+ Unless you explicitly state otherwise, any contribution intentionally
26
+ submitted for inclusion in the work by you, as defined in the Apache-2.0
27
+ license, shall be dual-licensed as above, without any additional terms or
28
+ conditions.
29
+
30
+ Contributions are accepted under the **Developer Certificate of Origin (DCO)**.
31
+ See [CONTRIBUTING.md](./CONTRIBUTING.md) for details on signing off your commits.
package/README.md ADDED
@@ -0,0 +1,129 @@
1
+ # @mindees/renderer
2
+
3
+ **Helix** โ€” MindeesNative's renderer. It turns the `@mindees/core` element tree
4
+ into host nodes through a swappable **host backend**, with **fine-grained
5
+ reactive bindings**: a changing signal patches exactly the affected
6
+ attribute/text/region โ€” no virtual-DOM diffing.
7
+
8
+ > **Status: ๐Ÿงช Experimental.** Implemented and tested: the reconciler, the
9
+ > **web/DOM backend**, **SSR + hydration**, a **headless** test backend, a
10
+ > **native command backend** (`createNativeCommandBackend()`), a strict reference
11
+ > host (`createReferenceHost()`), and iOS/Android host projects that compile and
12
+ > render the command stream into native view trees in CI. A full end-to-end native
13
+ > app bridge/embedded JS engine and GPU canvas remain **research tracks**; the
14
+ > direct `createNativeBackend()` and `createCanvasBackend()` seams throw
15
+ > `NotImplementedError`. APIs may change before `1.0`.
16
+
17
+ ## Why Helix
18
+
19
+ - **Fine-grained updates** โ€” built on `@mindees/core` signals: updates are
20
+ O(what-changed), not O(tree). A reactive text node is patched in place; a
21
+ reactive prop sets exactly one attribute.
22
+ - **Real SSR + SEO** โ€” `renderToString` emits crawlable HTML (`view`โ†’`div`,
23
+ `text`โ†’`span`, โ€ฆ), unlike canvas-based web renderers. `hydrate` attaches
24
+ reactivity on the client.
25
+ - **Backend-agnostic** โ€” the reconciler speaks only the `HostBackend` contract,
26
+ so a new platform is "implement `HostBackend<N>`." DOM, headless, native command
27
+ stream, and reference-host validation ship today.
28
+
29
+ ## Quick start
30
+
31
+ ```ts
32
+ import { signal, createElement as h } from '@mindees/core'
33
+ import { createDomBackend, render } from '@mindees/renderer'
34
+
35
+ function Counter() {
36
+ const n = signal(0)
37
+ return h('button', { onClick: () => n.set(n() + 1) }, () => `count: ${n()}`)
38
+ }
39
+
40
+ const backend = createDomBackend() // uses the global document in a browser
41
+ const app = render(Counter, {}, backend, document.getElementById('app'))
42
+ // app.dispose() unmounts and tears down every reactive binding (no leaks)
43
+ ```
44
+
45
+ ### Server-side rendering
46
+
47
+ ```ts
48
+ import { renderToString, hydrate } from '@mindees/renderer'
49
+
50
+ const html = renderToString(Counter, {}) // '<button>count: 0</button>'
51
+ // โ€ฆsend `html` in your server response, then on the client:
52
+ hydrate(document.getElementById('app'), Counter, {})
53
+ ```
54
+
55
+ ### Native command backend (Phase 8A)
56
+
57
+ The same reconciler can target a native host. `createNativeCommandBackend()`
58
+ implements the `HostBackend` contract but, instead of touching a DOM, records a
59
+ stream of serializable [`NativeCommand`](./src/native-protocol.ts)s
60
+ (`createNode`, `insertChild`, `setProp`, `updateText`, `disposeNode`, โ€ฆ) that a
61
+ native host (UIKit and Android View today; other surfaces later) can replay. It
62
+ runs in Node โ€” no DOM โ€” so the whole native path is testable.
63
+
64
+ ```ts
65
+ import { createNativeCommandBackend, render } from '@mindees/renderer'
66
+
67
+ const backend = createNativeCommandBackend()
68
+ const app = render(Counter, {}, backend, backend.root)
69
+ const commands = backend.flushCommands() // ship this batch to a native host
70
+ // Event handlers cross as stable ids, never as functions; the host calls back:
71
+ // backend.dispatchEvent(handlerId, event)
72
+ ```
73
+
74
+ > This is the **foundation** for native rendering โ€” it produces the command
75
+ > stream, and the host projects in
76
+ > [`examples/native-hosts/`](https://github.com/mindees/mindees/tree/main/examples/native-hosts)
77
+ > replay/render that stream in CI. You cannot build a native mobile app
78
+ > end-to-end yet because Phase 8F still needs an embedded JS engine plus a
79
+ > JSโ†”native bridge running the reactive app on-device.
80
+
81
+ ### Reference host + conformance contract (Phase 8B)
82
+
83
+ `createReferenceHost()` is the **inverse** of the command backend: it consumes a
84
+ `NativeCommand` stream, reconstructs the view tree, and **strictly validates** it
85
+ (throws `NativeHostError` on any malformed/leaking sequence). It is the executable
86
+ **contract** a real native host implements (in Swift/Kotlin against platform
87
+ views). Piping the backend through it proves the stream is valid and non-leaking
88
+ end to end:
89
+
90
+ ```ts
91
+ import { createNativeCommandBackend, createReferenceHost, render } from '@mindees/renderer'
92
+
93
+ const host = createReferenceHost()
94
+ const backend = createNativeCommandBackend({ rootId: host.rootId, onCommand: (c) => host.apply(c) })
95
+ const app = render(Counter, {}, backend, backend.root)
96
+ host.serialize() // the reconstructed tree, e.g. '<button>count: 0</button>'
97
+ host.liveNodeCount() // 0 after app.dispose() โ€” no orphaned/leaked nodes
98
+ ```
99
+
100
+ ## API
101
+
102
+ | Export | Kind | Description |
103
+ | --- | --- | --- |
104
+ | `render(node\|component, [props,] backend, container)` | fn | Mount a tree; returns `Mounted` with `dispose()`. |
105
+ | `renderToString(node\|component, [props])` | fn | SSR โ†’ crawlable HTML string. |
106
+ | `hydrate(container, node\|component, [props,] opts?)` | fn | Attach reactivity to server HTML (developer preview). |
107
+ | `createDomBackend(doc?)` | fn | Web/DOM `HostBackend`. |
108
+ | `createHeadlessBackend()` / `createHeadlessRoot()` | fn | In-memory backend (tests, snapshots, SSR). |
109
+ | `domTagFor(tag)` | fn | Map a semantic tag to its HTML tag. |
110
+ | `HostBackend` / `SerializableBackend` | type | The platform seam. |
111
+ | `createNativeCommandBackend(opts?)` | fn | Native `HostBackend` that emits a serializable `NativeCommand` stream (Phase 8A). |
112
+ | `createReferenceHost(rootId?)` | fn | Strict reference host: replays + validates a `NativeCommand` stream (Phase 8B). |
113
+ | `NativeCommand` + `isNativeCommand` / `isNativePropValue` / `normalizeNativeProp` / `createNativeNodeIdFactory` | type/fn | The native command protocol + helpers. |
114
+ | `createNativeBackend` / `createCanvasBackend` | fn | ๐Ÿ”ฌ research tracks (direct runtime native backend + GPU canvas) โ€” throw `NotImplementedError`. |
115
+
116
+ ### Reactive bindings
117
+
118
+ Pass a **function** as a child or prop value to make it reactive:
119
+
120
+ ```ts
121
+ h('view', { class: () => theme() }, () => label())
122
+ // ^ reactive prop ^ reactive child region
123
+ ```
124
+
125
+ Static values (`h('view', { id: 'x' }, 'hello')`) are applied once.
126
+
127
+ ## License
128
+
129
+ `MIT OR Apache-2.0`
@@ -0,0 +1,68 @@
1
+ //#region src/backend.d.ts
2
+ /**
3
+ * Helix host-backend contract.
4
+ *
5
+ * A {@link HostBackend} is the seam between the renderer and a concrete target
6
+ * (DOM today; native iOS/Android and a GPU canvas are research tracks). The
7
+ * reconciler ({@link import('./render').render}) speaks only this interface, so
8
+ * a new platform is "implement `HostBackend<N>`" โ€” nothing in the reconciler
9
+ * changes.
10
+ *
11
+ * `N` is the opaque host-node type (a real DOM `Node`, a headless record, a
12
+ * native view handle, โ€ฆ). The backend never interprets MindeesNative elements;
13
+ * it just creates, mutates, and arranges host nodes on command.
14
+ *
15
+ * @module
16
+ */
17
+ /**
18
+ * A platform target. Implementations create/mutate/arrange host nodes of type
19
+ * `N`. All methods are synchronous and side-effecting on the host tree.
20
+ */
21
+ interface HostBackend<N> {
22
+ /** Create an element host node for `type` (e.g. `"view"`, `"text"`). */
23
+ createElement(type: string): N;
24
+ /** Create a text host node holding `value`. */
25
+ createText(value: string): N;
26
+ /**
27
+ * Apply a single prop/attribute. `prev` is the previously-applied value (or
28
+ * `undefined` on first apply) so the backend can diff/cleanup if needed
29
+ * (e.g. removing an old event listener). Event props are `onX` (capitalized).
30
+ */
31
+ setProp(node: N, key: string, value: unknown, prev: unknown): void;
32
+ /** Update a text host node's value. */
33
+ setText(node: N, value: string): void;
34
+ /**
35
+ * Insert `node` into `parent` immediately before `anchor`, or append when
36
+ * `anchor` is `null`.
37
+ */
38
+ insert(parent: N, node: N, anchor: N | null): void;
39
+ /** Remove `node` from `parent`. */
40
+ remove(parent: N, node: N): void;
41
+ /** The parent of `node`, or `null` if it has none (detached / root). */
42
+ parentOf(node: N): N | null;
43
+ /** The next sibling of `node`, or `null`. Used to compute insertion anchors. */
44
+ nextSibling(node: N): N | null;
45
+ /** True if `node` is a text host node (vs an element). */
46
+ isText(node: N): boolean;
47
+ }
48
+ /** Options controlling {@link SerializableBackend.serialize}. */
49
+ interface SerializeOptions {
50
+ /**
51
+ * Map a MindeesNative element tag to the tag actually emitted (e.g. the web
52
+ * target maps `view`โ†’`div`, `text`โ†’`span`). Defaults to identity.
53
+ */
54
+ mapTag?: (type: string) => string;
55
+ }
56
+ /**
57
+ * Optional capability: serialize a host subtree to an HTML string. Backends
58
+ * that support server-side rendering implement this; the headless backend does.
59
+ */
60
+ interface SerializableBackend<N> extends HostBackend<N> {
61
+ /** Serialize `node` (and its subtree) to an HTML string. */
62
+ serialize(node: N, options?: SerializeOptions): string;
63
+ }
64
+ /** Type guard: does `backend` support {@link SerializableBackend.serialize}? */
65
+ declare function isSerializable<N>(backend: HostBackend<N>): backend is SerializableBackend<N>;
66
+ //#endregion
67
+ export { HostBackend, SerializableBackend, isSerializable };
68
+ //# sourceMappingURL=backend.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"backend.d.ts","names":[],"sources":["../src/backend.ts"],"mappings":";;AAoBA;;;;;;;;;;;;;;;;;;UAAiB,WAAA;EAAY;EAE3B,aAAA,CAAc,IAAA,WAAe,CAAA;EAAf;EAEd,UAAA,CAAW,KAAA,WAAgB,CAAA;EAA3B;;;;;EAMA,OAAA,CAAQ,IAAA,EAAM,CAAA,EAAG,GAAA,UAAa,KAAA,WAAgB,IAAA;EAA7B;EAEjB,OAAA,CAAQ,IAAA,EAAM,CAAA,EAAG,KAAA;EAF6B;;;;EAO9C,MAAA,CAAO,MAAA,EAAQ,CAAA,EAAG,IAAA,EAAM,CAAA,EAAG,MAAA,EAAQ,CAAA;EAAnC;EAEA,MAAA,CAAO,MAAA,EAAQ,CAAA,EAAG,IAAA,EAAM,CAAA;EAFjB;EAIP,QAAA,CAAS,IAAA,EAAM,CAAA,GAAI,CAAA;EAJD;EAMlB,WAAA,CAAY,IAAA,EAAM,CAAA,GAAI,CAAA;EANK;EAQ3B,MAAA,CAAO,IAAA,EAAM,CAAA;AAAA;;UAIE,gBAAA;EAVG;;;;EAelB,MAAA,IAAU,IAAY;AAAA;;;;;UAOP,mBAAA,YAA+B,WAAA,CAAY,CAAA;EAhBnD;EAkBP,SAAA,CAAU,IAAA,EAAM,CAAA,EAAG,OAAA,GAAU,gBAAA;AAAA;AAd/B;AAAA,iBAkBgB,cAAA,IAAkB,OAAA,EAAS,WAAA,CAAY,CAAA,IAAK,OAAA,IAAW,mBAAA,CAAoB,CAAA"}
@@ -0,0 +1,9 @@
1
+ //#region src/backend.ts
2
+ /** Type guard: does `backend` support {@link SerializableBackend.serialize}? */
3
+ function isSerializable(backend) {
4
+ return typeof backend.serialize === "function";
5
+ }
6
+ //#endregion
7
+ export { isSerializable };
8
+
9
+ //# sourceMappingURL=backend.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"backend.js","names":[],"sources":["../src/backend.ts"],"sourcesContent":["/**\n * Helix host-backend contract.\n *\n * A {@link HostBackend} is the seam between the renderer and a concrete target\n * (DOM today; native iOS/Android and a GPU canvas are research tracks). The\n * reconciler ({@link import('./render').render}) speaks only this interface, so\n * a new platform is \"implement `HostBackend<N>`\" โ€” nothing in the reconciler\n * changes.\n *\n * `N` is the opaque host-node type (a real DOM `Node`, a headless record, a\n * native view handle, โ€ฆ). The backend never interprets MindeesNative elements;\n * it just creates, mutates, and arranges host nodes on command.\n *\n * @module\n */\n\n/**\n * A platform target. Implementations create/mutate/arrange host nodes of type\n * `N`. All methods are synchronous and side-effecting on the host tree.\n */\nexport interface HostBackend<N> {\n /** Create an element host node for `type` (e.g. `\"view\"`, `\"text\"`). */\n createElement(type: string): N\n /** Create a text host node holding `value`. */\n createText(value: string): N\n /**\n * Apply a single prop/attribute. `prev` is the previously-applied value (or\n * `undefined` on first apply) so the backend can diff/cleanup if needed\n * (e.g. removing an old event listener). Event props are `onX` (capitalized).\n */\n setProp(node: N, key: string, value: unknown, prev: unknown): void\n /** Update a text host node's value. */\n setText(node: N, value: string): void\n /**\n * Insert `node` into `parent` immediately before `anchor`, or append when\n * `anchor` is `null`.\n */\n insert(parent: N, node: N, anchor: N | null): void\n /** Remove `node` from `parent`. */\n remove(parent: N, node: N): void\n /** The parent of `node`, or `null` if it has none (detached / root). */\n parentOf(node: N): N | null\n /** The next sibling of `node`, or `null`. Used to compute insertion anchors. */\n nextSibling(node: N): N | null\n /** True if `node` is a text host node (vs an element). */\n isText(node: N): boolean\n}\n\n/** Options controlling {@link SerializableBackend.serialize}. */\nexport interface SerializeOptions {\n /**\n * Map a MindeesNative element tag to the tag actually emitted (e.g. the web\n * target maps `view`โ†’`div`, `text`โ†’`span`). Defaults to identity.\n */\n mapTag?: (type: string) => string\n}\n\n/**\n * Optional capability: serialize a host subtree to an HTML string. Backends\n * that support server-side rendering implement this; the headless backend does.\n */\nexport interface SerializableBackend<N> extends HostBackend<N> {\n /** Serialize `node` (and its subtree) to an HTML string. */\n serialize(node: N, options?: SerializeOptions): string\n}\n\n/** Type guard: does `backend` support {@link SerializableBackend.serialize}? */\nexport function isSerializable<N>(backend: HostBackend<N>): backend is SerializableBackend<N> {\n return typeof (backend as Partial<SerializableBackend<N>>).serialize === 'function'\n}\n"],"mappings":";;AAmEA,SAAgB,eAAkB,SAA4D;CAC5F,OAAO,OAAQ,QAA4C,cAAc;AAC3E"}
package/dist/dom.d.ts ADDED
@@ -0,0 +1,39 @@
1
+ import { HostBackend } from "./backend.js";
2
+
3
+ //#region src/dom.d.ts
4
+ /** A minimal structural view of the DOM we use (so types don't require `lib.dom`). */
5
+ interface DomDocument {
6
+ createElement(tag: string): DomElement;
7
+ createTextNode(data: string): DomText;
8
+ }
9
+ interface DomNode {
10
+ parentNode: DomNode | null;
11
+ nextSibling: DomNode | null;
12
+ nodeType: number;
13
+ removeChild(child: DomNode): DomNode;
14
+ insertBefore(node: DomNode, ref: DomNode | null): DomNode;
15
+ }
16
+ interface DomElement extends DomNode {
17
+ setAttribute(name: string, value: string): void;
18
+ removeAttribute(name: string): void;
19
+ addEventListener(type: string, listener: (e: unknown) => void): void;
20
+ removeEventListener(type: string, listener: (e: unknown) => void): void;
21
+ style: Record<string, string> & {
22
+ cssText: string;
23
+ };
24
+ }
25
+ interface DomText extends DomNode {
26
+ data: string;
27
+ }
28
+ /** Map a MindeesNative tag to its DOM tag. Unknown tags pass through. */
29
+ declare function domTagFor(type: string): string;
30
+ /**
31
+ * Create a {@link HostBackend} that renders to real DOM nodes.
32
+ *
33
+ * @param doc - The document to create nodes with. Defaults to the global
34
+ * `document`; pass a happy-dom/jsdom document for headless tests.
35
+ */
36
+ declare function createDomBackend(doc?: DomDocument): HostBackend<DomNode>;
37
+ //#endregion
38
+ export { type DomDocument, type DomElement, type DomNode, type DomText, createDomBackend, domTagFor };
39
+ //# sourceMappingURL=dom.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dom.d.ts","names":[],"sources":["../src/dom.ts"],"mappings":";;;;UAeU,WAAA;EACR,aAAA,CAAc,GAAA,WAAc,UAAA;EAC5B,cAAA,CAAe,IAAA,WAAe,OAAO;AAAA;AAAA,UAE7B,OAAA;EACR,UAAA,EAAY,OAAA;EACZ,WAAA,EAAa,OAAA;EACb,QAAA;EACA,WAAA,CAAY,KAAA,EAAO,OAAA,GAAU,OAAA;EAC7B,YAAA,CAAa,IAAA,EAAM,OAAA,EAAS,GAAA,EAAK,OAAA,UAAiB,OAAA;AAAA;AAAA,UAE1C,UAAA,SAAmB,OAAO;EAClC,YAAA,CAAa,IAAA,UAAc,KAAA;EAC3B,eAAA,CAAgB,IAAA;EAChB,gBAAA,CAAiB,IAAA,UAAc,QAAA,GAAW,CAAA;EAC1C,mBAAA,CAAoB,IAAA,UAAc,QAAA,GAAW,CAAA;EAC7C,KAAA,EAAO,MAAA;IAA2B,OAAA;EAAA;AAAA;AAAA,UAE1B,OAAA,SAAgB,OAAO;EAC/B,IAAI;AAAA;;iBAgBU,SAAA,CAAU,IAAY;;;;;;AA1BqB;iBAmF3C,gBAAA,CAAiB,GAAA,GAAM,WAAA,GAAc,WAAA,CAAY,OAAA"}
package/dist/dom.js ADDED
@@ -0,0 +1,125 @@
1
+ //#region src/dom.ts
2
+ const TEXT_NODE = 3;
3
+ /** Tag aliases: MindeesNative semantic tags โ†’ HTML elements on the web target. */
4
+ const TAG_ALIASES = {
5
+ view: "div",
6
+ text: "span",
7
+ image: "img",
8
+ scrollview: "div",
9
+ textinput: "input",
10
+ button: "button"
11
+ };
12
+ /** Map a MindeesNative tag to its DOM tag. Unknown tags pass through. */
13
+ function domTagFor(type) {
14
+ return TAG_ALIASES[type] ?? type;
15
+ }
16
+ function isEventProp(key) {
17
+ return key.length > 2 && key[0] === "o" && key[1] === "n" && key[2] === (key[2] ?? "").toUpperCase();
18
+ }
19
+ /**
20
+ * CSS properties whose numeric value is unitless (no `px`). Mirrors React DOM's
21
+ * `isUnitlessNumber` set โ€” everything else gets `px` appended to a bare number, so a
22
+ * platform-agnostic `{ width: 12 }` renders as `12px` on web (and stays `12` on native).
23
+ */
24
+ const UNITLESS_STYLE_PROPS = new Set([
25
+ "opacity",
26
+ "flex",
27
+ "flexGrow",
28
+ "flexShrink",
29
+ "order",
30
+ "zIndex",
31
+ "fontWeight",
32
+ "lineHeight",
33
+ "aspectRatio"
34
+ ]);
35
+ /** Stringify a style value, appending `px` to a finite number on a non-unitless property. */
36
+ function styleValue(prop, value) {
37
+ if (typeof value === "number") {
38
+ if (!Number.isFinite(value)) return "";
39
+ return UNITLESS_STYLE_PROPS.has(prop) ? String(value) : `${value}px`;
40
+ }
41
+ return String(value);
42
+ }
43
+ /**
44
+ * Form-control props that must be written as the live DOM **property** (not an attribute). The
45
+ * `value`/`checked` *attribute* only seeds the DEFAULT; once the user edits, the property and
46
+ * attribute diverge, so a controlled update must set the property to change what's shown.
47
+ */
48
+ const FORM_PROPERTIES = new Set([
49
+ "value",
50
+ "checked",
51
+ "selected",
52
+ "indeterminate"
53
+ ]);
54
+ /** `onClick` โ†’ `click`, `onPointerDown` โ†’ `pointerdown`. */
55
+ function eventNameFor(key) {
56
+ return key.slice(2).toLowerCase();
57
+ }
58
+ /** Listeners we've attached, so reactive updates can swap them cleanly. */
59
+ const listeners = /* @__PURE__ */ new WeakMap();
60
+ /**
61
+ * Create a {@link HostBackend} that renders to real DOM nodes.
62
+ *
63
+ * @param doc - The document to create nodes with. Defaults to the global
64
+ * `document`; pass a happy-dom/jsdom document for headless tests.
65
+ */
66
+ function createDomBackend(doc) {
67
+ const documentRef = doc ?? globalThis.document;
68
+ if (!documentRef) throw new Error("createDomBackend: no document available (pass one explicitly outside a browser)");
69
+ const document = documentRef;
70
+ return {
71
+ createElement: (type) => document.createElement(domTagFor(type)),
72
+ createText: (value) => document.createTextNode(value),
73
+ setProp(node, key, value, prev) {
74
+ const el = node;
75
+ if (isEventProp(key)) {
76
+ const event = eventNameFor(key);
77
+ let map = listeners.get(el);
78
+ if (!map) {
79
+ map = /* @__PURE__ */ new Map();
80
+ listeners.set(el, map);
81
+ }
82
+ const old = map.get(event);
83
+ if (old) el.removeEventListener(event, old);
84
+ if (typeof value === "function") {
85
+ const fn = value;
86
+ el.addEventListener(event, fn);
87
+ map.set(event, fn);
88
+ } else map.delete(event);
89
+ return;
90
+ }
91
+ if (key === "style") {
92
+ const style = el.style;
93
+ const next = value && typeof value === "object" ? value : null;
94
+ const prevObj = prev && typeof prev === "object" ? prev : null;
95
+ if (prevObj) {
96
+ for (const prop of Object.keys(prevObj)) if (!next || !(prop in next)) style[prop] = "";
97
+ }
98
+ if (next) for (const [prop, v] of Object.entries(next)) style[prop] = v === null || v === void 0 ? "" : styleValue(prop, v);
99
+ return;
100
+ }
101
+ if (FORM_PROPERTIES.has(key)) {
102
+ el[key] = value === null || value === void 0 ? "" : value;
103
+ return;
104
+ }
105
+ if (value === false || value === null || value === void 0) el.removeAttribute(key);
106
+ else el.setAttribute(key, value === true ? "" : String(value));
107
+ },
108
+ setText(node, value) {
109
+ node.data = value;
110
+ },
111
+ insert(parent, node, anchor) {
112
+ parent.insertBefore(node, anchor);
113
+ },
114
+ remove(parent, node) {
115
+ parent.removeChild(node);
116
+ },
117
+ parentOf: (node) => node.parentNode,
118
+ nextSibling: (node) => node.nextSibling,
119
+ isText: (node) => node.nodeType === TEXT_NODE
120
+ };
121
+ }
122
+ //#endregion
123
+ export { createDomBackend, domTagFor };
124
+
125
+ //# sourceMappingURL=dom.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dom.js","names":[],"sources":["../src/dom.ts"],"sourcesContent":["/**\n * DOM host backend โ€” renders to real `Node`s in a browser (or any DOM, such as\n * happy-dom/jsdom in tests).\n *\n * MindeesNative element tags map to DOM via a small alias table so the same app\n * tree (`view`, `text`, โ€ฆ) renders to sensible HTML; unknown tags pass through\n * as custom elements. Props become attributes, except `onX` event props (added\n * as listeners) and `style` objects (applied to `style`).\n *\n * @module\n */\n\nimport type { HostBackend } from './backend'\n\n/** A minimal structural view of the DOM we use (so types don't require `lib.dom`). */\ninterface DomDocument {\n createElement(tag: string): DomElement\n createTextNode(data: string): DomText\n}\ninterface DomNode {\n parentNode: DomNode | null\n nextSibling: DomNode | null\n nodeType: number\n removeChild(child: DomNode): DomNode\n insertBefore(node: DomNode, ref: DomNode | null): DomNode\n}\ninterface DomElement extends DomNode {\n setAttribute(name: string, value: string): void\n removeAttribute(name: string): void\n addEventListener(type: string, listener: (e: unknown) => void): void\n removeEventListener(type: string, listener: (e: unknown) => void): void\n style: Record<string, string> & { cssText: string }\n}\ninterface DomText extends DomNode {\n data: string\n}\n\nconst TEXT_NODE = 3\n\n/** Tag aliases: MindeesNative semantic tags โ†’ HTML elements on the web target. */\nconst TAG_ALIASES: Record<string, string> = {\n view: 'div',\n text: 'span',\n image: 'img',\n scrollview: 'div',\n textinput: 'input',\n button: 'button',\n}\n\n/** Map a MindeesNative tag to its DOM tag. Unknown tags pass through. */\nexport function domTagFor(type: string): string {\n return TAG_ALIASES[type] ?? type\n}\n\nfunction isEventProp(key: string): boolean {\n return (\n key.length > 2 && key[0] === 'o' && key[1] === 'n' && key[2] === (key[2] ?? '').toUpperCase()\n )\n}\n\n/**\n * CSS properties whose numeric value is unitless (no `px`). Mirrors React DOM's\n * `isUnitlessNumber` set โ€” everything else gets `px` appended to a bare number, so a\n * platform-agnostic `{ width: 12 }` renders as `12px` on web (and stays `12` on native).\n */\nconst UNITLESS_STYLE_PROPS = new Set([\n 'opacity',\n 'flex',\n 'flexGrow',\n 'flexShrink',\n 'order',\n 'zIndex',\n 'fontWeight',\n 'lineHeight',\n 'aspectRatio',\n])\n\n/** Stringify a style value, appending `px` to a finite number on a non-unitless property. */\nfunction styleValue(prop: string, value: unknown): string {\n if (typeof value === 'number') {\n if (!Number.isFinite(value)) return '' // NaN/Infinity โ†’ unset, never the literal \"NaN\"\n return UNITLESS_STYLE_PROPS.has(prop) ? String(value) : `${value}px`\n }\n return String(value)\n}\n\n/**\n * Form-control props that must be written as the live DOM **property** (not an attribute). The\n * `value`/`checked` *attribute* only seeds the DEFAULT; once the user edits, the property and\n * attribute diverge, so a controlled update must set the property to change what's shown.\n */\nconst FORM_PROPERTIES = new Set(['value', 'checked', 'selected', 'indeterminate'])\n\n/** `onClick` โ†’ `click`, `onPointerDown` โ†’ `pointerdown`. */\nfunction eventNameFor(key: string): string {\n return key.slice(2).toLowerCase()\n}\n\n/** Listeners we've attached, so reactive updates can swap them cleanly. */\nconst listeners = new WeakMap<object, Map<string, (e: unknown) => void>>()\n\n/**\n * Create a {@link HostBackend} that renders to real DOM nodes.\n *\n * @param doc - The document to create nodes with. Defaults to the global\n * `document`; pass a happy-dom/jsdom document for headless tests.\n */\nexport function createDomBackend(doc?: DomDocument): HostBackend<DomNode> {\n const documentRef = doc ?? (globalThis as unknown as { document?: DomDocument }).document\n if (!documentRef) {\n throw new Error(\n 'createDomBackend: no document available (pass one explicitly outside a browser)',\n )\n }\n const document = documentRef\n\n return {\n createElement: (type) => document.createElement(domTagFor(type)),\n createText: (value) => document.createTextNode(value),\n\n setProp(node, key, value, prev): void {\n const el = node as DomElement\n if (isEventProp(key)) {\n const event = eventNameFor(key)\n let map = listeners.get(el)\n if (!map) {\n map = new Map()\n listeners.set(el, map)\n }\n const old = map.get(event)\n if (old) el.removeEventListener(event, old)\n if (typeof value === 'function') {\n const fn = value as (e: unknown) => void\n el.addEventListener(event, fn)\n map.set(event, fn)\n } else {\n map.delete(event)\n }\n return\n }\n if (key === 'style') {\n const style = el.style\n const next = value && typeof value === 'object' ? (value as Record<string, unknown>) : null\n const prevObj = prev && typeof prev === 'object' ? (prev as Record<string, unknown>) : null\n // Clear keys present in the previous style object but absent from the\n // new one, so stale inline styles don't persist across reactive updates.\n if (prevObj) {\n for (const prop of Object.keys(prevObj)) {\n if (!next || !(prop in next)) style[prop] = ''\n }\n }\n if (next) {\n for (const [prop, v] of Object.entries(next)) {\n // Nullish โ†’ unset (don't write the literal \"undefined\"/\"null\").\n style[prop] = v === null || v === undefined ? '' : styleValue(prop, v)\n }\n }\n return\n }\n if (FORM_PROPERTIES.has(key)) {\n // Live property, so a controlled `value`/`checked` updates what the user sees.\n ;(el as unknown as Record<string, unknown>)[key] =\n value === null || value === undefined ? '' : value\n return\n }\n if (value === false || value === null || value === undefined) {\n el.removeAttribute(key)\n } else {\n el.setAttribute(key, value === true ? '' : String(value))\n }\n void prev\n },\n\n setText(node, value): void {\n ;(node as DomText).data = value\n },\n\n insert(parent, node, anchor): void {\n ;(parent as DomNode).insertBefore(node, anchor)\n },\n\n remove(parent, node): void {\n ;(parent as DomNode).removeChild(node)\n },\n\n parentOf: (node) => node.parentNode,\n nextSibling: (node) => node.nextSibling,\n isText: (node) => node.nodeType === TEXT_NODE,\n }\n}\n\nexport type { DomDocument, DomElement, DomNode, DomText }\n"],"mappings":"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"}
@@ -0,0 +1,25 @@
1
+ import { SerializableBackend } from "./backend.js";
2
+
3
+ //#region src/headless.d.ts
4
+ /** A headless host node: an element (with tag/props/children) or a text node. */
5
+ interface HeadlessNode {
6
+ /** `"#text"` for text nodes, otherwise the element tag. */
7
+ type: string;
8
+ /** Applied props (elements only). */
9
+ props: Record<string, unknown>;
10
+ /** Text content (text nodes only). */
11
+ text: string;
12
+ /** Child nodes (elements only). */
13
+ children: HeadlessNode[];
14
+ /** Back-pointer to the parent, or `null` when detached. */
15
+ parent: HeadlessNode | null;
16
+ }
17
+ /** Create a {@link SerializableBackend} backed by an in-memory tree. */
18
+ declare function createHeadlessBackend(): SerializableBackend<HeadlessNode>;
19
+ /** Whether a prop key is an event handler (`onClick`, `onPress`, โ€ฆ). */
20
+ declare function isEventProp(key: string): boolean;
21
+ /** Convenience: create a detached headless root element (default tag `"root"`). */
22
+ declare function createHeadlessRoot(type?: string): HeadlessNode;
23
+ //#endregion
24
+ export { HeadlessNode, createHeadlessBackend, createHeadlessRoot, isEventProp };
25
+ //# sourceMappingURL=headless.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"headless.d.ts","names":[],"sources":["../src/headless.ts"],"mappings":";;;;UAaiB,YAAA;EAIf;EAFA,IAAA;EAIA;EAFA,KAAA,EAAO,MAAA;EAIG;EAFV,IAAA;EAIQ;EAFR,QAAA,EAAU,YAAA;EAEU;EAApB,MAAA,EAAQ,YAAA;AAAA;;iBAiEM,qBAAA,IAAyB,mBAAmB,CAAC,YAAA;AAAY;AAAA,iBAkEzD,WAAA,CAAY,GAAW;;iBAOvB,kBAAA,CAAmB,IAAA,YAAgB,YAAY"}
@@ -0,0 +1,116 @@
1
+ //#region src/headless.ts
2
+ const TEXT = "#text";
3
+ function escapeAttr(value) {
4
+ return value.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;");
5
+ }
6
+ function escapeText(value) {
7
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
8
+ }
9
+ /** Render an attribute value: a `style` object becomes a CSS string. */
10
+ function serializeAttrValue(value) {
11
+ if (value && typeof value === "object") return Object.entries(value).map(([prop, v]) => `${prop}:${String(v)}`).join(";");
12
+ return String(value);
13
+ }
14
+ /**
15
+ * Whether `key` is a safe HTML attribute name. Attribute NAMES are interpolated
16
+ * into markup unescaped, so a name containing `>`, whitespace, quotes, `=`, `/`,
17
+ * etc. could break out of the tag and inject markup (stored XSS when props are
18
+ * built from user/server data). We emit only names that match the HTML name
19
+ * grammar โ€” matching what the DOM's `setAttribute` would accept โ€” and drop the
20
+ * rest, exactly as an invalid name would never reach the DOM either.
21
+ */
22
+ function isValidAttrName(key) {
23
+ return /^[A-Za-z_:][\w:.-]*$/.test(key);
24
+ }
25
+ /**
26
+ * Serialize a headless node (and subtree) to HTML. A standalone function, not an
27
+ * object method: the public {@link SerializableBackend.serialize} is typed as a
28
+ * plain function member, so a consumer may legally detach it
29
+ * (`const { serialize } = backend`). Recursing through this lexical helper rather
30
+ * than `this.serialize` keeps it binding-independent.
31
+ */
32
+ function serializeHeadless(node, options) {
33
+ if (node.type === TEXT) return escapeText(node.text);
34
+ const tag = (options?.mapTag ?? ((t) => t))(node.type);
35
+ if (!isValidAttrName(tag)) throw new Error(`refusing to serialize unsafe element tag: ${JSON.stringify(tag)}`);
36
+ return `<${tag}${Object.entries(node.props).filter(([key]) => !isEventProp(key) && isValidAttrName(key)).map(([key, value]) => value === true ? ` ${key}=""` : ` ${key}="${escapeAttr(serializeAttrValue(value))}"`).join("")}>${node.children.map((c) => serializeHeadless(c, options)).join("")}</${tag}>`;
37
+ }
38
+ /** Create a {@link SerializableBackend} backed by an in-memory tree. */
39
+ function createHeadlessBackend() {
40
+ return {
41
+ createElement(type) {
42
+ return {
43
+ type,
44
+ props: {},
45
+ text: "",
46
+ children: [],
47
+ parent: null
48
+ };
49
+ },
50
+ createText(value) {
51
+ return {
52
+ type: TEXT,
53
+ props: {},
54
+ text: value,
55
+ children: [],
56
+ parent: null
57
+ };
58
+ },
59
+ setProp(node, key, value) {
60
+ if (value === void 0 || value === null || value === false) delete node.props[key];
61
+ else node.props[key] = value;
62
+ },
63
+ setText(node, value) {
64
+ node.text = value;
65
+ },
66
+ insert(parent, node, anchor) {
67
+ if (node.parent) {
68
+ const prevSiblings = node.parent.children;
69
+ const at = prevSiblings.indexOf(node);
70
+ if (at >= 0) prevSiblings.splice(at, 1);
71
+ }
72
+ node.parent = parent;
73
+ if (anchor === null) parent.children.push(node);
74
+ else {
75
+ const idx = parent.children.indexOf(anchor);
76
+ parent.children.splice(idx < 0 ? parent.children.length : idx, 0, node);
77
+ }
78
+ },
79
+ remove(parent, node) {
80
+ const idx = parent.children.indexOf(node);
81
+ if (idx >= 0) parent.children.splice(idx, 1);
82
+ node.parent = null;
83
+ },
84
+ parentOf(node) {
85
+ return node.parent;
86
+ },
87
+ nextSibling(node) {
88
+ const parent = node.parent;
89
+ if (!parent) return null;
90
+ const idx = parent.children.indexOf(node);
91
+ return idx >= 0 && idx + 1 < parent.children.length ? parent.children[idx + 1] ?? null : null;
92
+ },
93
+ isText(node) {
94
+ return node.type === TEXT;
95
+ },
96
+ serialize: serializeHeadless
97
+ };
98
+ }
99
+ /** Whether a prop key is an event handler (`onClick`, `onPress`, โ€ฆ). */
100
+ function isEventProp(key) {
101
+ return key.length > 2 && key[0] === "o" && key[1] === "n" && key[2] === (key[2] ?? "").toUpperCase();
102
+ }
103
+ /** Convenience: create a detached headless root element (default tag `"root"`). */
104
+ function createHeadlessRoot(type = "root") {
105
+ return {
106
+ type,
107
+ props: {},
108
+ text: "",
109
+ children: [],
110
+ parent: null
111
+ };
112
+ }
113
+ //#endregion
114
+ export { createHeadlessBackend, createHeadlessRoot, isEventProp };
115
+
116
+ //# sourceMappingURL=headless.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"headless.js","names":[],"sources":["../src/headless.ts"],"sourcesContent":["/**\n * Headless host backend โ€” an in-memory host tree, no browser required.\n *\n * This is the **reference backend**: it implements the full {@link HostBackend}\n * (plus {@link SerializableBackend}) so the entire reconciler can be exercised\n * in CI without a DOM. It's also handy for snapshot-testing rendered output.\n *\n * @module\n */\n\nimport type { SerializableBackend, SerializeOptions } from './backend'\n\n/** A headless host node: an element (with tag/props/children) or a text node. */\nexport interface HeadlessNode {\n /** `\"#text\"` for text nodes, otherwise the element tag. */\n type: string\n /** Applied props (elements only). */\n props: Record<string, unknown>\n /** Text content (text nodes only). */\n text: string\n /** Child nodes (elements only). */\n children: HeadlessNode[]\n /** Back-pointer to the parent, or `null` when detached. */\n parent: HeadlessNode | null\n}\n\nconst TEXT = '#text'\n\nfunction escapeAttr(value: string): string {\n return value.replace(/&/g, '&amp;').replace(/\"/g, '&quot;').replace(/</g, '&lt;')\n}\n\nfunction escapeText(value: string): string {\n return value.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')\n}\n\n/** Render an attribute value: a `style` object becomes a CSS string. */\nfunction serializeAttrValue(value: unknown): string {\n if (value && typeof value === 'object') {\n return Object.entries(value as Record<string, unknown>)\n .map(([prop, v]) => `${prop}:${String(v)}`)\n .join(';')\n }\n return String(value)\n}\n\n/**\n * Whether `key` is a safe HTML attribute name. Attribute NAMES are interpolated\n * into markup unescaped, so a name containing `>`, whitespace, quotes, `=`, `/`,\n * etc. could break out of the tag and inject markup (stored XSS when props are\n * built from user/server data). We emit only names that match the HTML name\n * grammar โ€” matching what the DOM's `setAttribute` would accept โ€” and drop the\n * rest, exactly as an invalid name would never reach the DOM either.\n */\nfunction isValidAttrName(key: string): boolean {\n return /^[A-Za-z_:][\\w:.-]*$/.test(key)\n}\n\n/**\n * Serialize a headless node (and subtree) to HTML. A standalone function, not an\n * object method: the public {@link SerializableBackend.serialize} is typed as a\n * plain function member, so a consumer may legally detach it\n * (`const { serialize } = backend`). Recursing through this lexical helper rather\n * than `this.serialize` keeps it binding-independent.\n */\nfunction serializeHeadless(node: HeadlessNode, options?: SerializeOptions): string {\n if (node.type === TEXT) return escapeText(node.text)\n const mapTag = options?.mapTag ?? ((t: string) => t)\n const tag = mapTag(node.type)\n // The tag is interpolated into `<tag>`/`</tag>` unescaped, so a tag containing `>`,\n // whitespace, etc. would break out of the element and inject markup. Reject any tag\n // that isn't a valid name (same grammar as attribute names) โ€” fail closed.\n if (!isValidAttrName(tag)) {\n throw new Error(`refusing to serialize unsafe element tag: ${JSON.stringify(tag)}`)\n }\n const attrs = Object.entries(node.props)\n .filter(([key]) => !isEventProp(key) && isValidAttrName(key))\n .map(([key, value]) =>\n // Boolean `true` โ†’ a valueless attribute (`disabled=\"\"`), matching the DOM\n // backend (dom.ts) so SSR markup equals hydrated markup.\n value === true ? ` ${key}=\"\"` : ` ${key}=\"${escapeAttr(serializeAttrValue(value))}\"`,\n )\n .join('')\n const inner = node.children.map((c) => serializeHeadless(c, options)).join('')\n return `<${tag}${attrs}>${inner}</${tag}>`\n}\n\n/** Create a {@link SerializableBackend} backed by an in-memory tree. */\nexport function createHeadlessBackend(): SerializableBackend<HeadlessNode> {\n return {\n createElement(type: string): HeadlessNode {\n return { type, props: {}, text: '', children: [], parent: null }\n },\n\n createText(value: string): HeadlessNode {\n return { type: TEXT, props: {}, text: value, children: [], parent: null }\n },\n\n setProp(node, key, value): void {\n // Event handlers and falsy values are tracked but not serialized as attrs.\n if (value === undefined || value === null || value === false) {\n delete node.props[key]\n } else {\n node.props[key] = value\n }\n },\n\n setText(node, value): void {\n node.text = value\n },\n\n insert(parent, node, anchor): void {\n if (node.parent) {\n const prevSiblings = node.parent.children\n const at = prevSiblings.indexOf(node)\n if (at >= 0) prevSiblings.splice(at, 1)\n }\n node.parent = parent\n if (anchor === null) {\n parent.children.push(node)\n } else {\n const idx = parent.children.indexOf(anchor)\n parent.children.splice(idx < 0 ? parent.children.length : idx, 0, node)\n }\n },\n\n remove(parent, node): void {\n const idx = parent.children.indexOf(node)\n if (idx >= 0) parent.children.splice(idx, 1)\n node.parent = null\n },\n\n parentOf(node): HeadlessNode | null {\n return node.parent\n },\n\n nextSibling(node): HeadlessNode | null {\n const parent = node.parent\n if (!parent) return null\n const idx = parent.children.indexOf(node)\n return idx >= 0 && idx + 1 < parent.children.length\n ? (parent.children[idx + 1] ?? null)\n : null\n },\n\n isText(node): boolean {\n return node.type === TEXT\n },\n\n serialize: serializeHeadless,\n }\n}\n\n/** Whether a prop key is an event handler (`onClick`, `onPress`, โ€ฆ). */\nexport function isEventProp(key: string): boolean {\n return (\n key.length > 2 && key[0] === 'o' && key[1] === 'n' && key[2] === (key[2] ?? '').toUpperCase()\n )\n}\n\n/** Convenience: create a detached headless root element (default tag `\"root\"`). */\nexport function createHeadlessRoot(type = 'root'): HeadlessNode {\n return { type, props: {}, text: '', children: [], parent: null }\n}\n"],"mappings":"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"}
@@ -0,0 +1,32 @@
1
+ import { HostBackend, SerializableBackend, isSerializable } from "./backend.js";
2
+ import { DomDocument, DomElement, DomNode, DomText, createDomBackend, domTagFor } from "./dom.js";
3
+ import { HeadlessNode, createHeadlessBackend, createHeadlessRoot, isEventProp } from "./headless.js";
4
+ import { CreateNodeCommand, CreateTextCommand, DisposeNodeCommand, InsertChildCommand, NativeCommand, NativeNodeId, NativePropValue, RegisterEventCommand, RemoveChildCommand, RemovePropCommand, SetPropCommand, UnregisterEventCommand, UpdateTextCommand, createNativeNodeIdFactory, isNativeCommand, isNativePropValue, normalizeNativeProp } from "./native-protocol.js";
5
+ import { NativeCommandBackend, NativeCommandBackendOptions, NativeCommandNode, createNativeCommandBackend } from "./native-command-backend.js";
6
+ import { CanvasBackend, NativeBackend, createCanvasBackend, createNativeBackend } from "./native.js";
7
+ import { NativeHostError, ReferenceHost, ReferenceHostNode, createReferenceHost } from "./native-host.js";
8
+ import { Mounted, render } from "./render.js";
9
+ import { hydrate, renderToString } from "./ssr.js";
10
+ import { Maturity, NotImplementedError, PackageInfo, notImplemented } from "@mindees/core";
11
+
12
+ //#region src/index.d.ts
13
+ /** The npm package name. */
14
+ declare const name = "@mindees/renderer";
15
+ /** The package version. All `@mindees/*` packages share one locked version line. */
16
+ declare const VERSION = "0.1.0";
17
+ /**
18
+ * Current maturity. The Helix **web/DOM** renderer (reconciler, DOM backend,
19
+ * headless backend, SSR + hydration) is implemented and tested. Native
20
+ * (iOS/Android) and the GPU canvas are research tracks (throw
21
+ * `NotImplementedError`). See the repository `STATUS.md`.
22
+ */
23
+ declare const maturity: Maturity;
24
+ /**
25
+ * Static identity + maturity metadata for this package. Frozen so the
26
+ * self-reported identity tooling introspects cannot be mutated at runtime,
27
+ * matching the `readonly` fields of {@link PackageInfo}.
28
+ */
29
+ declare const info: PackageInfo;
30
+ //#endregion
31
+ export { type CanvasBackend, type CreateNodeCommand, type CreateTextCommand, type DisposeNodeCommand, type DomDocument, type DomElement, type DomNode, type DomText, type HeadlessNode, type HostBackend, type InsertChildCommand, type Maturity, type Mounted, type NativeBackend, type NativeCommand, type NativeCommandBackend, type NativeCommandBackendOptions, type NativeCommandNode, NativeHostError, type NativeNodeId, type NativePropValue, NotImplementedError, type PackageInfo, type ReferenceHost, type ReferenceHostNode, type RegisterEventCommand, type RemoveChildCommand, type RemovePropCommand, type SerializableBackend, type SetPropCommand, type UnregisterEventCommand, type UpdateTextCommand, VERSION, createCanvasBackend, createDomBackend, createHeadlessBackend, createHeadlessRoot, createNativeBackend, createNativeCommandBackend, createNativeNodeIdFactory, createReferenceHost, domTagFor, hydrate, info, isEventProp, isNativeCommand, isNativePropValue, isSerializable, maturity, name, normalizeNativeProp, notImplemented, render, renderToString };
32
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","names":[],"sources":["../src/index.ts"],"mappings":";;;;;;;;;;;;;cA4Ea,IAAA;AAkBb;AAAA,cAfa,OAAA;;;AAeuE;;;;cAPvE,QAAA,EAAU,QAAyB;;;;;;cAOnC,IAAA,EAAM,WAAiE"}