From 8a243a3141196052f0ff75d65f47df177574211b Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 29 Jan 2026 20:28:44 -0500 Subject: [PATCH] add test, fix --- .../src/internal/client/dom/operations.js | 25 +++++++++++++++++++ .../src/internal/client/dom/template.js | 18 +++---------- .../hydrate-large-text-node/_config.js | 24 ++++++++++++++++++ .../hydrate-large-text-node/main.svelte | 5 ++++ packages/svelte/tests/runtime-browser/test.ts | 2 +- 5 files changed, 58 insertions(+), 16 deletions(-) create mode 100644 packages/svelte/tests/runtime-browser/samples/hydrate-large-text-node/_config.js create mode 100644 packages/svelte/tests/runtime-browser/samples/hydrate-large-text-node/main.svelte diff --git a/packages/svelte/src/internal/client/dom/operations.js b/packages/svelte/src/internal/client/dom/operations.js index 479c2ba0a5..2912107507 100644 --- a/packages/svelte/src/internal/client/dom/operations.js +++ b/packages/svelte/src/internal/client/dom/operations.js @@ -122,6 +122,10 @@ export function child(node, is_text) { return text; } + if (is_text) { + merge_text_nodes(/** @type {Text} */ (child)); + } + set_hydrate_node(child); return child; } @@ -258,3 +262,24 @@ export function set_attribute(element, key, value = '') { } return element.setAttribute(key, value); } + +/** + * Browsers split text nodes larger than 65536 bytes when parsing. + * For hydration to succeed, we need to stitch them back together + * @param {Text} text + */ +export function merge_text_nodes(text) { + if (/** @type {string} */ (text.nodeValue).length < 65536) { + return; + } + + let next = text.nextSibling; + + while (next !== null && next.nodeType === TEXT_NODE) { + next.remove(); + + /** @type {string} */ (text.nodeValue) += /** @type {string} */ (next.nodeValue); + + next = text.nextSibling; + } +} diff --git a/packages/svelte/src/internal/client/dom/template.js b/packages/svelte/src/internal/client/dom/template.js index 64f8b6f817..567fbeabf0 100644 --- a/packages/svelte/src/internal/client/dom/template.js +++ b/packages/svelte/src/internal/client/dom/template.js @@ -9,7 +9,8 @@ import { create_element, create_fragment, create_comment, - set_attribute + set_attribute, + merge_text_nodes } from './operations.js'; import { create_fragment_from_html } from './reconciler.js'; import { active_effect } from '../runtime.js'; @@ -312,20 +313,7 @@ export function text(value = '') { node.before((node = create_text())); set_hydrate_node(node); } else { - // Browsers may split text nodes > 65536 characters into multiple consecutive text nodes - // during HTML parsing. We need to merge them into a single text node. - var next = get_next_sibling(node); - var merged_text = /** @type {Text} */ (node).textContent; - - while (next !== null && next.nodeType === TEXT_NODE) { - merged_text += /** @type {Text} */ (next).textContent; - var to_remove = next; - next = get_next_sibling(next); - to_remove.remove(); - } - - // Update the text node with the merged content - /** @type {Text} */ (node).textContent = merged_text; + merge_text_nodes(/** @type {Text} */ (node)); } assign_nodes(node, node); diff --git a/packages/svelte/tests/runtime-browser/samples/hydrate-large-text-node/_config.js b/packages/svelte/tests/runtime-browser/samples/hydrate-large-text-node/_config.js new file mode 100644 index 0000000000..3a52b1ccd3 --- /dev/null +++ b/packages/svelte/tests/runtime-browser/samples/hydrate-large-text-node/_config.js @@ -0,0 +1,24 @@ +import { test } from '../../assert'; + +// Browsers split text nodes > 65536 characters into multiple consecutive text nodes +// during HTML parsing. This test verifies that hydration correctly merges them. +const LARGE_TEXT = 'x'.repeat(70000); + +export default test({ + mode: ['hydrate'], + skip_mode: ['client'], + + props: { + text: LARGE_TEXT + }, + + async test({ assert, target }) { + const [p] = target.querySelectorAll('p'); + + // The text content should be preserved after hydration + assert.equal(p.textContent?.trim(), LARGE_TEXT); + // After hydration, there should be only one text node (plus possible comment nodes) + const textNodes = [...p.childNodes].filter((node) => node.nodeType === 3); + assert.equal(textNodes.length, 1, `Expected 1 text node, got ${textNodes.length}`); + } +}); diff --git a/packages/svelte/tests/runtime-browser/samples/hydrate-large-text-node/main.svelte b/packages/svelte/tests/runtime-browser/samples/hydrate-large-text-node/main.svelte new file mode 100644 index 0000000000..d65358dabc --- /dev/null +++ b/packages/svelte/tests/runtime-browser/samples/hydrate-large-text-node/main.svelte @@ -0,0 +1,5 @@ + + +

{text}

diff --git a/packages/svelte/tests/runtime-browser/test.ts b/packages/svelte/tests/runtime-browser/test.ts index 525f9ba89f..54cdc0f8be 100644 --- a/packages/svelte/tests/runtime-browser/test.ts +++ b/packages/svelte/tests/runtime-browser/test.ts @@ -213,7 +213,7 @@ async function run_test( } // uncomment to see what was generated - // fs.writeFileSync(`${test_dir}/_actual.js`, build_result.outputFiles[0].text); + // fs.writeFileSync(`${test_dir}/_output/bundle-${hydrate}.js`, build_result.outputFiles[0].text); const test_result = await page.evaluate( build_result.outputFiles[0].text + ";test.default(document.querySelector('main'))" );