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.
Files changed (91) hide show
  1. package/README.md +5 -2
  2. package/dist/buildToken.d.ts +3 -0
  3. package/dist/buildToken.d.ts.map +1 -0
  4. package/dist/buildToken.js +21 -0
  5. package/dist/controlledDomState.d.ts +25 -0
  6. package/dist/controlledDomState.d.ts.map +1 -0
  7. package/dist/controlledDomState.js +240 -0
  8. package/dist/cssStyleProperties.d.ts +6 -0
  9. package/dist/cssStyleProperties.d.ts.map +1 -0
  10. package/dist/cssStyleProperties.js +91 -0
  11. package/dist/customElement/index.d.ts.map +1 -1
  12. package/dist/customElement/index.js +23 -0
  13. package/dist/domReflection.d.ts +73 -0
  14. package/dist/domReflection.d.ts.map +1 -0
  15. package/dist/domReflection.js +557 -0
  16. package/dist/experimental/index.d.ts +1 -0
  17. package/dist/experimental/index.d.ts.map +1 -1
  18. package/dist/experimental/index.js +1 -0
  19. package/dist/experimental/machine/machine.d.ts +22 -3
  20. package/dist/experimental/machine/machine.d.ts.map +1 -1
  21. package/dist/experimental/machine/machine.js +8 -0
  22. package/dist/experimental/server/entry.d.ts +73 -0
  23. package/dist/experimental/server/entry.d.ts.map +1 -0
  24. package/dist/experimental/server/entry.js +41 -0
  25. package/dist/experimental/server/host.d.ts +136 -0
  26. package/dist/experimental/server/host.d.ts.map +1 -0
  27. package/dist/experimental/server/host.js +359 -0
  28. package/dist/experimental/server/index.d.ts +5 -0
  29. package/dist/experimental/server/index.d.ts.map +1 -0
  30. package/dist/experimental/server/index.js +4 -0
  31. package/dist/experimental/server/public.d.ts +3 -0
  32. package/dist/experimental/server/public.d.ts.map +1 -0
  33. package/dist/experimental/server/public.js +1 -0
  34. package/dist/experimental/server/serialize.d.ts +43 -0
  35. package/dist/experimental/server/serialize.d.ts.map +1 -0
  36. package/dist/experimental/server/serialize.js +786 -0
  37. package/dist/experimental/server/server.d.ts +242 -0
  38. package/dist/experimental/server/server.d.ts.map +1 -0
  39. package/dist/experimental/server/server.js +946 -0
  40. package/dist/experimental/server/template.d.ts +53 -0
  41. package/dist/experimental/server/template.d.ts.map +1 -0
  42. package/dist/experimental/server/template.js +617 -0
  43. package/dist/html/index.d.ts +5 -0
  44. package/dist/html/index.d.ts.map +1 -1
  45. package/dist/html/index.js +474 -33
  46. package/dist/hydrate.d.ts +4 -0
  47. package/dist/hydrate.d.ts.map +1 -0
  48. package/dist/hydrate.js +850 -0
  49. package/dist/hydrationMarker.d.ts +10 -0
  50. package/dist/hydrationMarker.d.ts.map +1 -0
  51. package/dist/hydrationMarker.js +9 -0
  52. package/dist/hydrationMarkers.d.ts +15 -0
  53. package/dist/hydrationMarkers.d.ts.map +1 -0
  54. package/dist/hydrationMarkers.js +70 -0
  55. package/dist/nativeInnerHtml.d.ts +13 -0
  56. package/dist/nativeInnerHtml.d.ts.map +1 -0
  57. package/dist/nativeInnerHtml.js +30 -0
  58. package/dist/propertyProvenance.d.ts +33 -0
  59. package/dist/propertyProvenance.d.ts.map +1 -0
  60. package/dist/propertyProvenance.js +78 -0
  61. package/dist/propsModule.d.ts.map +1 -1
  62. package/dist/propsModule.js +149 -15
  63. package/dist/runtime/public.d.ts +2 -2
  64. package/dist/runtime/public.d.ts.map +1 -1
  65. package/dist/runtime/public.js +1 -1
  66. package/dist/runtime/runtime.d.ts +99 -36
  67. package/dist/runtime/runtime.d.ts.map +1 -1
  68. package/dist/runtime/runtime.js +412 -71
  69. package/dist/snabbdom/attributes.d.ts.map +1 -1
  70. package/dist/snabbdom/attributes.js +65 -37
  71. package/dist/snabbdom/h.d.ts +1 -0
  72. package/dist/snabbdom/h.d.ts.map +1 -1
  73. package/dist/snabbdom/h.js +85 -4
  74. package/dist/snabbdom/style.d.ts.map +1 -1
  75. package/dist/snabbdom/style.js +53 -34
  76. package/dist/snabbdom/tovnode.d.ts.map +1 -1
  77. package/dist/snabbdom/tovnode.js +5 -1
  78. package/dist/tagName.d.ts +6 -0
  79. package/dist/tagName.d.ts.map +1 -0
  80. package/dist/tagName.js +11 -0
  81. package/dist/test/apps/attributes.d.ts +1 -0
  82. package/dist/test/apps/attributes.d.ts.map +1 -1
  83. package/dist/test/apps/attributes.js +8 -1
  84. package/dist/test/apps/login.js +1 -1
  85. package/dist/test/matchers.d.ts.map +1 -1
  86. package/dist/test/matchers.js +2 -1
  87. package/dist/test/scene.d.ts.map +1 -1
  88. package/dist/test/scene.js +2 -1
  89. package/dist/vdom.d.ts.map +1 -1
  90. package/dist/vdom.js +25 -1
  91. package/package.json +9 -2
@@ -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
+ };