diff --git a/.changeset/angry-pigs-float.md b/.changeset/angry-pigs-float.md new file mode 100644 index 000000000000..59f186f4966e --- /dev/null +++ b/.changeset/angry-pigs-float.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: improve hydration of altered html diff --git a/packages/svelte/src/internal/client/dom/blocks/svelte-head.js b/packages/svelte/src/internal/client/dom/blocks/svelte-head.js index 66d337183637..b7ed6d4deb8a 100644 --- a/packages/svelte/src/internal/client/dom/blocks/svelte-head.js +++ b/packages/svelte/src/internal/client/dom/blocks/svelte-head.js @@ -3,10 +3,11 @@ import { hydrate_node, hydrating, set_hydrate_node, set_hydrating } from '../hyd import { create_text, get_first_child, get_next_sibling } from '../operations.js'; import { block } from '../../reactivity/effects.js'; import { COMMENT_NODE, HEAD_EFFECT } from '#client/constants'; -import { HYDRATION_START } from '../../../../constants.js'; +import { HYDRATION_END, HYDRATION_ERROR, HYDRATION_START } from '../../../../constants.js'; +import { hydration_mismatch } from '../../warnings.js'; /** - * @type {Node | undefined} + * @type {Node | null | undefined} */ let head_anchor; @@ -32,7 +33,7 @@ export function head(render_fn) { // There might be multiple head blocks in our app, so we need to account for each one needing independent hydration. if (head_anchor === undefined) { - head_anchor = /** @type {TemplateNode} */ (get_first_child(document.head)); + head_anchor = get_first_child(document.head); } while ( @@ -40,7 +41,7 @@ export function head(render_fn) { (head_anchor.nodeType !== COMMENT_NODE || /** @type {Comment} */ (head_anchor).data !== HYDRATION_START) ) { - head_anchor = /** @type {TemplateNode} */ (get_next_sibling(head_anchor)); + head_anchor = get_next_sibling(head_anchor); } // If we can't find an opening hydration marker, skip hydration (this can happen @@ -58,6 +59,43 @@ export function head(render_fn) { try { block(() => render_fn(anchor), HEAD_EFFECT); + check_end(); + } catch (error) { + // Remount only this svelte:head + if (was_hydrating && head_anchor != null) { + hydration_mismatch(); + // Here head_anchor is the node next after HYDRATION_START + /** @type {Node | null} */ + var node = head_anchor.previousSibling; + // Remove nodes that failed to hydrate + var depth = 0; + while (node !== null) { + var prev = /** @type {TemplateNode} */ (node); + node = get_next_sibling(node); + prev.remove(); + if (prev.nodeType === COMMENT_NODE) { + var data = /** @type {Comment} */ (prev).data; + if (data === HYDRATION_END) { + depth -= 1; + if (depth === 0) break; + } else if (data === HYDRATION_START) { + depth += 1; + } + } + } + // Setup hydration for the next svelte:head + if (node === null) { + head_anchor = null; + } else { + head_anchor = set_hydrate_node(/** @type {TemplateNode} */ (node)); + } + + set_hydrating(false); + anchor = document.head.appendChild(create_text()); + block(() => render_fn(anchor), HEAD_EFFECT); + } else { + throw error; + } } finally { if (was_hydrating) { set_hydrating(true); @@ -66,3 +104,11 @@ export function head(render_fn) { } } } + +// treeshaking of hydrate node fails when this is directly in the try-catch +function check_end() { + if (hydrating && /** @type {Comment|null} */ (hydrate_node)?.data !== HYDRATION_END) { + hydration_mismatch(); + throw HYDRATION_ERROR; + } +} diff --git a/packages/svelte/tests/hydration/samples/head-corrupted-2/_config.js b/packages/svelte/tests/hydration/samples/head-corrupted-2/_config.js new file mode 100644 index 000000000000..91470f805211 --- /dev/null +++ b/packages/svelte/tests/hydration/samples/head-corrupted-2/_config.js @@ -0,0 +1,18 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + expect_hydration_error: true, + test(assert, target, snapshot, component, window) { + assert.equal(window.document.querySelectorAll('meta').length, 5); + + const [button] = target.getElementsByTagName('button'); + button.click(); + flushSync(); + + /** @type {NodeList} */ + const metas = window.document.querySelectorAll('meta[name=count]'); + assert.equal(metas.length, 4); + metas.forEach((meta) => assert.equal(/** @type {HTMLMetaElement} */ (meta).content, '2')); + } +}); diff --git a/packages/svelte/tests/hydration/samples/head-corrupted-2/_expected_head.html b/packages/svelte/tests/hydration/samples/head-corrupted-2/_expected_head.html new file mode 100644 index 000000000000..3ce4f3237b46 --- /dev/null +++ b/packages/svelte/tests/hydration/samples/head-corrupted-2/_expected_head.html @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/packages/svelte/tests/hydration/samples/head-corrupted-2/_override_head.html b/packages/svelte/tests/hydration/samples/head-corrupted-2/_override_head.html new file mode 100644 index 000000000000..f7404d045e06 --- /dev/null +++ b/packages/svelte/tests/hydration/samples/head-corrupted-2/_override_head.html @@ -0,0 +1,5 @@ + + diff --git a/packages/svelte/tests/hydration/samples/head-corrupted-2/head.svelte b/packages/svelte/tests/hydration/samples/head-corrupted-2/head.svelte new file mode 100644 index 000000000000..07f1acef653f --- /dev/null +++ b/packages/svelte/tests/hydration/samples/head-corrupted-2/head.svelte @@ -0,0 +1,6 @@ + + + {@render children()} + diff --git a/packages/svelte/tests/hydration/samples/head-corrupted-2/main.svelte b/packages/svelte/tests/hydration/samples/head-corrupted-2/main.svelte new file mode 100644 index 000000000000..471ebd12cf94 --- /dev/null +++ b/packages/svelte/tests/hydration/samples/head-corrupted-2/main.svelte @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/packages/svelte/tests/hydration/samples/head-corrupted-3/_config.js b/packages/svelte/tests/hydration/samples/head-corrupted-3/_config.js new file mode 100644 index 000000000000..29cd156f927a --- /dev/null +++ b/packages/svelte/tests/hydration/samples/head-corrupted-3/_config.js @@ -0,0 +1,8 @@ +import { test } from '../../test'; + +export default test({ + expect_hydration_error: true, + test(assert, target, snapshot, component, window) { + assert.equal(window.document.querySelectorAll('meta').length, 2); + } +}); diff --git a/packages/svelte/tests/hydration/samples/head-corrupted-3/_expected_head.html b/packages/svelte/tests/hydration/samples/head-corrupted-3/_expected_head.html new file mode 100644 index 000000000000..ae8f27c2d2e0 --- /dev/null +++ b/packages/svelte/tests/hydration/samples/head-corrupted-3/_expected_head.html @@ -0,0 +1 @@ + diff --git a/packages/svelte/tests/hydration/samples/head-corrupted-3/_override_head.html b/packages/svelte/tests/hydration/samples/head-corrupted-3/_override_head.html new file mode 100644 index 000000000000..104301cd90ad --- /dev/null +++ b/packages/svelte/tests/hydration/samples/head-corrupted-3/_override_head.html @@ -0,0 +1,2 @@ + + diff --git a/packages/svelte/tests/hydration/samples/head-corrupted-3/main.svelte b/packages/svelte/tests/hydration/samples/head-corrupted-3/main.svelte new file mode 100644 index 000000000000..57b03a95a487 --- /dev/null +++ b/packages/svelte/tests/hydration/samples/head-corrupted-3/main.svelte @@ -0,0 +1,10 @@ + + + + + + + +
Just a dummy page.
diff --git a/packages/svelte/tests/hydration/samples/head-corrupted/_config.js b/packages/svelte/tests/hydration/samples/head-corrupted/_config.js new file mode 100644 index 000000000000..29cd156f927a --- /dev/null +++ b/packages/svelte/tests/hydration/samples/head-corrupted/_config.js @@ -0,0 +1,8 @@ +import { test } from '../../test'; + +export default test({ + expect_hydration_error: true, + test(assert, target, snapshot, component, window) { + assert.equal(window.document.querySelectorAll('meta').length, 2); + } +}); diff --git a/packages/svelte/tests/hydration/samples/head-corrupted/_expected_head.html b/packages/svelte/tests/hydration/samples/head-corrupted/_expected_head.html new file mode 100644 index 000000000000..c3aeef232a69 --- /dev/null +++ b/packages/svelte/tests/hydration/samples/head-corrupted/_expected_head.html @@ -0,0 +1 @@ + diff --git a/packages/svelte/tests/hydration/samples/head-corrupted/_override_head.html b/packages/svelte/tests/hydration/samples/head-corrupted/_override_head.html new file mode 100644 index 000000000000..6e11ce19d413 --- /dev/null +++ b/packages/svelte/tests/hydration/samples/head-corrupted/_override_head.html @@ -0,0 +1 @@ + diff --git a/packages/svelte/tests/hydration/samples/head-corrupted/main.svelte b/packages/svelte/tests/hydration/samples/head-corrupted/main.svelte new file mode 100644 index 000000000000..fee6ed54a3a9 --- /dev/null +++ b/packages/svelte/tests/hydration/samples/head-corrupted/main.svelte @@ -0,0 +1,6 @@ + + + + + +
Just a dummy page.