@ccheever/exact-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.
- package/package.json +118 -0
- package/src/__tests__/adapter-window-state.test.tsx +190 -0
- package/src/__tests__/attrs.test.ts +157 -0
- package/src/__tests__/classname.test.ts +332 -0
- package/src/__tests__/color.test.ts +169 -0
- package/src/__tests__/dom-mirror.test.ts +682 -0
- package/src/__tests__/dom-shim.test.ts +274 -0
- package/src/__tests__/fixtures/SvelteCounter.svelte +7 -0
- package/src/__tests__/fixtures/SvelteInput.svelte +8 -0
- package/src/__tests__/host-config.test.ts +51 -0
- package/src/__tests__/host-ops.test.ts +2234 -0
- package/src/__tests__/image-source.test.ts +135 -0
- package/src/__tests__/liquid-glass.test.ts +72 -0
- package/src/__tests__/multi-root.test.ts +118 -0
- package/src/__tests__/native-view-events.test.ts +102 -0
- package/src/__tests__/nodes.test.ts +399 -0
- package/src/__tests__/normalize.test.ts +576 -0
- package/src/__tests__/paragraph-lowering.test.tsx +144 -0
- package/src/__tests__/props.test.ts +518 -0
- package/src/__tests__/protocol-encoder.test.ts +732 -0
- package/src/__tests__/protocol-fixture-bytes.test.ts +41 -0
- package/src/__tests__/reconciler.test.tsx +241 -0
- package/src/__tests__/svelte-adapter.test.ts +166 -0
- package/src/__tests__/svg-source.test.ts +71 -0
- package/src/__tests__/tags.test.ts +354 -0
- package/src/__tests__/toggle.test.ts +441 -0
- package/src/__tests__/transitions.test.ts +106 -0
- package/src/__tests__/web-primitives.test.tsx +454 -0
- package/src/__tests__/window-hooks.test.tsx +447 -0
- package/src/adapter-contract.ts +68 -0
- package/src/attrs.ts +596 -0
- package/src/classname-contract.ts +87 -0
- package/src/classname-resolve.ts +553 -0
- package/src/classname-runtime.ts +29 -0
- package/src/components.ts +214 -0
- package/src/css-variable-context.ts +83 -0
- package/src/dom-hydration.ts +160 -0
- package/src/dom-mirror.ts +1459 -0
- package/src/dom-shim.ts +1736 -0
- package/src/group-context.ts +69 -0
- package/src/host-config.ts +431 -0
- package/src/host-ops.ts +3167 -0
- package/src/image-source.native.ts +703 -0
- package/src/image-source.ts +554 -0
- package/src/index.ts +278 -0
- package/src/inspector-runtime.ts +244 -0
- package/src/inspector.ts +3570 -0
- package/src/jsx-augmentations.ts +54 -0
- package/src/keyboard-avoidance.ts +217 -0
- package/src/native-primitives.ts +43 -0
- package/src/native-view-events.ts +322 -0
- package/src/native-view.ts +60 -0
- package/src/nodes/index.ts +41 -0
- package/src/nodes/node.ts +531 -0
- package/src/peer-context.ts +100 -0
- package/src/primitives.native.ts +8 -0
- package/src/primitives.ts +8 -0
- package/src/props/index.ts +14 -0
- package/src/props/normalize.ts +816 -0
- package/src/protocol/encoder.ts +940 -0
- package/src/protocol/index.ts +33 -0
- package/src/reconciler.ts +581 -0
- package/src/runtime.ts +11 -0
- package/src/safe-area.ts +543 -0
- package/src/solid.ts +490 -0
- package/src/style/color.js +1 -0
- package/src/style/color.ts +15 -0
- package/src/style/index.js +1 -0
- package/src/style/index.ts +22 -0
- package/src/style/normalize.js +1 -0
- package/src/style/normalize.ts +1426 -0
- package/src/svelte.ts +349 -0
- package/src/svg-source.ts +222 -0
- package/src/tags/index.ts +21 -0
- package/src/tags/tag-map.ts +289 -0
- package/src/text/paragraph-lowering.ts +310 -0
- package/src/types.ts +1175 -0
- package/src/vue.ts +535 -0
- package/src/web-host.ts +19 -0
- package/src/web-primitives.ts +1654 -0
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Protocol Encoding - Public Exports
|
|
3
|
+
*
|
|
4
|
+
* This module re-exports the protocol encoding utilities.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export type { ProtocolEncoder } from './encoder.js';
|
|
8
|
+
export {
|
|
9
|
+
createProtocolEncoder,
|
|
10
|
+
setStyle,
|
|
11
|
+
setTransform,
|
|
12
|
+
setOpacity,
|
|
13
|
+
setBackgroundColor,
|
|
14
|
+
setTransition,
|
|
15
|
+
setTextContent,
|
|
16
|
+
setLang,
|
|
17
|
+
setImageSource,
|
|
18
|
+
setPlaceholder,
|
|
19
|
+
setAccessibilityLabel,
|
|
20
|
+
setToggleValue,
|
|
21
|
+
setGlassEffect,
|
|
22
|
+
setTintColor,
|
|
23
|
+
setDisabled,
|
|
24
|
+
setShowsScrollIndicator,
|
|
25
|
+
dispatchProtocol,
|
|
26
|
+
getScreenDimensions,
|
|
27
|
+
encodeCreateElement,
|
|
28
|
+
encodeCreateText,
|
|
29
|
+
encodeProps,
|
|
30
|
+
encodeEvents,
|
|
31
|
+
encodeChildren,
|
|
32
|
+
encodeDestroy,
|
|
33
|
+
} from './encoder.js';
|
|
@@ -0,0 +1,581 @@
|
|
|
1
|
+
// @system @ref LLP 0007 — React reconciler surface for Exact
|
|
2
|
+
/**
|
|
3
|
+
* Exact React Reconciler
|
|
4
|
+
*
|
|
5
|
+
* This module provides the public API for rendering React elements
|
|
6
|
+
* to the Exact runtime. It wraps the react-reconciler with our
|
|
7
|
+
* custom host config.
|
|
8
|
+
*
|
|
9
|
+
* Public API:
|
|
10
|
+
* - render(element): Render a React element tree
|
|
11
|
+
* - unmount(): Unmount the current tree
|
|
12
|
+
*
|
|
13
|
+
* Event Dispatch:
|
|
14
|
+
* - Events from native are dispatched through __exactDispatchEvent (in host-ops.ts)
|
|
15
|
+
* - Each framework's reactivity handles updates
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
// @ts-ignore - react-reconciler types don't exactly match runtime
|
|
19
|
+
import Reconciler from 'react-reconciler';
|
|
20
|
+
import type { ReactElement } from 'react';
|
|
21
|
+
|
|
22
|
+
import {
|
|
23
|
+
__setActiveWindowRenderer,
|
|
24
|
+
subscribeToRootWindowState,
|
|
25
|
+
type Unsubscribe,
|
|
26
|
+
} from '@exact/core/window-state';
|
|
27
|
+
import { hostConfig, _clearHandlers } from './host-config.js';
|
|
28
|
+
import {
|
|
29
|
+
createRoot,
|
|
30
|
+
destroyRoot,
|
|
31
|
+
renderErrorFallback,
|
|
32
|
+
setNativeEventBatcher,
|
|
33
|
+
syncRootInheritedWindowState,
|
|
34
|
+
syncRootWindowState,
|
|
35
|
+
unregisterInspectorRoot,
|
|
36
|
+
type RootNode,
|
|
37
|
+
} from './host-ops.js';
|
|
38
|
+
import { _resetInspectorState } from './inspector-runtime.js';
|
|
39
|
+
import { _resetNativeViewEventBridgeForTests } from './native-view-events.js';
|
|
40
|
+
import { _resetNodeIdCounter } from './nodes/index.js';
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Development mode flag - gate verbose logging.
|
|
44
|
+
*/
|
|
45
|
+
const __DEV__ = process.env.NODE_ENV !== 'production';
|
|
46
|
+
|
|
47
|
+
// =============================================================================
|
|
48
|
+
// Create Reconciler
|
|
49
|
+
// =============================================================================
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Create the React reconciler with our host config.
|
|
53
|
+
*/
|
|
54
|
+
const reconciler = Reconciler(hostConfig);
|
|
55
|
+
type ReconcilerWithSyncInternals = typeof reconciler & {
|
|
56
|
+
updateContainerSync?: (
|
|
57
|
+
element: ReactElement | null,
|
|
58
|
+
fiber: unknown,
|
|
59
|
+
parentComponent: unknown,
|
|
60
|
+
callback: () => void,
|
|
61
|
+
) => void;
|
|
62
|
+
flushSyncWork?: () => void;
|
|
63
|
+
batchedUpdates?: (callback: () => void) => void;
|
|
64
|
+
};
|
|
65
|
+
const reconcilerWithSyncInternals = reconciler as ReconcilerWithSyncInternals;
|
|
66
|
+
if (typeof reconcilerWithSyncInternals.batchedUpdates === 'function') {
|
|
67
|
+
setNativeEventBatcher((callback) => {
|
|
68
|
+
reconcilerWithSyncInternals.batchedUpdates!(callback);
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// =============================================================================
|
|
73
|
+
// Root State
|
|
74
|
+
// =============================================================================
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* The root container node.
|
|
78
|
+
*/
|
|
79
|
+
let rootContainer: RootNode | null = null;
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* The fiber root for React.
|
|
83
|
+
*/
|
|
84
|
+
let rootFiber: unknown = null;
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* The currently rendered element (for explicit forceUpdate() replays).
|
|
88
|
+
*/
|
|
89
|
+
let currentElement: ReactElement | null = null;
|
|
90
|
+
let rootWindowStateUnsubscribe: Unsubscribe | null = null;
|
|
91
|
+
|
|
92
|
+
type RendererDebugState = Partial<{
|
|
93
|
+
enteredRender: boolean;
|
|
94
|
+
createdRoot: boolean;
|
|
95
|
+
updateScheduled: boolean;
|
|
96
|
+
renderError: string | null;
|
|
97
|
+
}>;
|
|
98
|
+
|
|
99
|
+
const rendererDebugScope = globalThis as typeof globalThis & {
|
|
100
|
+
__exactRendererDebugState?: RendererDebugState;
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
function recordRendererDebugState(patch: RendererDebugState): void {
|
|
104
|
+
Object.assign(rendererDebugScope.__exactRendererDebugState ??= {}, patch);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// =============================================================================
|
|
108
|
+
// Render State Management
|
|
109
|
+
// =============================================================================
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Whether we're currently in the middle of a render.
|
|
113
|
+
*/
|
|
114
|
+
let isRendering = false;
|
|
115
|
+
|
|
116
|
+
function updateContainerSyncIfAvailable(
|
|
117
|
+
element: ReactElement | null,
|
|
118
|
+
fiber: unknown,
|
|
119
|
+
onComplete: () => void,
|
|
120
|
+
): boolean {
|
|
121
|
+
if (typeof reconcilerWithSyncInternals.updateContainerSync !== 'function') {
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
reconcilerWithSyncInternals.updateContainerSync(element, fiber, null, onComplete);
|
|
126
|
+
if (typeof reconcilerWithSyncInternals.flushSyncWork === 'function') {
|
|
127
|
+
reconcilerWithSyncInternals.flushSyncWork();
|
|
128
|
+
}
|
|
129
|
+
return true;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function performContainerUpdate(
|
|
133
|
+
element: ReactElement | null,
|
|
134
|
+
fiber: unknown,
|
|
135
|
+
onComplete: () => void,
|
|
136
|
+
): void {
|
|
137
|
+
if (updateContainerSyncIfAvailable(element, fiber, onComplete)) {
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (typeof reconciler.flushSync === 'function') {
|
|
142
|
+
reconciler.flushSync(() => {
|
|
143
|
+
reconciler.updateContainer(element, fiber, null, onComplete);
|
|
144
|
+
});
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
reconciler.updateContainer(element, fiber, null, onComplete);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function formatRenderErrorMessage(error: unknown): string {
|
|
152
|
+
if (__DEV__ && error instanceof Error && error.message) {
|
|
153
|
+
return `Render error: ${error.message}`;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return 'Something went wrong.';
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function handleRenderFailure(
|
|
160
|
+
label: string,
|
|
161
|
+
container: RootNode | null,
|
|
162
|
+
clearCurrentElement: () => void,
|
|
163
|
+
error: unknown
|
|
164
|
+
): void {
|
|
165
|
+
clearCurrentElement();
|
|
166
|
+
console.error(`[${label}] Render failed:`, error);
|
|
167
|
+
|
|
168
|
+
if (!container) {
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
try {
|
|
173
|
+
renderErrorFallback(container, formatRenderErrorMessage(error));
|
|
174
|
+
} catch (fallbackError) {
|
|
175
|
+
console.error(`[${label}] Fallback render failed:`, fallbackError);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// =============================================================================
|
|
180
|
+
// Event Handling
|
|
181
|
+
// =============================================================================
|
|
182
|
+
|
|
183
|
+
// Note: Event dispatch is handled in host-ops.ts via __exactDispatchEvent.
|
|
184
|
+
// React state updates should flush through hostConfig.resetAfterCommit().
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Trigger an explicit re-render of the current tree.
|
|
188
|
+
* This is only used for manual forceUpdate() calls and explicit remounts.
|
|
189
|
+
*/
|
|
190
|
+
function triggerRerender(): void {
|
|
191
|
+
if (!rootFiber || !currentElement) {
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (isRendering) {
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
isRendering = true;
|
|
200
|
+
|
|
201
|
+
try {
|
|
202
|
+
performContainerUpdate(currentElement, rootFiber, () => {
|
|
203
|
+
isRendering = false;
|
|
204
|
+
});
|
|
205
|
+
} catch (error) {
|
|
206
|
+
isRendering = false;
|
|
207
|
+
handleRenderFailure('Reconciler', rootContainer, () => {
|
|
208
|
+
currentElement = null;
|
|
209
|
+
}, error);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function subscribeMainRootWindowState(): void {
|
|
214
|
+
rootWindowStateUnsubscribe?.();
|
|
215
|
+
if (!rootContainer) {
|
|
216
|
+
rootWindowStateUnsubscribe = null;
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
rootWindowStateUnsubscribe = subscribeToRootWindowState(rootContainer.rootId, () => {
|
|
221
|
+
if (!rootFiber || !currentElement || isRendering) {
|
|
222
|
+
if (rootContainer) {
|
|
223
|
+
syncRootWindowState(rootContainer);
|
|
224
|
+
}
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
syncRootInheritedWindowState(rootContainer);
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// =============================================================================
|
|
233
|
+
// Public API
|
|
234
|
+
// =============================================================================
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Render a React element tree to the Exact runtime.
|
|
238
|
+
*
|
|
239
|
+
* @param element - The React element to render
|
|
240
|
+
*
|
|
241
|
+
* @example
|
|
242
|
+
* ```tsx
|
|
243
|
+
* import { render } from '@exact/runtime';
|
|
244
|
+
*
|
|
245
|
+
* function App() {
|
|
246
|
+
* return <div style={{ flex: 1, backgroundColor: '#fff' }}>Hello Exact!</div>;
|
|
247
|
+
* }
|
|
248
|
+
*
|
|
249
|
+
* render(<App />);
|
|
250
|
+
* ```
|
|
251
|
+
*/
|
|
252
|
+
export function render(element: ReactElement): void {
|
|
253
|
+
if ((globalThis as any).__exactLoadTimings) (globalThis as any).__exactLoadTimings.renderNativeStart = Date.now();
|
|
254
|
+
recordRendererDebugState({
|
|
255
|
+
enteredRender: true,
|
|
256
|
+
renderError: null,
|
|
257
|
+
});
|
|
258
|
+
__setActiveWindowRenderer('react');
|
|
259
|
+
// Create root container if needed
|
|
260
|
+
if (!rootContainer) {
|
|
261
|
+
// Use shared createRoot from host-ops (creates node and registers with native)
|
|
262
|
+
rootContainer = createRoot(0, 'react');
|
|
263
|
+
recordRendererDebugState({
|
|
264
|
+
createdRoot: true,
|
|
265
|
+
});
|
|
266
|
+
subscribeMainRootWindowState();
|
|
267
|
+
|
|
268
|
+
// @ts-expect-error - createContainer args vary by react-reconciler version
|
|
269
|
+
rootFiber = reconciler.createContainer(
|
|
270
|
+
rootContainer,
|
|
271
|
+
1, // LegacyRoot (sync rendering) - ConcurrentRoot (0) doesn't work in JSC
|
|
272
|
+
null, // hydrationCallbacks
|
|
273
|
+
false, // isStrictMode
|
|
274
|
+
false, // concurrentUpdatesByDefaultOverride
|
|
275
|
+
'', // identifierPrefix
|
|
276
|
+
(error: Error) => console.error('[Reconciler] Recoverable error:', error),
|
|
277
|
+
null // transitionCallbacks
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Store the current element so forceUpdate() can replay it if needed.
|
|
282
|
+
currentElement = element;
|
|
283
|
+
|
|
284
|
+
isRendering = true;
|
|
285
|
+
|
|
286
|
+
try {
|
|
287
|
+
performContainerUpdate(element, rootFiber, () => {
|
|
288
|
+
isRendering = false;
|
|
289
|
+
});
|
|
290
|
+
recordRendererDebugState({
|
|
291
|
+
updateScheduled: true,
|
|
292
|
+
});
|
|
293
|
+
} catch (error) {
|
|
294
|
+
isRendering = false;
|
|
295
|
+
recordRendererDebugState({
|
|
296
|
+
renderError: error instanceof Error ? error.message : String(error),
|
|
297
|
+
});
|
|
298
|
+
handleRenderFailure('Reconciler', rootContainer, () => {
|
|
299
|
+
currentElement = null;
|
|
300
|
+
}, error);
|
|
301
|
+
}
|
|
302
|
+
if ((globalThis as any).__exactLoadTimings) (globalThis as any).__exactLoadTimings.renderNativeEnd = Date.now();
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Unmount the current React tree.
|
|
307
|
+
*
|
|
308
|
+
* @example
|
|
309
|
+
* ```tsx
|
|
310
|
+
* import { render, unmount } from '@exact/runtime';
|
|
311
|
+
*
|
|
312
|
+
* render(<App />);
|
|
313
|
+
*
|
|
314
|
+
* // Later...
|
|
315
|
+
* unmount();
|
|
316
|
+
* ```
|
|
317
|
+
*/
|
|
318
|
+
export function unmount(): void {
|
|
319
|
+
if (rootFiber) {
|
|
320
|
+
performContainerUpdate(null, rootFiber, () => {});
|
|
321
|
+
currentElement = null;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Fully reset the renderer state.
|
|
327
|
+
* Clears the root container, fiber, event handlers, and event queue.
|
|
328
|
+
* Use this for tests or multi-root scenarios that need a clean slate.
|
|
329
|
+
*
|
|
330
|
+
* @example
|
|
331
|
+
* ```tsx
|
|
332
|
+
* import { render, reset } from '@exact/runtime';
|
|
333
|
+
*
|
|
334
|
+
* // Render an app
|
|
335
|
+
* render(<App />);
|
|
336
|
+
*
|
|
337
|
+
* // Reset everything for a fresh start
|
|
338
|
+
* reset();
|
|
339
|
+
*
|
|
340
|
+
* // Render a different app
|
|
341
|
+
* render(<OtherApp />);
|
|
342
|
+
* ```
|
|
343
|
+
*/
|
|
344
|
+
export function reset(): void {
|
|
345
|
+
// Unmount current tree if any
|
|
346
|
+
if (rootFiber) {
|
|
347
|
+
performContainerUpdate(null, rootFiber, () => {});
|
|
348
|
+
}
|
|
349
|
+
for (const [rootId, instance] of rootInstances) {
|
|
350
|
+
performContainerUpdate(null, instance.fiber, () => {});
|
|
351
|
+
instance.unsubscribeWindowState?.();
|
|
352
|
+
destroyRoot(rootId);
|
|
353
|
+
}
|
|
354
|
+
rootInstances.clear();
|
|
355
|
+
|
|
356
|
+
// Clear all state
|
|
357
|
+
if (rootContainer) {
|
|
358
|
+
destroyRoot(rootContainer.rootId);
|
|
359
|
+
}
|
|
360
|
+
rootContainer = null;
|
|
361
|
+
rootFiber = null;
|
|
362
|
+
currentElement = null;
|
|
363
|
+
isRendering = false;
|
|
364
|
+
rootWindowStateUnsubscribe?.();
|
|
365
|
+
rootWindowStateUnsubscribe = null;
|
|
366
|
+
|
|
367
|
+
// Clear handler registry (in host-ops)
|
|
368
|
+
_clearHandlers();
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Reset the renderer for native bundle replacement without dispatching a
|
|
373
|
+
* teardown batch to the host.
|
|
374
|
+
*
|
|
375
|
+
* The native shell already resets the kernel as part of bundle replacement, so
|
|
376
|
+
* sending destroy operations from the old JS app is both redundant and risky:
|
|
377
|
+
* those stale teardown batches can race with the new bundle's initial mount and
|
|
378
|
+
* leave the fresh kernel tree empty. This path intentionally clears only the
|
|
379
|
+
* JS-side renderer state.
|
|
380
|
+
*/
|
|
381
|
+
export function prepareForBundleReplacement(): void {
|
|
382
|
+
for (const instance of rootInstances.values()) {
|
|
383
|
+
instance.unsubscribeWindowState?.();
|
|
384
|
+
unregisterInspectorRoot(instance.container.rootId);
|
|
385
|
+
}
|
|
386
|
+
rootInstances.clear();
|
|
387
|
+
|
|
388
|
+
if (rootContainer) {
|
|
389
|
+
unregisterInspectorRoot(rootContainer.rootId);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
rootContainer = null;
|
|
393
|
+
rootFiber = null;
|
|
394
|
+
currentElement = null;
|
|
395
|
+
isRendering = false;
|
|
396
|
+
rootWindowStateUnsubscribe?.();
|
|
397
|
+
rootWindowStateUnsubscribe = null;
|
|
398
|
+
|
|
399
|
+
_clearHandlers();
|
|
400
|
+
_resetNativeViewEventBridgeForTests();
|
|
401
|
+
_resetInspectorState();
|
|
402
|
+
_resetNodeIdCounter();
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* Force a re-render of the current element.
|
|
407
|
+
* Useful for debugging or when external state changes.
|
|
408
|
+
*/
|
|
409
|
+
export function forceUpdate(): void {
|
|
410
|
+
if (currentElement) {
|
|
411
|
+
triggerRerender();
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* Re-sync viewport/safe-area dependent host state without relying on a React
|
|
417
|
+
* replay. This is useful when window metrics changed outside the framework's
|
|
418
|
+
* reactive graph but the mounted tree still needs its inherited layout state
|
|
419
|
+
* recomputed immediately.
|
|
420
|
+
*/
|
|
421
|
+
export function syncWindowState(rootId?: number): void {
|
|
422
|
+
if ((rootId === undefined || rootId === 0) && rootContainer) {
|
|
423
|
+
syncRootInheritedWindowState(rootContainer);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
for (const [instanceRootId, instance] of rootInstances) {
|
|
427
|
+
if (instanceRootId === 0) {
|
|
428
|
+
continue;
|
|
429
|
+
}
|
|
430
|
+
if (rootId !== undefined && instanceRootId !== rootId) {
|
|
431
|
+
continue;
|
|
432
|
+
}
|
|
433
|
+
syncRootInheritedWindowState(instance.container);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* Get the current root container (for debugging).
|
|
439
|
+
* @internal
|
|
440
|
+
*/
|
|
441
|
+
export function _getRootContainer(): RootNode | null {
|
|
442
|
+
return rootContainer;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/**
|
|
446
|
+
* Check if a render is in progress.
|
|
447
|
+
* @internal
|
|
448
|
+
*/
|
|
449
|
+
export function _isRendering(): boolean {
|
|
450
|
+
return isRendering;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/**
|
|
454
|
+
* Get the event queue length (for debugging).
|
|
455
|
+
* @internal
|
|
456
|
+
* @deprecated Event queue removed - events handled immediately via host-ops
|
|
457
|
+
*/
|
|
458
|
+
export function _getEventQueueLength(): number {
|
|
459
|
+
return 0;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// =============================================================================
|
|
463
|
+
// Multi-Root API
|
|
464
|
+
// =============================================================================
|
|
465
|
+
|
|
466
|
+
/**
|
|
467
|
+
* Per-root state for multi-root support.
|
|
468
|
+
*/
|
|
469
|
+
interface RootInstance {
|
|
470
|
+
container: RootNode;
|
|
471
|
+
fiber: unknown;
|
|
472
|
+
element: ReactElement | null;
|
|
473
|
+
isRendering: boolean;
|
|
474
|
+
unsubscribeWindowState: Unsubscribe | null;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
const rootInstances = new Map<number, RootInstance>();
|
|
478
|
+
|
|
479
|
+
/**
|
|
480
|
+
* An independent rendering surface.
|
|
481
|
+
* Each ExactRoot has its own React tree, container, and render cycle.
|
|
482
|
+
*/
|
|
483
|
+
export interface ExactRoot {
|
|
484
|
+
/** The root ID for this rendering surface. */
|
|
485
|
+
readonly rootId: number;
|
|
486
|
+
|
|
487
|
+
/** Render a React element tree into this root. */
|
|
488
|
+
render(element: ReactElement): void;
|
|
489
|
+
|
|
490
|
+
/** Unmount the React tree from this root. */
|
|
491
|
+
unmount(): void;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
/**
|
|
495
|
+
* Create an independent rendering surface with its own React tree.
|
|
496
|
+
*
|
|
497
|
+
* @param rootId - A unique identifier for this root (must be > 0)
|
|
498
|
+
* @returns An ExactRoot object with render() and unmount() methods
|
|
499
|
+
*
|
|
500
|
+
* @example
|
|
501
|
+
* ```tsx
|
|
502
|
+
* import { createExactRoot } from '@exact/runtime';
|
|
503
|
+
*
|
|
504
|
+
* const root1 = createExactRoot(1);
|
|
505
|
+
* const root2 = createExactRoot(2);
|
|
506
|
+
*
|
|
507
|
+
* root1.render(<Sidebar />);
|
|
508
|
+
* root2.render(<MainContent />);
|
|
509
|
+
* ```
|
|
510
|
+
*/
|
|
511
|
+
export function createExactRoot(rootId: number): ExactRoot {
|
|
512
|
+
__setActiveWindowRenderer('react');
|
|
513
|
+
if (rootId === 0) {
|
|
514
|
+
throw new Error('createExactRoot: rootId must be > 0. Use render() for the default root.');
|
|
515
|
+
}
|
|
516
|
+
if (rootInstances.has(rootId)) {
|
|
517
|
+
throw new Error(`createExactRoot: rootId ${rootId} is already in use.`);
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
const container = createRoot(rootId, 'react');
|
|
521
|
+
|
|
522
|
+
// @ts-expect-error - createContainer args vary by react-reconciler version
|
|
523
|
+
const fiber = reconciler.createContainer(
|
|
524
|
+
container,
|
|
525
|
+
1, // LegacyRoot (sync rendering)
|
|
526
|
+
null, // hydrationCallbacks
|
|
527
|
+
false, // isStrictMode
|
|
528
|
+
false, // concurrentUpdatesByDefaultOverride
|
|
529
|
+
'', // identifierPrefix
|
|
530
|
+
(error: Error) => console.error(`[Reconciler:${rootId}] Recoverable error:`, error),
|
|
531
|
+
null // transitionCallbacks
|
|
532
|
+
);
|
|
533
|
+
|
|
534
|
+
const instance: RootInstance = {
|
|
535
|
+
container,
|
|
536
|
+
fiber,
|
|
537
|
+
element: null,
|
|
538
|
+
isRendering: false,
|
|
539
|
+
unsubscribeWindowState: null,
|
|
540
|
+
};
|
|
541
|
+
|
|
542
|
+
instance.unsubscribeWindowState = subscribeToRootWindowState(rootId, () => {
|
|
543
|
+
if (!instance.element || instance.isRendering) {
|
|
544
|
+
syncRootWindowState(instance.container);
|
|
545
|
+
return;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
syncRootInheritedWindowState(instance.container);
|
|
549
|
+
});
|
|
550
|
+
|
|
551
|
+
rootInstances.set(rootId, instance);
|
|
552
|
+
|
|
553
|
+
return {
|
|
554
|
+
rootId,
|
|
555
|
+
|
|
556
|
+
render(element: ReactElement): void {
|
|
557
|
+
instance.element = element;
|
|
558
|
+
instance.isRendering = true;
|
|
559
|
+
|
|
560
|
+
try {
|
|
561
|
+
performContainerUpdate(element, instance.fiber, () => {
|
|
562
|
+
instance.isRendering = false;
|
|
563
|
+
});
|
|
564
|
+
} catch (error) {
|
|
565
|
+
instance.isRendering = false;
|
|
566
|
+
handleRenderFailure(`Reconciler:${rootId}`, instance.container, () => {
|
|
567
|
+
instance.element = null;
|
|
568
|
+
}, error);
|
|
569
|
+
}
|
|
570
|
+
},
|
|
571
|
+
|
|
572
|
+
unmount(): void {
|
|
573
|
+
performContainerUpdate(null, instance.fiber, () => {});
|
|
574
|
+
instance.element = null;
|
|
575
|
+
instance.unsubscribeWindowState?.();
|
|
576
|
+
instance.unsubscribeWindowState = null;
|
|
577
|
+
rootInstances.delete(rootId);
|
|
578
|
+
destroyRoot(rootId);
|
|
579
|
+
},
|
|
580
|
+
};
|
|
581
|
+
}
|