foldkit 0.146.0 → 0.148.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/README.md +5 -2
- 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/customElement/index.d.ts.map +1 -1
- package/dist/customElement/index.js +23 -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/index.d.ts +1 -0
- package/dist/experimental/index.d.ts.map +1 -1
- package/dist/experimental/index.js +1 -0
- package/dist/experimental/machine/machine.d.ts +22 -3
- package/dist/experimental/machine/machine.d.ts.map +1 -1
- package/dist/experimental/machine/machine.js +8 -0
- package/dist/experimental/server/entry.d.ts +73 -0
- package/dist/experimental/server/entry.d.ts.map +1 -0
- package/dist/experimental/server/entry.js +41 -0
- package/dist/experimental/server/host.d.ts +136 -0
- package/dist/experimental/server/host.d.ts.map +1 -0
- package/dist/experimental/server/host.js +359 -0
- package/dist/experimental/server/index.d.ts +5 -0
- package/dist/experimental/server/index.d.ts.map +1 -0
- package/dist/experimental/server/index.js +4 -0
- package/dist/experimental/server/public.d.ts +3 -0
- package/dist/experimental/server/public.d.ts.map +1 -0
- package/dist/experimental/server/public.js +1 -0
- package/dist/experimental/server/serialize.d.ts +43 -0
- package/dist/experimental/server/serialize.d.ts.map +1 -0
- package/dist/experimental/server/serialize.js +786 -0
- package/dist/experimental/server/server.d.ts +242 -0
- package/dist/experimental/server/server.d.ts.map +1 -0
- package/dist/experimental/server/server.js +946 -0
- package/dist/experimental/server/template.d.ts +53 -0
- package/dist/experimental/server/template.d.ts.map +1 -0
- package/dist/experimental/server/template.js +617 -0
- package/dist/html/index.d.ts +5 -0
- package/dist/html/index.d.ts.map +1 -1
- package/dist/html/index.js +474 -33
- package/dist/hydrate.d.ts +4 -0
- package/dist/hydrate.d.ts.map +1 -0
- package/dist/hydrate.js +850 -0
- package/dist/hydrationMarker.d.ts +10 -0
- package/dist/hydrationMarker.d.ts.map +1 -0
- package/dist/hydrationMarker.js +9 -0
- package/dist/hydrationMarkers.d.ts +15 -0
- package/dist/hydrationMarkers.d.ts.map +1 -0
- package/dist/hydrationMarkers.js +70 -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 +2 -2
- package/dist/runtime/public.d.ts.map +1 -1
- package/dist/runtime/public.js +1 -1
- package/dist/runtime/runtime.d.ts +99 -36
- package/dist/runtime/runtime.d.ts.map +1 -1
- package/dist/runtime/runtime.js +412 -71
- package/dist/snabbdom/attributes.d.ts.map +1 -1
- package/dist/snabbdom/attributes.js +65 -37
- package/dist/snabbdom/h.d.ts +1 -0
- package/dist/snabbdom/h.d.ts.map +1 -1
- package/dist/snabbdom/h.js +85 -4
- package/dist/snabbdom/style.d.ts.map +1 -1
- package/dist/snabbdom/style.js +53 -34
- package/dist/snabbdom/tovnode.d.ts.map +1 -1
- package/dist/snabbdom/tovnode.js +5 -1
- package/dist/tagName.d.ts +6 -0
- package/dist/tagName.d.ts.map +1 -0
- package/dist/tagName.js +11 -0
- 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/dist/vdom.d.ts.map +1 -1
- package/dist/vdom.js +25 -1
- package/package.json +9 -2
package/dist/hydrate.js
ADDED
|
@@ -0,0 +1,850 @@
|
|
|
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';
|
|
8
|
+
import { h, toVNode } from './snabbdom/index.js';
|
|
9
|
+
import { tagNameFromSelector } from './tagName.js';
|
|
10
|
+
import { dedupeSharedVNodes, patch } from './vdom.js';
|
|
11
|
+
// NOTE: the differ only knows the page through the `elm` pointers on its
|
|
12
|
+
// vnodes; `patch` never queries the document. On a server-rendered page the
|
|
13
|
+
// browser has already built real DOM by parsing the HTML the server sent
|
|
14
|
+
// (the "server DOM" below), but snabbdom did not create those nodes, so no
|
|
15
|
+
// vnode anywhere points at them and the differ cannot see them. Hydration's
|
|
16
|
+
// whole job is to make that DOM visible to the differ, then let one
|
|
17
|
+
// ordinary patch attach behavior to it.
|
|
18
|
+
//
|
|
19
|
+
// The mechanism has three steps. First, walk the first render's vnode tree
|
|
20
|
+
// and the server DOM together, position by position. Second, wherever the
|
|
21
|
+
// two agree, record the existing DOM node: those records form a second
|
|
22
|
+
// vnode tree, a clone of the first render's tree whose `elm` fields point
|
|
23
|
+
// at the server DOM nodes. The clone copies exactly what `sameVnode`
|
|
24
|
+
// compares (`sel`, `key`, `identity`, `data.is`), so `patchVnode` reuses
|
|
25
|
+
// every adopted element, and deliberately nothing else, so every module
|
|
26
|
+
// update hook re-asserts the new tree's attrs, props, classes, styles, and
|
|
27
|
+
// listeners onto the adopted elements. Third, run `patch(clone, newTree)`:
|
|
28
|
+
// to the differ this is a completely ordinary update from a tree that
|
|
29
|
+
// happens to point at existing nodes, so the vendored differ stays
|
|
30
|
+
// untouched and never learns the nodes came from a server.
|
|
31
|
+
//
|
|
32
|
+
// Where the DOM disagrees with the vnode tree, the walk clears the nearest
|
|
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.
|
|
39
|
+
const HTML_NAMESPACE = 'http://www.w3.org/1999/xhtml';
|
|
40
|
+
const HYDRATION_STAMP_ATTRIBUTE = 'data-foldkit-app';
|
|
41
|
+
const classListOf = (element) => {
|
|
42
|
+
const classes = {};
|
|
43
|
+
for (const className of Array.from(element.classList)) {
|
|
44
|
+
classes[className] = true;
|
|
45
|
+
}
|
|
46
|
+
return classes;
|
|
47
|
+
};
|
|
48
|
+
const inlineStyleOf = (element) => {
|
|
49
|
+
const style = {};
|
|
50
|
+
if ('style' in element && element.style instanceof CSSStyleDeclaration) {
|
|
51
|
+
const inlineStyle = element.style;
|
|
52
|
+
for (let index = 0; index < inlineStyle.length; index += 1) {
|
|
53
|
+
const property = inlineStyle.item(index);
|
|
54
|
+
const value = inlineStyle.getPropertyValue(property);
|
|
55
|
+
const priority = inlineStyle.getPropertyPriority(property);
|
|
56
|
+
style[property] = priority === '' ? value : `${value} !${priority}`;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return style;
|
|
60
|
+
};
|
|
61
|
+
// NOTE: for a custom element, seed only the class tokens the vnode declares so
|
|
62
|
+
// the class module leaves component-added tokens in place; the view does not
|
|
63
|
+
// reassert them, so the full class list would reconcile them away as stale.
|
|
64
|
+
// Reading each token from the element keeps an agreeing render a no-op. A normal
|
|
65
|
+
// element seeds its whole class list, so stale tokens reconcile away.
|
|
66
|
+
const seedClasses = (element, vnode, classOwnedByModule, isCustomElement) => {
|
|
67
|
+
if (!classOwnedByModule) {
|
|
68
|
+
return {};
|
|
69
|
+
}
|
|
70
|
+
if (!isCustomElement) {
|
|
71
|
+
return classListOf(element);
|
|
72
|
+
}
|
|
73
|
+
const declared = {};
|
|
74
|
+
for (const token of Object.keys(vnode.data?.class ?? {})) {
|
|
75
|
+
declared[token] = element.classList.contains(token);
|
|
76
|
+
}
|
|
77
|
+
return declared;
|
|
78
|
+
};
|
|
79
|
+
// NOTE: the style counterpart to seedClasses. For a custom element, seed only
|
|
80
|
+
// the style properties the vnode declares so the style module leaves
|
|
81
|
+
// component-added properties in place; a normal element seeds its whole inline
|
|
82
|
+
// style so stale properties reconcile away.
|
|
83
|
+
const seedStyle = (element, vnode, styleOwnedByModule, isCustomElement) => {
|
|
84
|
+
if (!styleOwnedByModule) {
|
|
85
|
+
return {};
|
|
86
|
+
}
|
|
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 {};
|
|
94
|
+
}
|
|
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));
|
|
107
|
+
}
|
|
108
|
+
}
|
|
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;
|
|
120
|
+
};
|
|
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) {
|
|
135
|
+
return [];
|
|
136
|
+
}
|
|
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;
|
|
189
|
+
};
|
|
190
|
+
const byName = ([leftName], [rightName]) => leftName.localeCompare(rightName);
|
|
191
|
+
// A structured, order-independent snapshot of the state an element and its
|
|
192
|
+
// vnode share: the DOM attributes, the non-reflecting properties the vnode
|
|
193
|
+
// owns (read from the element, not the attribute), the class set, and inline
|
|
194
|
+
// style. Comparing the snapshot taken during adoption (the server DOM) with
|
|
195
|
+
// the element's state after the client patch flags any attribute, property,
|
|
196
|
+
// class, or style the two disagree on. Both sides pass through the same DOM
|
|
197
|
+
// APIs, so spelling differences never register, and the snapshot is JSON so no
|
|
198
|
+
// value can collide with a delimiter. Values feed the comparison, never a log.
|
|
199
|
+
export const __elementSignature = (element, vnode) => {
|
|
200
|
+
const propertyNames = controlledStatePropertyNames(element, vnode.data?.props);
|
|
201
|
+
const attributes = [];
|
|
202
|
+
for (const attribute of Array.from(element.attributes)) {
|
|
203
|
+
const name = attribute.name;
|
|
204
|
+
if (name === HYDRATION_STAMP_ATTRIBUTE ||
|
|
205
|
+
name === 'class' ||
|
|
206
|
+
name === 'style') {
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
attributes.push([name, attribute.value]);
|
|
210
|
+
}
|
|
211
|
+
attributes.sort(byName);
|
|
212
|
+
const properties = [];
|
|
213
|
+
for (const name of propertyNames) {
|
|
214
|
+
properties.push([name, String(Reflect.get(element, name))]);
|
|
215
|
+
}
|
|
216
|
+
properties.sort(byName);
|
|
217
|
+
const styles = Object.entries(inlineStyleOf(element)).sort(byName);
|
|
218
|
+
const classes = Array.from(element.classList).sort();
|
|
219
|
+
return JSON.stringify({ attributes, properties, classes, styles });
|
|
220
|
+
};
|
|
221
|
+
// NOTE: reconcile stale server DOM against the client's first render by
|
|
222
|
+
// seeding the adopted clone with the element's current attributes, classes,
|
|
223
|
+
// and inline styles. Server DOM state is all view-produced, so the diff
|
|
224
|
+
// modules remove any value the client tree does not reassert, converging a
|
|
225
|
+
// nondeterministic render instead of leaving stale, behavior-affecting state
|
|
226
|
+
// (a stale href, a stale class) on the page. class and inline style are
|
|
227
|
+
// seeded into their own module only when the client view owns them solely
|
|
228
|
+
// through that module. When the view also sets `class` or `style` through a
|
|
229
|
+
// raw attribute, or does not use the module at all, the whole attribute rides
|
|
230
|
+
// in attrs and the module (if present) re-asserts its tokens on top, so no
|
|
231
|
+
// value is written by one module and then removed as stale state by another.
|
|
232
|
+
// The hydration stamp is never seeded, so it is never removed.
|
|
233
|
+
const seedAdoptedState = (element, vnode, clone, status, isCustomElement) => {
|
|
234
|
+
const classOwnedByModule = vnode.data?.class !== undefined &&
|
|
235
|
+
htmlAttributeValue(vnode.data?.attrs, 'class') === undefined;
|
|
236
|
+
const styleOwnedByModule = vnode.data?.style !== undefined &&
|
|
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
|
+
]));
|
|
249
|
+
// NOTE: a custom element that upgraded before hydration adds attributes of its
|
|
250
|
+
// own in connectedCallback. Seeding only the attributes the vnode declares
|
|
251
|
+
// leaves those component-owned attributes in place, while a vnode-declared
|
|
252
|
+
// attribute still reconciles.
|
|
253
|
+
const declaredAttributes = isCustomElement
|
|
254
|
+
? new Set(Object.keys(vnode.data?.attrs ?? {}).map(name => name.toLowerCase()))
|
|
255
|
+
: undefined;
|
|
256
|
+
const attrs = {};
|
|
257
|
+
for (const attribute of Array.from(element.attributes)) {
|
|
258
|
+
const name = attribute.name;
|
|
259
|
+
if (name === HYDRATION_STAMP_ATTRIBUTE) {
|
|
260
|
+
continue;
|
|
261
|
+
}
|
|
262
|
+
if (name === 'class' && classOwnedByModule) {
|
|
263
|
+
continue;
|
|
264
|
+
}
|
|
265
|
+
if (name === 'style' && styleOwnedByModule) {
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
268
|
+
if (propertyAttributeNames.has(name)) {
|
|
269
|
+
continue;
|
|
270
|
+
}
|
|
271
|
+
if (datasetAttributeNames.has(name)) {
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
274
|
+
if (declaredAttributes !== undefined &&
|
|
275
|
+
!declaredAttributes.has(name.toLowerCase())) {
|
|
276
|
+
continue;
|
|
277
|
+
}
|
|
278
|
+
attrs[name] = attribute.value;
|
|
279
|
+
}
|
|
280
|
+
const classes = seedClasses(element, vnode, classOwnedByModule, isCustomElement);
|
|
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
|
+
}
|
|
290
|
+
clone.data = {
|
|
291
|
+
...clone.data,
|
|
292
|
+
...(Object.keys(attrs).length > 0 ? { attrs } : {}),
|
|
293
|
+
...(Object.keys(classes).length > 0 ? { class: classes } : {}),
|
|
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 } : {}),
|
|
299
|
+
};
|
|
300
|
+
// In development, record the server DOM signature so the post-patch pass can
|
|
301
|
+
// report an attribute-only mismatch the structural walk cannot see. Gated on
|
|
302
|
+
// the dev flag so a production hydrate does no extra work.
|
|
303
|
+
if (import.meta.hot && !status.adoptedSignatures.has(element)) {
|
|
304
|
+
status.adoptedSignatures.set(element, {
|
|
305
|
+
vnode,
|
|
306
|
+
server: __elementSignature(element, vnode),
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
};
|
|
310
|
+
const detectMismatch = (status) => {
|
|
311
|
+
status.isMismatchDetected = true;
|
|
312
|
+
};
|
|
313
|
+
const reportMismatch = (status) => {
|
|
314
|
+
if (import.meta.hot && status.isMismatchDetected) {
|
|
315
|
+
console.warn('[foldkit] The server DOM did not match the first client view during ' +
|
|
316
|
+
'hydration. Foldkit reconciled the mismatching subtree. Ensure Flags, ' +
|
|
317
|
+
'init, and view produce deterministic initial markup.');
|
|
318
|
+
}
|
|
319
|
+
};
|
|
320
|
+
const isText = (node) => node.nodeType === Node.TEXT_NODE;
|
|
321
|
+
const isComment = (node) => node.nodeType === Node.COMMENT_NODE;
|
|
322
|
+
const isElement = (node) => node.nodeType === Node.ELEMENT_NODE;
|
|
323
|
+
const hasOnlyTextContent = (element) => {
|
|
324
|
+
const firstChild = element.firstChild;
|
|
325
|
+
return (firstChild === null ||
|
|
326
|
+
(firstChild.nextSibling === null && isText(firstChild)));
|
|
327
|
+
};
|
|
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
|
+
};
|
|
333
|
+
// The namespace a vnode expects is carried in `data.ns` for foreign content
|
|
334
|
+
// (SVG, MathML) and is otherwise HTML. An element whose namespace disagrees
|
|
335
|
+
// (an HTML element parsed inside an SVG integration point, say) must be
|
|
336
|
+
// rebuilt rather than adopted, since the two are not interchangeable.
|
|
337
|
+
const namespaceOf = (vnode) => typeof vnode.data?.ns === 'string' ? vnode.data.ns : HTML_NAMESPACE;
|
|
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 digest of key and identity; here
|
|
343
|
+
// the same digest is computed for the vnode and compared, so a mismatch rebuilds
|
|
344
|
+
// instead of transferring state to the wrong row or branch. A key type the
|
|
345
|
+
// digest does not support (a symbol, which a hydratable render refuses) never
|
|
346
|
+
// 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
|
+
// A key type the digest cannot represent (a symbol, which a hydratable
|
|
355
|
+
// render refuses) cannot be compared, so it never matches: rebuilding beats
|
|
356
|
+
// adopting on a 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
|
+
};
|
|
371
|
+
const cloneOf = (vnode, elm) => {
|
|
372
|
+
const clone = {
|
|
373
|
+
sel: vnode.sel,
|
|
374
|
+
data: vnode.data?.is === undefined ? {} : { is: vnode.data.is },
|
|
375
|
+
children: undefined,
|
|
376
|
+
elm,
|
|
377
|
+
text: undefined,
|
|
378
|
+
key: vnode.key,
|
|
379
|
+
};
|
|
380
|
+
if (vnode.identity !== undefined) {
|
|
381
|
+
clone.identity = vnode.identity;
|
|
382
|
+
}
|
|
383
|
+
return clone;
|
|
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
|
+
};
|
|
442
|
+
const asVNode = (child) => typeof child === 'string'
|
|
443
|
+
? {
|
|
444
|
+
sel: undefined,
|
|
445
|
+
data: undefined,
|
|
446
|
+
children: undefined,
|
|
447
|
+
elm: undefined,
|
|
448
|
+
text: child,
|
|
449
|
+
key: undefined,
|
|
450
|
+
}
|
|
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
|
+
};
|
|
480
|
+
const clearChildren = (element) => {
|
|
481
|
+
element.textContent = '';
|
|
482
|
+
};
|
|
483
|
+
const adoptText = (element, domChild, text) => {
|
|
484
|
+
if (domChild !== null && isText(domChild)) {
|
|
485
|
+
const domText = domChild.data;
|
|
486
|
+
if (domText === text) {
|
|
487
|
+
return Option.some({
|
|
488
|
+
adoptedNode: domChild,
|
|
489
|
+
nextDomChild: domChild.nextSibling,
|
|
490
|
+
});
|
|
491
|
+
}
|
|
492
|
+
if (domText.startsWith(text)) {
|
|
493
|
+
domChild.splitText(text.length);
|
|
494
|
+
return Option.some({
|
|
495
|
+
adoptedNode: domChild,
|
|
496
|
+
nextDomChild: domChild.nextSibling,
|
|
497
|
+
});
|
|
498
|
+
}
|
|
499
|
+
return Option.none();
|
|
500
|
+
}
|
|
501
|
+
if (text === '') {
|
|
502
|
+
const emptyTextNode = element.ownerDocument.createTextNode('');
|
|
503
|
+
element.insertBefore(emptyTextNode, domChild);
|
|
504
|
+
return Option.some({ adoptedNode: emptyTextNode, nextDomChild: domChild });
|
|
505
|
+
}
|
|
506
|
+
return Option.none();
|
|
507
|
+
};
|
|
508
|
+
const adoptElement = (element, vnode, status) => {
|
|
509
|
+
if (shouldRebuildCustomElement(element, vnode)) {
|
|
510
|
+
return clonePreparedTree(replaceHydrationElement(element, vnode));
|
|
511
|
+
}
|
|
512
|
+
const clone = cloneOf(vnode, element);
|
|
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);
|
|
518
|
+
// NOTE: an autonomous custom element (an HTML-namespace element whose name
|
|
519
|
+
// carries a hyphen) that upgraded before hydration adds attributes, classes,
|
|
520
|
+
// styles, and light DOM of its own in connectedCallback. The attributes, class
|
|
521
|
+
// tokens, and style properties the vnode does not declare are always preserved
|
|
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
|
|
527
|
+
// `customElements.get`: whether the element has upgraded is timing-dependent
|
|
528
|
+
// at hydration (its definition may register after the server DOM parses), so a
|
|
529
|
+
// name test is deterministic. A hyphenated element that never upgrades is
|
|
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') &&
|
|
547
|
+
vnode.data?.props?.['value'] !== undefined) {
|
|
548
|
+
clone.children = [];
|
|
549
|
+
return finishAdoption();
|
|
550
|
+
}
|
|
551
|
+
const authoredInnerHtml = vnode.data?.props?.['innerHTML'];
|
|
552
|
+
const hasAuthoredInnerHtml = authoredInnerHtml !== undefined && hasTrustedInnerHtml(vnode.data?.props);
|
|
553
|
+
const vnodeChildren = vnode.children;
|
|
554
|
+
if (hasAuthoredInnerHtml) {
|
|
555
|
+
// NOTE: the browser normalizes markup as it parses (entity forms, tag
|
|
556
|
+
// case, attribute order), so the served innerHTML string rarely equals
|
|
557
|
+
// the authored one byte for byte. Parsing the authored string through a
|
|
558
|
+
// probe element of the same tag compares the two in normalized form;
|
|
559
|
+
// when they agree the clone carries the authored string, the props
|
|
560
|
+
// module sees no change, and the adopted subtree survives. The probe is
|
|
561
|
+
// created in the element's own namespace: foreign content such as SVG
|
|
562
|
+
// parses with case-preserved names (pathLength, viewBox) that an
|
|
563
|
+
// HTML-context parse would lowercase, false-mismatching every camelCase
|
|
564
|
+
// attribute.
|
|
565
|
+
if (typeof authoredInnerHtml === 'string') {
|
|
566
|
+
const probe = createInertProbe(element);
|
|
567
|
+
writeNativeInnerHtml(probe, authoredInnerHtml);
|
|
568
|
+
const currentInnerHtml = readNativeInnerHtml(element);
|
|
569
|
+
const isEquivalentMarkup = readNativeInnerHtml(probe) === currentInnerHtml;
|
|
570
|
+
if (!isEquivalentMarkup) {
|
|
571
|
+
detectMismatch(status);
|
|
572
|
+
writeNativeInnerHtml(element, authoredInnerHtml);
|
|
573
|
+
}
|
|
574
|
+
const props = {
|
|
575
|
+
...clone.data?.props,
|
|
576
|
+
innerHTML: authoredInnerHtml,
|
|
577
|
+
};
|
|
578
|
+
markTrustedInnerHtml(props, props.innerHTML);
|
|
579
|
+
clone.data = {
|
|
580
|
+
...clone.data,
|
|
581
|
+
props,
|
|
582
|
+
};
|
|
583
|
+
}
|
|
584
|
+
else {
|
|
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
|
+
};
|
|
594
|
+
}
|
|
595
|
+
clone.children = [];
|
|
596
|
+
return finishAdoption();
|
|
597
|
+
}
|
|
598
|
+
// NOTE: no children (undefined) and an empty child list both mean the view
|
|
599
|
+
// declares no children, so they share the childless path. This is also where
|
|
600
|
+
// a custom element's ownership splits: a childless vnode has no view child to
|
|
601
|
+
// adopt, so the component's light DOM is left untouched, while a vnode with
|
|
602
|
+
// real children (the branch below) owns the light DOM and reconciles it.
|
|
603
|
+
if (vnodeChildren === undefined || vnodeChildren.length === 0) {
|
|
604
|
+
// NOTE: only adopt the text shortcut when the element already holds a
|
|
605
|
+
// single text node. `textContent` flattens across element children, so
|
|
606
|
+
// copying it for an element that carries stray markup would compare equal
|
|
607
|
+
// to the vnode text and leave that markup in place. Leaving `clone.text`
|
|
608
|
+
// undefined makes `patchVnode` overwrite the element's content with the
|
|
609
|
+
// vnode text instead, rebuilding the mismatching shape.
|
|
610
|
+
if (vnode.text !== undefined) {
|
|
611
|
+
if (hasOnlyTextContent(element)) {
|
|
612
|
+
clone.text = element.textContent ?? '';
|
|
613
|
+
if (clone.text !== vnode.text) {
|
|
614
|
+
detectMismatch(status);
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
else {
|
|
618
|
+
detectMismatch(status);
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
else if (!isCustomElement && element.firstChild !== null) {
|
|
622
|
+
// NOTE: a childless vnode (no children or an empty child list, and no
|
|
623
|
+
// text) owns an empty element. Server DOM left under it, an older build's
|
|
624
|
+
// content behind a cache race, would otherwise survive every future
|
|
625
|
+
// render, since patchVnode has nothing to diff it against. Clear it so the
|
|
626
|
+
// empty client tree wins, matching the mismatch branches below. A custom
|
|
627
|
+
// element is exempt: its light DOM is component-owned, not stale server
|
|
628
|
+
// state.
|
|
629
|
+
detectMismatch(status);
|
|
630
|
+
clearChildren(element);
|
|
631
|
+
clone.children = [];
|
|
632
|
+
}
|
|
633
|
+
return finishAdoption();
|
|
634
|
+
}
|
|
635
|
+
const cloneChildren = [];
|
|
636
|
+
let domChild = element.firstChild;
|
|
637
|
+
for (const rawChild of vnodeChildren) {
|
|
638
|
+
const child = asVNode(rawChild);
|
|
639
|
+
if (child.sel === undefined || child.sel === '') {
|
|
640
|
+
const childText = child.text ?? '';
|
|
641
|
+
const maybeAdoption = adoptText(element, domChild, childText);
|
|
642
|
+
if (Option.isNone(maybeAdoption)) {
|
|
643
|
+
detectMismatch(status);
|
|
644
|
+
if (domChild === null) {
|
|
645
|
+
break;
|
|
646
|
+
}
|
|
647
|
+
clearChildren(element);
|
|
648
|
+
clone.children = [];
|
|
649
|
+
return finishAdoption();
|
|
650
|
+
}
|
|
651
|
+
const adoption = maybeAdoption.value;
|
|
652
|
+
const textClone = cloneOf(child, adoption.adoptedNode);
|
|
653
|
+
textClone.text = childText;
|
|
654
|
+
cloneChildren.push(textClone);
|
|
655
|
+
domChild = adoption.nextDomChild;
|
|
656
|
+
continue;
|
|
657
|
+
}
|
|
658
|
+
if (domChild === null) {
|
|
659
|
+
detectMismatch(status);
|
|
660
|
+
break;
|
|
661
|
+
}
|
|
662
|
+
if (child.sel === '!') {
|
|
663
|
+
if (!isComment(domChild)) {
|
|
664
|
+
detectMismatch(status);
|
|
665
|
+
clearChildren(element);
|
|
666
|
+
clone.children = [];
|
|
667
|
+
return finishAdoption();
|
|
668
|
+
}
|
|
669
|
+
const commentClone = cloneOf(child, domChild);
|
|
670
|
+
commentClone.text = domChild.data;
|
|
671
|
+
cloneChildren.push(commentClone);
|
|
672
|
+
domChild = domChild.nextSibling;
|
|
673
|
+
continue;
|
|
674
|
+
}
|
|
675
|
+
if (!isElement(domChild) ||
|
|
676
|
+
!matchesTag(domChild, child) ||
|
|
677
|
+
!matchesNamespace(domChild, child) ||
|
|
678
|
+
!matchesAdoptionIdentity(domChild, child)) {
|
|
679
|
+
detectMismatch(status);
|
|
680
|
+
clearChildren(element);
|
|
681
|
+
clone.children = [];
|
|
682
|
+
return finishAdoption();
|
|
683
|
+
}
|
|
684
|
+
const nextDomChild = domChild.nextSibling;
|
|
685
|
+
cloneChildren.push(adoptElement(domChild, child, status));
|
|
686
|
+
domChild = nextDomChild;
|
|
687
|
+
}
|
|
688
|
+
while (domChild !== null) {
|
|
689
|
+
detectMismatch(status);
|
|
690
|
+
const nextDomChild = domChild.nextSibling;
|
|
691
|
+
element.removeChild(domChild);
|
|
692
|
+
domChild = nextDomChild;
|
|
693
|
+
}
|
|
694
|
+
clone.children = cloneChildren;
|
|
695
|
+
return finishAdoption();
|
|
696
|
+
};
|
|
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;
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
for (const { vnode, insert } of deferred) {
|
|
766
|
+
insert(vnode);
|
|
767
|
+
}
|
|
768
|
+
return patchedVNode;
|
|
769
|
+
};
|
|
770
|
+
/** Hydrates a server-rendered root element against the first render's vnode
|
|
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. */
|
|
780
|
+
// Replace the hydration root with a fresh render of the vnode. snabbdom's
|
|
781
|
+
// sameVnode compares tag but not namespace, so patching the root directly
|
|
782
|
+
// would reuse a same-tag element even across a namespace change. Patching
|
|
783
|
+
// against a comment placed where the root was is never sameVnode with a new
|
|
784
|
+
// element, so the differ builds a fresh node in the correct namespace and
|
|
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.
|
|
792
|
+
const replaceHydrationRoot = (hydrationRoot, vnode) => {
|
|
793
|
+
const ownerDocument = hydrationRoot.ownerDocument;
|
|
794
|
+
const parent = hydrationRoot.parentNode ?? ownerDocument.createDocumentFragment();
|
|
795
|
+
if (hydrationRoot.parentNode === null) {
|
|
796
|
+
parent.appendChild(hydrationRoot);
|
|
797
|
+
}
|
|
798
|
+
const placeholder = ownerDocument.createComment('');
|
|
799
|
+
parent.replaceChild(placeholder, hydrationRoot);
|
|
800
|
+
return patch(toVNode(placeholder), vnode);
|
|
801
|
+
};
|
|
802
|
+
export const __hydrateVNode = (hydrationRoot, nextVNode, seen, buildId) => {
|
|
803
|
+
const dedupedVNode = nextVNode !== null ? dedupeSharedVNodes(nextVNode, seen) : h('!');
|
|
804
|
+
const status = {
|
|
805
|
+
isMismatchDetected: false,
|
|
806
|
+
adoptedSignatures: new Map(),
|
|
807
|
+
};
|
|
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 ||
|
|
823
|
+
dedupedVNode.sel === '' ||
|
|
824
|
+
dedupedVNode.sel === '!' ||
|
|
825
|
+
!matchesTag(hydrationRoot, dedupedVNode) ||
|
|
826
|
+
!matchesNamespace(hydrationRoot, dedupedVNode) ||
|
|
827
|
+
!matchesAdoptionIdentity(hydrationRoot, dedupedVNode);
|
|
828
|
+
if (isRootMismatch) {
|
|
829
|
+
detectMismatch(status);
|
|
830
|
+
const patchedVNode = replaceHydrationRoot(hydrationRoot, dedupedVNode);
|
|
831
|
+
reportMismatch(status);
|
|
832
|
+
return patchedVNode;
|
|
833
|
+
}
|
|
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);
|
|
840
|
+
if (import.meta.hot && !status.isMismatchDetected) {
|
|
841
|
+
for (const [element, { vnode, server }] of status.adoptedSignatures) {
|
|
842
|
+
if (__elementSignature(element, vnode) !== server) {
|
|
843
|
+
detectMismatch(status);
|
|
844
|
+
break;
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
reportMismatch(status);
|
|
849
|
+
return patchedVNode;
|
|
850
|
+
};
|