@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,214 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Exact Primitive Components
|
|
3
|
+
*
|
|
4
|
+
* This module exports the primitive components for building Exact UIs.
|
|
5
|
+
* Components are organized into two categories:
|
|
6
|
+
*
|
|
7
|
+
* 1. Web-style tags (lowercase): div, span, text, img, input, button
|
|
8
|
+
* - Use CSS flexbox defaults (flexDirection: 'row')
|
|
9
|
+
* - Use web-style props (src, alt, objectFit)
|
|
10
|
+
*
|
|
11
|
+
* 2. React Native-style components (PascalCase): View, Text, Image, ScrollView, Pressable
|
|
12
|
+
* - Use RN defaults (flexDirection: 'column')
|
|
13
|
+
* - Use RN-style props (source, resizeMode)
|
|
14
|
+
*
|
|
15
|
+
* Both styles render to the same underlying views. Choose based on your
|
|
16
|
+
* preferred API style and which defaults make sense for your UI.
|
|
17
|
+
*
|
|
18
|
+
* @example Web-style
|
|
19
|
+
* ```tsx
|
|
20
|
+
* <div style={{ flex: 1, backgroundColor: '#fff' }}>
|
|
21
|
+
* <text style={{ fontSize: 16 }}>Hello</text>
|
|
22
|
+
* <img src="https://example.com/image.png" style={{ width: 100, height: 100 }} />
|
|
23
|
+
* </div>
|
|
24
|
+
* ```
|
|
25
|
+
*
|
|
26
|
+
* @example React Native-style
|
|
27
|
+
* ```tsx
|
|
28
|
+
* <View style={{ flex: 1, backgroundColor: '#fff' }}>
|
|
29
|
+
* <Text style={{ fontSize: 16 }}>Hello</Text>
|
|
30
|
+
* <Image source={{ uri: 'https://example.com/image.png' }} style={{ width: 100, height: 100 }} />
|
|
31
|
+
* </View>
|
|
32
|
+
* ```
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
import { createElement } from 'react';
|
|
36
|
+
|
|
37
|
+
import type {
|
|
38
|
+
ContainerProps,
|
|
39
|
+
TextElementProps,
|
|
40
|
+
ImageElementProps,
|
|
41
|
+
PressableElementProps,
|
|
42
|
+
TextInputProps,
|
|
43
|
+
ToggleProps,
|
|
44
|
+
} from './types.js';
|
|
45
|
+
import {
|
|
46
|
+
button as platformButton,
|
|
47
|
+
div as platformDiv,
|
|
48
|
+
img as platformImg,
|
|
49
|
+
input as platformInput,
|
|
50
|
+
span as platformSpan,
|
|
51
|
+
text as platformText,
|
|
52
|
+
View as platformView,
|
|
53
|
+
} from './web-primitives.js';
|
|
54
|
+
|
|
55
|
+
// =============================================================================
|
|
56
|
+
// Web-Style Tags
|
|
57
|
+
// =============================================================================
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Generic container element.
|
|
61
|
+
*
|
|
62
|
+
* Default flex direction: 'row' (CSS default)
|
|
63
|
+
*
|
|
64
|
+
* @example
|
|
65
|
+
* ```tsx
|
|
66
|
+
* <div style={{ flex: 1, flexDirection: 'column' }}>
|
|
67
|
+
* <div style={{ height: 100, backgroundColor: '#f00' }} />
|
|
68
|
+
* <div style={{ height: 100, backgroundColor: '#0f0' }} />
|
|
69
|
+
* </div>
|
|
70
|
+
* ```
|
|
71
|
+
*/
|
|
72
|
+
/**
|
|
73
|
+
* Lowercase `div` now uses the same platform-aware bridge as the other
|
|
74
|
+
* primitives. On React DOM it renders a standard `<div>` with row defaults;
|
|
75
|
+
* on the custom reconciler path it still emits the raw `div` tag.
|
|
76
|
+
*/
|
|
77
|
+
export const div = platformDiv;
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Inline container element.
|
|
81
|
+
*
|
|
82
|
+
* Alias for div. Default flex direction: 'row' (CSS default)
|
|
83
|
+
*
|
|
84
|
+
* @example
|
|
85
|
+
* ```tsx
|
|
86
|
+
* <span style={{ padding: 8, backgroundColor: '#eee' }}>
|
|
87
|
+
* <text>Hello</text>
|
|
88
|
+
* </span>
|
|
89
|
+
* ```
|
|
90
|
+
*/
|
|
91
|
+
/**
|
|
92
|
+
* Lowercase `span` follows the same bridge pattern as `div`, but preserves
|
|
93
|
+
* inline semantics on the React DOM path.
|
|
94
|
+
*/
|
|
95
|
+
export const span = platformSpan;
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Text content element.
|
|
99
|
+
*
|
|
100
|
+
* Use this to display text. Text content should be placed as children.
|
|
101
|
+
*
|
|
102
|
+
* @example
|
|
103
|
+
* ```tsx
|
|
104
|
+
* <text style={{ fontSize: 16, color: '#333' }}>
|
|
105
|
+
* Hello, world!
|
|
106
|
+
* </text>
|
|
107
|
+
* ```
|
|
108
|
+
*/
|
|
109
|
+
export const text = platformText;
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Image element (web-style).
|
|
113
|
+
*
|
|
114
|
+
* Uses web-style props: src, alt, objectFit.
|
|
115
|
+
*
|
|
116
|
+
* @example
|
|
117
|
+
* ```tsx
|
|
118
|
+
* <img
|
|
119
|
+
* src="https://example.com/image.png"
|
|
120
|
+
* alt="Description"
|
|
121
|
+
* style={{ width: 200, height: 200 }}
|
|
122
|
+
* objectFit="cover"
|
|
123
|
+
* />
|
|
124
|
+
* ```
|
|
125
|
+
*/
|
|
126
|
+
/**
|
|
127
|
+
* Lowercase `img` is now platform-aware too. This keeps Facet's web-style
|
|
128
|
+
* image usage working on React DOM without giving up the custom-reconciler tag.
|
|
129
|
+
*/
|
|
130
|
+
export const img = platformImg;
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Text input element (web-style).
|
|
134
|
+
*
|
|
135
|
+
* @example
|
|
136
|
+
* ```tsx
|
|
137
|
+
* <input
|
|
138
|
+
* style={{ height: 40, borderWidth: 1, borderColor: '#ccc' }}
|
|
139
|
+
* placeholder="Enter text..."
|
|
140
|
+
* value={text}
|
|
141
|
+
* onChangeText={setText}
|
|
142
|
+
* />
|
|
143
|
+
* ```
|
|
144
|
+
*/
|
|
145
|
+
/**
|
|
146
|
+
* Lowercase `input` now maps to the web text-input wrapper on React DOM so
|
|
147
|
+
* `onChangeText`, `multiline`, and the rest of Exact's input contract still
|
|
148
|
+
* work when apps render outside the custom reconciler.
|
|
149
|
+
*/
|
|
150
|
+
export const input = platformInput;
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Button element (pressable).
|
|
154
|
+
*
|
|
155
|
+
* A pressable container that responds to touch events.
|
|
156
|
+
* Default flex direction: 'row' (CSS default)
|
|
157
|
+
*
|
|
158
|
+
* @example
|
|
159
|
+
* ```tsx
|
|
160
|
+
* <button
|
|
161
|
+
* style={{ padding: 12, backgroundColor: '#007AFF', borderRadius: 8 }}
|
|
162
|
+
* onPress={() => console.log('Pressed!')}
|
|
163
|
+
* >
|
|
164
|
+
* <text style={{ color: '#fff' }}>Click me</text>
|
|
165
|
+
* </button>
|
|
166
|
+
* ```
|
|
167
|
+
*/
|
|
168
|
+
export const button = platformButton;
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Toggle/Switch element (web-style).
|
|
172
|
+
*
|
|
173
|
+
* A native iOS toggle with optional liquid glass effect.
|
|
174
|
+
*
|
|
175
|
+
* @example
|
|
176
|
+
* ```tsx
|
|
177
|
+
* <toggle
|
|
178
|
+
* value={isOn}
|
|
179
|
+
* onValueChange={setIsOn}
|
|
180
|
+
* glassEffect
|
|
181
|
+
* tintColor="#34d399"
|
|
182
|
+
* />
|
|
183
|
+
* ```
|
|
184
|
+
*/
|
|
185
|
+
export const toggle = 'toggle' as unknown as React.FC<ToggleProps>;
|
|
186
|
+
|
|
187
|
+
// =============================================================================
|
|
188
|
+
// React Native-Style Components
|
|
189
|
+
//
|
|
190
|
+
// These are platform-aware: on web they render standard HTML elements with
|
|
191
|
+
// React Native-like defaults (flex column, onPress → onClick, etc.); on
|
|
192
|
+
// native they emit string tags for the Exact custom reconciler.
|
|
193
|
+
// =============================================================================
|
|
194
|
+
|
|
195
|
+
export {
|
|
196
|
+
View,
|
|
197
|
+
Text,
|
|
198
|
+
Image,
|
|
199
|
+
Svg,
|
|
200
|
+
List,
|
|
201
|
+
ScrollView,
|
|
202
|
+
TextInput,
|
|
203
|
+
Pressable,
|
|
204
|
+
Toggle,
|
|
205
|
+
Switch,
|
|
206
|
+
} from './web-primitives.js';
|
|
207
|
+
|
|
208
|
+
export const SelectionGroup: React.FC<ContainerProps> = function SelectionGroup(props) {
|
|
209
|
+
return createElement(platformView, {
|
|
210
|
+
...props,
|
|
211
|
+
selectable: props.selectable ?? 'contain',
|
|
212
|
+
__exactComponentName: props.__exactComponentName ?? 'SelectionGroup',
|
|
213
|
+
});
|
|
214
|
+
};
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CSS Custom Property context for native className resolution (RFC 0024 Phase 3).
|
|
3
|
+
*
|
|
4
|
+
* On native, CSS custom properties set via `style={{ '--brand': '#ff6b35' }}`
|
|
5
|
+
* are extracted from the style prop and provided to descendants via React context.
|
|
6
|
+
* The className resolver reads this context to resolve `var()` references at
|
|
7
|
+
* runtime (Layer 2).
|
|
8
|
+
*
|
|
9
|
+
* On web, `--`-prefixed keys are forwarded to the DOM `style` attribute and the
|
|
10
|
+
* browser handles `var()` resolution natively. This module is not used on web.
|
|
11
|
+
*/
|
|
12
|
+
import { createContext, createElement, useContext, useMemo, type ReactNode } from 'react';
|
|
13
|
+
|
|
14
|
+
export type CSSVariableMap = Readonly<Record<string, string>>;
|
|
15
|
+
|
|
16
|
+
const EMPTY_MAP: CSSVariableMap = Object.freeze({});
|
|
17
|
+
|
|
18
|
+
const CSSVariableContext = createContext<CSSVariableMap>(EMPTY_MAP);
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Read the inherited CSS variable map from context. Used by the className
|
|
22
|
+
* resolver to resolve runtime `var()` references.
|
|
23
|
+
*/
|
|
24
|
+
export function useCSSVariables(): CSSVariableMap {
|
|
25
|
+
return useContext(CSSVariableContext);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Provide scoped CSS custom properties to descendants.
|
|
30
|
+
*
|
|
31
|
+
* When `declaredVars` is empty or undefined, the provider is a passthrough
|
|
32
|
+
* (returns the inherited context unchanged — zero cost via useMemo identity).
|
|
33
|
+
* This allows unconditional rendering without unmount/remount concerns.
|
|
34
|
+
*/
|
|
35
|
+
export function CSSVariableProvider({
|
|
36
|
+
declaredVars,
|
|
37
|
+
children,
|
|
38
|
+
}: {
|
|
39
|
+
declaredVars: CSSVariableMap | undefined;
|
|
40
|
+
children: ReactNode;
|
|
41
|
+
}): ReactNode {
|
|
42
|
+
const inherited = useContext(CSSVariableContext);
|
|
43
|
+
const merged = useMemo(() => {
|
|
44
|
+
if (!declaredVars || Object.keys(declaredVars).length === 0) {
|
|
45
|
+
return inherited;
|
|
46
|
+
}
|
|
47
|
+
return { ...inherited, ...declaredVars };
|
|
48
|
+
}, [inherited, declaredVars]);
|
|
49
|
+
|
|
50
|
+
return createElement(CSSVariableContext.Provider, { value: merged }, children);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Extract `--`-prefixed keys from a style object, returning the extracted
|
|
55
|
+
* variables and the remaining style without them.
|
|
56
|
+
*/
|
|
57
|
+
export function extractCSSVariables(
|
|
58
|
+
style: Record<string, unknown> | undefined,
|
|
59
|
+
): { cssVars: CSSVariableMap | undefined; cleanStyle: Record<string, unknown> | undefined } {
|
|
60
|
+
if (!style) {
|
|
61
|
+
return { cssVars: undefined, cleanStyle: undefined };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
let cssVars: Record<string, string> | undefined;
|
|
65
|
+
let cleanStyle: Record<string, unknown> | undefined;
|
|
66
|
+
|
|
67
|
+
for (const key of Object.keys(style)) {
|
|
68
|
+
if (key.startsWith('--')) {
|
|
69
|
+
if (!cssVars) cssVars = {};
|
|
70
|
+
cssVars[key] = String(style[key]);
|
|
71
|
+
} else {
|
|
72
|
+
if (!cleanStyle) cleanStyle = {};
|
|
73
|
+
cleanStyle[key] = style[key];
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// If no -- keys found, return original style object to preserve identity
|
|
78
|
+
if (!cssVars) {
|
|
79
|
+
return { cssVars: undefined, cleanStyle: style };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return { cssVars, cleanStyle };
|
|
83
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
// @system @ref LLP 0201 W3 / LLP 0210 — Contract web host hydration adoption.
|
|
2
|
+
|
|
3
|
+
import type {
|
|
4
|
+
DomHydrationAdopter,
|
|
5
|
+
DomHydrationAdopterFactoryOptions,
|
|
6
|
+
DomHydrationManifestLike,
|
|
7
|
+
DomMirrorHostChild,
|
|
8
|
+
} from './dom-mirror.js';
|
|
9
|
+
import type { ElementNode, TextNode } from './nodes/node.js';
|
|
10
|
+
import { NodeKind } from './nodes/node.js';
|
|
11
|
+
|
|
12
|
+
type HydrationNode = NonNullable<DomHydrationManifestLike['nodes']>[number];
|
|
13
|
+
|
|
14
|
+
function buildHydrationAnchorMap(
|
|
15
|
+
hydrate: DomHydrationAdopterFactoryOptions['hydrate'],
|
|
16
|
+
): Map<string, HydrationNode> {
|
|
17
|
+
const out = new Map<string, HydrationNode>();
|
|
18
|
+
for (const node of hydrate.manifest.nodes ?? []) {
|
|
19
|
+
if (typeof node.nodeId === 'string' && node.nodeId.length > 0) {
|
|
20
|
+
out.set(node.nodeId, node);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return out;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function resolveDomPath(root: ParentNode, path: readonly number[]): Node | null {
|
|
27
|
+
let current: ParentNode | Node = root;
|
|
28
|
+
for (const index of path) {
|
|
29
|
+
if (!Number.isInteger(index) || index < 0 || !('childNodes' in current)) {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
const next = current.childNodes.item(index);
|
|
33
|
+
if (!next) {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
current = next;
|
|
37
|
+
}
|
|
38
|
+
return current as Node;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function hydrationIdFor(child: DomMirrorHostChild): string | undefined {
|
|
42
|
+
if (child.kind === NodeKind.Text) {
|
|
43
|
+
return (child as TextNode & { __exactHydrationId?: string }).__exactHydrationId;
|
|
44
|
+
}
|
|
45
|
+
const value = ((child as ElementNode).originalProps ?? {})['data-exact-hydration-id'];
|
|
46
|
+
return typeof value === 'string' && value.length > 0 ? value : undefined;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function createDomHydrationAdopter(
|
|
50
|
+
options: DomHydrationAdopterFactoryOptions,
|
|
51
|
+
): DomHydrationAdopter | null {
|
|
52
|
+
const hydrationAnchors = buildHydrationAnchorMap(options.hydrate);
|
|
53
|
+
const hydrationResults = new Map<number, {
|
|
54
|
+
adoptedNodes: number;
|
|
55
|
+
recreatedNodes: number;
|
|
56
|
+
mismatches: string[];
|
|
57
|
+
rootId: number;
|
|
58
|
+
}>();
|
|
59
|
+
const reportedHydrationRoots = new Set<number>();
|
|
60
|
+
|
|
61
|
+
function resultFor(rootId: number) {
|
|
62
|
+
let result = hydrationResults.get(rootId);
|
|
63
|
+
if (!result) {
|
|
64
|
+
result = { rootId, adoptedNodes: 0, recreatedNodes: 0, mismatches: [] };
|
|
65
|
+
hydrationResults.set(rootId, result);
|
|
66
|
+
}
|
|
67
|
+
return result;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
adoptRootContainer(rootId, base, rootAttribute) {
|
|
72
|
+
if (options.hydrate.rootId !== rootId) {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
if (base.getAttribute(rootAttribute) === String(rootId)) {
|
|
76
|
+
options.markMirrored(base);
|
|
77
|
+
return base;
|
|
78
|
+
}
|
|
79
|
+
const existing = base.querySelector(`[${rootAttribute}="${rootId}"]`);
|
|
80
|
+
if (existing && existing.nodeType === 1) {
|
|
81
|
+
options.markMirrored(existing);
|
|
82
|
+
return existing as HTMLElement;
|
|
83
|
+
}
|
|
84
|
+
return null;
|
|
85
|
+
},
|
|
86
|
+
|
|
87
|
+
adoptDom(child, rootId, rootContainer, desiredTag) {
|
|
88
|
+
if (options.hydrate.rootId !== rootId) {
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
const hydrationId = hydrationIdFor(child);
|
|
92
|
+
if (!hydrationId) {
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
const anchor = hydrationAnchors.get(hydrationId);
|
|
96
|
+
if (!anchor?.domPath) {
|
|
97
|
+
resultFor(rootId).mismatches.push(`${hydrationId}: missing hydration anchor`);
|
|
98
|
+
resultFor(rootId).recreatedNodes += 1;
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
const candidate = resolveDomPath(rootContainer, anchor.domPath);
|
|
102
|
+
if (!candidate) {
|
|
103
|
+
resultFor(rootId).mismatches.push(
|
|
104
|
+
`${hydrationId}: domPath ${anchor.domPath.join('.')} did not resolve`,
|
|
105
|
+
);
|
|
106
|
+
resultFor(rootId).recreatedNodes += 1;
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
if (child.kind === NodeKind.Text) {
|
|
110
|
+
if (candidate.nodeType !== 3) {
|
|
111
|
+
resultFor(rootId).mismatches.push(`${hydrationId}: expected text node`);
|
|
112
|
+
resultFor(rootId).recreatedNodes += 1;
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
if (anchor.textGuard !== undefined && candidate.nodeValue !== anchor.textGuard) {
|
|
116
|
+
resultFor(rootId).mismatches.push(`${hydrationId}: textGuard mismatch`);
|
|
117
|
+
resultFor(rootId).recreatedNodes += 1;
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
options.markMirrored(candidate);
|
|
121
|
+
resultFor(rootId).adoptedNodes += 1;
|
|
122
|
+
return candidate;
|
|
123
|
+
}
|
|
124
|
+
if (candidate.nodeType !== 1) {
|
|
125
|
+
resultFor(rootId).mismatches.push(`${hydrationId}: expected element node`);
|
|
126
|
+
resultFor(rootId).recreatedNodes += 1;
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
const elementCandidate = candidate as HTMLElement;
|
|
130
|
+
if (desiredTag && elementCandidate.localName !== desiredTag) {
|
|
131
|
+
resultFor(rootId).mismatches.push(
|
|
132
|
+
`${hydrationId}: expected <${desiredTag}>, got <${elementCandidate.localName}>`,
|
|
133
|
+
);
|
|
134
|
+
resultFor(rootId).recreatedNodes += 1;
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
const marker = elementCandidate.getAttribute('data-exact-hydration-id');
|
|
138
|
+
if (marker !== hydrationId) {
|
|
139
|
+
resultFor(rootId).mismatches.push(`${hydrationId}: marker mismatch`);
|
|
140
|
+
resultFor(rootId).recreatedNodes += 1;
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
options.markMirrored(elementCandidate);
|
|
144
|
+
resultFor(rootId).adoptedNodes += 1;
|
|
145
|
+
return elementCandidate;
|
|
146
|
+
},
|
|
147
|
+
|
|
148
|
+
report(rootId) {
|
|
149
|
+
if (options.hydrate.rootId !== rootId || reportedHydrationRoots.has(rootId)) {
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
reportedHydrationRoots.add(rootId);
|
|
153
|
+
options.hydrate.onResult?.(resultFor(rootId));
|
|
154
|
+
},
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export function installDomHydrationAdopterFactory(): void {
|
|
159
|
+
globalThis.__exactDomHydrationAdopterFactory = createDomHydrationAdopter;
|
|
160
|
+
}
|