@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
@@ -0,0 +1,141 @@
1
+ //#region src/native-protocol.d.ts
2
+ /**
3
+ * The **native command protocol** — a small, strongly-typed, *serializable*
4
+ * description of how to build and mutate a native view tree.
5
+ *
6
+ * The Helix reconciler ({@link import('./render').render}) drives a
7
+ * {@link import('./backend').HostBackend}; the
8
+ * {@link import('./native-command-backend').NativeCommandBackend} implements that
9
+ * contract by emitting a stream of {@link NativeCommand}s instead of touching the
10
+ * DOM. A native host (UIKit on iOS, Android View on Android, or another future
11
+ * surface) consumes the stream and materializes real views.
12
+ *
13
+ * The protocol is deliberately platform-neutral and JSON-serializable: it carries
14
+ * **no functions**. Event handlers are represented as stable handler-id
15
+ * *registrations* ({@link RegisterEventCommand}); the host invokes a handler by
16
+ * id (see {@link import('./native-command-backend').NativeCommandBackend.dispatchEvent}),
17
+ * so a closure never has to cross a serialization boundary.
18
+ *
19
+ * This is the Phase 8A foundation for native rendering. It is **not** itself an
20
+ * iOS/Android renderer — it is the wire format a real host backend will speak.
21
+ *
22
+ * @module
23
+ */
24
+ /** Identifier for a native node. Stable for the node's lifetime. */
25
+ type NativeNodeId = string | number;
26
+ /**
27
+ * A value that may be sent as a native prop. Strictly serializable: primitives,
28
+ * `null`, and (recursively) arrays/plain-objects of the same. Notably **not**
29
+ * functions, `undefined`, symbols, bigints, or non-finite numbers — those cannot
30
+ * cross the protocol boundary safely. Event handlers are modeled separately
31
+ * (see {@link RegisterEventCommand}).
32
+ */
33
+ type NativePropValue = string | number | boolean | null | readonly NativePropValue[] | {
34
+ readonly [key: string]: NativePropValue;
35
+ };
36
+ /** Create an element node with a tag (e.g. `"view"`, `"text"`, `"button"`). */
37
+ interface CreateNodeCommand {
38
+ readonly type: 'createNode';
39
+ readonly id: NativeNodeId;
40
+ readonly tag: string;
41
+ }
42
+ /** Create a text node holding `text`. */
43
+ interface CreateTextCommand {
44
+ readonly type: 'createText';
45
+ readonly id: NativeNodeId;
46
+ readonly text: string;
47
+ }
48
+ /** Set (or replace) a serializable prop on a node. */
49
+ interface SetPropCommand {
50
+ readonly type: 'setProp';
51
+ readonly id: NativeNodeId;
52
+ readonly name: string;
53
+ readonly value: NativePropValue;
54
+ }
55
+ /** Remove a previously-set prop from a node. */
56
+ interface RemovePropCommand {
57
+ readonly type: 'removeProp';
58
+ readonly id: NativeNodeId;
59
+ readonly name: string;
60
+ }
61
+ /** Insert `childId` into `parentId` at `index` (0-based, among current children). */
62
+ interface InsertChildCommand {
63
+ readonly type: 'insertChild';
64
+ readonly parentId: NativeNodeId;
65
+ readonly childId: NativeNodeId;
66
+ readonly index: number;
67
+ }
68
+ /** Detach `childId` from `parentId` (does not free the node — see {@link DisposeNodeCommand}). */
69
+ interface RemoveChildCommand {
70
+ readonly type: 'removeChild';
71
+ readonly parentId: NativeNodeId;
72
+ readonly childId: NativeNodeId;
73
+ }
74
+ /** Update a text node's content. */
75
+ interface UpdateTextCommand {
76
+ readonly type: 'updateText';
77
+ readonly id: NativeNodeId;
78
+ readonly text: string;
79
+ }
80
+ /**
81
+ * Free a node's host resources. When a subtree is removed, exactly one
82
+ * {@link RemoveChildCommand} detaches the subtree's **root**, then a `disposeNode`
83
+ * is emitted for the root **and every descendant** (deepest-first). Interior nodes
84
+ * are not individually detached — they are freed in the same batch as their parent
85
+ * — so a host should treat `disposeNode` as "free this node (and remove it from any
86
+ * parent it still holds)". No surviving node ever references a freed one.
87
+ */
88
+ interface DisposeNodeCommand {
89
+ readonly type: 'disposeNode';
90
+ readonly id: NativeNodeId;
91
+ }
92
+ /**
93
+ * Register an event handler on a node under a stable `handlerId`. The host should
94
+ * call back into the runtime (`dispatchEvent(handlerId, event)`) when the native
95
+ * event fires. The handler function itself never crosses the protocol.
96
+ */
97
+ interface RegisterEventCommand {
98
+ readonly type: 'registerEvent';
99
+ readonly id: NativeNodeId;
100
+ /** Normalized event name, e.g. `"press"` for an `onPress` prop. */
101
+ readonly eventName: string;
102
+ /** Stable id the host echoes back to invoke the handler. */
103
+ readonly handlerId: string;
104
+ }
105
+ /** Remove a previously-registered event handler. */
106
+ interface UnregisterEventCommand {
107
+ readonly type: 'unregisterEvent';
108
+ readonly id: NativeNodeId;
109
+ readonly eventName: string;
110
+ readonly handlerId: string;
111
+ }
112
+ /** A single instruction in the native command stream. */
113
+ type NativeCommand = CreateNodeCommand | CreateTextCommand | SetPropCommand | RemovePropCommand | InsertChildCommand | RemoveChildCommand | UpdateTextCommand | DisposeNodeCommand | RegisterEventCommand | UnregisterEventCommand;
114
+ /**
115
+ * Type guard: is `value` a valid {@link NativePropValue}? Rejects functions,
116
+ * `undefined`, symbols, bigints, non-finite numbers, and non-plain objects
117
+ * (recursively).
118
+ */
119
+ declare function isNativePropValue(value: unknown): value is NativePropValue;
120
+ /**
121
+ * Coerce `value` to a {@link NativePropValue}, or `undefined` if it cannot be
122
+ * represented (signalling the prop should be removed rather than set).
123
+ *
124
+ * - Primitives/`null` pass through (non-finite numbers are rejected).
125
+ * - Arrays are rejected wholesale if **any** element is unrepresentable (so
126
+ * element indices are never silently shifted).
127
+ * - Plain objects keep only their representable entries (an unrepresentable value
128
+ * drops that key); non-plain objects (Date, Map, class instances, …) are rejected.
129
+ */
130
+ declare function normalizeNativeProp(value: unknown): NativePropValue | undefined;
131
+ /** Type guard: is `value` a well-formed {@link NativeCommand}? */
132
+ declare function isNativeCommand(value: unknown): value is NativeCommand;
133
+ /**
134
+ * Create a generator of unique node ids. Each call returns the next id as
135
+ * `` `${prefix}${n}` `` with a monotonically increasing `n`. Pass a distinct
136
+ * `prefix` per backend instance so ids from different backends never collide.
137
+ */
138
+ declare function createNativeNodeIdFactory(prefix?: string): () => string;
139
+ //#endregion
140
+ export { CreateNodeCommand, CreateTextCommand, DisposeNodeCommand, InsertChildCommand, NativeCommand, NativeNodeId, NativePropValue, RegisterEventCommand, RemoveChildCommand, RemovePropCommand, SetPropCommand, UnregisterEventCommand, UpdateTextCommand, createNativeNodeIdFactory, isNativeCommand, isNativePropValue, normalizeNativeProp };
141
+ //# sourceMappingURL=native-protocol.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"native-protocol.d.ts","names":[],"sources":["../src/native-protocol.ts"],"mappings":";;AAwBA;;;;AAAwB;AASxB;;;;;;;;AAM6C;AAG7C;;;;;;;;KAlBY,YAAA;AAqBE;AAId;;;;;;AAJc,KAZF,eAAA,+CAKC,eAAA;EAAA,UACG,GAAA,WAAc,eAAe;AAAA;AAa9B;AAAA,UAVE,iBAAA;EAAA,SACN,IAAA;EAAA,SACA,EAAA,EAAI,YAAY;EAAA,SAChB,GAAA;AAAA;;UAIM,iBAAA;EAAA,SACN,IAAA;EAAA,SACA,EAAA,EAAI,YAAY;EAAA,SAChB,IAAA;AAAA;AAQsB;AAAA,UAJhB,cAAA;EAAA,SACN,IAAA;EAAA,SACA,EAAA,EAAI,YAAA;EAAA,SACJ,IAAA;EAAA,SACA,KAAA,EAAO,eAAe;AAAA;;UAIhB,iBAAA;EAAA,SACN,IAAA;EAAA,SACA,EAAA,EAAI,YAAY;EAAA,SAChB,IAAA;AAAA;;UAIM,kBAAA;EAAA,SACN,IAAA;EAAA,SACA,QAAA,EAAU,YAAA;EAAA,SACV,OAAA,EAAS,YAAY;EAAA,SACrB,KAAA;AAAA;;UAIM,kBAAA;EAAA,SACN,IAAA;EAAA,SACA,QAAA,EAAU,YAAA;EAAA,SACV,OAAA,EAAS,YAAY;AAAA;;UAIf,iBAAA;EAAA,SACN,IAAA;EAAA,SACA,EAAA,EAAI,YAAY;EAAA,SAChB,IAAA;AAAA;;AAPqB;AAIhC;;;;;;UAciB,kBAAA;EAAA,SACN,IAAA;EAAA,SACA,EAAA,EAAI,YAAY;AAAA;AAF3B;;;;;AAAA,UAUiB,oBAAA;EAAA,SACN,IAAA;EAAA,SACA,EAAA,EAAI,YAAY;EAVA;EAAA,SAYhB,SAAA;EAJ0B;EAAA,SAM1B,SAAA;AAAA;;UAIM,sBAAA;EAAA,SACN,IAAA;EAAA,SACA,EAAA,EAAI,YAAY;EAAA,SAChB,SAAA;EAAA,SACA,SAAA;AAAA;AAJX;AAAA,KAQY,aAAA,GACR,iBAAA,GACA,iBAAA,GACA,cAAA,GACA,iBAAA,GACA,kBAAA,GACA,kBAAA,GACA,iBAAA,GACA,kBAAA,GACA,oBAAA,GACA,sBAAA;;;;;;iBAmCY,iBAAA,CAAkB,KAAA,YAAiB,KAAA,IAAS,eAAe;;;AAjDvD;AAIpB;;;;;;;iBA2FgB,mBAAA,CAAoB,KAAA,YAAiB,eAAe;;iBA8CpD,eAAA,CAAgB,KAAA,YAAiB,KAAA,IAAS,aAAa;;;;;;iBAyCvD,yBAAA,CAA0B,MAAY"}
@@ -0,0 +1,135 @@
1
+ //#region src/native-protocol.ts
2
+ const COMMAND_TYPES = new Set([
3
+ "createNode",
4
+ "createText",
5
+ "setProp",
6
+ "removeProp",
7
+ "insertChild",
8
+ "removeChild",
9
+ "updateText",
10
+ "disposeNode",
11
+ "registerEvent",
12
+ "unregisterEvent"
13
+ ]);
14
+ function isObject(value) {
15
+ return typeof value === "object" && value !== null;
16
+ }
17
+ function isPlainObject(value) {
18
+ const proto = Object.getPrototypeOf(value);
19
+ return proto === Object.prototype || proto === null;
20
+ }
21
+ function isId(value) {
22
+ return typeof value === "string" || typeof value === "number" && Number.isFinite(value);
23
+ }
24
+ /**
25
+ * Type guard: is `value` a valid {@link NativePropValue}? Rejects functions,
26
+ * `undefined`, symbols, bigints, non-finite numbers, and non-plain objects
27
+ * (recursively).
28
+ */
29
+ function isNativePropValue(value) {
30
+ return isPropValue(value, /* @__PURE__ */ new WeakSet());
31
+ }
32
+ function isPropValue(value, seen) {
33
+ switch (typeof value) {
34
+ case "string":
35
+ case "boolean": return true;
36
+ case "number": return Number.isFinite(value);
37
+ case "object":
38
+ if (value === null) return true;
39
+ if (seen.has(value)) return false;
40
+ seen.add(value);
41
+ try {
42
+ if (Array.isArray(value)) {
43
+ for (const item of value) if (!isPropValue(item, seen)) return false;
44
+ return true;
45
+ }
46
+ if (isPlainObject(value)) {
47
+ for (const v of Object.values(value)) if (!isPropValue(v, seen)) return false;
48
+ return true;
49
+ }
50
+ return false;
51
+ } finally {
52
+ seen.delete(value);
53
+ }
54
+ default: return false;
55
+ }
56
+ }
57
+ /**
58
+ * Coerce `value` to a {@link NativePropValue}, or `undefined` if it cannot be
59
+ * represented (signalling the prop should be removed rather than set).
60
+ *
61
+ * - Primitives/`null` pass through (non-finite numbers are rejected).
62
+ * - Arrays are rejected wholesale if **any** element is unrepresentable (so
63
+ * element indices are never silently shifted).
64
+ * - Plain objects keep only their representable entries (an unrepresentable value
65
+ * drops that key); non-plain objects (Date, Map, class instances, …) are rejected.
66
+ */
67
+ function normalizeNativeProp(value) {
68
+ return normalizeProp(value, /* @__PURE__ */ new WeakSet());
69
+ }
70
+ function normalizeProp(value, seen) {
71
+ switch (typeof value) {
72
+ case "string":
73
+ case "boolean": return value;
74
+ case "number": return Number.isFinite(value) ? value : void 0;
75
+ case "object":
76
+ if (value === null) return null;
77
+ if (seen.has(value)) return void 0;
78
+ seen.add(value);
79
+ try {
80
+ if (Array.isArray(value)) {
81
+ const out = [];
82
+ for (const item of value) {
83
+ const n = normalizeProp(item, seen);
84
+ if (n === void 0) return void 0;
85
+ out.push(n);
86
+ }
87
+ return out;
88
+ }
89
+ if (isPlainObject(value)) {
90
+ const out = Object.create(null);
91
+ for (const [k, v] of Object.entries(value)) {
92
+ const n = normalizeProp(v, seen);
93
+ if (n !== void 0) out[k] = n;
94
+ }
95
+ return out;
96
+ }
97
+ return;
98
+ } finally {
99
+ seen.delete(value);
100
+ }
101
+ default: return;
102
+ }
103
+ }
104
+ /** Type guard: is `value` a well-formed {@link NativeCommand}? */
105
+ function isNativeCommand(value) {
106
+ if (!isObject(value)) return false;
107
+ const type = value.type;
108
+ if (typeof type !== "string" || !COMMAND_TYPES.has(type)) return false;
109
+ switch (type) {
110
+ case "createNode": return isId(value.id) && typeof value.tag === "string";
111
+ case "createText":
112
+ case "updateText": return isId(value.id) && typeof value.text === "string";
113
+ case "setProp": return isId(value.id) && typeof value.name === "string" && isNativePropValue(value.value);
114
+ case "removeProp": return isId(value.id) && typeof value.name === "string";
115
+ case "insertChild": return isId(value.parentId) && isId(value.childId) && typeof value.index === "number" && Number.isInteger(value.index) && value.index >= 0;
116
+ case "removeChild": return isId(value.parentId) && isId(value.childId);
117
+ case "disposeNode": return isId(value.id);
118
+ case "registerEvent":
119
+ case "unregisterEvent": return isId(value.id) && typeof value.eventName === "string" && typeof value.handlerId === "string";
120
+ default: return false;
121
+ }
122
+ }
123
+ /**
124
+ * Create a generator of unique node ids. Each call returns the next id as
125
+ * `` `${prefix}${n}` `` with a monotonically increasing `n`. Pass a distinct
126
+ * `prefix` per backend instance so ids from different backends never collide.
127
+ */
128
+ function createNativeNodeIdFactory(prefix = "n") {
129
+ let n = 0;
130
+ return () => `${prefix}${++n}`;
131
+ }
132
+ //#endregion
133
+ export { createNativeNodeIdFactory, isNativeCommand, isNativePropValue, normalizeNativeProp };
134
+
135
+ //# sourceMappingURL=native-protocol.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"native-protocol.js","names":[],"sources":["../src/native-protocol.ts"],"sourcesContent":["/**\n * The **native command protocol** — a small, strongly-typed, *serializable*\n * description of how to build and mutate a native view tree.\n *\n * The Helix reconciler ({@link import('./render').render}) drives a\n * {@link import('./backend').HostBackend}; the\n * {@link import('./native-command-backend').NativeCommandBackend} implements that\n * contract by emitting a stream of {@link NativeCommand}s instead of touching the\n * DOM. A native host (UIKit on iOS, Android View on Android, or another future\n * surface) consumes the stream and materializes real views.\n *\n * The protocol is deliberately platform-neutral and JSON-serializable: it carries\n * **no functions**. Event handlers are represented as stable handler-id\n * *registrations* ({@link RegisterEventCommand}); the host invokes a handler by\n * id (see {@link import('./native-command-backend').NativeCommandBackend.dispatchEvent}),\n * so a closure never has to cross a serialization boundary.\n *\n * This is the Phase 8A foundation for native rendering. It is **not** itself an\n * iOS/Android renderer — it is the wire format a real host backend will speak.\n *\n * @module\n */\n\n/** Identifier for a native node. Stable for the node's lifetime. */\nexport type NativeNodeId = string | number\n\n/**\n * A value that may be sent as a native prop. Strictly serializable: primitives,\n * `null`, and (recursively) arrays/plain-objects of the same. Notably **not**\n * functions, `undefined`, symbols, bigints, or non-finite numbers — those cannot\n * cross the protocol boundary safely. Event handlers are modeled separately\n * (see {@link RegisterEventCommand}).\n */\nexport type NativePropValue =\n | string\n | number\n | boolean\n | null\n | readonly NativePropValue[]\n | { readonly [key: string]: NativePropValue }\n\n/** Create an element node with a tag (e.g. `\"view\"`, `\"text\"`, `\"button\"`). */\nexport interface CreateNodeCommand {\n readonly type: 'createNode'\n readonly id: NativeNodeId\n readonly tag: string\n}\n\n/** Create a text node holding `text`. */\nexport interface CreateTextCommand {\n readonly type: 'createText'\n readonly id: NativeNodeId\n readonly text: string\n}\n\n/** Set (or replace) a serializable prop on a node. */\nexport interface SetPropCommand {\n readonly type: 'setProp'\n readonly id: NativeNodeId\n readonly name: string\n readonly value: NativePropValue\n}\n\n/** Remove a previously-set prop from a node. */\nexport interface RemovePropCommand {\n readonly type: 'removeProp'\n readonly id: NativeNodeId\n readonly name: string\n}\n\n/** Insert `childId` into `parentId` at `index` (0-based, among current children). */\nexport interface InsertChildCommand {\n readonly type: 'insertChild'\n readonly parentId: NativeNodeId\n readonly childId: NativeNodeId\n readonly index: number\n}\n\n/** Detach `childId` from `parentId` (does not free the node — see {@link DisposeNodeCommand}). */\nexport interface RemoveChildCommand {\n readonly type: 'removeChild'\n readonly parentId: NativeNodeId\n readonly childId: NativeNodeId\n}\n\n/** Update a text node's content. */\nexport interface UpdateTextCommand {\n readonly type: 'updateText'\n readonly id: NativeNodeId\n readonly text: string\n}\n\n/**\n * Free a node's host resources. When a subtree is removed, exactly one\n * {@link RemoveChildCommand} detaches the subtree's **root**, then a `disposeNode`\n * is emitted for the root **and every descendant** (deepest-first). Interior nodes\n * are not individually detached — they are freed in the same batch as their parent\n * — so a host should treat `disposeNode` as \"free this node (and remove it from any\n * parent it still holds)\". No surviving node ever references a freed one.\n */\nexport interface DisposeNodeCommand {\n readonly type: 'disposeNode'\n readonly id: NativeNodeId\n}\n\n/**\n * Register an event handler on a node under a stable `handlerId`. The host should\n * call back into the runtime (`dispatchEvent(handlerId, event)`) when the native\n * event fires. The handler function itself never crosses the protocol.\n */\nexport interface RegisterEventCommand {\n readonly type: 'registerEvent'\n readonly id: NativeNodeId\n /** Normalized event name, e.g. `\"press\"` for an `onPress` prop. */\n readonly eventName: string\n /** Stable id the host echoes back to invoke the handler. */\n readonly handlerId: string\n}\n\n/** Remove a previously-registered event handler. */\nexport interface UnregisterEventCommand {\n readonly type: 'unregisterEvent'\n readonly id: NativeNodeId\n readonly eventName: string\n readonly handlerId: string\n}\n\n/** A single instruction in the native command stream. */\nexport type NativeCommand =\n | CreateNodeCommand\n | CreateTextCommand\n | SetPropCommand\n | RemovePropCommand\n | InsertChildCommand\n | RemoveChildCommand\n | UpdateTextCommand\n | DisposeNodeCommand\n | RegisterEventCommand\n | UnregisterEventCommand\n\nconst COMMAND_TYPES = new Set<NativeCommand['type']>([\n 'createNode',\n 'createText',\n 'setProp',\n 'removeProp',\n 'insertChild',\n 'removeChild',\n 'updateText',\n 'disposeNode',\n 'registerEvent',\n 'unregisterEvent',\n])\n\nfunction isObject(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null\n}\n\nfunction isPlainObject(value: object): boolean {\n const proto = Object.getPrototypeOf(value)\n return proto === Object.prototype || proto === null\n}\n\nfunction isId(value: unknown): value is NativeNodeId {\n // Reject NaN/Infinity: they don't round-trip through JSON (→ null), which would\n // corrupt the most identity-critical field on the wire.\n return typeof value === 'string' || (typeof value === 'number' && Number.isFinite(value))\n}\n\n/**\n * Type guard: is `value` a valid {@link NativePropValue}? Rejects functions,\n * `undefined`, symbols, bigints, non-finite numbers, and non-plain objects\n * (recursively).\n */\nexport function isNativePropValue(value: unknown): value is NativePropValue {\n return isPropValue(value, new WeakSet())\n}\n\nfunction isPropValue(value: unknown, seen: WeakSet<object>): boolean {\n switch (typeof value) {\n case 'string':\n case 'boolean':\n return true\n case 'number':\n return Number.isFinite(value)\n case 'object': {\n if (value === null) return true\n if (seen.has(value)) return false // a cycle is not serializable\n seen.add(value)\n try {\n // for…of yields `undefined` for array holes, so sparse arrays are rejected\n // (matching normalizeNativeProp — the two stay a consistent accept/coerce pair).\n if (Array.isArray(value)) {\n for (const item of value) if (!isPropValue(item, seen)) return false\n return true\n }\n if (isPlainObject(value)) {\n for (const v of Object.values(value)) if (!isPropValue(v, seen)) return false\n return true\n }\n return false\n } finally {\n seen.delete(value) // shared (diamond) references are fine — only true cycles fail\n }\n }\n default:\n return false\n }\n}\n\n/**\n * Coerce `value` to a {@link NativePropValue}, or `undefined` if it cannot be\n * represented (signalling the prop should be removed rather than set).\n *\n * - Primitives/`null` pass through (non-finite numbers are rejected).\n * - Arrays are rejected wholesale if **any** element is unrepresentable (so\n * element indices are never silently shifted).\n * - Plain objects keep only their representable entries (an unrepresentable value\n * drops that key); non-plain objects (Date, Map, class instances, …) are rejected.\n */\nexport function normalizeNativeProp(value: unknown): NativePropValue | undefined {\n return normalizeProp(value, new WeakSet())\n}\n\nfunction normalizeProp(value: unknown, seen: WeakSet<object>): NativePropValue | undefined {\n switch (typeof value) {\n case 'string':\n case 'boolean':\n return value\n case 'number':\n return Number.isFinite(value) ? value : undefined\n case 'object': {\n if (value === null) return null\n if (seen.has(value)) return undefined // a cycle can't be represented — drop it\n seen.add(value)\n try {\n if (Array.isArray(value)) {\n const out: NativePropValue[] = []\n for (const item of value) {\n const n = normalizeProp(item, seen)\n if (n === undefined) return undefined\n out.push(n)\n }\n return out\n }\n if (isPlainObject(value)) {\n // Null-prototype accumulator so an own `__proto__` key (e.g. from\n // JSON.parse) becomes a real data key instead of mutating the prototype.\n const out: Record<string, NativePropValue> = Object.create(null)\n for (const [k, v] of Object.entries(value)) {\n const n = normalizeProp(v, seen)\n if (n !== undefined) out[k] = n\n }\n return out\n }\n return undefined\n } finally {\n seen.delete(value)\n }\n }\n default:\n return undefined\n }\n}\n\n/** Type guard: is `value` a well-formed {@link NativeCommand}? */\nexport function isNativeCommand(value: unknown): value is NativeCommand {\n if (!isObject(value)) return false\n const type = value.type\n if (typeof type !== 'string' || !COMMAND_TYPES.has(type as NativeCommand['type'])) return false\n switch (type as NativeCommand['type']) {\n case 'createNode':\n return isId(value.id) && typeof value.tag === 'string'\n case 'createText':\n case 'updateText':\n return isId(value.id) && typeof value.text === 'string'\n case 'setProp':\n return isId(value.id) && typeof value.name === 'string' && isNativePropValue(value.value)\n case 'removeProp':\n return isId(value.id) && typeof value.name === 'string'\n case 'insertChild':\n return (\n isId(value.parentId) &&\n isId(value.childId) &&\n typeof value.index === 'number' &&\n Number.isInteger(value.index) &&\n value.index >= 0\n )\n case 'removeChild':\n return isId(value.parentId) && isId(value.childId)\n case 'disposeNode':\n return isId(value.id)\n case 'registerEvent':\n case 'unregisterEvent':\n return (\n isId(value.id) && typeof value.eventName === 'string' && typeof value.handlerId === 'string'\n )\n default:\n return false\n }\n}\n\n/**\n * Create a generator of unique node ids. Each call returns the next id as\n * `` `${prefix}${n}` `` with a monotonically increasing `n`. Pass a distinct\n * `prefix` per backend instance so ids from different backends never collide.\n */\nexport function createNativeNodeIdFactory(prefix = 'n'): () => string {\n let n = 0\n return () => `${prefix}${++n}`\n}\n"],"mappings":"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"}
@@ -0,0 +1,42 @@
1
+ import { HostBackend } from "./backend.js";
2
+ import { NativeCommandBackend, NativeCommandBackendOptions, NativeCommandNode, createNativeCommandBackend } from "./native-command-backend.js";
3
+
4
+ //#region src/native.d.ts
5
+ /**
6
+ * 🔬 Research track. The native (iOS/Android) host backend. Extends the same
7
+ * {@link HostBackend} contract the reconciler already speaks, so when it lands,
8
+ * `render()` works against native views with no reconciler changes.
9
+ */
10
+ interface NativeBackend<N> extends HostBackend<N> {
11
+ /** The target platform this backend drives. */
12
+ readonly platform: 'ios' | 'android';
13
+ }
14
+ /**
15
+ * 🔬 Research track. A GPU-canvas backend (wgpu/WebGPU) for pixel-perfect custom
16
+ * UI. Composes with the native strand: a canvas subtree renders to a GPU surface
17
+ * embedded among native host nodes.
18
+ */
19
+ interface CanvasBackend<N> extends HostBackend<N> {
20
+ /** Marks this backend as drawing to a GPU canvas surface. */
21
+ readonly surface: 'gpu-canvas';
22
+ }
23
+ /**
24
+ * 🔬 Research track — a direct iOS/Android runtime backend that draws platform views.
25
+ * **Not implemented**: throws {@link NotImplementedError}.
26
+ *
27
+ * Today, drive {@link createNativeCommandBackend} to produce the native command
28
+ * stream consumed by the verified host projects in `examples/native-hosts/`, or
29
+ * {@link createDomBackend} for the web.
30
+ *
31
+ * @experimental
32
+ */
33
+ declare function createNativeBackend(_platform: 'ios' | 'android'): never;
34
+ /**
35
+ * 🔬 Research track — not implemented. Throws {@link NotImplementedError}.
36
+ *
37
+ * @experimental
38
+ */
39
+ declare function createCanvasBackend(): never;
40
+ //#endregion
41
+ export { CanvasBackend, NativeBackend, createCanvasBackend, createNativeBackend };
42
+ //# sourceMappingURL=native.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"native.d.ts","names":[],"sources":["../src/native.ts"],"mappings":";;;;;AA0EmC;;;;UAlClB,aAAA,YAAyB,WAAW,CAAC,CAAA;;WAE3C,QAAA;AAAA;;;;;;UAQM,aAAA,YAAyB,WAAW,CAAC,CAAA;;WAE3C,OAAA;AAAA;;;;;;;;;;;iBAaK,mBAAA,CAAoB,SAA4B;;;;;;iBAShD,mBAAA"}
package/dist/native.js ADDED
@@ -0,0 +1,59 @@
1
+ import "./native-command-backend.js";
2
+ import { NotImplementedError } from "@mindees/core";
3
+ //#region src/native.ts
4
+ /**
5
+ * Native rendering backends.
6
+ *
7
+ * Two layers live here, at different maturities:
8
+ *
9
+ * - ✅ **Native command backend** ({@link createNativeCommandBackend},
10
+ * re-exported below) — **implemented today**. It turns the Helix element tree +
11
+ * fine-grained reactive updates into a serializable {@link NativeCommand}
12
+ * stream that a native host can replay. This is the Phase 8A foundation for
13
+ * native rendering; it does not itself draw to the screen.
14
+ * - 🔬 **Direct runtime backends** ({@link createNativeBackend},
15
+ * {@link createCanvasBackend}) — **research tracks**. They define the contracts
16
+ * so the Helix architecture is real and the public API is honest, but they are
17
+ * **not implemented**: the constructors throw {@link NotImplementedError}.
18
+ *
19
+ * The web/DOM backend ({@link createDomBackend}), the headless backend, the native
20
+ * command backend, and the strict reference host are the fully working render targets
21
+ * and protocol validation path today. The iOS/UIKit and Android View host projects in
22
+ * `examples/native-hosts/` compile and render the command stream in CI.
23
+ *
24
+ * - **Native strand** (`NativeBackend`): a direct runtime backend that will connect
25
+ * a running JS app to real platform views. The command protocol and reference host
26
+ * projects exist today; the full app bridge/embedded JS engine remains future work.
27
+ * - **GPU canvas strand** (`CanvasBackend`): a wgpu/WebGPU surface with
28
+ * build-time-precompiled shaders, for pixel-perfect custom UI composited next
29
+ * to native nodes.
30
+ *
31
+ * See ROADMAP.md and STATUS.md for the honest maturity breakdown.
32
+ *
33
+ * @module
34
+ */
35
+ /**
36
+ * 🔬 Research track — a direct iOS/Android runtime backend that draws platform views.
37
+ * **Not implemented**: throws {@link NotImplementedError}.
38
+ *
39
+ * Today, drive {@link createNativeCommandBackend} to produce the native command
40
+ * stream consumed by the verified host projects in `examples/native-hosts/`, or
41
+ * {@link createDomBackend} for the web.
42
+ *
43
+ * @experimental
44
+ */
45
+ function createNativeBackend(_platform) {
46
+ throw new NotImplementedError("Native (iOS/Android) platform host backend");
47
+ }
48
+ /**
49
+ * 🔬 Research track — not implemented. Throws {@link NotImplementedError}.
50
+ *
51
+ * @experimental
52
+ */
53
+ function createCanvasBackend() {
54
+ throw new NotImplementedError("GPU canvas renderer backend (wgpu/WebGPU)");
55
+ }
56
+ //#endregion
57
+ export { createCanvasBackend, createNativeBackend };
58
+
59
+ //# sourceMappingURL=native.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"native.js","names":[],"sources":["../src/native.ts"],"sourcesContent":["/**\n * Native rendering backends.\n *\n * Two layers live here, at different maturities:\n *\n * - ✅ **Native command backend** ({@link createNativeCommandBackend},\n * re-exported below) — **implemented today**. It turns the Helix element tree +\n * fine-grained reactive updates into a serializable {@link NativeCommand}\n * stream that a native host can replay. This is the Phase 8A foundation for\n * native rendering; it does not itself draw to the screen.\n * - 🔬 **Direct runtime backends** ({@link createNativeBackend},\n * {@link createCanvasBackend}) — **research tracks**. They define the contracts\n * so the Helix architecture is real and the public API is honest, but they are\n * **not implemented**: the constructors throw {@link NotImplementedError}.\n *\n * The web/DOM backend ({@link createDomBackend}), the headless backend, the native\n * command backend, and the strict reference host are the fully working render targets\n * and protocol validation path today. The iOS/UIKit and Android View host projects in\n * `examples/native-hosts/` compile and render the command stream in CI.\n *\n * - **Native strand** (`NativeBackend`): a direct runtime backend that will connect\n * a running JS app to real platform views. The command protocol and reference host\n * projects exist today; the full app bridge/embedded JS engine remains future work.\n * - **GPU canvas strand** (`CanvasBackend`): a wgpu/WebGPU surface with\n * build-time-precompiled shaders, for pixel-perfect custom UI composited next\n * to native nodes.\n *\n * See ROADMAP.md and STATUS.md for the honest maturity breakdown.\n *\n * @module\n */\n\nimport { NotImplementedError } from '@mindees/core'\nimport type { HostBackend } from './backend'\n\n/**\n * 🔬 Research track. The native (iOS/Android) host backend. Extends the same\n * {@link HostBackend} contract the reconciler already speaks, so when it lands,\n * `render()` works against native views with no reconciler changes.\n */\nexport interface NativeBackend<N> extends HostBackend<N> {\n /** The target platform this backend drives. */\n readonly platform: 'ios' | 'android'\n}\n\n/**\n * 🔬 Research track. A GPU-canvas backend (wgpu/WebGPU) for pixel-perfect custom\n * UI. Composes with the native strand: a canvas subtree renders to a GPU surface\n * embedded among native host nodes.\n */\nexport interface CanvasBackend<N> extends HostBackend<N> {\n /** Marks this backend as drawing to a GPU canvas surface. */\n readonly surface: 'gpu-canvas'\n}\n\n/**\n * 🔬 Research track — a direct iOS/Android runtime backend that draws platform views.\n * **Not implemented**: throws {@link NotImplementedError}.\n *\n * Today, drive {@link createNativeCommandBackend} to produce the native command\n * stream consumed by the verified host projects in `examples/native-hosts/`, or\n * {@link createDomBackend} for the web.\n *\n * @experimental\n */\nexport function createNativeBackend(_platform: 'ios' | 'android'): never {\n throw new NotImplementedError('Native (iOS/Android) platform host backend')\n}\n\n/**\n * 🔬 Research track — not implemented. Throws {@link NotImplementedError}.\n *\n * @experimental\n */\nexport function createCanvasBackend(): never {\n throw new NotImplementedError('GPU canvas renderer backend (wgpu/WebGPU)')\n}\n\n/**\n * ✅ The implemented native MVP: a backend that emits a serializable\n * {@link NativeCommand} stream for a native host to replay. See\n * {@link import('./native-command-backend').createNativeCommandBackend}.\n */\nexport {\n createNativeCommandBackend,\n type NativeCommandBackend,\n type NativeCommandBackendOptions,\n type NativeCommandNode,\n} from './native-command-backend'\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAiEA,SAAgB,oBAAoB,WAAqC;CACvE,MAAM,IAAI,oBAAoB,4CAA4C;AAC5E;;;;;;AAOA,SAAgB,sBAA6B;CAC3C,MAAM,IAAI,oBAAoB,2CAA2C;AAC3E"}
@@ -0,0 +1,28 @@
1
+ import { HostBackend } from "./backend.js";
2
+ import { Component, MindeesNode } from "@mindees/core";
3
+
4
+ //#region src/render.d.ts
5
+ /** A mounted subtree: its host nodes plus a disposer that unmounts + cleans up. */
6
+ interface Mounted<N> {
7
+ /** The top-level host nodes produced (a fragment can yield several). */
8
+ readonly nodes: N[];
9
+ /** Unmount: remove host nodes and dispose all reactive bindings. */
10
+ dispose(): void;
11
+ }
12
+ /**
13
+ * Render `node` into `container` using `backend`. Returns a {@link Mounted}
14
+ * handle whose `dispose()` removes the produced nodes and tears down every
15
+ * reactive binding created during the render.
16
+ *
17
+ * @example
18
+ * const backend = createHeadlessBackend()
19
+ * const root = createHeadlessRoot()
20
+ * const app = render(MyComponent, {}, backend, root) // component form
21
+ * const m = render(element, backend, root) // element form
22
+ * m.dispose()
23
+ */
24
+ declare function render<N>(node: MindeesNode, backend: HostBackend<N>, container: N): Mounted<N>;
25
+ declare function render<N, P>(component: Component<P>, props: P, backend: HostBackend<N>, container: N): Mounted<N>;
26
+ //#endregion
27
+ export { Mounted, render };
28
+ //# sourceMappingURL=render.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"render.d.ts","names":[],"sources":["../src/render.ts"],"mappings":";;;;;UAgDiB,OAAA;EAmB2E;EAAA,SAjBjF,KAAA,EAAO,CAAC;EAiBI;EAfrB,OAAA;AAAA;;;;;;;;;AAe4F;AAC9F;;;iBADgB,MAAA,IAAU,IAAA,EAAM,WAAA,EAAa,OAAA,EAAS,WAAA,CAAY,CAAA,GAAI,SAAA,EAAW,CAAA,GAAI,OAAA,CAAQ,CAAA;AAAA,iBAC7E,MAAA,OACd,SAAA,EAAW,SAAA,CAAU,CAAA,GACrB,KAAA,EAAO,CAAA,EACP,OAAA,EAAS,WAAA,CAAY,CAAA,GACrB,SAAA,EAAW,CAAA,GACV,OAAA,CAAQ,CAAA"}
package/dist/render.js ADDED
@@ -0,0 +1,145 @@
1
+ import { ELEMENT_TYPE, createRoot, effect, onCleanup, untrack } from "@mindees/core";
2
+ //#region src/render.ts
3
+ /**
4
+ * Helix reconciler — turns a MindeesNative element tree into host nodes via a
5
+ * {@link HostBackend}, with **fine-grained reactive bindings**.
6
+ *
7
+ * There is no virtual-DOM diff. Instead:
8
+ * - A dynamic prop (a function value) becomes an `effect` that patches exactly
9
+ * that one attribute when its signals change.
10
+ * - A dynamic child (a function returning nodes) becomes an `effect` that
11
+ * replaces exactly that region of the host tree.
12
+ * - Everything created during render is owned by a reactive scope, so unmounting
13
+ * disposes every binding — no leaks.
14
+ *
15
+ * This is the Phase 1/2 reactivity paying off: updates are O(what-changed), not
16
+ * O(tree).
17
+ *
18
+ * @module
19
+ */
20
+ function isElementLike(value) {
21
+ return typeof value === "object" && value !== null && value.$$typeof === ELEMENT_TYPE;
22
+ }
23
+ function isEventProp(key) {
24
+ return key.length > 2 && key[0] === "o" && key[1] === "n" && key[2] === (key[2] ?? "").toUpperCase();
25
+ }
26
+ function render(a, b, c, d) {
27
+ const isComponentForm = d !== void 0;
28
+ const backend = isComponentForm ? c : b;
29
+ const container = isComponentForm ? d : c;
30
+ let nodes = [];
31
+ let dispose;
32
+ try {
33
+ createRoot((d) => {
34
+ dispose = d;
35
+ nodes = mountNode(isComponentForm ? a(b) : a, backend, container, null);
36
+ });
37
+ } catch (err) {
38
+ dispose?.();
39
+ throw err;
40
+ }
41
+ return {
42
+ nodes,
43
+ dispose() {
44
+ for (const n of nodes) {
45
+ const parent = backend.parentOf(n);
46
+ if (parent) backend.remove(parent, n);
47
+ }
48
+ dispose();
49
+ }
50
+ };
51
+ }
52
+ /**
53
+ * Mount a node into `parent` before `anchor`. Returns the top-level host nodes
54
+ * created (for fragments / arrays this can be more than one).
55
+ */
56
+ function mountNode(node, backend, parent, anchor) {
57
+ if (node === null || node === void 0 || typeof node === "boolean") return [];
58
+ if (typeof node === "function") return bindReactiveChild(node, backend, parent, anchor);
59
+ if (typeof node === "string" || typeof node === "number") {
60
+ const text = backend.createText(String(node));
61
+ backend.insert(parent, text, anchor);
62
+ return [text];
63
+ }
64
+ if (Array.isArray(node)) {
65
+ const out = [];
66
+ for (const child of node) out.push(...mountNode(child, backend, parent, anchor));
67
+ return out;
68
+ }
69
+ if (isElementLike(node)) {
70
+ const { type } = node;
71
+ if (typeof type === "function") return mountNode(type({
72
+ ...node.props,
73
+ children: node.children
74
+ }), backend, parent, anchor);
75
+ const el = backend.createElement(type);
76
+ for (const [key, value] of Object.entries(node.props)) bindProp(backend, el, key, value);
77
+ mountChildren(node.children, backend, el);
78
+ backend.insert(parent, el, anchor);
79
+ return [el];
80
+ }
81
+ return [];
82
+ }
83
+ /** Mount a list of children into `parent`, appending in order. */
84
+ function mountChildren(children, backend, parent) {
85
+ for (const child of children) mountNode(child, backend, parent, null);
86
+ }
87
+ /**
88
+ * Apply a prop. A function value is a **reactive binding**: an effect re-applies
89
+ * exactly this attribute when its dependencies change. Event props (`onX`) are
90
+ * applied once (the handler itself can close over signals).
91
+ */
92
+ function bindProp(backend, el, key, value) {
93
+ if (key === "children") return;
94
+ if (isEventProp(key)) {
95
+ backend.setProp(el, key, value, void 0);
96
+ onCleanup(() => backend.setProp(el, key, void 0, value));
97
+ return;
98
+ }
99
+ if (typeof value === "function") {
100
+ let prev;
101
+ effect(() => {
102
+ const next = value();
103
+ backend.setProp(el, key, next, prev);
104
+ prev = next;
105
+ });
106
+ return;
107
+ }
108
+ backend.setProp(el, key, value, void 0);
109
+ }
110
+ /**
111
+ * Bind a reactive child region: an effect that, when the accessor changes,
112
+ * unmounts the previous nodes and mounts the new ones at the same position. A
113
+ * stable text-only fast path patches the text node in place.
114
+ *
115
+ * The effect runs synchronously on creation, so `current` is populated before
116
+ * we return it — letting the caller report the region's initial host nodes.
117
+ */
118
+ function bindReactiveChild(accessor, backend, parent, initialAnchor) {
119
+ const marker = backend.createText("");
120
+ backend.insert(parent, marker, initialAnchor);
121
+ const nodes = [marker];
122
+ let content = [];
123
+ onCleanup(() => {
124
+ for (const n of content) if (backend.parentOf(n)) backend.remove(parent, n);
125
+ if (backend.parentOf(marker)) backend.remove(parent, marker);
126
+ });
127
+ effect(() => {
128
+ const value = accessor();
129
+ untrack(() => {
130
+ if (content.length === 1 && content[0] !== void 0 && backend.isText(content[0]) && (typeof value === "string" || typeof value === "number")) {
131
+ backend.setText(content[0], String(value));
132
+ return;
133
+ }
134
+ for (const n of content) if (backend.parentOf(n)) backend.remove(parent, n);
135
+ content = mountNode(value, backend, parent, marker);
136
+ nodes.length = 0;
137
+ nodes.push(...content, marker);
138
+ });
139
+ });
140
+ return nodes;
141
+ }
142
+ //#endregion
143
+ export { render };
144
+
145
+ //# sourceMappingURL=render.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"render.js","names":["component"],"sources":["../src/render.ts"],"sourcesContent":["/**\n * Helix reconciler — turns a MindeesNative element tree into host nodes via a\n * {@link HostBackend}, with **fine-grained reactive bindings**.\n *\n * There is no virtual-DOM diff. Instead:\n * - A dynamic prop (a function value) becomes an `effect` that patches exactly\n * that one attribute when its signals change.\n * - A dynamic child (a function returning nodes) becomes an `effect` that\n * replaces exactly that region of the host tree.\n * - Everything created during render is owned by a reactive scope, so unmounting\n * disposes every binding — no leaks.\n *\n * This is the Phase 1/2 reactivity paying off: updates are O(what-changed), not\n * O(tree).\n *\n * @module\n */\n\nimport {\n type Component,\n createRoot,\n ELEMENT_TYPE,\n effect,\n type MindeesElement,\n type MindeesNode,\n onCleanup,\n untrack,\n} from '@mindees/core'\nimport type { HostBackend } from './backend'\n\n/** A dynamic value: pass a function and the binding reacts to its signals. */\ntype MaybeReactive<T> = T | (() => T)\n\nfunction isElementLike(value: unknown): value is MindeesElement {\n return (\n typeof value === 'object' &&\n value !== null &&\n (value as { $$typeof?: unknown }).$$typeof === ELEMENT_TYPE\n )\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/** A mounted subtree: its host nodes plus a disposer that unmounts + cleans up. */\nexport interface Mounted<N> {\n /** The top-level host nodes produced (a fragment can yield several). */\n readonly nodes: N[]\n /** Unmount: remove host nodes and dispose all reactive bindings. */\n dispose(): void\n}\n\n/**\n * Render `node` into `container` using `backend`. Returns a {@link Mounted}\n * handle whose `dispose()` removes the produced nodes and tears down every\n * reactive binding created during the render.\n *\n * @example\n * const backend = createHeadlessBackend()\n * const root = createHeadlessRoot()\n * const app = render(MyComponent, {}, backend, root) // component form\n * const m = render(element, backend, root) // element form\n * m.dispose()\n */\nexport function render<N>(node: MindeesNode, backend: HostBackend<N>, container: N): Mounted<N>\nexport function render<N, P>(\n component: Component<P>,\n props: P,\n backend: HostBackend<N>,\n container: N,\n): Mounted<N>\nexport function render<N, P>(\n a: MindeesNode | Component<P>,\n b: HostBackend<N> | P,\n c?: HostBackend<N> | N,\n d?: N,\n): Mounted<N> {\n // Disambiguate by ARITY, not `typeof a`: `MindeesNode` now includes the\n // accessor form `() => MindeesNode`, so a function `a` may be either a\n // component (4-arg form) or a reactive node (3-arg form). The component form\n // is the only one with a 4th argument (`container = d`).\n const isComponentForm = d !== undefined\n const backend = (isComponentForm ? c : b) as HostBackend<N>\n const container = (isComponentForm ? d : c) as N\n\n let nodes: N[] = []\n // Capture the disposer eagerly — createRoot passes it to the callback\n // synchronously, BEFORE the body runs. If the component or mountNode throws\n // part-way, effects/regions created before the throw are already adopted on\n // this root; createRoot does NOT auto-dispose on a throw, so without this they\n // would leak (stay subscribed forever) and the caller would get no disposer.\n // Dispose the partial scope, then rethrow — restoring the \"no leaks\" guarantee.\n let dispose!: () => void\n try {\n createRoot((d) => {\n dispose = d\n // Evaluate the component INSIDE the root scope so its effects/memos are\n // owned here and disposed with us. A non-component node is mounted as-is\n // (an accessor node becomes a reactive region during mount).\n const node: MindeesNode = isComponentForm ? (a as Component<P>)(b as P) : (a as MindeesNode)\n nodes = mountNode(node, backend, container, null)\n })\n } catch (err) {\n dispose?.()\n throw err\n }\n\n return {\n nodes,\n dispose() {\n for (const n of nodes) {\n const parent = backend.parentOf(n)\n if (parent) backend.remove(parent, n)\n }\n dispose()\n },\n }\n}\n\n/**\n * Mount a node into `parent` before `anchor`. Returns the top-level host nodes\n * created (for fragments / arrays this can be more than one).\n */\nfunction mountNode<N>(\n node: MindeesNode,\n backend: HostBackend<N>,\n parent: N,\n anchor: N | null,\n): N[] {\n // Null-ish / boolean → nothing.\n if (node === null || node === undefined || typeof node === 'boolean') return []\n\n // Function node → a reactive region (an accessor `() => MindeesNode`). Handled\n // uniformly here so it works at the top level and as a child.\n if (typeof node === 'function') {\n return bindReactiveChild(node as () => MindeesNode, backend, parent, anchor)\n }\n\n // Text-like primitives.\n if (typeof node === 'string' || typeof node === 'number') {\n const text = backend.createText(String(node))\n backend.insert(parent, text, anchor)\n return [text]\n }\n\n // Arrays / fragments → mount each child in order.\n if (Array.isArray(node)) {\n const out: N[] = []\n for (const child of node) out.push(...mountNode(child, backend, parent, anchor))\n return out\n }\n\n if (isElementLike(node)) {\n const { type } = node\n // Function component: invoked directly. We are already inside render()'s\n // createRoot owner scope, so any effects/memos the component creates are\n // owned here and disposed on unmount. `children` is passed through props.\n if (typeof type === 'function') {\n const component = type as Component<Record<string, unknown>>\n const rendered = component({ ...node.props, children: node.children })\n return mountNode(rendered, backend, parent, anchor)\n }\n\n // Host element.\n const el = backend.createElement(type)\n for (const [key, value] of Object.entries(node.props)) {\n bindProp(backend, el, key, value)\n }\n mountChildren(node.children, backend, el)\n backend.insert(parent, el, anchor)\n return [el]\n }\n\n return []\n}\n\n/** Mount a list of children into `parent`, appending in order. */\nfunction mountChildren<N>(\n children: readonly MindeesNode[],\n backend: HostBackend<N>,\n parent: N,\n): void {\n // mountNode handles every node kind uniformly, including function children\n // (reactive regions) via the function-node branch.\n for (const child of children) {\n mountNode(child, backend, parent, null)\n }\n}\n\n/**\n * Apply a prop. A function value is a **reactive binding**: an effect re-applies\n * exactly this attribute when its dependencies change. Event props (`onX`) are\n * applied once (the handler itself can close over signals).\n */\nfunction bindProp<N>(backend: HostBackend<N>, el: N, key: string, value: unknown): void {\n if (key === 'children') return\n if (isEventProp(key)) {\n backend.setProp(el, key, value, undefined)\n // Symmetric teardown: remove the listener when this scope is disposed (unmount\n // or an enclosing region re-run), so a backend's addEventListener always has a\n // matching removal — not left to GC. Passing `undefined` drives the backend's\n // own listener-removal path (e.g. the DOM backend's removeEventListener).\n onCleanup(() => backend.setProp(el, key, undefined, value))\n return\n }\n if (typeof value === 'function') {\n let prev: unknown\n effect(() => {\n const next = (value as () => unknown)()\n backend.setProp(el, key, next, prev)\n prev = next\n })\n return\n }\n backend.setProp(el, key, value, undefined)\n}\n\n/**\n * Bind a reactive child region: an effect that, when the accessor changes,\n * unmounts the previous nodes and mounts the new ones at the same position. A\n * stable text-only fast path patches the text node in place.\n *\n * The effect runs synchronously on creation, so `current` is populated before\n * we return it — letting the caller report the region's initial host nodes.\n */\nfunction bindReactiveChild<N>(\n accessor: () => MindeesNode,\n backend: HostBackend<N>,\n parent: N,\n initialAnchor: N | null,\n): N[] {\n // Pin the region's slot with a persistent, invisible empty-text marker, and\n // always (re)mount content immediately BEFORE it. This keeps the region's\n // exact position across empty↔content transitions (an empty region previously\n // collapsed — its content reappeared at the parent's end, breaking the\n // `() => cond() ? <X/> : null` pattern when the region had following siblings)\n // and keeps adjacent regions in order. The marker serializes to '' so it is\n // invisible in output.\n const marker = backend.createText('')\n backend.insert(parent, marker, initialAnchor)\n\n // `nodes` is a STABLE, live array — the region's current content followed by\n // the slot marker — mutated in place on every run. Returning the same array\n // reference (not a one-time snapshot) means a caller that captures it once\n // (e.g. render()'s root disposer) always removes the CURRENT content, not the\n // first-run nodes.\n const nodes: N[] = [marker]\n let content: N[] = []\n\n // Authoritative unmount for the region. Reading the LIVE `content`/`marker` at\n // teardown means it removes whatever is mounted NOW, regardless of how the\n // region was composed. This is required for correctness: when the region is a\n // child of a top-level array/fragment, render()'s disposer only captured a\n // flattened ONE-TIME snapshot of the host nodes (the array branch in mountNode\n // spreads `nodes` into a fresh array), so after a content swap it can no longer\n // remove the current content — that node would leak. This owner-scoped cleanup\n // closes that gap. It is owned by whoever mounted the region: render()'s root\n // (fires once, on final dispose) or an enclosing region effect (fires on each\n // re-run, tearing the nested region down). Guarded by parentOf so it is a safe\n // no-op for nodes already detached by render()'s disposer or a swap.\n onCleanup(() => {\n for (const n of content) {\n if (backend.parentOf(n)) backend.remove(parent, n)\n }\n if (backend.parentOf(marker)) backend.remove(parent, marker)\n })\n\n effect(() => {\n const value = accessor()\n untrack(() => {\n // Fast path: single existing text node + new text-like value → patch.\n if (\n content.length === 1 &&\n content[0] !== undefined &&\n backend.isText(content[0]) &&\n (typeof value === 'string' || typeof value === 'number')\n ) {\n backend.setText(content[0], String(value))\n return\n }\n // Guarded: when this region is nested in another region, the parent's\n // re-run fires this region's onCleanup first (detaching `content`), so a\n // second removal here would hit an already-detached node — the DOM\n // backend's removeChild throws on a non-child.\n for (const n of content) {\n if (backend.parentOf(n)) backend.remove(parent, n)\n }\n content = mountNode(value, backend, parent, marker)\n nodes.length = 0\n nodes.push(...content, marker)\n })\n })\n return nodes\n}\n\nexport type { MaybeReactive }\n"],"mappings":"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"}