diff --git a/.changeset/stupid-rivers-stare.md b/.changeset/stupid-rivers-stare.md new file mode 100644 index 0000000000..d1d730cd21 --- /dev/null +++ b/.changeset/stupid-rivers-stare.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: provide more hydration mismatch coverage diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js index 2063027782..94864fecef 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js @@ -327,9 +327,8 @@ export function RegularElement(node, context) { // set the value of `hydrate_node` to `node.content` if (node.name === 'template') { needs_reset = true; - + child_state.init.push(b.stmt(b.call('$.hydrate_template', arg))); arg = b.member(arg, b.id('content')); - child_state.init.push(b.stmt(b.call('$.reset', arg))); } process_children(trimmed, () => b.call('$.child', arg), true, { diff --git a/packages/svelte/src/internal/client/dom/hydration.js b/packages/svelte/src/internal/client/dom/hydration.js index 5f0880a676..82ad0df613 100644 --- a/packages/svelte/src/internal/client/dom/hydration.js +++ b/packages/svelte/src/internal/client/dom/hydration.js @@ -30,21 +30,38 @@ export let hydrate_node; /** @param {TemplateNode} node */ export function set_hydrate_node(node) { + if (node === null) { + w.hydration_mismatch(); + throw HYDRATION_ERROR; + } + return (hydrate_node = node); } export function hydrate_next() { - if (hydrate_node === null) { + return set_hydrate_node(/** @type {TemplateNode} */ (hydrate_node.nextSibling)); +} + +/** @param {TemplateNode} node */ +export function reset(node) { + if (!hydrating) return; + + // If the node has remaining siblings, something has gone wrong + if (hydrate_node.nextSibling !== null) { w.hydration_mismatch(); throw HYDRATION_ERROR; } - return (hydrate_node = /** @type {TemplateNode} */ (hydrate_node.nextSibling)); + + hydrate_node = node; } -/** @param {TemplateNode} node */ -export function reset(node) { +/** + * @param {HTMLTemplateElement} template + */ +export function hydrate_template(template) { if (hydrating) { - hydrate_node = node; + // @ts-expect-error TemplateNode doesn't include DocumentFragment, but it's actually fine + hydrate_node = template.content; } } diff --git a/packages/svelte/src/internal/client/index.js b/packages/svelte/src/internal/client/index.js index edd50bff32..d1885e7d00 100644 --- a/packages/svelte/src/internal/client/index.js +++ b/packages/svelte/src/internal/client/index.js @@ -66,7 +66,7 @@ export { bind_focused } from './dom/elements/bindings/universal.js'; export { bind_window_scroll, bind_window_size } from './dom/elements/bindings/window.js'; -export { next, reset } from './dom/hydration.js'; +export { hydrate_template, next, reset } from './dom/hydration.js'; export { once, preventDefault, diff --git a/packages/svelte/tests/hydration/samples/safari-borking/_config.js b/packages/svelte/tests/hydration/samples/safari-borking/_config.js new file mode 100644 index 0000000000..cf22ff2c85 --- /dev/null +++ b/packages/svelte/tests/hydration/samples/safari-borking/_config.js @@ -0,0 +1,5 @@ +import { test } from '../../test'; + +export default test({ + expect_hydration_error: true +}); diff --git a/packages/svelte/tests/hydration/samples/safari-borking/_expected.html b/packages/svelte/tests/hydration/samples/safari-borking/_expected.html new file mode 100644 index 0000000000..3f23afe0bf --- /dev/null +++ b/packages/svelte/tests/hydration/samples/safari-borking/_expected.html @@ -0,0 +1 @@ +

call +636-555-3226 now

diff --git a/packages/svelte/tests/hydration/samples/safari-borking/_override.html b/packages/svelte/tests/hydration/samples/safari-borking/_override.html new file mode 100644 index 0000000000..fcbb5c1830 --- /dev/null +++ b/packages/svelte/tests/hydration/samples/safari-borking/_override.html @@ -0,0 +1 @@ +

call +636-555-3226 now

diff --git a/packages/svelte/tests/hydration/samples/safari-borking/main.svelte b/packages/svelte/tests/hydration/samples/safari-borking/main.svelte new file mode 100644 index 0000000000..1385fbb2a4 --- /dev/null +++ b/packages/svelte/tests/hydration/samples/safari-borking/main.svelte @@ -0,0 +1,5 @@ + + +

{message}

diff --git a/packages/svelte/tests/hydration/samples/surrounding-whitespace/_expected.html b/packages/svelte/tests/hydration/samples/surrounding-whitespace/_expected.html new file mode 100644 index 0000000000..e728b682d0 --- /dev/null +++ b/packages/svelte/tests/hydration/samples/surrounding-whitespace/_expected.html @@ -0,0 +1,2 @@ + + hello diff --git a/packages/svelte/tests/hydration/test.ts b/packages/svelte/tests/hydration/test.ts index d592a65de3..7247778d0f 100644 --- a/packages/svelte/tests/hydration/test.ts +++ b/packages/svelte/tests/hydration/test.ts @@ -113,15 +113,16 @@ const { test, run } = suite(async (config, cwd) => { throw new Error(`Unexpected errors: ${errors.join('\n')}`); } - if (!override) { - const expected = read(`${cwd}/_expected.html`) ?? rendered.html; - flushSync(); - assert.equal(target.innerHTML.trim(), expected.trim()); - } + flushSync(); + + const normalize = (string: string) => string.trim().replace(/\r\n/g, '\n'); + + const expected = read(`${cwd}/_expected.html`) ?? rendered.html; + assert.equal(normalize(target.innerHTML), normalize(expected)); if (rendered.head) { const expected = read(`${cwd}/_expected_head.html`) ?? rendered.head; - assert.equal(head.innerHTML.trim(), expected.trim()); + assert.equal(normalize(head.innerHTML), normalize(expected)); } if (config.snapshot) { diff --git a/playgrounds/demo/ssr-dev.js b/playgrounds/demo/ssr-dev.js index 617b49e652..65390b70ca 100644 --- a/playgrounds/demo/ssr-dev.js +++ b/playgrounds/demo/ssr-dev.js @@ -27,7 +27,9 @@ polka() const html = transformed_template .replace(``, head) - .replace(``, body); + .replace(``, body) + // check that Safari doesn't break hydration + .replaceAll('+636-555-3226', '+636-555-3226'); res.writeHead(200, { 'Content-Type': 'text/html' }).end(html); })