diff --git a/.changeset/unlucky-nails-stare.md b/.changeset/unlucky-nails-stare.md new file mode 100644 index 0000000000..1cb63a366e --- /dev/null +++ b/.changeset/unlucky-nails-stare.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: `pre` tag with single `\n` incorrect hydration 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 2c2c287f12..b403eea159 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 @@ -393,13 +393,21 @@ export function RegularElement(node, context) { arg = b.member(arg, 'content'); } - process_children(trimmed, (is_text) => b.call('$.child', arg, is_text && b.true), true, { - ...context, - state: child_state - }); + process_children( + trimmed, + (is_text) => b.call('$.child', arg, is_text && b.true), + true, + { + ...context, + state: child_state + }, + node.name === 'pre' + ); if (needs_reset) { - child_state.init.push(b.stmt(b.call('$.reset', context.state.node))); + child_state.init.push( + b.stmt(b.call('$.reset', context.state.node, node.name === 'pre' ? b.true : undefined)) + ); } } 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 7674fd1eb2..c2bc6d38e8 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 @@ -14,8 +14,9 @@ import { build_template_chunk, build_update } from './utils.js'; * @param {(is_text: boolean) => Expression} initial * @param {boolean} is_element * @param {ComponentContext} context + * @param {boolean} [is_pre] */ -export function process_children(nodes, initial, is_element, { visit, state }) { +export function process_children(nodes, initial, is_element, { visit, state }, is_pre = false) { const within_bound_contenteditable = state.metadata.bound_contenteditable; let prev = initial; let skipped = 0; @@ -62,7 +63,13 @@ export function process_children(nodes, initial, is_element, { visit, state }) { */ function flush_sequence(sequence) { if (sequence.every((node) => node.type === 'Text')) { - skipped += 1; + // a pre tag for some reason skips the first text node if it's a single + // \n so if that's the case we need to not increase skipped or hydration + // will fail because `child` will return the actual child and `sibling will + // return a text node + if (!(is_pre && sequence.length === 1 && sequence[0].raw === '\n')) { + skipped += 1; + } state.template.push(sequence.map((node) => node.raw).join('')); return; } diff --git a/packages/svelte/src/internal/client/dom/hydration.js b/packages/svelte/src/internal/client/dom/hydration.js index 8523ff97d5..a2cd3c0486 100644 --- a/packages/svelte/src/internal/client/dom/hydration.js +++ b/packages/svelte/src/internal/client/dom/hydration.js @@ -43,12 +43,24 @@ export function hydrate_next() { return set_hydrate_node(/** @type {TemplateNode} */ (get_next_sibling(hydrate_node))); } -/** @param {TemplateNode} node */ -export function reset(node) { +/** + * @param {TemplateNode} node + * @param {boolean} [is_pre] + */ +export function reset(node, is_pre = false) { if (!hydrating) return; + let sibling = get_next_sibling(hydrate_node); + // if the first text child of a pre tag is a single \n + // we don't skip to the sibling (for some reason if the firstChild + // of a pre tag is a single \n text node it's skipped by the browser) + // it might happen that the at this point there's still a \n sibling + // in that case is fine to get the next sibling and check for that + if (is_pre && sibling) { + sibling = get_next_sibling(sibling); + } // If the node has remaining siblings, something has gone wrong - if (get_next_sibling(hydrate_node) !== null) { + if (sibling !== null) { w.hydration_mismatch(); throw HYDRATION_ERROR; } diff --git a/packages/svelte/tests/hydration/samples/pre-first-node-backslash-n/_config.js b/packages/svelte/tests/hydration/samples/pre-first-node-backslash-n/_config.js new file mode 100644 index 0000000000..f47bee71df --- /dev/null +++ b/packages/svelte/tests/hydration/samples/pre-first-node-backslash-n/_config.js @@ -0,0 +1,3 @@ +import { test } from '../../test'; + +export default test({}); diff --git a/packages/svelte/tests/hydration/samples/pre-first-node-backslash-n/_expected.html b/packages/svelte/tests/hydration/samples/pre-first-node-backslash-n/_expected.html new file mode 100644 index 0000000000..b2e8065031 --- /dev/null +++ b/packages/svelte/tests/hydration/samples/pre-first-node-backslash-n/_expected.html @@ -0,0 +1,2 @@ +
+
\ No newline at end of file diff --git a/packages/svelte/tests/hydration/samples/pre-first-node-backslash-n/main.svelte b/packages/svelte/tests/hydration/samples/pre-first-node-backslash-n/main.svelte new file mode 100644 index 0000000000..abd29eed72 --- /dev/null +++ b/packages/svelte/tests/hydration/samples/pre-first-node-backslash-n/main.svelte @@ -0,0 +1,7 @@ + + +
+
{name}
+
\ No newline at end of file