Skip to content

Commit 6cc509b

Browse files
committed
fix: improve hydration of altered html
1 parent c4b32c2 commit 6cc509b

File tree

24 files changed

+184
-32
lines changed

24 files changed

+184
-32
lines changed

.changeset/angry-pigs-float.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'svelte': patch
3+
---
4+
5+
fix: improve hydration of altered html

packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/fragment.js

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { is_event_attribute, is_text_attribute } from '../../../../../utils/ast.
66
import * as b from '#compiler/builders';
77
import { is_custom_element_node } from '../../../../nodes.js';
88
import { build_template_chunk } from './utils.js';
9+
import { ELEMENT_NODE, TEXT_NODE } from '#client/constants';
910

1011
/**
1112
* Processes an array of template nodes, joining sibling text/expression nodes
@@ -25,15 +26,19 @@ export function process_children(nodes, initial, is_element, context) {
2526
/** @type {Sequence} */
2627
let sequence = [];
2728

28-
/** @param {boolean} is_text */
29-
function get_node(is_text) {
29+
/**
30+
* @param {boolean} is_text
31+
* @param {number} node_type
32+
**/
33+
function get_node(is_text, node_type) {
3034
if (skipped === 0) {
3135
return prev(is_text);
3236
}
3337

3438
return b.call(
3539
'$.sibling',
3640
prev(false),
41+
b.literal(node_type),
3742
(is_text || skipped !== 1) && b.literal(skipped),
3843
is_text && b.true
3944
);
@@ -44,7 +49,10 @@ export function process_children(nodes, initial, is_element, context) {
4449
* @param {string} name
4550
*/
4651
function flush_node(is_text, name) {
47-
const expression = get_node(is_text);
52+
const expression = get_node(
53+
is_text,
54+
name === 'text' ? TEXT_NODE : name === 'node' ? 0 : ELEMENT_NODE
55+
);
4856
let id = expression;
4957

5058
if (id.type !== 'Identifier') {

packages/svelte/src/internal/client/dom/blocks/svelte-head.js

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@ import { hydrate_node, hydrating, set_hydrate_node, set_hydrating } from '../hyd
33
import { create_text, get_first_child, get_next_sibling } from '../operations.js';
44
import { block } from '../../reactivity/effects.js';
55
import { COMMENT_NODE, HEAD_EFFECT } from '#client/constants';
6-
import { HYDRATION_START } from '../../../../constants.js';
6+
import { HYDRATION_END, HYDRATION_ERROR, HYDRATION_START } from '../../../../constants.js';
7+
import { hydration_mismatch } from '../../warnings.js';
78

89
/**
910
* @type {Node | undefined}
@@ -58,6 +59,41 @@ export function head(render_fn) {
5859

5960
try {
6061
block(() => render_fn(anchor), HEAD_EFFECT);
62+
if (hydrating) {
63+
if (hydrate_node === null || /** @type {Comment} */ (hydrate_node).data !== HYDRATION_END) {
64+
hydration_mismatch();
65+
throw HYDRATION_ERROR;
66+
}
67+
}
68+
} catch (error) {
69+
// re-mount only this svelte:head
70+
if (was_hydrating && head_anchor && error === HYDRATION_ERROR) {
71+
// Here head_anchor is the node next after HYDRATION_START
72+
/** @type {Node | null} */
73+
let prev = head_anchor.previousSibling;
74+
/** @type {Node | null} */
75+
let next = head_anchor;
76+
// remove nodes that failed to hydrate
77+
while (
78+
prev !== null &&
79+
(prev.nodeType !== COMMENT_NODE || /** @type {Comment} */ (prev).data !== HYDRATION_END)
80+
) {
81+
document.head.removeChild(prev);
82+
prev = next;
83+
next = get_next_sibling(/** @type {Node} */ (next));
84+
}
85+
if (prev?.parentNode) document.head.removeChild(prev);
86+
if (next !== null) {
87+
// allow the next head block try to hydrate
88+
head_anchor = set_hydrate_node(/** @type {TemplateNode} */ (next));
89+
}
90+
91+
set_hydrating(false);
92+
anchor = document.head.appendChild(create_text());
93+
block(() => render_fn(anchor), HEAD_EFFECT);
94+
} else {
95+
throw error;
96+
}
6197
} finally {
6298
if (was_hydrating) {
6399
set_hydrating(true);

packages/svelte/src/internal/client/dom/operations.js

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@ import { hydrate_node, hydrating, set_hydrate_node } from './hydration.js';
33
import { DEV } from 'esm-env';
44
import { init_array_prototype_warnings } from '../dev/equality.js';
55
import { get_descriptor, is_extensible } from '../../shared/utils.js';
6-
import { TEXT_NODE } from '#client/constants';
6+
import { COMMENT_NODE, TEXT_NODE } from '#client/constants';
7+
import { HYDRATION_END, HYDRATION_ERROR } from '../../../constants.js';
8+
import { hydration_mismatch } from '../warnings.js';
79

810
// export these for reference in the compiled code, making global name deduplication unnecessary
911
/** @type {Window} */
@@ -158,26 +160,40 @@ export function first_child(fragment, is_text) {
158160
/**
159161
* Don't mark this as side-effect-free, hydration needs to walk all nodes
160162
* @param {TemplateNode} node
163+
* @param {number} node_type
161164
* @param {number} count
162-
* @param {boolean} is_text
165+
* @param {boolean} add_text
163166
* @returns {Node | null}
164167
*/
165-
export function sibling(node, count = 1, is_text = false) {
166-
let next_sibling = hydrating ? hydrate_node : node;
168+
export function sibling(node, node_type, count = 1, add_text = false) {
169+
var next_sibling = hydrating ? hydrate_node : node;
167170
var last_sibling;
168171

169172
while (count--) {
170173
last_sibling = next_sibling;
171174
next_sibling = /** @type {TemplateNode} */ (get_next_sibling(next_sibling));
175+
if (
176+
(next_sibling === null && !add_text) ||
177+
(next_sibling?.nodeType === COMMENT_NODE &&
178+
/** @type {Comment} */ (next_sibling).data === HYDRATION_END)
179+
) {
180+
hydration_mismatch();
181+
throw HYDRATION_ERROR;
182+
}
172183
}
173184

174185
if (!hydrating) {
175186
return next_sibling;
176187
}
177188

189+
if (hydrating && node_type !== 0 && !add_text && next_sibling?.nodeType !== node_type) {
190+
hydration_mismatch();
191+
throw HYDRATION_ERROR;
192+
}
193+
178194
// if a sibling {expression} is empty during SSR, there might be no
179195
// text node to hydrate — we must therefore create one
180-
if (is_text && next_sibling?.nodeType !== TEXT_NODE) {
196+
if (add_text && next_sibling?.nodeType !== TEXT_NODE) {
181197
var text = create_text();
182198
// If the next sibling is `null` and we're handling text then it's because
183199
// the SSR content was empty for the text, so we need to generate a new text
@@ -192,7 +208,7 @@ export function sibling(node, count = 1, is_text = false) {
192208
}
193209

194210
set_hydrate_node(next_sibling);
195-
return /** @type {TemplateNode} */ (next_sibling);
211+
return next_sibling;
196212
}
197213

198214
/**
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { flushSync } from 'svelte';
2+
import { test } from '../../test';
3+
4+
export default test({
5+
expect_hydration_error: true,
6+
test(assert, target, snapshot, component, window) {
7+
assert.equal(window.document.querySelectorAll('meta').length, 5);
8+
9+
const [button] = target.getElementsByTagName('button');
10+
button.click();
11+
flushSync();
12+
13+
/** @type {NodeList} */
14+
const metas = window.document.querySelectorAll('meta[name=count]');
15+
assert.equal(metas.length, 4);
16+
metas.forEach((meta) => assert.equal(/** @type {HTMLMetaElement} */ (meta).content, '2'));
17+
}
18+
});
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
<!--[--><meta name="count" content="1"><!----><!--]--><!--[--><meta name="count" content="1"><!----><!--]-->
2+
<!----><meta name="count" content="1"> <meta name="will-be-missing"> <meta name="count" content="1">
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<!--[--><meta name="count" content="1" /><!----><!--]--><!--[--><meta name="count" content="1" />
2+
<meta name="count" content="1" /><!----><!--]--><!--[--><meta
3+
name="count"
4+
content="1"
5+
/><!----><!--]-->
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
<script>
2+
let { children } = $props();
3+
</script>
4+
<svelte:head>
5+
{@render children()}
6+
</svelte:head>
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<script>
2+
import Head from './head.svelte';
3+
4+
let count = $state(1);
5+
</script>
6+
7+
<Head>
8+
<meta name="count" content={count}>
9+
</Head>
10+
<Head>
11+
<meta name="count" content={count}>
12+
<meta name="will-be-missing">
13+
<meta name="count" content={count}>
14+
</Head>
15+
<Head>
16+
<meta name="count" content={count}>
17+
</Head>
18+
19+
<button onclick={() => count++}>inc</button>
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { test } from '../../test';
2+
3+
export default test({
4+
expect_hydration_error: true,
5+
test(assert, target, snapshot, component, window) {
6+
assert.equal(window.document.querySelectorAll('meta').length, 2);
7+
}
8+
});

0 commit comments

Comments
 (0)