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/compiler/phases/3-transform/client/visitors/shared/fragment.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/fragment.js
index 62d07014eea4..1dd2d04f73a6 100644
--- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/fragment.js
+++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/fragment.js
@@ -6,6 +6,7 @@ import { is_event_attribute, is_text_attribute } from '../../../../../utils/ast.
import * as b from '#compiler/builders';
import { is_custom_element_node } from '../../../../nodes.js';
import { build_template_chunk } from './utils.js';
+import { ELEMENT_NODE, TEXT_NODE } from '#client/constants';
/**
* Processes an array of template nodes, joining sibling text/expression nodes
@@ -25,8 +26,11 @@ export function process_children(nodes, initial, is_element, context) {
/** @type {Sequence} */
let sequence = [];
- /** @param {boolean} is_text */
- function get_node(is_text) {
+ /**
+ * @param {boolean} is_text
+ * @param {number} node_type
+ **/
+ function get_node(is_text, node_type) {
if (skipped === 0) {
return prev(is_text);
}
@@ -34,6 +38,7 @@ export function process_children(nodes, initial, is_element, context) {
return b.call(
'$.sibling',
prev(false),
+ b.literal(node_type),
(is_text || skipped !== 1) && b.literal(skipped),
is_text && b.true
);
@@ -44,7 +49,10 @@ export function process_children(nodes, initial, is_element, context) {
* @param {string} name
*/
function flush_node(is_text, name) {
- const expression = get_node(is_text);
+ const expression = get_node(
+ is_text,
+ name === 'text' ? TEXT_NODE : name === 'node' ? 0 : ELEMENT_NODE
+ );
let id = expression;
if (id.type !== 'Identifier') {
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..02c4f622106e 100644
--- a/packages/svelte/src/internal/client/dom/blocks/svelte-head.js
+++ b/packages/svelte/src/internal/client/dom/blocks/svelte-head.js
@@ -3,7 +3,8 @@ 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}
@@ -58,6 +59,36 @@ export function head(render_fn) {
try {
block(() => render_fn(anchor), HEAD_EFFECT);
+ check_end();
+ } catch (error) {
+ // re-mount only this svelte:head
+ if (was_hydrating && head_anchor && error === HYDRATION_ERROR) {
+ // Here head_anchor is the node next after HYDRATION_START
+ /** @type {Node | null} */
+ let prev = head_anchor.previousSibling;
+ /** @type {Node | null} */
+ let next = head_anchor;
+ // remove nodes that failed to hydrate
+ while (
+ prev !== null &&
+ (prev.nodeType !== COMMENT_NODE || /** @type {Comment} */ (prev).data !== HYDRATION_END)
+ ) {
+ document.head.removeChild(prev);
+ prev = next;
+ next = get_next_sibling(/** @type {Node} */ (next));
+ }
+ if (prev?.parentNode) document.head.removeChild(prev);
+ if (next !== null) {
+ // allow the next head block try to hydrate
+ head_anchor = set_hydrate_node(/** @type {TemplateNode} */ (next));
+ }
+
+ 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 +97,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/src/internal/client/dom/operations.js b/packages/svelte/src/internal/client/dom/operations.js
index 4b35f0802f4a..099c22b8d774 100644
--- a/packages/svelte/src/internal/client/dom/operations.js
+++ b/packages/svelte/src/internal/client/dom/operations.js
@@ -3,7 +3,9 @@ import { hydrate_node, hydrating, set_hydrate_node } from './hydration.js';
import { DEV } from 'esm-env';
import { init_array_prototype_warnings } from '../dev/equality.js';
import { get_descriptor, is_extensible } from '../../shared/utils.js';
-import { TEXT_NODE } from '#client/constants';
+import { COMMENT_NODE, TEXT_NODE } from '#client/constants';
+import { HYDRATION_END, HYDRATION_ERROR } from '../../../constants.js';
+import { hydration_mismatch } from '../warnings.js';
// export these for reference in the compiled code, making global name deduplication unnecessary
/** @type {Window} */
@@ -158,26 +160,40 @@ export function first_child(fragment, is_text) {
/**
* Don't mark this as side-effect-free, hydration needs to walk all nodes
* @param {TemplateNode} node
+ * @param {number} node_type
* @param {number} count
- * @param {boolean} is_text
+ * @param {boolean} add_text
* @returns {Node | null}
*/
-export function sibling(node, count = 1, is_text = false) {
- let next_sibling = hydrating ? hydrate_node : node;
+export function sibling(node, node_type, count = 1, add_text = false) {
+ var next_sibling = hydrating ? hydrate_node : node;
var last_sibling;
while (count--) {
last_sibling = next_sibling;
next_sibling = /** @type {TemplateNode} */ (get_next_sibling(next_sibling));
+ if (
+ (next_sibling === null && !add_text) ||
+ (next_sibling?.nodeType === COMMENT_NODE &&
+ /** @type {Comment} */ (next_sibling).data === HYDRATION_END)
+ ) {
+ hydration_mismatch();
+ throw HYDRATION_ERROR;
+ }
}
if (!hydrating) {
return next_sibling;
}
+ if (hydrating && node_type !== 0 && !add_text && next_sibling?.nodeType !== node_type) {
+ hydration_mismatch();
+ throw HYDRATION_ERROR;
+ }
+
// if a sibling {expression} is empty during SSR, there might be no
// text node to hydrate — we must therefore create one
- if (is_text && next_sibling?.nodeType !== TEXT_NODE) {
+ if (add_text && next_sibling?.nodeType !== TEXT_NODE) {
var text = create_text();
// If the next sibling is `null` and we're handling text then it's because
// 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) {
}
set_hydrate_node(next_sibling);
- return /** @type {TemplateNode} */ (next_sibling);
+ return next_sibling;
}
/**
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.
diff --git a/packages/svelte/tests/snapshot/samples/await-block-scope/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/await-block-scope/_expected/client/index.svelte.js
index 9bb45ebf78e6..5fe156e51a7f 100644
--- a/packages/svelte/tests/snapshot/samples/await-block-scope/_expected/client/index.svelte.js
+++ b/packages/svelte/tests/snapshot/samples/await-block-scope/_expected/client/index.svelte.js
@@ -19,11 +19,11 @@ export default function Await_block_scope($$anchor) {
$.reset(button);
- var node = $.sibling(button, 2);
+ var node = $.sibling(button, 0, 2);
$.await(node, () => $.get(promise), null, ($$anchor, counter) => {});
- var text_1 = $.sibling(node);
+ var text_1 = $.sibling(node, 3);
$.template_effect(() => {
$.set_text(text, `clicks: ${counter.count ?? ''}`);
diff --git a/packages/svelte/tests/snapshot/samples/bind-component-snippet/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/bind-component-snippet/_expected/client/index.svelte.js
index ba3f4b155a31..d6425a4df3f9 100644
--- a/packages/svelte/tests/snapshot/samples/bind-component-snippet/_expected/client/index.svelte.js
+++ b/packages/svelte/tests/snapshot/samples/bind-component-snippet/_expected/client/index.svelte.js
@@ -27,7 +27,7 @@ export default function Bind_component_snippet($$anchor) {
}
});
- var text_1 = $.sibling(node);
+ var text_1 = $.sibling(node, 3);
$.template_effect(() => $.set_text(text_1, ` value: ${$.get(value) ?? ''}`));
$.append($$anchor, fragment);
diff --git a/packages/svelte/tests/snapshot/samples/dynamic-attributes-casing/_expected/client/main.svelte.js b/packages/svelte/tests/snapshot/samples/dynamic-attributes-casing/_expected/client/main.svelte.js
index 28bb01fb18df..ff3ec03fc220 100644
--- a/packages/svelte/tests/snapshot/samples/dynamic-attributes-casing/_expected/client/main.svelte.js
+++ b/packages/svelte/tests/snapshot/samples/dynamic-attributes-casing/_expected/client/main.svelte.js
@@ -12,17 +12,17 @@ export default function Main($$anchor) {
$.set_attribute(div, 'foobar', x);
- var svg = $.sibling(div, 2);
+ var svg = $.sibling(div, 1, 2);
$.set_attribute(svg, 'viewBox', x);
- var custom_element = $.sibling(svg, 2);
+ var custom_element = $.sibling(svg, 1, 2);
$.set_custom_element_data(custom_element, 'fooBar', x);
- var div_1 = $.sibling(custom_element, 2);
- var svg_1 = $.sibling(div_1, 2);
- var custom_element_1 = $.sibling(svg_1, 2);
+ var div_1 = $.sibling(custom_element, 1, 2);
+ var svg_1 = $.sibling(div_1, 1, 2);
+ var custom_element_1 = $.sibling(svg_1, 1, 2);
$.template_effect(() => $.set_custom_element_data(custom_element_1, 'fooBar', y()));
diff --git a/packages/svelte/tests/snapshot/samples/nullish-coallescence-omittance/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/nullish-coallescence-omittance/_expected/client/index.svelte.js
index b46acee82e14..704d8080fde7 100644
--- a/packages/svelte/tests/snapshot/samples/nullish-coallescence-omittance/_expected/client/index.svelte.js
+++ b/packages/svelte/tests/snapshot/samples/nullish-coallescence-omittance/_expected/client/index.svelte.js
@@ -12,11 +12,11 @@ export default function Nullish_coallescence_omittance($$anchor) {
h1.textContent = 'Hello, world!';
- var b = $.sibling(h1, 2);
+ var b = $.sibling(h1, 1, 2);
b.textContent = '123';
- var button = $.sibling(b, 2);
+ var button = $.sibling(b, 1, 2);
button.__click = [on_click, count];
@@ -24,7 +24,7 @@ export default function Nullish_coallescence_omittance($$anchor) {
$.reset(button);
- var h1_1 = $.sibling(button, 2);
+ var h1_1 = $.sibling(button, 1, 2);
h1_1.textContent = 'Hello, world';
$.template_effect(() => $.set_text(text, `Count is ${$.get(count) ?? ''}`));
diff --git a/packages/svelte/tests/snapshot/samples/purity/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/purity/_expected/client/index.svelte.js
index da6fdf44d881..118dac12c469 100644
--- a/packages/svelte/tests/snapshot/samples/purity/_expected/client/index.svelte.js
+++ b/packages/svelte/tests/snapshot/samples/purity/_expected/client/index.svelte.js
@@ -12,11 +12,11 @@ export default function Purity($$anchor) {
$.untrack(() => Math.max(0, Math.min(0, 100)))
);
- var p_1 = $.sibling(p, 2);
+ var p_1 = $.sibling(p, 1, 2);
p_1.textContent = ($.untrack(() => location.href));
- var node = $.sibling(p_1, 2);
+ var node = $.sibling(p_1, 0, 2);
Child(node, { prop: encodeURIComponent('hello') });
$.append($$anchor, fragment);
diff --git a/packages/svelte/tests/snapshot/samples/skip-static-subtree/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/skip-static-subtree/_expected/client/index.svelte.js
index 78147659ff40..f28b0addee36 100644
--- a/packages/svelte/tests/snapshot/samples/skip-static-subtree/_expected/client/index.svelte.js
+++ b/packages/svelte/tests/snapshot/samples/skip-static-subtree/_expected/client/index.svelte.js
@@ -5,43 +5,43 @@ var root = $.from_html(`