foldkit 0.147.0 → 0.148.1
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/dist/buildToken.d.ts +3 -0
- package/dist/buildToken.d.ts.map +1 -0
- package/dist/buildToken.js +21 -0
- package/dist/controlledDomState.d.ts +25 -0
- package/dist/controlledDomState.d.ts.map +1 -0
- package/dist/controlledDomState.js +240 -0
- package/dist/cssStyleProperties.d.ts +6 -0
- package/dist/cssStyleProperties.d.ts.map +1 -0
- package/dist/cssStyleProperties.js +91 -0
- package/dist/domReflection.d.ts +73 -0
- package/dist/domReflection.d.ts.map +1 -0
- package/dist/domReflection.js +557 -0
- package/dist/experimental/server/host.d.ts +102 -8
- package/dist/experimental/server/host.d.ts.map +1 -1
- package/dist/experimental/server/host.js +203 -13
- package/dist/experimental/server/public.d.ts +2 -2
- package/dist/experimental/server/public.d.ts.map +1 -1
- package/dist/experimental/server/public.js +1 -1
- package/dist/experimental/server/serialize.d.ts +15 -5
- package/dist/experimental/server/serialize.d.ts.map +1 -1
- package/dist/experimental/server/serialize.js +373 -144
- package/dist/experimental/server/server.d.ts +55 -14
- package/dist/experimental/server/server.d.ts.map +1 -1
- package/dist/experimental/server/server.js +546 -22
- package/dist/experimental/server/template.d.ts +8 -0
- package/dist/experimental/server/template.d.ts.map +1 -1
- package/dist/experimental/server/template.js +437 -2
- package/dist/html/index.d.ts.map +1 -1
- package/dist/html/index.js +436 -29
- package/dist/hydrate.d.ts +1 -1
- package/dist/hydrate.d.ts.map +1 -1
- package/dist/hydrate.js +449 -121
- package/dist/hydrationMarkers.d.ts +15 -0
- package/dist/hydrationMarkers.d.ts.map +1 -0
- package/dist/hydrationMarkers.js +72 -0
- package/dist/nativeInnerHtml.d.ts +13 -0
- package/dist/nativeInnerHtml.d.ts.map +1 -0
- package/dist/nativeInnerHtml.js +30 -0
- package/dist/propertyProvenance.d.ts +33 -0
- package/dist/propertyProvenance.d.ts.map +1 -0
- package/dist/propertyProvenance.js +78 -0
- package/dist/propsModule.d.ts.map +1 -1
- package/dist/propsModule.js +149 -15
- package/dist/runtime/public.d.ts +1 -1
- package/dist/runtime/public.d.ts.map +1 -1
- package/dist/runtime/runtime.d.ts +33 -6
- package/dist/runtime/runtime.d.ts.map +1 -1
- package/dist/runtime/runtime.js +240 -27
- package/dist/snabbdom/attributes.d.ts.map +1 -1
- package/dist/snabbdom/attributes.js +65 -37
- package/dist/snabbdom/class.d.ts.map +1 -1
- package/dist/snabbdom/class.js +61 -5
- package/dist/snabbdom/style.d.ts.map +1 -1
- package/dist/snabbdom/style.js +57 -34
- package/dist/test/apps/attributes.d.ts +1 -0
- package/dist/test/apps/attributes.d.ts.map +1 -1
- package/dist/test/apps/attributes.js +8 -1
- package/dist/test/apps/login.js +1 -1
- package/dist/test/matchers.d.ts.map +1 -1
- package/dist/test/matchers.js +2 -1
- package/dist/test/scene.d.ts.map +1 -1
- package/dist/test/scene.js +2 -1
- package/package.json +2 -2
package/dist/hydrate.js
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
import { Option } from 'effect';
|
|
2
|
+
import { HYDRATION_BUILD_ATTRIBUTE } from './buildToken.js';
|
|
3
|
+
import { controlledStatePropertyNames } from './controlledDomState.js';
|
|
4
|
+
import { BOOLEAN_PROPERTIES, htmlAttributeValue, parsedAttributeName, reflectedAttributeName, serializedHtmlPropertyValue, serializedStylePropertyName, } from './domReflection.js';
|
|
5
|
+
import { HYDRATION_IDENTITY_ATTRIBUTE, HYDRATION_KEY_ATTRIBUTE, hydrationIdentityMarker, hydrationKeyMarker, } from './hydrationMarkers.js';
|
|
6
|
+
import { readNativeInnerHtml, writeNativeInnerHtml } from './nativeInnerHtml.js';
|
|
7
|
+
import { hasTrustedInnerHtml, isClientOnlyProperty, markTrustedInnerHtml, } from './propertyProvenance.js';
|
|
2
8
|
import { h, toVNode } from './snabbdom/index.js';
|
|
3
9
|
import { tagNameFromSelector } from './tagName.js';
|
|
4
10
|
import { dedupeSharedVNodes, patch } from './vdom.js';
|
|
@@ -24,19 +30,14 @@ import { dedupeSharedVNodes, patch } from './vdom.js';
|
|
|
24
30
|
// untouched and never learns the nodes came from a server.
|
|
25
31
|
//
|
|
26
32
|
// Where the DOM disagrees with the vnode tree, the walk clears the nearest
|
|
27
|
-
// parent's children and hands `patch` an empty child list, so the
|
|
28
|
-
// rebuilt through `createElm
|
|
29
|
-
//
|
|
30
|
-
//
|
|
31
|
-
//
|
|
33
|
+
// ordinary parent's children and hands `patch` an empty child list, so the
|
|
34
|
+
// subtree is rebuilt through `createElm`. A Custom Element whose light DOM is
|
|
35
|
+
// declared by the view is a replacement boundary instead: the host and its
|
|
36
|
+
// children are built while detached, exactly as they are during a fresh
|
|
37
|
+
// render. Trailing vnode children with no DOM counterpart are simply absent
|
|
38
|
+
// from the clone; `updateChildren` appends them.
|
|
32
39
|
const HTML_NAMESPACE = 'http://www.w3.org/1999/xhtml';
|
|
33
40
|
const HYDRATION_STAMP_ATTRIBUTE = 'data-foldkit-app';
|
|
34
|
-
// The style module keys regular properties in camelCase (`backgroundColor`)
|
|
35
|
-
// and custom properties as written (`--accent`), so a property read from the
|
|
36
|
-
// DOM in kebab case is converted to the module's key form before seeding.
|
|
37
|
-
const styleModuleKey = (property) => property.startsWith('--')
|
|
38
|
-
? property
|
|
39
|
-
: property.replace(/-([a-z])/g, (_match, character) => character.toUpperCase());
|
|
40
41
|
const classListOf = (element) => {
|
|
41
42
|
const classes = {};
|
|
42
43
|
for (const className of Array.from(element.classList)) {
|
|
@@ -46,11 +47,13 @@ const classListOf = (element) => {
|
|
|
46
47
|
};
|
|
47
48
|
const inlineStyleOf = (element) => {
|
|
48
49
|
const style = {};
|
|
49
|
-
if (
|
|
50
|
+
if ('style' in element && element.style instanceof CSSStyleDeclaration) {
|
|
50
51
|
const inlineStyle = element.style;
|
|
51
52
|
for (let index = 0; index < inlineStyle.length; index += 1) {
|
|
52
53
|
const property = inlineStyle.item(index);
|
|
53
|
-
|
|
54
|
+
const value = inlineStyle.getPropertyValue(property);
|
|
55
|
+
const priority = inlineStyle.getPropertyPriority(property);
|
|
56
|
+
style[property] = priority === '' ? value : `${value} !${priority}`;
|
|
54
57
|
}
|
|
55
58
|
}
|
|
56
59
|
return style;
|
|
@@ -81,36 +84,108 @@ const seedStyle = (element, vnode, styleOwnedByModule, isCustomElement) => {
|
|
|
81
84
|
if (!styleOwnedByModule) {
|
|
82
85
|
return {};
|
|
83
86
|
}
|
|
84
|
-
|
|
85
|
-
|
|
87
|
+
const vnodeStyle = vnode.data?.style ?? {};
|
|
88
|
+
const declared = Object.fromEntries(Object.entries(vnodeStyle).flatMap(([key, value]) => typeof value === 'string'
|
|
89
|
+
? [[serializedStylePropertyName(key), value]]
|
|
90
|
+
: []));
|
|
91
|
+
const style = Reflect.get(element, 'style');
|
|
92
|
+
if (!(style instanceof CSSStyleDeclaration)) {
|
|
93
|
+
return {};
|
|
86
94
|
}
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
95
|
+
const seeded = isCustomElement
|
|
96
|
+
? {}
|
|
97
|
+
: inlineStyleOf(element);
|
|
98
|
+
const expected = element.ownerDocument.createElement('span').style;
|
|
99
|
+
const ownedProperties = new Set();
|
|
100
|
+
for (const [propertyName, value] of Object.entries(declared)) {
|
|
101
|
+
expected.setProperty(propertyName, value);
|
|
102
|
+
const ownershipProbe = element.ownerDocument.createElement('span').style;
|
|
103
|
+
ownershipProbe.setProperty(propertyName, value);
|
|
104
|
+
ownedProperties.add(propertyName);
|
|
105
|
+
for (let index = 0; index < ownershipProbe.length; index += 1) {
|
|
106
|
+
ownedProperties.add(ownershipProbe.item(index));
|
|
92
107
|
}
|
|
93
108
|
}
|
|
94
|
-
|
|
109
|
+
for (const propertyName of ownedProperties) {
|
|
110
|
+
delete seeded[propertyName];
|
|
111
|
+
}
|
|
112
|
+
const isEquivalent = Object.keys(declared).every(propertyName => style.getPropertyValue(propertyName) ===
|
|
113
|
+
expected.getPropertyValue(propertyName) &&
|
|
114
|
+
style.getPropertyPriority(propertyName) ===
|
|
115
|
+
expected.getPropertyPriority(propertyName));
|
|
116
|
+
if (isEquivalent) {
|
|
117
|
+
Object.assign(seeded, declared);
|
|
118
|
+
}
|
|
119
|
+
return seeded;
|
|
95
120
|
};
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
const
|
|
109
|
-
|
|
110
|
-
if (props === undefined) {
|
|
121
|
+
const createInertProbe = (element) => {
|
|
122
|
+
const inertDocument = element.ownerDocument.implementation.createHTMLDocument();
|
|
123
|
+
const base = inertDocument.createElement('base');
|
|
124
|
+
base.href = element.ownerDocument.baseURI;
|
|
125
|
+
inertDocument.head.appendChild(base);
|
|
126
|
+
if (element.namespaceURI === null ||
|
|
127
|
+
element.namespaceURI === HTML_NAMESPACE) {
|
|
128
|
+
return inertDocument.createElement(element.localName);
|
|
129
|
+
}
|
|
130
|
+
return inertDocument.createElementNS(element.namespaceURI, element.localName);
|
|
131
|
+
};
|
|
132
|
+
const reflectedPropertyNames = (vnode) => {
|
|
133
|
+
const properties = vnode.data?.props;
|
|
134
|
+
if (properties === undefined) {
|
|
111
135
|
return [];
|
|
112
136
|
}
|
|
113
|
-
return
|
|
137
|
+
return Object.keys(properties).filter(name => !isClientOnlyProperty(properties, name) &&
|
|
138
|
+
reflectedAttributeName(name) !== undefined);
|
|
139
|
+
};
|
|
140
|
+
const reflectedAttributeValue = (tagName, propertyName, value) => {
|
|
141
|
+
if (BOOLEAN_PROPERTIES.has(propertyName)) {
|
|
142
|
+
return value === true ? '' : null;
|
|
143
|
+
}
|
|
144
|
+
if (value === false) {
|
|
145
|
+
return propertyName === 'draggable' ? 'false' : String(value);
|
|
146
|
+
}
|
|
147
|
+
if (propertyName === 'draggable') {
|
|
148
|
+
return value === true ? 'true' : 'false';
|
|
149
|
+
}
|
|
150
|
+
return serializedHtmlPropertyValue(tagName, propertyName, value);
|
|
151
|
+
};
|
|
152
|
+
const isControlledCurrentState = (element, propertyName) => {
|
|
153
|
+
const tagName = element.localName;
|
|
154
|
+
return ((propertyName === 'value' &&
|
|
155
|
+
(tagName === 'input' ||
|
|
156
|
+
tagName === 'textarea' ||
|
|
157
|
+
tagName === 'output' ||
|
|
158
|
+
tagName === 'select')) ||
|
|
159
|
+
(propertyName === 'checked' && tagName === 'input') ||
|
|
160
|
+
(propertyName === 'selected' && tagName === 'option') ||
|
|
161
|
+
(propertyName === 'muted' && (tagName === 'audio' || tagName === 'video')));
|
|
162
|
+
};
|
|
163
|
+
const STALE_REFLECTED_PROPERTY = Symbol('foldkit/stale-reflected-property');
|
|
164
|
+
const seedReflectedProperties = (element, vnode, propertyNames, isCustomElement, status) => {
|
|
165
|
+
const properties = vnode.data?.props;
|
|
166
|
+
if (properties === undefined || propertyNames.length === 0) {
|
|
167
|
+
return {};
|
|
168
|
+
}
|
|
169
|
+
const seeded = {};
|
|
170
|
+
for (const name of propertyNames) {
|
|
171
|
+
const authored = properties[name];
|
|
172
|
+
const attributeName = reflectedAttributeName(name);
|
|
173
|
+
if (attributeName === undefined) {
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
const isEquivalent = !isCustomElement && isControlledCurrentState(element, name)
|
|
177
|
+
? Object.is(Reflect.get(element, name), authored)
|
|
178
|
+
: element.getAttribute(attributeName) ===
|
|
179
|
+
reflectedAttributeValue(element.localName, name, authored);
|
|
180
|
+
if (isEquivalent) {
|
|
181
|
+
seeded[name] = authored;
|
|
182
|
+
}
|
|
183
|
+
else {
|
|
184
|
+
seeded[name] = STALE_REFLECTED_PROPERTY;
|
|
185
|
+
detectMismatch(status);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return seeded;
|
|
114
189
|
};
|
|
115
190
|
const byName = ([leftName], [rightName]) => leftName.localeCompare(rightName);
|
|
116
191
|
// A structured, order-independent snapshot of the state an element and its
|
|
@@ -122,14 +197,13 @@ const byName = ([leftName], [rightName]) => leftName.localeCompare(rightName);
|
|
|
122
197
|
// APIs, so spelling differences never register, and the snapshot is JSON so no
|
|
123
198
|
// value can collide with a delimiter. Values feed the comparison, never a log.
|
|
124
199
|
export const __elementSignature = (element, vnode) => {
|
|
125
|
-
const propertyNames =
|
|
200
|
+
const propertyNames = controlledStatePropertyNames(element, vnode.data?.props);
|
|
126
201
|
const attributes = [];
|
|
127
202
|
for (const attribute of Array.from(element.attributes)) {
|
|
128
203
|
const name = attribute.name;
|
|
129
204
|
if (name === HYDRATION_STAMP_ATTRIBUTE ||
|
|
130
205
|
name === 'class' ||
|
|
131
|
-
name === 'style'
|
|
132
|
-
propertyNames.includes(name)) {
|
|
206
|
+
name === 'style') {
|
|
133
207
|
continue;
|
|
134
208
|
}
|
|
135
209
|
attributes.push([name, attribute.value]);
|
|
@@ -158,9 +232,20 @@ export const __elementSignature = (element, vnode) => {
|
|
|
158
232
|
// The hydration stamp is never seeded, so it is never removed.
|
|
159
233
|
const seedAdoptedState = (element, vnode, clone, status, isCustomElement) => {
|
|
160
234
|
const classOwnedByModule = vnode.data?.class !== undefined &&
|
|
161
|
-
vnode.data?.attrs
|
|
235
|
+
htmlAttributeValue(vnode.data?.attrs, 'class') === undefined;
|
|
162
236
|
const styleOwnedByModule = vnode.data?.style !== undefined &&
|
|
163
|
-
vnode.data?.attrs
|
|
237
|
+
htmlAttributeValue(vnode.data?.attrs, 'style') === undefined;
|
|
238
|
+
const propertyNames = reflectedPropertyNames(vnode);
|
|
239
|
+
const propertyAttributeNames = new Set(propertyNames.flatMap(name => {
|
|
240
|
+
const attributeName = reflectedAttributeName(name);
|
|
241
|
+
return attributeName === undefined
|
|
242
|
+
? []
|
|
243
|
+
: [parsedAttributeName(element.namespaceURI, attributeName)];
|
|
244
|
+
}));
|
|
245
|
+
const datasetAttributeNames = new Map(Object.keys(vnode.data?.dataset ?? {}).map(name => [
|
|
246
|
+
`data-${name.replace(/[A-Z]/g, match => `-${match.toLowerCase()}`)}`,
|
|
247
|
+
name,
|
|
248
|
+
]));
|
|
164
249
|
// NOTE: a custom element that upgraded before hydration adds attributes of its
|
|
165
250
|
// own in connectedCallback. Seeding only the attributes the vnode declares
|
|
166
251
|
// leaves those component-owned attributes in place, while a vnode-declared
|
|
@@ -180,6 +265,12 @@ const seedAdoptedState = (element, vnode, clone, status, isCustomElement) => {
|
|
|
180
265
|
if (name === 'style' && styleOwnedByModule) {
|
|
181
266
|
continue;
|
|
182
267
|
}
|
|
268
|
+
if (propertyAttributeNames.has(name)) {
|
|
269
|
+
continue;
|
|
270
|
+
}
|
|
271
|
+
if (datasetAttributeNames.has(name)) {
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
183
274
|
if (declaredAttributes !== undefined &&
|
|
184
275
|
!declaredAttributes.has(name.toLowerCase())) {
|
|
185
276
|
continue;
|
|
@@ -188,16 +279,28 @@ const seedAdoptedState = (element, vnode, clone, status, isCustomElement) => {
|
|
|
188
279
|
}
|
|
189
280
|
const classes = seedClasses(element, vnode, classOwnedByModule, isCustomElement);
|
|
190
281
|
const style = seedStyle(element, vnode, styleOwnedByModule, isCustomElement);
|
|
282
|
+
const props = seedReflectedProperties(element, vnode, propertyNames, isCustomElement, status);
|
|
283
|
+
const dataset = {};
|
|
284
|
+
for (const [attributeName, propertyName] of datasetAttributeNames) {
|
|
285
|
+
const value = element.getAttribute(attributeName);
|
|
286
|
+
if (value !== null) {
|
|
287
|
+
dataset[propertyName] = value;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
191
290
|
clone.data = {
|
|
192
291
|
...clone.data,
|
|
193
292
|
...(Object.keys(attrs).length > 0 ? { attrs } : {}),
|
|
194
293
|
...(Object.keys(classes).length > 0 ? { class: classes } : {}),
|
|
195
294
|
...(Object.keys(style).length > 0 ? { style } : {}),
|
|
295
|
+
...(Object.keys(props).length > 0
|
|
296
|
+
? { props: { ...clone.data?.props, ...props } }
|
|
297
|
+
: {}),
|
|
298
|
+
...(Object.keys(dataset).length > 0 ? { dataset } : {}),
|
|
196
299
|
};
|
|
197
300
|
// In development, record the server DOM signature so the post-patch pass can
|
|
198
301
|
// report an attribute-only mismatch the structural walk cannot see. Gated on
|
|
199
302
|
// the dev flag so a production hydrate does no extra work.
|
|
200
|
-
if (import.meta.hot) {
|
|
303
|
+
if (import.meta.hot && !status.adoptedSignatures.has(element)) {
|
|
201
304
|
status.adoptedSignatures.set(element, {
|
|
202
305
|
vnode,
|
|
203
306
|
server: __elementSignature(element, vnode),
|
|
@@ -222,13 +325,49 @@ const hasOnlyTextContent = (element) => {
|
|
|
222
325
|
return (firstChild === null ||
|
|
223
326
|
(firstChild.nextSibling === null && isText(firstChild)));
|
|
224
327
|
};
|
|
225
|
-
const matchesTag = (element,
|
|
328
|
+
const matchesTag = (element, vnode) => {
|
|
329
|
+
const authored = tagNameFromSelector(vnode.sel ?? '');
|
|
330
|
+
const expected = vnode.data?.ns === undefined ? authored.toLowerCase() : authored;
|
|
331
|
+
return element.localName === expected;
|
|
332
|
+
};
|
|
226
333
|
// The namespace a vnode expects is carried in `data.ns` for foreign content
|
|
227
334
|
// (SVG, MathML) and is otherwise HTML. An element whose namespace disagrees
|
|
228
335
|
// (an HTML element parsed inside an SVG integration point, say) must be
|
|
229
336
|
// rebuilt rather than adopted, since the two are not interchangeable.
|
|
230
337
|
const namespaceOf = (vnode) => typeof vnode.data?.ns === 'string' ? vnode.data.ns : HTML_NAMESPACE;
|
|
231
338
|
const matchesNamespace = (element, vnode) => (element.namespaceURI ?? HTML_NAMESPACE) === namespaceOf(vnode);
|
|
339
|
+
// The server DOM does not encode a vnode's key or identity, so a positional
|
|
340
|
+
// match on tag and namespace alone could adopt a different logical entity: a
|
|
341
|
+
// reordered or stale keyed list would take over the wrong DOM node, and the user
|
|
342
|
+
// state sitting on it. The serializer stamps a fingerprint of the key and
|
|
343
|
+
// identity; here the same fingerprint is computed for the vnode and compared,
|
|
344
|
+
// so a mismatch rebuilds instead of transferring state to the wrong row or
|
|
345
|
+
// branch. An unsupported key (NaN or a symbol, which a hydratable render
|
|
346
|
+
// refuses) never matches, so it rebuilds rather than adopting on a guess.
|
|
347
|
+
const matchesAdoptionKey = (element, vnode) => {
|
|
348
|
+
const serverKey = element.getAttribute(HYDRATION_KEY_ATTRIBUTE);
|
|
349
|
+
if (vnode.key === undefined) {
|
|
350
|
+
return serverKey === null;
|
|
351
|
+
}
|
|
352
|
+
const clientKey = hydrationKeyMarker(vnode.key);
|
|
353
|
+
if (clientKey === undefined) {
|
|
354
|
+
// An unsupported key (NaN or a symbol, which a hydratable render refuses)
|
|
355
|
+
// cannot be compared, so it never matches: rebuilding beats adopting on a
|
|
356
|
+
// guess.
|
|
357
|
+
return false;
|
|
358
|
+
}
|
|
359
|
+
return serverKey === clientKey;
|
|
360
|
+
};
|
|
361
|
+
const matchesAdoptionIdentity = (element, vnode) => {
|
|
362
|
+
if (!matchesAdoptionKey(element, vnode)) {
|
|
363
|
+
return false;
|
|
364
|
+
}
|
|
365
|
+
const serverIdentity = element.getAttribute(HYDRATION_IDENTITY_ATTRIBUTE);
|
|
366
|
+
const clientIdentity = vnode.identity === undefined
|
|
367
|
+
? null
|
|
368
|
+
: hydrationIdentityMarker(vnode.identity);
|
|
369
|
+
return serverIdentity === clientIdentity;
|
|
370
|
+
};
|
|
232
371
|
const cloneOf = (vnode, elm) => {
|
|
233
372
|
const clone = {
|
|
234
373
|
sel: vnode.sel,
|
|
@@ -243,6 +382,63 @@ const cloneOf = (vnode, elm) => {
|
|
|
243
382
|
}
|
|
244
383
|
return clone;
|
|
245
384
|
};
|
|
385
|
+
const isAutonomousCustomElement = (element) => (element.namespaceURI === null || element.namespaceURI === HTML_NAMESPACE) &&
|
|
386
|
+
element.localName.includes('-');
|
|
387
|
+
const hasViewOwnedLightDom = (vnode) => vnode.text !== undefined ||
|
|
388
|
+
(vnode.children !== undefined && vnode.children.length > 0) ||
|
|
389
|
+
hasTrustedInnerHtml(vnode.data?.props);
|
|
390
|
+
const shouldRebuildCustomElement = (element, vnode) => isAutonomousCustomElement(element) && hasViewOwnedLightDom(vnode);
|
|
391
|
+
const collectInsertHooks = (vnode, collected) => {
|
|
392
|
+
const children = vnode.children;
|
|
393
|
+
if (children !== undefined) {
|
|
394
|
+
for (const child of children) {
|
|
395
|
+
if (typeof child !== 'string') {
|
|
396
|
+
collectInsertHooks(child, collected);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
const insert = vnode.data?.hook?.insert;
|
|
401
|
+
if (insert !== undefined) {
|
|
402
|
+
collected.push({ vnode, insert });
|
|
403
|
+
}
|
|
404
|
+
};
|
|
405
|
+
const withoutInsertHooks = (vnode, body) => {
|
|
406
|
+
const deferred = [];
|
|
407
|
+
collectInsertHooks(vnode, deferred);
|
|
408
|
+
for (const { vnode: deferredVNode } of deferred) {
|
|
409
|
+
const hook = deferredVNode.data?.hook;
|
|
410
|
+
if (hook !== undefined) {
|
|
411
|
+
delete hook.insert;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
try {
|
|
415
|
+
return body();
|
|
416
|
+
}
|
|
417
|
+
finally {
|
|
418
|
+
for (const { vnode: deferredVNode, insert } of deferred) {
|
|
419
|
+
const hook = deferredVNode.data?.hook;
|
|
420
|
+
if (hook !== undefined) {
|
|
421
|
+
hook.insert = insert;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
};
|
|
426
|
+
// NOTE: removing children from an adopted Custom Element would run their
|
|
427
|
+
// disconnected callbacks while the host was still live. Those callbacks can
|
|
428
|
+
// synchronously mutate the host after hydration has sampled it. Replace the
|
|
429
|
+
// host with a comment first, then let the ordinary differ build the vnode while
|
|
430
|
+
// detached and insert it in the same position. Its insert hooks remain deferred
|
|
431
|
+
// until the whole hydration tree can fire them in render order.
|
|
432
|
+
const replaceHydrationElement = (element, vnode) => {
|
|
433
|
+
const ownerDocument = element.ownerDocument;
|
|
434
|
+
const parent = element.parentNode ?? ownerDocument.createDocumentFragment();
|
|
435
|
+
if (element.parentNode === null) {
|
|
436
|
+
parent.appendChild(element);
|
|
437
|
+
}
|
|
438
|
+
const placeholder = ownerDocument.createComment('');
|
|
439
|
+
parent.replaceChild(placeholder, element);
|
|
440
|
+
return withoutInsertHooks(vnode, () => patch(toVNode(placeholder), vnode));
|
|
441
|
+
};
|
|
246
442
|
const asVNode = (child) => typeof child === 'string'
|
|
247
443
|
? {
|
|
248
444
|
sel: undefined,
|
|
@@ -253,6 +449,34 @@ const asVNode = (child) => typeof child === 'string'
|
|
|
253
449
|
key: undefined,
|
|
254
450
|
}
|
|
255
451
|
: child;
|
|
452
|
+
const clonePreparedTree = (vnode) => {
|
|
453
|
+
const elm = vnode.elm;
|
|
454
|
+
if (elm === undefined) {
|
|
455
|
+
throw new Error('[foldkit] A prepared hydration vnode has no DOM node.');
|
|
456
|
+
}
|
|
457
|
+
const clone = cloneOf(vnode, elm);
|
|
458
|
+
clone.data = {
|
|
459
|
+
...clone.data,
|
|
460
|
+
...(vnode.data?.on === undefined ? {} : { on: vnode.data.on }),
|
|
461
|
+
...(vnode.data?.props === undefined ? {} : { props: vnode.data.props }),
|
|
462
|
+
};
|
|
463
|
+
clone.text = vnode.text;
|
|
464
|
+
const children = vnode.children;
|
|
465
|
+
if (children !== undefined) {
|
|
466
|
+
const cloneChildren = [];
|
|
467
|
+
let domChild = elm.firstChild;
|
|
468
|
+
for (const rawChild of children) {
|
|
469
|
+
const child = asVNode(rawChild);
|
|
470
|
+
if (child.elm === undefined) {
|
|
471
|
+
child.elm = domChild ?? undefined;
|
|
472
|
+
}
|
|
473
|
+
cloneChildren.push(clonePreparedTree(child));
|
|
474
|
+
domChild = child.elm?.nextSibling ?? null;
|
|
475
|
+
}
|
|
476
|
+
clone.children = cloneChildren;
|
|
477
|
+
}
|
|
478
|
+
return clone;
|
|
479
|
+
};
|
|
256
480
|
const clearChildren = (element) => {
|
|
257
481
|
element.textContent = '';
|
|
258
482
|
};
|
|
@@ -281,43 +505,53 @@ const adoptText = (element, domChild, text) => {
|
|
|
281
505
|
}
|
|
282
506
|
return Option.none();
|
|
283
507
|
};
|
|
284
|
-
const adoptElement = (element, vnode,
|
|
508
|
+
const adoptElement = (element, vnode, status) => {
|
|
509
|
+
if (shouldRebuildCustomElement(element, vnode)) {
|
|
510
|
+
return clonePreparedTree(replaceHydrationElement(element, vnode));
|
|
511
|
+
}
|
|
285
512
|
const clone = cloneOf(vnode, element);
|
|
286
|
-
|
|
513
|
+
// Strip the hydration markers the serializer stamped: they are internal to the
|
|
514
|
+
// handoff, already verified by the parent's positional walk before this call,
|
|
515
|
+
// and must not remain on the adopted element.
|
|
516
|
+
element.removeAttribute(HYDRATION_KEY_ATTRIBUTE);
|
|
517
|
+
element.removeAttribute(HYDRATION_IDENTITY_ATTRIBUTE);
|
|
287
518
|
// NOTE: an autonomous custom element (an HTML-namespace element whose name
|
|
288
519
|
// carries a hyphen) that upgraded before hydration adds attributes, classes,
|
|
289
520
|
// styles, and light DOM of its own in connectedCallback. The attributes, class
|
|
290
521
|
// tokens, and style properties the vnode does not declare are always preserved
|
|
291
|
-
// (here and in seedAdoptedState).
|
|
292
|
-
//
|
|
293
|
-
//
|
|
294
|
-
//
|
|
295
|
-
//
|
|
296
|
-
// take full ownership rather than interleave. The test is the name shape, not
|
|
522
|
+
// (here and in seedAdoptedState). One that declares no content leaves the
|
|
523
|
+
// component's light DOM untouched. A host with view-owned text, children, or
|
|
524
|
+
// trusted innerHTML is rebuilt before reaching this state-seeding path. The
|
|
525
|
+
// two cannot share because hydration cannot distinguish a component node from
|
|
526
|
+
// a matching view node. The test is the name shape, not
|
|
297
527
|
// `customElements.get`: whether the element has upgraded is timing-dependent
|
|
298
528
|
// at hydration (its definition may register after the server DOM parses), so a
|
|
299
529
|
// name test is deterministic. A hyphenated element that never upgrades is
|
|
300
|
-
// treated the same way, which is safe: with no component light DOM,
|
|
301
|
-
//
|
|
302
|
-
|
|
303
|
-
const
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
// textarea
|
|
312
|
-
//
|
|
313
|
-
|
|
530
|
+
// treated the same way, which is safe: with no component light DOM,
|
|
531
|
+
// undeclared content leaves an empty element and declared content is rebuilt.
|
|
532
|
+
const isCustomElement = isAutonomousCustomElement(element);
|
|
533
|
+
const finishAdoption = () => {
|
|
534
|
+
if (!isCustomElement) {
|
|
535
|
+
return clone;
|
|
536
|
+
}
|
|
537
|
+
seedAdoptedState(element, vnode, clone, status, true);
|
|
538
|
+
withoutInsertHooks(vnode, () => patch(clone, vnode));
|
|
539
|
+
return clonePreparedTree(vnode);
|
|
540
|
+
};
|
|
541
|
+
// A controlled textarea or output serializes its value as text content.
|
|
542
|
+
// That text is its parsed default state, which the fresh client path now
|
|
543
|
+
// synchronizes as well. Keep the server text outside the vnode child walk;
|
|
544
|
+
// the controlled property owns it and its insert hook reasserts both current
|
|
545
|
+
// and default state after the patch.
|
|
546
|
+
if ((element.tagName === 'TEXTAREA' || element.tagName === 'OUTPUT') &&
|
|
314
547
|
vnode.data?.props?.['value'] !== undefined) {
|
|
315
|
-
clearChildren(element);
|
|
316
548
|
clone.children = [];
|
|
317
|
-
return
|
|
549
|
+
return finishAdoption();
|
|
318
550
|
}
|
|
319
551
|
const authoredInnerHtml = vnode.data?.props?.['innerHTML'];
|
|
320
|
-
|
|
552
|
+
const hasAuthoredInnerHtml = authoredInnerHtml !== undefined && hasTrustedInnerHtml(vnode.data?.props);
|
|
553
|
+
const vnodeChildren = vnode.children;
|
|
554
|
+
if (hasAuthoredInnerHtml) {
|
|
321
555
|
// NOTE: the browser normalizes markup as it parses (entity forms, tag
|
|
322
556
|
// case, attribute order), so the served innerHTML string rarely equals
|
|
323
557
|
// the authored one byte for byte. Parsing the authored string through a
|
|
@@ -329,28 +563,38 @@ const adoptElement = (element, vnode, adopted, status) => {
|
|
|
329
563
|
// HTML-context parse would lowercase, false-mismatching every camelCase
|
|
330
564
|
// attribute.
|
|
331
565
|
if (typeof authoredInnerHtml === 'string') {
|
|
332
|
-
const probe = element
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
const isEquivalentMarkup = probe.innerHTML === element.innerHTML;
|
|
566
|
+
const probe = createInertProbe(element);
|
|
567
|
+
writeNativeInnerHtml(probe, authoredInnerHtml);
|
|
568
|
+
const currentInnerHtml = readNativeInnerHtml(element);
|
|
569
|
+
const isEquivalentMarkup = readNativeInnerHtml(probe) === currentInnerHtml;
|
|
337
570
|
if (!isEquivalentMarkup) {
|
|
338
571
|
detectMismatch(status);
|
|
572
|
+
writeNativeInnerHtml(element, authoredInnerHtml);
|
|
339
573
|
}
|
|
574
|
+
const props = {
|
|
575
|
+
...clone.data?.props,
|
|
576
|
+
innerHTML: authoredInnerHtml,
|
|
577
|
+
};
|
|
578
|
+
markTrustedInnerHtml(props, props.innerHTML);
|
|
340
579
|
clone.data = {
|
|
341
580
|
...clone.data,
|
|
342
|
-
props
|
|
343
|
-
innerHTML: isEquivalentMarkup ? authoredInnerHtml : element.innerHTML,
|
|
344
|
-
},
|
|
581
|
+
props,
|
|
345
582
|
};
|
|
346
583
|
}
|
|
347
584
|
else {
|
|
348
|
-
|
|
585
|
+
const props = {
|
|
586
|
+
...clone.data?.props,
|
|
587
|
+
innerHTML: readNativeInnerHtml(element),
|
|
588
|
+
};
|
|
589
|
+
markTrustedInnerHtml(props, props.innerHTML);
|
|
590
|
+
clone.data = {
|
|
591
|
+
...clone.data,
|
|
592
|
+
props,
|
|
593
|
+
};
|
|
349
594
|
}
|
|
350
595
|
clone.children = [];
|
|
351
|
-
return
|
|
596
|
+
return finishAdoption();
|
|
352
597
|
}
|
|
353
|
-
const vnodeChildren = vnode.children;
|
|
354
598
|
// NOTE: no children (undefined) and an empty child list both mean the view
|
|
355
599
|
// declares no children, so they share the childless path. This is also where
|
|
356
600
|
// a custom element's ownership splits: a childless vnode has no view child to
|
|
@@ -386,7 +630,7 @@ const adoptElement = (element, vnode, adopted, status) => {
|
|
|
386
630
|
clearChildren(element);
|
|
387
631
|
clone.children = [];
|
|
388
632
|
}
|
|
389
|
-
return
|
|
633
|
+
return finishAdoption();
|
|
390
634
|
}
|
|
391
635
|
const cloneChildren = [];
|
|
392
636
|
let domChild = element.firstChild;
|
|
@@ -402,7 +646,7 @@ const adoptElement = (element, vnode, adopted, status) => {
|
|
|
402
646
|
}
|
|
403
647
|
clearChildren(element);
|
|
404
648
|
clone.children = [];
|
|
405
|
-
return
|
|
649
|
+
return finishAdoption();
|
|
406
650
|
}
|
|
407
651
|
const adoption = maybeAdoption.value;
|
|
408
652
|
const textClone = cloneOf(child, adoption.adoptedNode);
|
|
@@ -420,7 +664,7 @@ const adoptElement = (element, vnode, adopted, status) => {
|
|
|
420
664
|
detectMismatch(status);
|
|
421
665
|
clearChildren(element);
|
|
422
666
|
clone.children = [];
|
|
423
|
-
return
|
|
667
|
+
return finishAdoption();
|
|
424
668
|
}
|
|
425
669
|
const commentClone = cloneOf(child, domChild);
|
|
426
670
|
commentClone.text = domChild.data;
|
|
@@ -429,15 +673,17 @@ const adoptElement = (element, vnode, adopted, status) => {
|
|
|
429
673
|
continue;
|
|
430
674
|
}
|
|
431
675
|
if (!isElement(domChild) ||
|
|
432
|
-
!matchesTag(domChild, child
|
|
433
|
-
!matchesNamespace(domChild, child)
|
|
676
|
+
!matchesTag(domChild, child) ||
|
|
677
|
+
!matchesNamespace(domChild, child) ||
|
|
678
|
+
!matchesAdoptionIdentity(domChild, child)) {
|
|
434
679
|
detectMismatch(status);
|
|
435
680
|
clearChildren(element);
|
|
436
681
|
clone.children = [];
|
|
437
|
-
return
|
|
682
|
+
return finishAdoption();
|
|
438
683
|
}
|
|
439
|
-
|
|
440
|
-
domChild
|
|
684
|
+
const nextDomChild = domChild.nextSibling;
|
|
685
|
+
cloneChildren.push(adoptElement(domChild, child, status));
|
|
686
|
+
domChild = nextDomChild;
|
|
441
687
|
}
|
|
442
688
|
while (domChild !== null) {
|
|
443
689
|
detectMismatch(status);
|
|
@@ -446,68 +692,151 @@ const adoptElement = (element, vnode, adopted, status) => {
|
|
|
446
692
|
domChild = nextDomChild;
|
|
447
693
|
}
|
|
448
694
|
clone.children = cloneChildren;
|
|
449
|
-
return
|
|
695
|
+
return finishAdoption();
|
|
450
696
|
};
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
697
|
+
// NOTE: structural reconciliation can run Custom Element lifecycle callbacks.
|
|
698
|
+
// One replacement may mutate an ancestor or an earlier sibling after that node
|
|
699
|
+
// was first visited. Sample retained element state and text or comment data only
|
|
700
|
+
// after the entire walk has completed, so the ordinary patch compares the view
|
|
701
|
+
// against the final values those synchronous callbacks left behind. Custom
|
|
702
|
+
// Element code that changes another subtree's structure is outside this pass;
|
|
703
|
+
// components must keep structural DOM writes within their own host.
|
|
704
|
+
const resampleAdoptedTree = (clone, vnode, status) => {
|
|
705
|
+
const elm = clone.elm;
|
|
706
|
+
if (elm !== undefined && isElement(elm)) {
|
|
707
|
+
seedAdoptedState(elm, vnode, clone, status, isAutonomousCustomElement(elm));
|
|
708
|
+
if (vnode.text !== undefined) {
|
|
709
|
+
clone.text = hasOnlyTextContent(elm) ? (elm.textContent ?? '') : undefined;
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
else if (elm !== undefined && (isText(elm) || isComment(elm))) {
|
|
713
|
+
clone.text = elm.data;
|
|
714
|
+
}
|
|
715
|
+
const cloneChildren = clone.children;
|
|
716
|
+
const vnodeChildren = vnode.children;
|
|
717
|
+
if (cloneChildren === undefined || vnodeChildren === undefined) {
|
|
718
|
+
return;
|
|
719
|
+
}
|
|
720
|
+
const vnodeChildrenIterator = vnodeChildren.values();
|
|
721
|
+
for (const rawCloneChild of cloneChildren) {
|
|
722
|
+
const nextVnodeChild = vnodeChildrenIterator.next();
|
|
723
|
+
if (nextVnodeChild.done) {
|
|
724
|
+
return;
|
|
725
|
+
}
|
|
726
|
+
resampleAdoptedTree(asVNode(rawCloneChild), asVNode(nextVnodeChild.value), status);
|
|
727
|
+
}
|
|
728
|
+
};
|
|
729
|
+
// NOTE: hydration cannot let the patch fire its own `insert` hooks. The differ
|
|
730
|
+
// queues a hook when it creates a node and flushes the queue when the patch
|
|
731
|
+
// ends, which covers a fresh render but not a hydration, where some nodes are
|
|
732
|
+
// adopted and never created. Firing the created ones from the queue and the
|
|
733
|
+
// adopted ones afterward orders every created node before every adopted one:
|
|
734
|
+
// a `<main>` that adopts one child and creates its sibling ran the sibling's
|
|
735
|
+
// Mount first and the adopted child's second, the reverse of what a fresh
|
|
736
|
+
// render does. A Mount that depends on a sibling being initialized would work
|
|
737
|
+
// on a fresh boot and break on a hydrated one.
|
|
738
|
+
//
|
|
739
|
+
// So the hooks are detached before the patch, which leaves the differ's queue
|
|
740
|
+
// empty, and fired afterward in one pass over the whole tree, children first,
|
|
741
|
+
// the order the differ creates in. Adopted and created nodes are not
|
|
742
|
+
// distinguished, because every node in the new tree has a DOM node by then and
|
|
743
|
+
// a fresh render would have fired all of them.
|
|
744
|
+
const patchFiringInsertHooksInRenderOrder = (adoptedClone, nextVNode) => {
|
|
745
|
+
const deferred = [];
|
|
746
|
+
collectInsertHooks(nextVNode, deferred);
|
|
747
|
+
for (const { vnode } of deferred) {
|
|
748
|
+
const hook = vnode.data?.hook;
|
|
749
|
+
if (hook !== undefined) {
|
|
750
|
+
delete hook.insert;
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
let patchedVNode;
|
|
754
|
+
try {
|
|
755
|
+
patchedVNode = patch(adoptedClone, nextVNode);
|
|
756
|
+
}
|
|
757
|
+
finally {
|
|
758
|
+
for (const { vnode, insert } of deferred) {
|
|
759
|
+
const hook = vnode.data?.hook;
|
|
760
|
+
if (hook !== undefined) {
|
|
761
|
+
hook.insert = insert;
|
|
457
762
|
}
|
|
458
763
|
}
|
|
459
764
|
}
|
|
460
|
-
const
|
|
461
|
-
|
|
462
|
-
vnode.elm !== undefined &&
|
|
463
|
-
adopted.has(vnode.elm)) {
|
|
464
|
-
insertHook(vnode);
|
|
765
|
+
for (const { vnode, insert } of deferred) {
|
|
766
|
+
insert(vnode);
|
|
465
767
|
}
|
|
768
|
+
return patchedVNode;
|
|
466
769
|
};
|
|
467
770
|
/** Hydrates a server-rendered root element against the first render's vnode
|
|
468
|
-
* tree. Matching DOM nodes are adopted in place
|
|
469
|
-
*
|
|
470
|
-
*
|
|
471
|
-
*
|
|
472
|
-
*
|
|
473
|
-
*
|
|
474
|
-
*
|
|
475
|
-
* reconciliation is required. Returns the patched vnode to
|
|
476
|
-
* runtime's current tree. */
|
|
771
|
+
* tree. Matching DOM nodes are adopted in place: module hooks attach
|
|
772
|
+
* listeners and re-assert attrs and props onto the existing elements, and
|
|
773
|
+
* `insert` hooks (Mounts) fire for adopted nodes in the same children-first
|
|
774
|
+
* order the differ uses for created ones. A mismatching subtree falls back to
|
|
775
|
+
* a rebuild through `createElm` at the nearest parent. A Custom Element with
|
|
776
|
+
* view-owned light DOM is replaced so creation follows the fresh-render
|
|
777
|
+
* lifecycle. Any root-level mismatch also replaces the root. Development
|
|
778
|
+
* builds warn when reconciliation is required. Returns the patched vnode to
|
|
779
|
+
* store as the runtime's current tree. */
|
|
477
780
|
// Replace the hydration root with a fresh render of the vnode. snabbdom's
|
|
478
781
|
// sameVnode compares tag but not namespace, so patching the root directly
|
|
479
782
|
// would reuse a same-tag element even across a namespace change. Patching
|
|
480
783
|
// against a comment placed where the root was is never sameVnode with a new
|
|
481
784
|
// element, so the differ builds a fresh node in the correct namespace and
|
|
482
785
|
// swaps it in.
|
|
786
|
+
// A root with no parent gets one. Patching a detached root directly is the same
|
|
787
|
+
// reuse this function exists to avoid: snabbdom's sameVnode compares tag alone,
|
|
788
|
+
// so a same-tag root is kept along with the DOM state on it and its insert hooks
|
|
789
|
+
// never fire, which is how a rejected page could keep its own elements and their
|
|
790
|
+
// typed values. A fragment gives the placeholder a parent, so the replacement is
|
|
791
|
+
// built and swapped in exactly as it is for an attached root.
|
|
483
792
|
const replaceHydrationRoot = (hydrationRoot, vnode) => {
|
|
484
|
-
const
|
|
485
|
-
|
|
486
|
-
|
|
793
|
+
const ownerDocument = hydrationRoot.ownerDocument;
|
|
794
|
+
const parent = hydrationRoot.parentNode ?? ownerDocument.createDocumentFragment();
|
|
795
|
+
if (hydrationRoot.parentNode === null) {
|
|
796
|
+
parent.appendChild(hydrationRoot);
|
|
487
797
|
}
|
|
488
|
-
const placeholder =
|
|
798
|
+
const placeholder = ownerDocument.createComment('');
|
|
489
799
|
parent.replaceChild(placeholder, hydrationRoot);
|
|
490
800
|
return patch(toVNode(placeholder), vnode);
|
|
491
801
|
};
|
|
492
|
-
export const __hydrateVNode = (hydrationRoot, nextVNode, seen) => {
|
|
802
|
+
export const __hydrateVNode = (hydrationRoot, nextVNode, seen, buildId) => {
|
|
493
803
|
const dedupedVNode = nextVNode !== null ? dedupeSharedVNodes(nextVNode, seen) : h('!');
|
|
494
804
|
const status = {
|
|
495
805
|
isMismatchDetected: false,
|
|
496
806
|
adoptedSignatures: new Map(),
|
|
497
807
|
};
|
|
498
|
-
|
|
808
|
+
// The build token is checked again here, though the runtime has already
|
|
809
|
+
// refused a mismatch before reading the handoff. This is the last line of
|
|
810
|
+
// defense for a caller reaching the adoption step directly, and it never
|
|
811
|
+
// matches an absent marker: `buildId` is required and non-empty, so a page
|
|
812
|
+
// served before build ids existed cannot pass for one of this build's.
|
|
813
|
+
const servedBuild = hydrationRoot.getAttribute(HYDRATION_BUILD_ATTRIBUTE);
|
|
814
|
+
const isSameBuild = buildId !== '' && servedBuild === buildId;
|
|
815
|
+
hydrationRoot.removeAttribute(HYDRATION_BUILD_ATTRIBUTE);
|
|
816
|
+
// The root is checked for logical identity the same way every other adopted
|
|
817
|
+
// element is. A root whose key or view identity disagrees with the served one
|
|
818
|
+
// is a different logical root, so it is rebuilt rather than adopted: adopting
|
|
819
|
+
// it would carry the previous root's DOM state (a typed input's value) into a
|
|
820
|
+
// root the client never rendered there.
|
|
821
|
+
const isRootMismatch = !isSameBuild ||
|
|
822
|
+
dedupedVNode.sel === undefined ||
|
|
499
823
|
dedupedVNode.sel === '' ||
|
|
500
824
|
dedupedVNode.sel === '!' ||
|
|
501
|
-
!matchesTag(hydrationRoot, dedupedVNode
|
|
502
|
-
!matchesNamespace(hydrationRoot, dedupedVNode)
|
|
825
|
+
!matchesTag(hydrationRoot, dedupedVNode) ||
|
|
826
|
+
!matchesNamespace(hydrationRoot, dedupedVNode) ||
|
|
827
|
+
!matchesAdoptionIdentity(hydrationRoot, dedupedVNode);
|
|
828
|
+
if (isRootMismatch) {
|
|
503
829
|
detectMismatch(status);
|
|
504
830
|
const patchedVNode = replaceHydrationRoot(hydrationRoot, dedupedVNode);
|
|
505
831
|
reportMismatch(status);
|
|
506
832
|
return patchedVNode;
|
|
507
833
|
}
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
834
|
+
if (shouldRebuildCustomElement(hydrationRoot, dedupedVNode)) {
|
|
835
|
+
return replaceHydrationRoot(hydrationRoot, dedupedVNode);
|
|
836
|
+
}
|
|
837
|
+
const adoptedClone = adoptElement(hydrationRoot, dedupedVNode, status);
|
|
838
|
+
resampleAdoptedTree(adoptedClone, dedupedVNode, status);
|
|
839
|
+
const patchedVNode = patchFiringInsertHooksInRenderOrder(adoptedClone, dedupedVNode);
|
|
511
840
|
if (import.meta.hot && !status.isMismatchDetected) {
|
|
512
841
|
for (const [element, { vnode, server }] of status.adoptedSignatures) {
|
|
513
842
|
if (__elementSignature(element, vnode) !== server) {
|
|
@@ -516,7 +845,6 @@ export const __hydrateVNode = (hydrationRoot, nextVNode, seen) => {
|
|
|
516
845
|
}
|
|
517
846
|
}
|
|
518
847
|
}
|
|
519
|
-
fireAdoptedInsertHooks(patchedVNode, adopted);
|
|
520
848
|
reportMismatch(status);
|
|
521
849
|
return patchedVNode;
|
|
522
850
|
};
|