Skip to content

fix: improve hydration of altered html #16226

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/angry-pigs-float.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'svelte': patch
---

fix: improve hydration of altered html
54 changes: 50 additions & 4 deletions packages/svelte/src/internal/client/dom/blocks/svelte-head.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -32,15 +33,15 @@ 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 (
head_anchor !== null &&
(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
Expand All @@ -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);
Expand All @@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -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'));
}
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
<!--[--><meta name="count" content="1"><!----><!--]--><!--[--><meta name="count" content="1"><!----><!--]-->
<!----><meta name="count" content="1"> <meta name="will-be-missing"> <meta name="count" content="1">
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<!--[--><meta name="count" content="1" /><!----><!--]--><!--[--><meta name="count" content="1" />
<meta name="count" content="1" /><!----><!--]--><!--[--><meta
name="count"
content="1"
/><!----><!--]-->
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<script>
let { children } = $props();
</script>
<svelte:head>
{@render children()}
</svelte:head>
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<script>
import Head from './head.svelte';

let count = $state(1);
</script>

<Head>
<meta name="count" content={count}>
</Head>
<Head>
<meta name="count" content={count}>
<meta name="will-be-missing">
<meta name="count" content={count}>
</Head>
<Head>
<meta name="count" content={count}>
</Head>

<button onclick={() => count++}>inc</button>
Original file line number Diff line number Diff line change
@@ -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);
}
});
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<meta name="description" content="some description"> <meta name="keywords" content="some keywords">
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
<!--[--><meta name="description" content="some description" />
<meta name="foreign" content="alien" /> <meta name="keywords" content="some keywords" /><!--]-->
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<script>
let content = "some keywords"
</script>

<svelte:head>
<meta name="description" content="some description" />
<meta name="keywords" {content} />
</svelte:head>

<div>Just a dummy page.</div>
Original file line number Diff line number Diff line change
@@ -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);
}
});
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<!--[--><meta name="description" content="some description"> <meta name="keywords" content="some keywords">
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<!--[--><meta name="description" content="some description" /><!--]-->
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<svelte:head>
<meta name="description" content="some description" />
<meta name="keywords" content="some keywords" />
</svelte:head>

<div>Just a dummy page.</div>
Loading