@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.
Files changed (80) hide show
  1. package/package.json +118 -0
  2. package/src/__tests__/adapter-window-state.test.tsx +190 -0
  3. package/src/__tests__/attrs.test.ts +157 -0
  4. package/src/__tests__/classname.test.ts +332 -0
  5. package/src/__tests__/color.test.ts +169 -0
  6. package/src/__tests__/dom-mirror.test.ts +682 -0
  7. package/src/__tests__/dom-shim.test.ts +274 -0
  8. package/src/__tests__/fixtures/SvelteCounter.svelte +7 -0
  9. package/src/__tests__/fixtures/SvelteInput.svelte +8 -0
  10. package/src/__tests__/host-config.test.ts +51 -0
  11. package/src/__tests__/host-ops.test.ts +2234 -0
  12. package/src/__tests__/image-source.test.ts +135 -0
  13. package/src/__tests__/liquid-glass.test.ts +72 -0
  14. package/src/__tests__/multi-root.test.ts +118 -0
  15. package/src/__tests__/native-view-events.test.ts +102 -0
  16. package/src/__tests__/nodes.test.ts +399 -0
  17. package/src/__tests__/normalize.test.ts +576 -0
  18. package/src/__tests__/paragraph-lowering.test.tsx +144 -0
  19. package/src/__tests__/props.test.ts +518 -0
  20. package/src/__tests__/protocol-encoder.test.ts +732 -0
  21. package/src/__tests__/protocol-fixture-bytes.test.ts +41 -0
  22. package/src/__tests__/reconciler.test.tsx +241 -0
  23. package/src/__tests__/svelte-adapter.test.ts +166 -0
  24. package/src/__tests__/svg-source.test.ts +71 -0
  25. package/src/__tests__/tags.test.ts +354 -0
  26. package/src/__tests__/toggle.test.ts +441 -0
  27. package/src/__tests__/transitions.test.ts +106 -0
  28. package/src/__tests__/web-primitives.test.tsx +454 -0
  29. package/src/__tests__/window-hooks.test.tsx +447 -0
  30. package/src/adapter-contract.ts +68 -0
  31. package/src/attrs.ts +596 -0
  32. package/src/classname-contract.ts +87 -0
  33. package/src/classname-resolve.ts +553 -0
  34. package/src/classname-runtime.ts +29 -0
  35. package/src/components.ts +214 -0
  36. package/src/css-variable-context.ts +83 -0
  37. package/src/dom-hydration.ts +160 -0
  38. package/src/dom-mirror.ts +1459 -0
  39. package/src/dom-shim.ts +1736 -0
  40. package/src/group-context.ts +69 -0
  41. package/src/host-config.ts +431 -0
  42. package/src/host-ops.ts +3167 -0
  43. package/src/image-source.native.ts +703 -0
  44. package/src/image-source.ts +554 -0
  45. package/src/index.ts +278 -0
  46. package/src/inspector-runtime.ts +244 -0
  47. package/src/inspector.ts +3570 -0
  48. package/src/jsx-augmentations.ts +54 -0
  49. package/src/keyboard-avoidance.ts +217 -0
  50. package/src/native-primitives.ts +43 -0
  51. package/src/native-view-events.ts +322 -0
  52. package/src/native-view.ts +60 -0
  53. package/src/nodes/index.ts +41 -0
  54. package/src/nodes/node.ts +531 -0
  55. package/src/peer-context.ts +100 -0
  56. package/src/primitives.native.ts +8 -0
  57. package/src/primitives.ts +8 -0
  58. package/src/props/index.ts +14 -0
  59. package/src/props/normalize.ts +816 -0
  60. package/src/protocol/encoder.ts +940 -0
  61. package/src/protocol/index.ts +33 -0
  62. package/src/reconciler.ts +581 -0
  63. package/src/runtime.ts +11 -0
  64. package/src/safe-area.ts +543 -0
  65. package/src/solid.ts +490 -0
  66. package/src/style/color.js +1 -0
  67. package/src/style/color.ts +15 -0
  68. package/src/style/index.js +1 -0
  69. package/src/style/index.ts +22 -0
  70. package/src/style/normalize.js +1 -0
  71. package/src/style/normalize.ts +1426 -0
  72. package/src/svelte.ts +349 -0
  73. package/src/svg-source.ts +222 -0
  74. package/src/tags/index.ts +21 -0
  75. package/src/tags/tag-map.ts +289 -0
  76. package/src/text/paragraph-lowering.ts +310 -0
  77. package/src/types.ts +1175 -0
  78. package/src/vue.ts +535 -0
  79. package/src/web-host.ts +19 -0
  80. package/src/web-primitives.ts +1654 -0
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Framework JSX augmentations.
3
+ *
4
+ * RFC 0043 expands Exact's web-style surface beyond the original small tag
5
+ * set. React and Solid still source their intrinsic-element typings from their
6
+ * own framework packages, so we augment those packages here with the Exact
7
+ * interaction props that the runtime accepts on DOM-like tags.
8
+ *
9
+ * This module is imported for its side effects by the renderer entrypoints so
10
+ * the augmentations are present anywhere the adapter packages are used.
11
+ */
12
+
13
+ import type {} from 'react';
14
+ import type {} from 'solid-js';
15
+
16
+ import type { PressEvent } from './types.js';
17
+
18
+ declare module 'react' {
19
+ interface HTMLAttributes<T> {
20
+ onPress?: (event: PressEvent) => void;
21
+ onPressIn?: (event: PressEvent) => void;
22
+ onPressOut?: (event: PressEvent) => void;
23
+ onLongPress?: (event: PressEvent) => void;
24
+ }
25
+
26
+ interface InputHTMLAttributes<T> {
27
+ onChangeText?: (text: string) => void;
28
+ }
29
+
30
+ interface TextareaHTMLAttributes<T> {
31
+ onChangeText?: (text: string) => void;
32
+ }
33
+ }
34
+
35
+ declare module 'solid-js' {
36
+ namespace JSX {
37
+ interface HTMLAttributes<T> {
38
+ onPress?: (event: PressEvent) => void;
39
+ onPressIn?: (event: PressEvent) => void;
40
+ onPressOut?: (event: PressEvent) => void;
41
+ onLongPress?: (event: PressEvent) => void;
42
+ }
43
+
44
+ interface InputHTMLAttributes<T> {
45
+ onChangeText?: (text: string) => void;
46
+ }
47
+
48
+ interface TextareaHTMLAttributes<T> {
49
+ onChangeText?: (text: string) => void;
50
+ }
51
+ }
52
+ }
53
+
54
+ export {};
@@ -0,0 +1,217 @@
1
+ import type { Point } from '@exact/core/agent/types';
2
+ import { hostCallBridge } from '@exact/core/host-call-bridge';
3
+ import { getKeyboardStateForRoot } from '@exact/core/window-state';
4
+
5
+ type InspectorTargeting = Pick<
6
+ typeof import('./inspector.js'),
7
+ 'resolveElement' | 'resolveTargetDetails' | 'setScrollOffset'
8
+ >;
9
+
10
+ const focusedInputsByRoot = new Map<number, number>();
11
+
12
+ function inspectorGlobal(): typeof globalThis & {
13
+ __exactRendererInspectorTargeting?: InspectorTargeting;
14
+ } {
15
+ return globalThis as typeof globalThis & {
16
+ __exactRendererInspectorTargeting?: InspectorTargeting;
17
+ };
18
+ }
19
+
20
+ function getLoadedInspectorTargeting(): InspectorTargeting | null {
21
+ return inspectorGlobal().__exactRendererInspectorTargeting ?? null;
22
+ }
23
+
24
+ function loadInspectorTargeting(): Promise<InspectorTargeting> {
25
+ const loaded = getLoadedInspectorTargeting();
26
+ if (loaded) {
27
+ return Promise.resolve(loaded);
28
+ }
29
+
30
+ const specifier = './inspector.js';
31
+ return import(/* @vite-ignore */ specifier).then((inspector) => {
32
+ const targeting: InspectorTargeting = {
33
+ resolveElement: inspector.resolveElement,
34
+ resolveTargetDetails: inspector.resolveTargetDetails,
35
+ setScrollOffset: inspector.setScrollOffset,
36
+ };
37
+ inspectorGlobal().__exactRendererInspectorTargeting = targeting;
38
+ return targeting;
39
+ });
40
+ }
41
+
42
+ function framesIntersect(
43
+ left: { x: number; y: number; width: number; height: number },
44
+ right: { x: number; y: number; width: number; height: number },
45
+ ): boolean {
46
+ return !(
47
+ left.x + left.width <= right.x ||
48
+ right.x + right.width <= left.x ||
49
+ left.y + left.height <= right.y ||
50
+ right.y + right.height <= left.y
51
+ );
52
+ }
53
+
54
+ function clamp(value: number, min: number, max: number): number {
55
+ return Math.min(max, Math.max(min, value));
56
+ }
57
+
58
+ function readAutoScrollDelta(
59
+ inspector: InspectorTargeting,
60
+ rootId: number,
61
+ focusedViewId: number,
62
+ ): {
63
+ scrollViewId: number;
64
+ desiredOffset: Point;
65
+ currentOffset: Point;
66
+ contentSize: { width: number; height: number };
67
+ viewportSize: { width: number; height: number };
68
+ } | null {
69
+ const keyboard = getKeyboardStateForRoot(rootId);
70
+ if (!keyboard.visible || keyboard.interactive) {
71
+ return null;
72
+ }
73
+
74
+ const target = inspector.resolveTargetDetails({
75
+ rootId,
76
+ viewId: focusedViewId,
77
+ });
78
+ if (!target || target.scrollChain.length === 0) {
79
+ return null;
80
+ }
81
+
82
+ const nearestScroll = target.scrollChain[0];
83
+ const scrollContainer = inspector.resolveElement({
84
+ rootId,
85
+ viewId: nearestScroll.viewId,
86
+ });
87
+ if (!scrollContainer) {
88
+ return null;
89
+ }
90
+
91
+ if (
92
+ keyboard.mode !== 'docked' &&
93
+ !framesIntersect(target.frame, keyboard.occlusion)
94
+ ) {
95
+ return null;
96
+ }
97
+
98
+ const padding = 12;
99
+ const visibleTop = scrollContainer.frame.y + padding;
100
+ let visibleBottom = scrollContainer.frame.y + scrollContainer.frame.height - padding;
101
+ if (framesIntersect(scrollContainer.frame, keyboard.occlusion)) {
102
+ visibleBottom = Math.min(visibleBottom, keyboard.occlusion.y - padding);
103
+ }
104
+
105
+ let deltaY = 0;
106
+ if (target.frame.y < visibleTop) {
107
+ deltaY = target.frame.y - visibleTop;
108
+ } else if (target.frame.y + target.frame.height > visibleBottom) {
109
+ deltaY = target.frame.y + target.frame.height - visibleBottom;
110
+ }
111
+
112
+ if (deltaY === 0) {
113
+ return null;
114
+ }
115
+
116
+ const maxOffsetY = Math.max(
117
+ 0,
118
+ nearestScroll.contentSize.height - nearestScroll.viewportSize.height,
119
+ );
120
+
121
+ return {
122
+ scrollViewId: nearestScroll.viewId,
123
+ currentOffset: nearestScroll.currentOffset,
124
+ desiredOffset: {
125
+ x: nearestScroll.currentOffset.x,
126
+ y: clamp(nearestScroll.currentOffset.y + deltaY, 0, maxOffsetY),
127
+ },
128
+ contentSize: nearestScroll.contentSize,
129
+ viewportSize: nearestScroll.viewportSize,
130
+ };
131
+ }
132
+
133
+ export function noteFocusedInput(rootId: number, viewId: number): void {
134
+ focusedInputsByRoot.set(rootId, viewId);
135
+ }
136
+
137
+ export function clearFocusedInput(rootId: number, viewId?: number): void {
138
+ if (viewId == null || focusedInputsByRoot.get(rootId) === viewId) {
139
+ focusedInputsByRoot.delete(rootId);
140
+ }
141
+ }
142
+
143
+ /**
144
+ * Runs after a commit so the latest layout and keyboard state are both visible.
145
+ *
146
+ * The RFC’s ownership rule is intentionally conservative here: only the nearest
147
+ * scroll ancestor is allowed to respond, and interactive keyboard dismissal is
148
+ * explicitly ignored so the runtime does not fight the user.
149
+ */
150
+ export function syncKeyboardAvoidanceAfterCommit(rootId: number): void {
151
+ const focusedViewId = focusedInputsByRoot.get(rootId);
152
+ if (typeof focusedViewId !== 'number') {
153
+ return;
154
+ }
155
+
156
+ const loadedInspector = getLoadedInspectorTargeting();
157
+ if (loadedInspector) {
158
+ syncKeyboardAvoidanceAfterCommitWithInspector(rootId, focusedViewId, loadedInspector);
159
+ return;
160
+ }
161
+
162
+ void syncKeyboardAvoidanceAfterCommitAsync(rootId, focusedViewId).catch((error) => {
163
+ console.error('[ExactKeyboardAvoidance] failed to load inspector targeting:', error);
164
+ });
165
+ }
166
+
167
+ async function syncKeyboardAvoidanceAfterCommitAsync(
168
+ rootId: number,
169
+ focusedViewId: number,
170
+ ): Promise<void> {
171
+ const inspector = await loadInspectorTargeting();
172
+ syncKeyboardAvoidanceAfterCommitWithInspector(rootId, focusedViewId, inspector);
173
+ }
174
+
175
+ function syncKeyboardAvoidanceAfterCommitWithInspector(
176
+ rootId: number,
177
+ focusedViewId: number,
178
+ inspector: InspectorTargeting,
179
+ ): void {
180
+ const plan = readAutoScrollDelta(inspector, rootId, focusedViewId);
181
+ if (!plan) {
182
+ return;
183
+ }
184
+
185
+ if (
186
+ plan.desiredOffset.x === plan.currentOffset.x &&
187
+ plan.desiredOffset.y === plan.currentOffset.y
188
+ ) {
189
+ return;
190
+ }
191
+
192
+ const hostCall = hostCallBridge();
193
+ if (hostCall) {
194
+ try {
195
+ hostCall(
196
+ 'agent.scroll',
197
+ JSON.stringify({
198
+ rootId,
199
+ viewId: plan.scrollViewId,
200
+ waitForIdle: false,
201
+ offset: plan.desiredOffset,
202
+ contentSize: plan.contentSize,
203
+ viewportSize: plan.viewportSize,
204
+ }),
205
+ );
206
+ return;
207
+ } catch {
208
+ // Fall through to the local inspector-side scroll model.
209
+ }
210
+ }
211
+
212
+ inspector.setScrollOffset(plan.scrollViewId, plan.desiredOffset);
213
+ }
214
+
215
+ export function resetKeyboardAvoidanceState(): void {
216
+ focusedInputsByRoot.clear();
217
+ }
@@ -0,0 +1,43 @@
1
+ import { createElement } from 'react';
2
+ import type { FC } from 'react';
3
+
4
+ import type {
5
+ ContainerProps,
6
+ PressableElementProps,
7
+ ScrollContainerProps,
8
+ TextElementProps,
9
+ } from './types.js';
10
+
11
+ export const View: FC<ContainerProps> = function ExactNativeView(props) {
12
+ return createElement('View', props);
13
+ };
14
+
15
+ export const Text: FC<TextElementProps> = function ExactNativeText(props) {
16
+ return createElement('Text', props);
17
+ };
18
+
19
+ export const Pressable: FC<PressableElementProps> = function ExactNativePressable(
20
+ props,
21
+ ) {
22
+ return createElement('Pressable', props);
23
+ };
24
+
25
+ export const ScrollView: FC<ScrollContainerProps> = function ExactNativeScrollView(
26
+ props,
27
+ ) {
28
+ return createElement('ScrollView', props);
29
+ };
30
+
31
+ export const List: FC<ScrollContainerProps> = function ExactNativeList(props) {
32
+ return createElement('List', props);
33
+ };
34
+
35
+ export const SelectionGroup: FC<ContainerProps> = function ExactNativeSelectionGroup(
36
+ props,
37
+ ) {
38
+ return createElement('View', {
39
+ ...props,
40
+ selectable: props.selectable ?? 'contain',
41
+ __exactComponentName: props.__exactComponentName ?? 'SelectionGroup',
42
+ });
43
+ };
@@ -0,0 +1,322 @@
1
+ import { EventType } from '@exact/core/protocol/opcodes';
2
+
3
+ interface NativeViewEventHandler {
4
+ moduleName: string;
5
+ handle(eventName: string, payload: unknown): boolean;
6
+ }
7
+
8
+ interface NativeViewModuleEventBridgeState {
9
+ installed: boolean;
10
+ baseDispatcher?: ExactModuleEventDispatcher;
11
+ nativeViewHandlers: Map<number, NativeViewEventHandler>;
12
+ }
13
+
14
+ type ExactModuleEventDispatcher = (
15
+ moduleName: string,
16
+ eventName: string,
17
+ nodeIdOrPayload?: unknown,
18
+ payloadMaybe?: unknown,
19
+ ) => void;
20
+
21
+ interface NativeViewEventGlobalScope {
22
+ __exactModuleEvent?: ExactModuleEventDispatcher;
23
+ __exactModuleEventHandlers?: Map<string, (eventName: string, payload: unknown) => void>;
24
+ __exactNativeViewModuleEventBridgeState?: NativeViewModuleEventBridgeState;
25
+ }
26
+
27
+ function globalScope(): typeof globalThis & NativeViewEventGlobalScope {
28
+ return globalThis as typeof globalThis & NativeViewEventGlobalScope;
29
+ }
30
+
31
+ function getBridgeState(): NativeViewModuleEventBridgeState {
32
+ const scope = globalScope();
33
+
34
+ if (scope.__exactNativeViewModuleEventBridgeState) {
35
+ return scope.__exactNativeViewModuleEventBridgeState;
36
+ }
37
+
38
+ const state: NativeViewModuleEventBridgeState = {
39
+ installed: false,
40
+ baseDispatcher: undefined,
41
+ nativeViewHandlers: new Map(),
42
+ };
43
+ scope.__exactNativeViewModuleEventBridgeState = state;
44
+ return state;
45
+ }
46
+
47
+ function decodeNativeViewEventPayload(payload: unknown): unknown {
48
+ // Native Exact views send MessagePack so controls can report structured
49
+ // values without paying the JSON stringify/parse tax on every interaction.
50
+ // Keep the decoder local and tiny: RFC 0073 only needs the basic scalar/map
51
+ // surface used by the first-party slider/toggle modules.
52
+ const bytes =
53
+ payload instanceof Uint8Array
54
+ ? payload
55
+ : payload instanceof ArrayBuffer
56
+ ? new Uint8Array(payload)
57
+ : null;
58
+
59
+ if (!bytes) {
60
+ return payload;
61
+ }
62
+
63
+ try {
64
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
65
+ let offset = 0;
66
+ const textDecoder = new TextDecoder();
67
+
68
+ const ensureAvailable = (length: number): void => {
69
+ if (offset + length > bytes.byteLength) {
70
+ throw new Error('Unexpected end of MessagePack data');
71
+ }
72
+ };
73
+
74
+ const readBytes = (length: number): Uint8Array => {
75
+ ensureAvailable(length);
76
+ const start = offset;
77
+ offset += length;
78
+ return bytes.subarray(start, start + length);
79
+ };
80
+
81
+ const readUint8 = (): number => {
82
+ ensureAvailable(1);
83
+ return bytes[offset++]!;
84
+ };
85
+
86
+ const readUint16 = (): number => {
87
+ ensureAvailable(2);
88
+ const value = view.getUint16(offset, false);
89
+ offset += 2;
90
+ return value;
91
+ };
92
+
93
+ const readUint32 = (): number => {
94
+ ensureAvailable(4);
95
+ const value = view.getUint32(offset, false);
96
+ offset += 4;
97
+ return value;
98
+ };
99
+
100
+ const readInt8 = (): number => {
101
+ ensureAvailable(1);
102
+ const value = view.getInt8(offset);
103
+ offset += 1;
104
+ return value;
105
+ };
106
+
107
+ const readInt16 = (): number => {
108
+ ensureAvailable(2);
109
+ const value = view.getInt16(offset, false);
110
+ offset += 2;
111
+ return value;
112
+ };
113
+
114
+ const readInt32 = (): number => {
115
+ ensureAvailable(4);
116
+ const value = view.getInt32(offset, false);
117
+ offset += 4;
118
+ return value;
119
+ };
120
+
121
+ const readFloat32 = (): number => {
122
+ ensureAvailable(4);
123
+ const value = view.getFloat32(offset, false);
124
+ offset += 4;
125
+ return value;
126
+ };
127
+
128
+ const readFloat64 = (): number => {
129
+ ensureAvailable(8);
130
+ const value = view.getFloat64(offset, false);
131
+ offset += 8;
132
+ return value;
133
+ };
134
+
135
+ const decodeValue = (): unknown => {
136
+ const byte = readUint8();
137
+
138
+ if (byte <= 0x7f) {
139
+ return byte;
140
+ }
141
+ if (byte >= 0xe0) {
142
+ return byte - 256;
143
+ }
144
+ if ((byte & 0xf0) === 0x80) {
145
+ return decodeMap(byte & 0x0f);
146
+ }
147
+ if ((byte & 0xf0) === 0x90) {
148
+ return decodeArray(byte & 0x0f);
149
+ }
150
+ if ((byte & 0xe0) === 0xa0) {
151
+ return textDecoder.decode(readBytes(byte & 0x1f));
152
+ }
153
+
154
+ switch (byte) {
155
+ case 0xc0:
156
+ return null;
157
+ case 0xc2:
158
+ return false;
159
+ case 0xc3:
160
+ return true;
161
+ case 0xc4:
162
+ return readBytes(readUint8());
163
+ case 0xc5:
164
+ return readBytes(readUint16());
165
+ case 0xc6:
166
+ return readBytes(readUint32());
167
+ case 0xca:
168
+ return readFloat32();
169
+ case 0xcb:
170
+ return readFloat64();
171
+ case 0xcc:
172
+ return readUint8();
173
+ case 0xcd:
174
+ return readUint16();
175
+ case 0xce:
176
+ return readUint32();
177
+ case 0xd0:
178
+ return readInt8();
179
+ case 0xd1:
180
+ return readInt16();
181
+ case 0xd2:
182
+ return readInt32();
183
+ case 0xd9:
184
+ return textDecoder.decode(readBytes(readUint8()));
185
+ case 0xda:
186
+ return textDecoder.decode(readBytes(readUint16()));
187
+ case 0xdb:
188
+ return textDecoder.decode(readBytes(readUint32()));
189
+ case 0xdc:
190
+ return decodeArray(readUint16());
191
+ case 0xdd:
192
+ return decodeArray(readUint32());
193
+ case 0xde:
194
+ return decodeMap(readUint16());
195
+ case 0xdf:
196
+ return decodeMap(readUint32());
197
+ default:
198
+ throw new Error(`Unknown MessagePack type: 0x${byte.toString(16)}`);
199
+ }
200
+ };
201
+
202
+ const decodeArray = (length: number): unknown[] => {
203
+ const result: unknown[] = [];
204
+ for (let index = 0; index < length; index += 1) {
205
+ result.push(decodeValue());
206
+ }
207
+ return result;
208
+ };
209
+
210
+ const decodeMap = (length: number): Record<string, unknown> => {
211
+ const result: Record<string, unknown> = {};
212
+ for (let index = 0; index < length; index += 1) {
213
+ const key = decodeValue();
214
+ result[String(key)] = decodeValue();
215
+ }
216
+ return result;
217
+ };
218
+
219
+ return decodeValue();
220
+ } catch (error) {
221
+ console.warn('[NativeViewEvents] Failed to decode MessagePack payload.', error);
222
+ return payload;
223
+ }
224
+ }
225
+
226
+ function dispatchNativeViewModuleEvent(
227
+ moduleName: string,
228
+ eventName: string,
229
+ nodeId: number,
230
+ payload: unknown,
231
+ ): boolean {
232
+ const handler = getBridgeState().nativeViewHandlers.get(nodeId);
233
+ if (!handler || handler.moduleName !== moduleName) {
234
+ return false;
235
+ }
236
+
237
+ const decodedPayload = decodeNativeViewEventPayload(payload);
238
+ return handler.handle(eventName, decodedPayload);
239
+ }
240
+
241
+ function installModuleEventBridge(): void {
242
+ const state = getBridgeState();
243
+ const scope = globalScope();
244
+ if (state.installed) {
245
+ return;
246
+ }
247
+
248
+ if (typeof scope.__exactModuleEventHandlers === 'undefined') {
249
+ scope.__exactModuleEventHandlers = new Map();
250
+ }
251
+
252
+ const existingDispatcher = scope.__exactModuleEvent;
253
+ state.baseDispatcher =
254
+ typeof existingDispatcher === 'function'
255
+ ? existingDispatcher
256
+ : undefined;
257
+
258
+ scope.__exactModuleEvent = (
259
+ moduleName: string,
260
+ eventName: string,
261
+ nodeIdOrPayload?: unknown,
262
+ payloadMaybe?: unknown,
263
+ ) => {
264
+ const hasNodeId = typeof nodeIdOrPayload === 'number';
265
+ const nodeId = hasNodeId ? nodeIdOrPayload as number : undefined;
266
+ const payload = hasNodeId ? payloadMaybe : nodeIdOrPayload;
267
+
268
+ if (
269
+ typeof nodeId === 'number' &&
270
+ dispatchNativeViewModuleEvent(moduleName, eventName, nodeId, payload)
271
+ ) {
272
+ return;
273
+ }
274
+
275
+ if (state.baseDispatcher) {
276
+ // Preserve any pre-existing dispatcher that libraries installed before the
277
+ // renderer loaded. Older handlers only expect a 3-argument shape, so only
278
+ // forward the node id to dispatchers that explicitly opted into it.
279
+ if (hasNodeId && state.baseDispatcher.length >= 4) {
280
+ state.baseDispatcher(moduleName, eventName, nodeId, payload);
281
+ return;
282
+ }
283
+ state.baseDispatcher(moduleName, eventName, payload);
284
+ return;
285
+ }
286
+
287
+ scope.__exactModuleEventHandlers?.get(moduleName)?.(eventName, payload);
288
+ };
289
+
290
+ state.installed = true;
291
+ }
292
+
293
+ export function registerNativeViewEventHandler(
294
+ nodeId: number,
295
+ moduleName: string,
296
+ handler: NativeViewEventHandler['handle'],
297
+ ): void {
298
+ installModuleEventBridge();
299
+ getBridgeState().nativeViewHandlers.set(nodeId, {
300
+ moduleName,
301
+ handle: handler,
302
+ });
303
+ }
304
+
305
+ export function unregisterNativeViewEventHandler(nodeId: number): void {
306
+ getBridgeState().nativeViewHandlers.delete(nodeId);
307
+ }
308
+
309
+ export function _resetNativeViewEventBridgeForTests(): void {
310
+ const state = getBridgeState();
311
+ const scope = globalScope();
312
+ state.nativeViewHandlers.clear();
313
+ state.baseDispatcher = undefined;
314
+ state.installed = false;
315
+ delete scope.__exactModuleEvent;
316
+ delete scope.__exactModuleEventHandlers;
317
+ delete scope.__exactNativeViewModuleEventBridgeState;
318
+ }
319
+
320
+ installModuleEventBridge();
321
+
322
+ export { EventType };
@@ -0,0 +1,60 @@
1
+ import { createElement } from 'react';
2
+ import type { FC, ReactNode } from 'react';
3
+
4
+ import type {
5
+ BaseProps,
6
+ NativeViewSelectionGesturePolicy,
7
+ NativeViewSelectionTier,
8
+ ViewStyle,
9
+ } from './types.js';
10
+ import { registerNativeViewTag } from './tags/index.js';
11
+
12
+ export interface NativeViewSelectionDef {
13
+ readonly tier: NativeViewSelectionTier;
14
+ readonly gesturePolicy?: NativeViewSelectionGesturePolicy;
15
+ }
16
+
17
+ export interface NativeViewRuntimeDef {
18
+ readonly moduleName: string;
19
+ readonly propKeys: readonly string[];
20
+ readonly selection?: NativeViewSelectionDef;
21
+ readonly displayName?: string;
22
+ }
23
+
24
+ export type NativeViewComponentProps<Props extends Record<string, unknown> = Record<string, unknown>> =
25
+ BaseProps & {
26
+ style?: ViewStyle;
27
+ children?: ReactNode;
28
+ } & Props;
29
+
30
+ function getDisplayName(moduleName: string): string {
31
+ const lastSegment = moduleName.split('.').pop();
32
+ return lastSegment && lastSegment.length > 0
33
+ ? lastSegment
34
+ : moduleName;
35
+ }
36
+
37
+ export function createNativeViewComponent<Props extends Record<string, unknown>>(
38
+ def: NativeViewRuntimeDef,
39
+ ): FC<NativeViewComponentProps<Props>> {
40
+ const tag = registerNativeViewTag({
41
+ moduleName: def.moduleName,
42
+ propKeys: [...def.propKeys],
43
+ selection: def.selection
44
+ ? {
45
+ tier: def.selection.tier,
46
+ gesturePolicy:
47
+ def.selection.tier === 'delegated'
48
+ ? (def.selection.gesturePolicy ?? 'internal')
49
+ : def.selection.gesturePolicy,
50
+ }
51
+ : undefined,
52
+ });
53
+
54
+ const NativeViewComponent = (
55
+ props: NativeViewComponentProps<Props>,
56
+ ) => createElement(tag, props as Record<string, unknown>);
57
+
58
+ NativeViewComponent.displayName = def.displayName ?? getDisplayName(def.moduleName);
59
+ return NativeViewComponent;
60
+ }