Skip to content

Commit 19819d0

Browse files
trueadmRich-Harris
andauthored
fix: provide more hydration mismatch coverage (#12755)
* fix: provide more hydration mismatch coverage * tweak * add test for safari borking stuff * fix * fix windows test * failing test * oops * revert playground changes * simplify * template content hydration logic should really be separate from reset logic * actually the test is incorrect, and now i cant seem to recreate what i saw before... hmm * update comment to no longer mention templates * failing test * delete test for now --------- Co-authored-by: Rich Harris <[email protected]>
1 parent c32a918 commit 19819d0

File tree

11 files changed

+53
-15
lines changed

11 files changed

+53
-15
lines changed

.changeset/stupid-rivers-stare.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: provide more hydration mismatch coverage

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

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -327,9 +327,8 @@ export function RegularElement(node, context) {
327327
// set the value of `hydrate_node` to `node.content`
328328
if (node.name === 'template') {
329329
needs_reset = true;
330-
330+
child_state.init.push(b.stmt(b.call('$.hydrate_template', arg)));
331331
arg = b.member(arg, b.id('content'));
332-
child_state.init.push(b.stmt(b.call('$.reset', arg)));
333332
}
334333

335334
process_children(trimmed, () => b.call('$.child', arg), true, {

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

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -30,21 +30,38 @@ export let hydrate_node;
3030

3131
/** @param {TemplateNode} node */
3232
export function set_hydrate_node(node) {
33+
if (node === null) {
34+
w.hydration_mismatch();
35+
throw HYDRATION_ERROR;
36+
}
37+
3338
return (hydrate_node = node);
3439
}
3540

3641
export function hydrate_next() {
37-
if (hydrate_node === null) {
42+
return set_hydrate_node(/** @type {TemplateNode} */ (hydrate_node.nextSibling));
43+
}
44+
45+
/** @param {TemplateNode} node */
46+
export function reset(node) {
47+
if (!hydrating) return;
48+
49+
// If the node has remaining siblings, something has gone wrong
50+
if (hydrate_node.nextSibling !== null) {
3851
w.hydration_mismatch();
3952
throw HYDRATION_ERROR;
4053
}
41-
return (hydrate_node = /** @type {TemplateNode} */ (hydrate_node.nextSibling));
54+
55+
hydrate_node = node;
4256
}
4357

44-
/** @param {TemplateNode} node */
45-
export function reset(node) {
58+
/**
59+
* @param {HTMLTemplateElement} template
60+
*/
61+
export function hydrate_template(template) {
4662
if (hydrating) {
47-
hydrate_node = node;
63+
// @ts-expect-error TemplateNode doesn't include DocumentFragment, but it's actually fine
64+
hydrate_node = template.content;
4865
}
4966
}
5067

packages/svelte/src/internal/client/index.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ export {
6666
bind_focused
6767
} from './dom/elements/bindings/universal.js';
6868
export { bind_window_scroll, bind_window_size } from './dom/elements/bindings/window.js';
69-
export { next, reset } from './dom/hydration.js';
69+
export { hydrate_template, next, reset } from './dom/hydration.js';
7070
export {
7171
once,
7272
preventDefault,
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { test } from '../../test';
2+
3+
export default test({
4+
expect_hydration_error: true
5+
});
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
<h1>call +636-555-3226 now</h1>
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
<!--[--><h1>call <a href="tel:+636-555-3226">+636-555-3226</a> now</h1><!--]-->
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<script>
2+
const message = `call +636-555-3226 now`;
3+
</script>
4+
5+
<h1>{message}</h1>
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
<!-- unrelated comment -->
2+
<!--[--><!--[-->hello<!--]--><!--]-->

packages/svelte/tests/hydration/test.ts

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -113,15 +113,16 @@ const { test, run } = suite<HydrationTest>(async (config, cwd) => {
113113
throw new Error(`Unexpected errors: ${errors.join('\n')}`);
114114
}
115115

116-
if (!override) {
117-
const expected = read(`${cwd}/_expected.html`) ?? rendered.html;
118-
flushSync();
119-
assert.equal(target.innerHTML.trim(), expected.trim());
120-
}
116+
flushSync();
117+
118+
const normalize = (string: string) => string.trim().replace(/\r\n/g, '\n');
119+
120+
const expected = read(`${cwd}/_expected.html`) ?? rendered.html;
121+
assert.equal(normalize(target.innerHTML), normalize(expected));
121122

122123
if (rendered.head) {
123124
const expected = read(`${cwd}/_expected_head.html`) ?? rendered.head;
124-
assert.equal(head.innerHTML.trim(), expected.trim());
125+
assert.equal(normalize(head.innerHTML), normalize(expected));
125126
}
126127

127128
if (config.snapshot) {

playgrounds/demo/ssr-dev.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,9 @@ polka()
2727

2828
const html = transformed_template
2929
.replace(`<!--ssr-head-->`, head)
30-
.replace(`<!--ssr-body-->`, body);
30+
.replace(`<!--ssr-body-->`, body)
31+
// check that Safari doesn't break hydration
32+
.replaceAll('+636-555-3226', '<a href="tel:+636-555-3226">+636-555-3226</a>');
3133

3234
res.writeHead(200, { 'Content-Type': 'text/html' }).end(html);
3335
})

0 commit comments

Comments
 (0)