@symbiote-native/engine 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 (195) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +124 -0
  3. package/build/accessibility-info/index.android.d.ts +3 -0
  4. package/build/accessibility-info/index.android.js +166 -0
  5. package/build/accessibility-info/index.d.ts +1 -0
  6. package/build/accessibility-info/index.ios.d.ts +3 -0
  7. package/build/accessibility-info/index.ios.js +219 -0
  8. package/build/accessibility-info/index.js +5 -0
  9. package/build/accessibility-info/shared.d.ts +34 -0
  10. package/build/accessibility-info/shared.js +13 -0
  11. package/build/action-sheet-ios/index.d.ts +36 -0
  12. package/build/action-sheet-ios/index.js +74 -0
  13. package/build/alert/index.android.d.ts +5 -0
  14. package/build/alert/index.android.js +117 -0
  15. package/build/alert/index.d.ts +1 -0
  16. package/build/alert/index.ios.d.ts +7 -0
  17. package/build/alert/index.ios.js +83 -0
  18. package/build/alert/index.js +8 -0
  19. package/build/alert/shared.d.ts +19 -0
  20. package/build/alert/shared.js +17 -0
  21. package/build/animated/animated-component-shared.d.ts +5 -0
  22. package/build/animated/animated-component-shared.js +54 -0
  23. package/build/animated/animation.d.ts +9 -0
  24. package/build/animated/animation.js +6 -0
  25. package/build/animated/animations/base.d.ts +27 -0
  26. package/build/animated/animations/base.js +90 -0
  27. package/build/animated/animations/composition.d.ts +38 -0
  28. package/build/animated/animations/composition.js +236 -0
  29. package/build/animated/animations/decay.d.ts +22 -0
  30. package/build/animated/animations/decay.js +65 -0
  31. package/build/animated/animations/raf.d.ts +5 -0
  32. package/build/animated/animations/raf.js +39 -0
  33. package/build/animated/animations/spring-config.d.ts +6 -0
  34. package/build/animated/animations/spring-config.js +55 -0
  35. package/build/animated/animations/spring.d.ts +50 -0
  36. package/build/animated/animations/spring.js +207 -0
  37. package/build/animated/animations/timing.d.ts +27 -0
  38. package/build/animated/animations/timing.js +101 -0
  39. package/build/animated/animations/tracking.d.ts +14 -0
  40. package/build/animated/animations/tracking.js +43 -0
  41. package/build/animated/bezier.d.ts +1 -0
  42. package/build/animated/bezier.js +101 -0
  43. package/build/animated/color.d.ts +37 -0
  44. package/build/animated/color.js +183 -0
  45. package/build/animated/easing.d.ts +20 -0
  46. package/build/animated/easing.js +96 -0
  47. package/build/animated/event.d.ts +36 -0
  48. package/build/animated/event.js +252 -0
  49. package/build/animated/graph.d.ts +38 -0
  50. package/build/animated/graph.js +227 -0
  51. package/build/animated/index.d.ts +20 -0
  52. package/build/animated/index.js +28 -0
  53. package/build/animated/interpolation-node.d.ts +16 -0
  54. package/build/animated/interpolation-node.js +57 -0
  55. package/build/animated/interpolation.d.ts +22 -0
  56. package/build/animated/interpolation.js +199 -0
  57. package/build/animated/mock.d.ts +56 -0
  58. package/build/animated/mock.js +127 -0
  59. package/build/animated/native/native-animated.d.ts +43 -0
  60. package/build/animated/native/native-animated.js +146 -0
  61. package/build/animated/operators.d.ts +80 -0
  62. package/build/animated/operators.js +266 -0
  63. package/build/animated/props.d.ts +20 -0
  64. package/build/animated/props.js +187 -0
  65. package/build/animated/style.d.ts +26 -0
  66. package/build/animated/style.js +187 -0
  67. package/build/animated/value-xy.d.ts +35 -0
  68. package/build/animated/value-xy.js +106 -0
  69. package/build/animated/value.d.ts +36 -0
  70. package/build/animated/value.js +185 -0
  71. package/build/app-registry/index.d.ts +40 -0
  72. package/build/app-registry/index.js +144 -0
  73. package/build/app-state/index.d.ts +16 -0
  74. package/build/app-state/index.js +105 -0
  75. package/build/appearance/index.d.ts +12 -0
  76. package/build/appearance/index.js +84 -0
  77. package/build/back-handler/index.d.ts +14 -0
  78. package/build/back-handler/index.js +106 -0
  79. package/build/commit.d.ts +16 -0
  80. package/build/commit.js +678 -0
  81. package/build/debug.d.ts +5 -0
  82. package/build/debug.js +18 -0
  83. package/build/dimensions/index.d.ts +28 -0
  84. package/build/dimensions/index.js +148 -0
  85. package/build/dispatch.d.ts +2 -0
  86. package/build/dispatch.js +18 -0
  87. package/build/events/index.d.ts +1 -0
  88. package/build/events/index.js +691 -0
  89. package/build/fabric.d.ts +32 -0
  90. package/build/fabric.js +59 -0
  91. package/build/host-instance/index.d.ts +11 -0
  92. package/build/host-instance/index.js +49 -0
  93. package/build/i18n-manager/index.d.ts +13 -0
  94. package/build/i18n-manager/index.js +91 -0
  95. package/build/index.d.ts +80 -0
  96. package/build/index.js +72 -0
  97. package/build/interaction-manager/index.d.ts +45 -0
  98. package/build/interaction-manager/index.js +222 -0
  99. package/build/keyboard/index.d.ts +31 -0
  100. package/build/keyboard/index.js +142 -0
  101. package/build/layout-animation/index.d.ts +66 -0
  102. package/build/layout-animation/index.js +183 -0
  103. package/build/linking/index.android.d.ts +2 -0
  104. package/build/linking/index.android.js +18 -0
  105. package/build/linking/index.d.ts +1 -0
  106. package/build/linking/index.ios.d.ts +2 -0
  107. package/build/linking/index.ios.js +9 -0
  108. package/build/linking/index.js +6 -0
  109. package/build/linking/shared.d.ts +32 -0
  110. package/build/linking/shared.js +98 -0
  111. package/build/native-events.d.ts +24 -0
  112. package/build/native-events.js +129 -0
  113. package/build/native-modules.d.ts +6 -0
  114. package/build/native-modules.js +57 -0
  115. package/build/node.d.ts +36 -0
  116. package/build/node.js +194 -0
  117. package/build/pan-responder/index.d.ts +53 -0
  118. package/build/pan-responder/index.js +353 -0
  119. package/build/permissions-android/index.d.ts +115 -0
  120. package/build/permissions-android/index.js +185 -0
  121. package/build/pixel-ratio/index.d.ts +8 -0
  122. package/build/pixel-ratio/index.js +27 -0
  123. package/build/platform/index.android.d.ts +22 -0
  124. package/build/platform/index.android.js +60 -0
  125. package/build/platform/index.d.ts +1 -0
  126. package/build/platform/index.ios.d.ts +18 -0
  127. package/build/platform/index.ios.js +62 -0
  128. package/build/platform/index.js +5 -0
  129. package/build/platform/shared.d.ts +25 -0
  130. package/build/platform/shared.js +41 -0
  131. package/build/platform-color.d.ts +19 -0
  132. package/build/platform-color.js +25 -0
  133. package/build/post-commit.d.ts +4 -0
  134. package/build/post-commit.js +16 -0
  135. package/build/process-aspect-ratio.d.ts +1 -0
  136. package/build/process-aspect-ratio.js +34 -0
  137. package/build/process-background-image/index.d.ts +28 -0
  138. package/build/process-background-image/index.js +557 -0
  139. package/build/process-box-shadow/index.d.ts +11 -0
  140. package/build/process-box-shadow/index.js +193 -0
  141. package/build/process-filter.d.ts +31 -0
  142. package/build/process-filter.js +304 -0
  143. package/build/process-font-variant.d.ts +1 -0
  144. package/build/process-font-variant.js +17 -0
  145. package/build/process-transform/index.d.ts +5 -0
  146. package/build/process-transform/index.js +120 -0
  147. package/build/process-transform-origin/index.d.ts +3 -0
  148. package/build/process-transform-origin/index.js +108 -0
  149. package/build/registry.d.ts +31 -0
  150. package/build/registry.js +145 -0
  151. package/build/settings/index.d.ts +8 -0
  152. package/build/settings/index.js +126 -0
  153. package/build/share/index.android.d.ts +3 -0
  154. package/build/share/index.android.js +56 -0
  155. package/build/share/index.d.ts +1 -0
  156. package/build/share/index.ios.d.ts +3 -0
  157. package/build/share/index.ios.js +47 -0
  158. package/build/share/index.js +6 -0
  159. package/build/share/shared.d.ts +32 -0
  160. package/build/share/shared.js +32 -0
  161. package/build/status-bar/index.android.d.ts +5 -0
  162. package/build/status-bar/index.android.js +83 -0
  163. package/build/status-bar/index.d.ts +1 -0
  164. package/build/status-bar/index.ios.d.ts +5 -0
  165. package/build/status-bar/index.ios.js +66 -0
  166. package/build/status-bar/index.js +4 -0
  167. package/build/status-bar/shared.d.ts +22 -0
  168. package/build/status-bar/shared.js +22 -0
  169. package/build/style/index.d.ts +1 -0
  170. package/build/style/index.js +30 -0
  171. package/build/style-registry/index.d.ts +11 -0
  172. package/build/style-registry/index.js +165 -0
  173. package/build/style-sheet/index.d.ts +20 -0
  174. package/build/style-sheet/index.js +121 -0
  175. package/build/styles.d.ts +220 -0
  176. package/build/styles.js +7 -0
  177. package/build/surface.d.ts +16 -0
  178. package/build/surface.js +67 -0
  179. package/build/tags.d.ts +1 -0
  180. package/build/tags.js +10 -0
  181. package/build/text-input-state.d.ts +5 -0
  182. package/build/text-input-state.js +29 -0
  183. package/build/toast-android/index.d.ts +10 -0
  184. package/build/toast-android/index.js +108 -0
  185. package/build/vibration/index.android.d.ts +2 -0
  186. package/build/vibration/index.android.js +18 -0
  187. package/build/vibration/index.d.ts +1 -0
  188. package/build/vibration/index.ios.d.ts +2 -0
  189. package/build/vibration/index.ios.js +54 -0
  190. package/build/vibration/index.js +6 -0
  191. package/build/vibration/shared.d.ts +15 -0
  192. package/build/vibration/shared.js +68 -0
  193. package/build/view-config.d.ts +1 -0
  194. package/build/view-config.js +114 -0
  195. package/package.json +41 -0
