Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -25,15 +26,19 @@ 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);
}

return b.call(
'$.sibling',
prev(false),
b.literal(node_type),
(is_text || skipped !== 1) && b.literal(skipped),
is_text && b.true
);
Expand All @@ -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') {
Expand Down
38 changes: 37 additions & 1 deletion packages/svelte/src/internal/client/dom/blocks/svelte-head.js
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand Down Expand Up @@ -58,6 +59,41 @@ export function head(render_fn) {

try {
block(() => render_fn(anchor), HEAD_EFFECT);
if (hydrating) {
if (hydrate_node === null || /** @type {Comment} */ (hydrate_node).data !== HYDRATION_END) {
hydration_mismatch();
throw HYDRATION_ERROR;
}
}
} 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);
Expand Down
28 changes: 22 additions & 6 deletions packages/svelte/src/internal/client/dom/operations.js
Original file line number Diff line number Diff line change
Expand Up @@ -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} */
Expand Down Expand Up @@ -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
Expand All @@ -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;
}

/**
Expand Down
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>
Original file line number Diff line number Diff line change
Expand Up @@ -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 ?? ''}`);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()));

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,19 +12,19 @@ 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];

var text = $.child(button);

$.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) ?? ''}`));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Loading
Loading