@@ -0,0 +1,678 @@
1
+ // The clone-on-write engine. Fabric is persistent: you never mutate a committed
2
+ // node, you clone it with new props/children and atomically hand a fresh child
3
+ // set to completeRoot.
4
+ //
5
+ // Incremental strategy: each retained node keeps a "mirror" of what Fabric
6
+ // currently holds for it: its handle, the flat props last sent, the child
7
+ // identities last committed, and the resolved view name. On commit we walk the
8
+ // retained tree and only clone the nodes that actually changed; an untouched
9
+ // sibling subtree is reused by reference. That both skips work and preserves the
10
+ // native view state (scroll offset, text cursor) that a full rebuild would wipe
11
+ // on every commit. A change bubbles up: re-cloning a leaf
12
+ // forces each ancestor to re-clone too, because a persistent parent holds
13
+ // references to specific child handles. That bubble is inherent to a persistent
14
+ // tree and is exactly what React's own Fabric renderer does.
15
+ import { getSlot, } from './fabric';
16
+ import { createElement, isAnchor, RAW_TEXT_COMPONENT, VIRTUAL_TEXT_COMPONENT, } from './node';
17
+ import { dlog, isDebug } from './debug';
18
+ import { flattenStyle } from './style';
19
+ import { registeredProcessor } from './registry';
20
+ import { nextTag } from './tags';
21
+ import { isOpaqueColorValue } from './platform-color';
22
+ import { processBoxShadow } from './process-box-shadow';
23
+ import { registerPostCommit, runPostCommitHooks } from './post-commit';
24
+ import { processFilter } from './process-filter';
25
+ import { processTransformOrigin } from './process-transform-origin';
26
+ import { processTransform } from './process-transform';
27
+ import { processAspectRatio } from './process-aspect-ratio';
28
+ import { processFontVariant } from './process-font-variant';
29
+ import { processBackgroundImage } from './process-background-image';
30
+ // Per-commit work counters, surfaced via dlog so a device run can prove the
31
+ // engine is incremental (created=0 with clones after the first mount).
32
+ const stats = { created: 0, cloneProps: 0, cloneChildren: 0, reused: 0 };
33
+ function isRecord(value) {
34
+ return typeof value === 'object' && value !== null;
35
+ }
36
+ // Diagnostic (gated): Fabric serializes props to folly::dynamic, which rejects a JS
37
+ // Symbol or function with "JS Symbols are not convertible to dynamic". A hard native
38
+ // throw deep in cloneNode*. Walk a props payload and return the dotted path of the
39
+ // first non-serializable leaf (Symbol / function), or undefined when clean, so the
40
+ // offending key is named in logcat instead of a bare stack at the JSI boundary.
41
+ //
42
+ // Bounded on purpose: a real Fabric prop tree is shallow (style/transform ~depth 3) and
43
+ // a leaked React element trips at depth 2 (`children` -> element -> $$typeof). `seen`
44
+ // breaks reference cycles and DEPTH caps runaway nesting, so the diagnostic itself can
45
+ // never overflow the stack on cyclic props (an event-carrying handler, a self-referential
46
+ // style). A crashing guard would be worse than the bug it hunts.
47
+ const NON_SERIALIZABLE_SCAN_DEPTH = 6;
48
+ function firstNonSerializablePath(value, path, depth, seen) {
49
+ const kind = typeof value;
50
+ if (kind === 'symbol' || kind === 'function')
51
+ return `${path}=<${kind}>`;
52
+ if (depth >= NON_SERIALIZABLE_SCAN_DEPTH)
53
+ return undefined;
54
+ if (Array.isArray(value)) {
55
+ if (seen.has(value))
56
+ return undefined;
57
+ seen.add(value);
58
+ for (let index = 0; index < value.length; index += 1) {
59
+ const found = firstNonSerializablePath(value[index], `${path}[${index}]`, depth + 1, seen);
60
+ if (found !== undefined)
61
+ return found;
62
+ }
63
+ return undefined;
64
+ }
65
+ if (isRecord(value)) {
66
+ if (seen.has(value))
67
+ return undefined;
68
+ seen.add(value);
69
+ for (const key of Object.keys(value)) {
70
+ const next = path === '' ? key : `${path}.${key}`;
71
+ const found = firstNonSerializablePath(value[key], next, depth + 1, seen);
72
+ if (found !== undefined)
73
+ return found;
74
+ }
75
+ }
76
+ return undefined;
77
+ }
78
+ // Name the offending prop before the JSI boundary throws. Gated, so the deep walk only
79
+ // runs while debugging; in production the clone proceeds straight to native.
80
+ function guardSerializable(propsDiff, viewName, tag) {
81
+ if (!isDebug())
82
+ return;
83
+ const bad = firstNonSerializablePath(propsDiff, '', 0, new WeakSet());
84
+ if (bad !== undefined)
85
+ dlog(`NON-SERIALIZABLE prop on ${viewName}#${tag}: ${bad}`);
86
+ }
87
+ // Color props must reach Fabric as platform ints, not CSS strings. Fabric's C++
88
+ // color parser silently drops strings. The actual conversion (processColor) is
89
+ // RN-platform-specific, so it is injected here rather than imported, keeping
90
+ // shared free of a react-native dependency (and the headless harness working).
91
+ const COLOR_PROPS = new Set([
92
+ 'backgroundColor',
93
+ 'color',
94
+ 'borderColor',
95
+ 'borderTopColor',
96
+ 'borderRightColor',
97
+ 'borderBottomColor',
98
+ 'borderLeftColor',
99
+ // Logical (writing-direction-relative) border colors + the block axis, all wired to
100
+ // processColor in RN's ReactNativeStyleAttributes. borderStartColor/borderEndColor are
101
+ // even publicly typed ColorValue, so they silently dropped on iOS / threw on Android.
102
+ 'borderStartColor',
103
+ 'borderEndColor',
104
+ 'borderBlockColor',
105
+ 'borderBlockStartColor',
106
+ 'borderBlockEndColor',
107
+ 'shadowColor',
108
+ // Text shadow + the W3C `outline`/image `overlay` colors, also processColor in RN.
109
+ 'textShadowColor',
110
+ 'overlayColor',
111
+ 'outlineColor',
112
+ 'tintColor',
113
+ // TextInput color props. iOS's native input accepts a CSS string, but Android's
114
+ // AndroidTextInput is strict ("ColorValue: the value must be a number or Object"),
115
+ // so these must be processColor'd here too, same as any other color reaching Fabric.
116
+ 'placeholderTextColor',
117
+ 'selectionColor',
118
+ 'cursorColor',
119
+ 'underlineColorAndroid',
120
+ // Text decoration color (underline/strike): same Fabric strictness as any color.
121
+ 'textDecorationColor',
122
+ 'selectionHandleColor',
123
+ // Switch track/thumb colors. RN processColors each via the Switch ViewConfig
124
+ // (SwitchNativeComponent / AndroidSwitchNativeComponent validAttributes). iOS takes
125
+ // onTintColor (ON) / tintColor (OFF); Android takes trackColorForTrue/False +
126
+ // trackTintColor, and Android's ColorPropConverter is strict ("the value must be a
127
+ // number or Object"), so a raw CSS string crashes. thumbTintColor reaches both.
128
+ 'onTintColor',
129
+ 'thumbTintColor',
130
+ 'trackColorForTrue',
131
+ 'trackColorForFalse',
132
+ 'trackTintColor',
133
+ ]);
134
+ // Accepts a CSS string or an opaque PlatformColor / DynamicColorIOS object. RN's
135
+ // processColor (the value the canary injects) handles both, resolving the opaque
136
+ // shapes to the platform ints/dicts iOS UIColor expects.
137
+ let colorProcessor = value => value;
138
+ export function setColorProcessor(process) {
139
+ colorProcessor = process;
140
+ }
141
+ // Public mirror of RN's processColor: run a color through the injected platform
142
+ // processor (the canary wires RN's own). Off a real host it resolves CSS strings
143
+ // and opaque PlatformColor objects to the platform ints Fabric expects; headless
144
+ // (no processor wired) it is the identity, so smokes see the input unchanged.
145
+ export function processColor(color) {
146
+ return colorProcessor(color);
147
+ }
148
+ // A color-keyed value the platform processor must convert before Fabric: a CSS
149
+ // string, or an opaque PlatformColor / DynamicColorIOS object. Numbers (already
150
+ // platform ints) and undefined are left untouched.
151
+ function isProcessableColor(value) {
152
+ return typeof value === 'string' || isOpaqueColorValue(value);
153
+ }
154
+ // Structured CSS-style keys RN parses in JS before native (boxShadow/filter register
155
+ // with enableNativeCSSParsing(), which DEFAULTS TO FALSE, so native CSS parsing is off
156
+ // and the raw string is dropped). Each runs on the hoisted top-level style key, turning
157
+ // a CSS string or structured array into the processed array Fabric's C++ expects.
158
+ const STYLE_PROCESSORS = new Map([
159
+ ['boxShadow', value => processBoxShadow(asBoxShadowInput(value))],
160
+ ['filter', value => processFilter(asFilterInput(value))],
161
+ ['transformOrigin', value => processTransformOrigin(asTransformOriginInput(value))],
162
+ ['transform', processTransformValue],
163
+ ['aspectRatio', value => processAspectRatio(asAspectRatioInput(value))],
164
+ ['fontVariant', value => processFontVariant(asFontVariantInput(value))],
165
+ ['experimental_backgroundImage', value => processBackgroundImage(asBackgroundImageInput(value))],
166
+ ]);
167
+ // boxShadow accepts a CSS string or an array of shadow objects; anything else is
168
+ // undefined to processBoxShadow (which returns []). Narrowing avoids an `as` cast.
169
+ function asBoxShadowInput(value) {
170
+ if (typeof value === 'string')
171
+ return value;
172
+ if (Array.isArray(value))
173
+ return value.filter(isRecord);
174
+ return undefined;
175
+ }
176
+ // filter accepts a CSS string or an array of single-key filter objects; same narrowing.
177
+ function asFilterInput(value) {
178
+ if (typeof value === 'string')
179
+ return value;
180
+ if (Array.isArray(value))
181
+ return value.filter(isRecord);
182
+ return undefined;
183
+ }
184
+ // experimental_backgroundImage accepts a CSS string (gradient functions) or an array of
185
+ // structured gradient objects; same narrowing as boxShadow/filter.
186
+ function asBackgroundImageInput(value) {
187
+ if (typeof value === 'string')
188
+ return value;
189
+ if (Array.isArray(value))
190
+ return value.filter(isRecord);
191
+ return undefined;
192
+ }
193
+ // transformOrigin accepts a CSS string or a [x, y, z] array of strings/numbers; anything
194
+ // else is undefined to processTransformOrigin (which defaults to center/center/0).
195
+ function asTransformOriginInput(value) {
196
+ if (typeof value === 'string')
197
+ return value;
198
+ if (Array.isArray(value))
199
+ return value.filter(isStringOrNumber);
200
+ return undefined;
201
+ }
202
+ // aspectRatio accepts a number (the common, working form) or a ratio string; otherwise
203
+ // undefined, which processAspectRatio drops.
204
+ function asAspectRatioInput(value) {
205
+ if (typeof value === 'number' || typeof value === 'string')
206
+ return value;
207
+ return undefined;
208
+ }
209
+ // fontVariant accepts an array of variant strings (the common, working form) or a
210
+ // space-separated string; anything else becomes an empty string, which yields [].
211
+ function asFontVariantInput(value) {
212
+ if (typeof value === 'string')
213
+ return value;
214
+ if (Array.isArray(value))
215
+ return value.filter(isString);
216
+ return '';
217
+ }
218
+ // transform accepts a CSS string (processTransform parses it) or an array of single-key
219
+ // transform records (the hot animated / sticky-header path, passed through unchanged).
220
+ // A non-string non-array value is NOT dropped: it may already be processed, so it passes
221
+ // through verbatim rather than being coerced to [] (which would erase a valid transform).
222
+ function processTransformValue(value) {
223
+ if (typeof value === 'string')
224
+ return processTransform(value);
225
+ if (Array.isArray(value))
226
+ return processTransform(value.filter(isRecord));
227
+ return value;
228
+ }
229
+ function isStringOrNumber(value) {
230
+ return typeof value === 'string' || typeof value === 'number';
231
+ }
232
+ function isString(value) {
233
+ return typeof value === 'string';
234
+ }
235
+ // Convert a prop to the shape Fabric's C++ expects. A third-party view contributes
236
+ // its own processors, auto-derived from its ViewConfig (validAttributes[*].process,
237
+ // e.g. processColor for a slider's track tints); those run first. Then the structured
238
+ // CSS-style processors (boxShadow/filter). Built-ins are never in the registry, so they
239
+ // fall through to the global color path, where any CSS-string color is run through the
240
+ // injected platform processor (Fabric's C++ color parser silently drops strings).
241
+ function processValue(component, key, value) {
242
+ const processor = registeredProcessor(component, key);
243
+ if (processor !== undefined)
244
+ return processor(value);
245
+ const styleProcessor = STYLE_PROCESSORS.get(key);
246
+ if (styleProcessor !== undefined)
247
+ return styleProcessor(value);
248
+ if (COLOR_PROPS.has(key) && isProcessableColor(value))
249
+ return colorProcessor(value);
250
+ return value;
251
+ }
252
+ function viewNameFor(node, hasTextAncestor) {
253
+ // The only position-dependent name: a <Text> inside another <Text> becomes a
254
+ // virtual span. Everything else is the component string the adapter chose.
255
+ return node.isText && hasTextAncestor ? VIRTUAL_TEXT_COMPONENT : node.component;
256
+ }
257
+ // Translate the retained node's logical props into the flat payload Fabric's C++
258
+ // props expect: `style` keys are hoisted to the top level, event handlers and
259
+ // undefined values are dropped.
260
+ function fabricProps(node) {
261
+ if (node.component === RAW_TEXT_COMPONENT) {
262
+ return { text: node.props.text };
263
+ }
264
+ const out = {};
265
+ for (const [key, value] of Object.entries(node.props)) {
266
+ if (key === 'style')
267
+ continue;
268
+ if (typeof value === 'function')
269
+ continue;
270
+ if (value === undefined)
271
+ continue;
272
+ out[key] = processValue(node.component, key, value);
273
+ }
274
+ // Collapse style (object | array | nested arrays) into one flat payload before
275
+ // hoisting: `style={[base, override]}` is RN's idiom and Fabric wants it flat.
276
+ const style = flattenStyle(node.props.style);
277
+ for (const [key, value] of Object.entries(style)) {
278
+ if (value !== undefined)
279
+ out[key] = processValue(node.component, key, value);
280
+ }
281
+ return out;
282
+ }
283
+ // Fabric's clone*WithNewProps MERGES the raw payload onto the node's existing props,
284
+ // so the payload must be a MINIMAL diff: only the keys that actually changed, plus any
285
+ // key the node held last time but no longer has, sent as `null` so Fabric resets it to
286
+ // default (e.g. `opacity` when a pressed style releases). Mirror React's diffProperties
287
+ // exactly: re-sending an UNCHANGED key is not a no-op, it re-invokes that prop's native
288
+ // setter, and some ViewManagers rebuild on any set. AndroidProgressBar's `styleAttr`
289
+ // setter recreates the whole ProgressBar via setStyle(), so re-sending it on an
290
+ // animating-only toggle dropped and rebuilt the spinner each time, and it never came
291
+ // back. Only matters for clones: a fresh createNode starts from nothing.
292
+ function diffProps(previous, next) {
293
+ const out = {};
294
+ for (const key of Object.keys(next)) {
295
+ if (!jsonEqual(previous[key], next[key]))
296
+ out[key] = next[key];
297
+ }
298
+ for (const key of Object.keys(previous)) {
299
+ if (!(key in next))
300
+ out[key] = null;
301
+ }
302
+ return out;
303
+ }
304
+ // Deep structural equality over the JSON-shaped props payload (Fabric props are
305
+ // serializable: primitives, arrays, plain objects). Used to decide whether a
306
+ // node's props actually changed: `fabricProps` builds a fresh object each
307
+ // commit, so a reference check would report every node as dirty.
308
+ function propsEqual(a, b) {
309
+ return jsonEqual(a, b);
310
+ }
311
+ function jsonEqual(a, b) {
312
+ if (Object.is(a, b))
313
+ return true;
314
+ const aArray = Array.isArray(a);
315
+ const bArray = Array.isArray(b);
316
+ if (aArray && bArray) {
317
+ if (a.length !== b.length)
318
+ return false;
319
+ return a.every((value, index) => jsonEqual(value, b[index]));
320
+ }
321
+ if (aArray || bArray)
322
+ return false;
323
+ if (!isRecord(a) || !isRecord(b))
324
+ return false;
325
+ const keys = Object.keys(a);
326
+ if (keys.length !== Object.keys(b).length)
327
+ return false;
328
+ return keys.every(key => key in b && jsonEqual(a[key], b[key]));
329
+ }
330
+ const mirror = new WeakMap();
331
+ function renderableChildren(node) {
332
+ // Anchor nodes (Vue fragment/v-if/v-for placeholders, Angular component hosts that should
333
+ // not paint) live in the retained tree for sibling ordering but never become Fabric views.
334
+ // When an anchor owns children, flatten them into the parent's renderable list: this lets a
335
+ // DOM-less framework use an anchor as a fragment/component host without adding a native
336
+ // wrapper node. Fast path: no anchors reuses the array, so the common case allocates nothing.
337
+ if (!node.children.some(isAnchor))
338
+ return node.children;
339
+ const children = [];
340
+ for (const child of node.children) {
341
+ if (isAnchor(child))
342
+ children.push(...renderableChildren(child));
343
+ else
344
+ children.push(child);
345
+ }
346
+ return children;
347
+ }
348
+ function childrenIdentical(kids, committed) {
349
+ if (kids.length !== committed.length)
350
+ return false;
351
+ return kids.every((child, index) => child === committed[index]);
352
+ }
353
+ // Diagnostic seam (gated): a ScrollView on Android must hold exactly ONE direct
354
+ // child (its content container), or the native mount aborts with "ScrollView can
355
+ // host only one direct child". Logged after children reconcile so each child's
356
+ // committed tag/view-name is resolved. A `MULTI!!` line names the exact extra
357
+ // node (tag + view-name) that pushed the scroll view past one child.
358
+ function logScrollChildren(node, viewName, selfTag) {
359
+ if (!viewName.includes('Scroll') || viewName.includes('Content'))
360
+ return;
361
+ const kids = node.children.map(child => {
362
+ const committed = mirror.get(child);
363
+ return `${committed?.viewName ?? child.component}#${committed?.tag ?? 'NEW'}`;
364
+ });
365
+ const flag = kids.length === 1 ? 'OK' : 'MULTI!!';
366
+ dlog(`SCROLL-${flag} ${viewName} tag=${selfTag} children(${kids.length})=[${kids.join(',')}]`);
367
+ }
368
+ function reconcile(slot, node, rootTag, hasTextAncestor, renderableParent, forceFreshFamily) {
369
+ const viewName = viewNameFor(node, hasTextAncestor);
370
+ const props = fabricProps(node);
371
+ const childInText = node.isText || hasTextAncestor;
372
+ const committed = mirror.get(node);
373
+ // The children that actually reach Fabric. Anchors are filtered out here so the
374
+ // whole walk (child-set emission, identity diff, mirror) is anchor-blind.
375
+ const kids = renderableChildren(node);
376
+ // First mount, or the view kind flipped (RCTText <-> RCTVirtualText when a
377
+ // <Text> moves in or out of another <Text>): a different native component
378
+ // can't be cloned across, so create a fresh node from scratch.
379
+ const parentChanged = committed !== undefined && committed.parent !== renderableParent;
380
+ if (forceFreshFamily ||
381
+ committed === undefined ||
382
+ committed.viewName !== viewName ||
383
+ parentChanged) {
384
+ stats.created += 1;
385
+ const tag = nextTag();
386
+ const reason = committed === undefined
387
+ ? 'mount'
388
+ : forceFreshFamily
389
+ ? 'fresh-parent'
390
+ : committed.viewName !== viewName
391
+ ? 'view-kind'
392
+ : 'reparent';
393
+ dlog(`commit root=${rootTag} createNode tag=${tag} view=${viewName} reason=${reason}`);
394
+ if (viewName === 'RCTView' || viewName === 'RCTText') {
395
+ dlog(`commit root=${rootTag} colorProbe tag=${tag} view=${viewName} ` +
396
+ `bg=${JSON.stringify(props.backgroundColor)} color=${JSON.stringify(props.color)} ` +
397
+ `opacity=${JSON.stringify(props.opacity)}`);
398
+ }
399
+ if (viewName === 'AndroidSwipeRefreshLayout' || viewName === 'RCTScrollView') {
400
+ dlog(`commit root=${rootTag} layoutProbe tag=${tag} view=${viewName} ` +
401
+ `flex=${JSON.stringify(props.flex)} height=${JSON.stringify(props.height)} ` +
402
+ `width=${JSON.stringify(props.width)} minHeight=${JSON.stringify(props.minHeight)} ` +
403
+ `flexGrow=${JSON.stringify(props.flexGrow)}`);
404
+ }
405
+ const handle = slot.createNode(tag, viewName, rootTag, props, node);
406
+ for (const child of kids) {
407
+ slot.appendChild(handle, reconcile(slot, child, rootTag, childInText, node, true).handle);
408
+ }
409
+ logScrollChildren(node, viewName, tag);
410
+ mirror.set(node, {
411
+ handle,
412
+ tag,
413
+ rootTag,
414
+ props,
415
+ children: kids.slice(),
416
+ viewName,
417
+ parent: renderableParent,
418
+ });
419
+ return { handle, changed: true };
420
+ }
421
+ // Reconcile children first; a child that re-cloned forces this node to re-clone
422
+ // too, since Fabric parents point at specific child handles.
423
+ const childHandles = [];
424
+ let descendantChanged = false;
425
+ for (const child of kids) {
426
+ const result = reconcile(slot, child, rootTag, childInText, node, false);
427
+ childHandles.push(result.handle);
428
+ if (result.changed)
429
+ descendantChanged = true;
430
+ }
431
+ logScrollChildren(node, viewName, committed.tag);
432
+ const childrenChanged = !childrenIdentical(kids, committed.children) || descendantChanged;
433
+ const propsChanged = !propsEqual(committed.props, props);
434
+ if (!childrenChanged && !propsChanged) {
435
+ stats.reused += 1;
436
+ return { handle: committed.handle, changed: false };
437
+ }
438
+ let handle;
439
+ if (childrenChanged) {
440
+ stats.cloneChildren += 1;
441
+ if (propsChanged) {
442
+ const propsDiff = diffProps(committed.props, props);
443
+ guardSerializable(propsDiff, viewName, committed.tag);
444
+ handle = slot.cloneNodeWithNewChildrenAndProps(committed.handle, propsDiff);
445
+ }
446
+ else {
447
+ handle = slot.cloneNodeWithNewChildren(committed.handle);
448
+ }
449
+ for (const childHandle of childHandles) {
450
+ slot.appendChild(handle, childHandle);
451
+ }
452
+ }
453
+ else {
454
+ stats.cloneProps += 1;
455
+ const propsDiff = diffProps(committed.props, props);
456
+ guardSerializable(propsDiff, viewName, committed.tag);
457
+ handle = slot.cloneNodeWithNewProps(committed.handle, propsDiff);
458
+ }
459
+ // The clone keeps the node's family, so its reactTag is unchanged; carry it.
460
+ mirror.set(node, {
461
+ handle,
462
+ tag: committed.tag,
463
+ rootTag,
464
+ props,
465
+ // Store the same flattened child list we diffed against. Anchors are retained-tree
466
+ // bookkeeping only; keeping raw node.children here makes every anchored subtree look
467
+ // structurally changed on the next commit and can re-append already-parented Fabric
468
+ // ShadowNode families under a cloned parent.
469
+ children: kids.slice(),
470
+ viewName,
471
+ parent: renderableParent,
472
+ });
473
+ return { handle, changed: true };
474
+ }
475
+ // One persistent synthetic root container per surface, mirroring RN's AppContainer
476
+ // (renderApplication wraps the app in `<View style={{flex:1}} pointerEvents="box-none">`).
477
+ // Without it a non-flex root view collapses to content height, and touches outside the
478
+ // app's children have no box-none escape. Keeping it here (not in each adapter's
479
+ // mount()) gives every framework a full-screen flex root for free and keeps layout in
480
+ // shared (adapters_stay_thin). The container is just another persistent node in the
481
+ // clone-on-write engine: stable identity, so an unchanged subtree leaves it un-cloned.
482
+ const ROOT_VIEW_COMPONENT = 'RCTView';
483
+ const ROOT_CONTAINER_STYLE = { flex: 1 };
484
+ const ROOT_CONTAINER_POINTER_EVENTS = 'box-none';
485
+ const rootContainers = new Map();
486
+ function rootContainerFor(rootTag) {
487
+ let container = rootContainers.get(rootTag);
488
+ if (container === undefined) {
489
+ container = createElement(ROOT_VIEW_COMPONENT);
490
+ container.props = {
491
+ style: ROOT_CONTAINER_STYLE,
492
+ pointerEvents: ROOT_CONTAINER_POINTER_EVENTS,
493
+ };
494
+ rootContainers.set(rootTag, container);
495
+ dlog(`root container created root=${rootTag} (flex:1, box-none)`);
496
+ }
497
+ return container;
498
+ }
499
+ // Drop a surface's persistent root container so the NEXT mount on this rootTag starts
500
+ // from scratch (fresh tags, fresh mirror) instead of cloning handles that belonged to a
501
+ // now-stopped surface. Called from unmount (the bridgeless surface-stop path): the host stops then restarts a
502
+ // surface (Fast Refresh, focus/lifecycle) reusing the same rootTag, and a stale root
503
+ // container would re-clone dead handles into the new surface → a blank screen. The old
504
+ // container's descendants fall out of every reference and their mirror entries GC.
505
+ export function disposeRoot(rootTag) {
506
+ if (rootContainers.delete(rootTag))
507
+ dlog(`root container disposed root=${rootTag}`);
508
+ }
509
+ export function commitChildren(rootTag, children) {
510
+ // The wrapper holds the surface's top-level children; reconcile walks from it so the
511
+ // whole tree, synthetic root included, goes through the same clone-on-write path.
512
+ rootContainerFor(rootTag).children = children.slice();
513
+ commitContainer(rootTag);
514
+ }
515
+ // Re-run the scoped commit for a surface from its synthetic root container, reusing
516
+ // whatever top-level children it currently holds. The shared half of the engine: both
517
+ // a full mutation→commit and a single-node Animated frame (setNativeProps) funnel here.
518
+ function commitContainer(rootTag) {
519
+ const slot = getSlot();
520
+ const container = rootContainerFor(rootTag);
521
+ stats.created = 0;
522
+ stats.cloneProps = 0;
523
+ stats.cloneChildren = 0;
524
+ stats.reused = 0;
525
+ // Entry seam: brackets reconcile with the `reconciled` line below. If `start` prints
526
+ // but `reconciled` never does, the stall is inside reconcile (a JS loop/cycle in the
527
+ // tree walk); if `start` itself never prints, the stall is upstream: React's commit
528
+ // phase or the mutation ops before we are even called.
529
+ dlog(`commit root=${rootTag} start children=${container.children.length}`);
530
+ const result = reconcile(slot, container, rootTag, false, undefined, false);
531
+ // Boundary seam: prints once reconcile returns. If a commit hangs and this line
532
+ // never appears, the stall is inside reconcile (JS); if it appears but the
533
+ // post-completeRoot line below never does, the stall is inside the native commit.
534
+ dlog(`commit root=${rootTag} reconciled changed=${result.changed}`);
535
+ // The container's identity is stable, so its un-cloned flag is the no-op signal:
536
+ // an over-scheduled commit that touched nothing makes zero native calls.
537
+ if (!result.changed) {
538
+ dlog(`commit root=${rootTag} no-op (skipped completeRoot)`);
539
+ return;
540
+ }
541
+ const childSet = slot.createChildSet(rootTag);
542
+ slot.appendChildToSet(childSet, result.handle);
543
+ dlog(`commit root=${rootTag} pre-completeRoot`);
544
+ slot.completeRoot(rootTag, childSet);
545
+ // Fresh Fabric tags are now assigned: let any consumer that needed a committed tag
546
+ // and ran too early (the Animated native driver binding a props node to a view under
547
+ // an async-batched commit) retry now. No-op when nothing is pending.
548
+ runPostCommitHooks();
549
+ if (isDebug()) {
550
+ const mode = stats.created > 0 && stats.reused === 0 ? 'full' : 'incremental';
551
+ dlog(`commit root=${rootTag} ${mode} ` +
552
+ `created=${stats.created} cloneProps=${stats.cloneProps} ` +
553
+ `cloneChildren=${stats.cloneChildren} reused=${stats.reused}`);
554
+ }
555
+ }
556
+ // Targeted per-frame prop write for the JS-driven Animated path (ADR 0016). RN
557
+ // flushes an animation frame with an in-place `instance.setNativeProps(...)`; we have
558
+ // no in-place mutation (Fabric is persistent), so a frame is one scoped commit: mutate
559
+ // the node's desired props, then re-reconcile its surface. The engine clones only this
560
+ // node (props differ), bubbles the re-clone to the root, reuses every sibling subtree
561
+ // by reference, and emits a single completeRoot. This is the "slow tier", viable for a
562
+ // single shallow animation; the native driver (ADR 0017) is the answer for scale.
563
+ export function setNativeProps(node, partial) {
564
+ const record = mirror.get(node);
565
+ if (record === undefined) {
566
+ dlog('setNativeProps skipped: node not committed');
567
+ return;
568
+ }
569
+ for (const [key, value] of Object.entries(partial)) {
570
+ if (key === 'style') {
571
+ // A partial style override MERGES onto the declarative style (RN semantics):
572
+ // setNativeProps({style:{backgroundColor}}) recolors without dropping height
573
+ // or radius. Transient: the next React commit re-applies the full style.
574
+ node.props.style = { ...flattenStyle(node.props.style), ...flattenStyle(value) };
575
+ }
576
+ else {
577
+ node.props[key] = value;
578
+ }
579
+ }
580
+ dlog(`setNativeProps root=${record.rootTag} tag=${record.tag} keys=${Object.keys(partial)}`);
581
+ commitContainer(record.rootTag);
582
+ }
583
+ // The committed reactTag of a node (stable across clone-on-write), for binding the
584
+ // Animated native driver via connectAnimatedNodeToView (ADR 0017). Undefined until the
585
+ // node has been committed at least once.
586
+ export function getNativeTag(node) {
587
+ return mirror.get(node)?.tag;
588
+ }
589
+ // Actions waiting for their node's first commit. An adapter that wires an imperative/native call at
590
+ // lifecycle time (autoFocus, a native Animated.event attach) can run BEFORE completeRoot under an
591
+ // async-batched commit (Vue/Svelte schedule it on a microtask), so the node has no tag yet and the
592
+ // call silently no-ops. Each waiter retries after a commit may have assigned the tag and is dropped
593
+ // once it runs. React commits synchronously, so its actions run inline and never land here.
594
+ const pendingCommitWaiters = new Set();
595
+ registerPostCommit(() => {
596
+ for (const waiter of pendingCommitWaiters) {
597
+ if (waiter())
598
+ pendingCommitWaiters.delete(waiter);
599
+ }
600
+ });
601
+ // Run `action` once `node` has a committed Fabric tag — immediately if it already does, else after
602
+ // the commit that assigns it. The canonical fix for the Vue async-commit race: defer instead of
603
+ // silently no-opping. Returns a cancel fn (drop the pending retry, e.g. on unmount).
604
+ export function whenCommitted(node, action) {
605
+ const attempt = () => {
606
+ if (mirror.get(node) === undefined)
607
+ return false;
608
+ action();
609
+ return true;
610
+ };
611
+ if (!attempt())
612
+ pendingCommitWaiters.add(attempt);
613
+ return () => {
614
+ pendingCommitWaiters.delete(attempt);
615
+ };
616
+ }
617
+ // The node's current Fabric handle (the createNode/clone return value), identical in
618
+ // kind to React's stateNode.node, for the native driver's ShadowNodeFamily path.
619
+ export function getNativeNode(node) {
620
+ return mirror.get(node)?.handle;
621
+ }
622
+ // Imperative view command (e.g. TextInput's setTextAndSelection / focus / blur),
623
+ // aimed at a node's CURRENT Fabric handle. Only valid once the node has been
624
+ // committed at least once; its handle is read from the mirror.
625
+ export function dispatchViewCommand(node, commandName, args) {
626
+ const record = mirror.get(node);
627
+ if (record === undefined) {
628
+ dlog(`dispatchViewCommand "${commandName}" skipped: node not committed`);
629
+ return;
630
+ }
631
+ dlog(`dispatchViewCommand "${commandName}"`);
632
+ getSlot().dispatchCommand(record.handle, commandName, args);
633
+ }
634
+ // Emit an accessibility event (focus/click/viewHoverEnter/windowStateChange) at a node's
635
+ // CURRENT Fabric handle, routed through the slot exactly like dispatchViewCommand. RN's
636
+ // Fabric path hands the public-instance handle to nativeFabricUIManager.sendAccessibilityEvent
637
+ // with the STRING eventType; the C++ side maps it to the platform's accessibility-event kind.
638
+ // A no-op (logged) until the node is committed; there is no handle yet.
639
+ export function sendAccessibilityEvent(node, eventType) {
640
+ const record = mirror.get(node);
641
+ if (record === undefined) {
642
+ dlog(`sendAccessibilityEvent "${eventType}" skipped: node not committed`);
643
+ return;
644
+ }
645
+ dlog(`sendAccessibilityEvent "${eventType}"`);
646
+ getSlot().sendAccessibilityEvent(record.handle, eventType);
647
+ }
648
+ // Imperative measurement against a node's CURRENT Fabric handle (the public-instance
649
+ // measure family that reanimated / gesture-handler / scroll-to reach through). A
650
+ // no-op with a dlog until the node is committed; there is no handle to measure yet.
651
+ export function measure(node, callback) {
652
+ const record = mirror.get(node);
653
+ if (record === undefined) {
654
+ dlog('measure skipped: node not committed');
655
+ return;
656
+ }
657
+ getSlot().measure(record.handle, callback);
658
+ }
659
+ export function measureInWindow(node, callback) {
660
+ const record = mirror.get(node);
661
+ if (record === undefined) {
662
+ dlog('measureInWindow skipped: node not committed');
663
+ return;
664
+ }
665
+ getSlot().measureInWindow(record.handle, callback);
666
+ }
667
+ // Measure `node`'s frame relative to `relativeTo`. Both must be committed; RN's public
668
+ // signature is (relative, onSuccess, onFail) but the native slot wants the fail
669
+ // callback before success, so the order is swapped here.
670
+ export function measureLayout(node, relativeTo, onSuccess, onFail = () => { }) {
671
+ const record = mirror.get(node);
672
+ const relativeRecord = mirror.get(relativeTo);
673
+ if (record === undefined || relativeRecord === undefined) {
674
+ dlog('measureLayout skipped: a node is not committed');
675
+ return;
676
+ }
677
+ getSlot().measureLayout(record.handle, relativeRecord.handle, onFail, onSuccess);
678
+ }