From 78506c0e077da2788dfcdd55bdb3220dd6e68bbf Mon Sep 17 00:00:00 2001 From: Simon Holthausen Date: Thu, 8 Feb 2024 18:13:19 +0100 Subject: [PATCH 1/4] fix: handle sole empty expression tags When there's only a single expression tag and its value evaluates to the empty string, special handling is needed to create and insert a text node fixes #10426 --- .changeset/silent-apes-report.md | 5 +++++ packages/svelte/src/internal/client/operations.js | 2 +- packages/svelte/src/internal/client/render.js | 15 +++++++++++++-- .../hydration/samples/text-empty/_after.html | 1 + .../hydration/samples/text-empty/_before.html | 1 + .../tests/hydration/samples/text-empty/_config.js | 3 +++ .../hydration/samples/text-empty/main.svelte | 5 +++++ 7 files changed, 29 insertions(+), 3 deletions(-) create mode 100644 .changeset/silent-apes-report.md create mode 100644 packages/svelte/tests/hydration/samples/text-empty/_after.html create mode 100644 packages/svelte/tests/hydration/samples/text-empty/_before.html create mode 100644 packages/svelte/tests/hydration/samples/text-empty/_config.js create mode 100644 packages/svelte/tests/hydration/samples/text-empty/main.svelte diff --git a/.changeset/silent-apes-report.md b/.changeset/silent-apes-report.md new file mode 100644 index 000000000000..f7f96440beb6 --- /dev/null +++ b/.changeset/silent-apes-report.md @@ -0,0 +1,5 @@ +--- +"svelte": patch +--- + +fix: handle sole empty expression tags diff --git a/packages/svelte/src/internal/client/operations.js b/packages/svelte/src/internal/client/operations.js index 372b1e510ab4..d74b473440db 100644 --- a/packages/svelte/src/internal/client/operations.js +++ b/packages/svelte/src/internal/client/operations.js @@ -201,7 +201,7 @@ export function child_frag(node, is_text) { return text; } - if (first_node !== null) { + if (first_node) { return capture_fragment_from_node(first_node); } diff --git a/packages/svelte/src/internal/client/render.js b/packages/svelte/src/internal/client/render.js index eb51d41530e5..b5b42c7ece55 100644 --- a/packages/svelte/src/internal/client/render.js +++ b/packages/svelte/src/internal/client/render.js @@ -212,11 +212,22 @@ const space_template = template(' ', false); const comment_template = template('', true); /** - * @param {null | Text | Comment | Element} anchor + * @param {Text | Comment | Element | null} anchor */ /*#__NO_SIDE_EFFECTS__*/ export function space(anchor) { - return open(anchor, true, space_template); + /** @type {any} */ + var node = open(anchor, true, space_template); + // if an {expression} is empty during SSR, there might be no + // text node to hydrate (or a anchor comment is falsely detected instead) + // — we must therefore create one + if (current_hydration_fragment !== null && node?.nodeType !== 3) { + node = document.createTextNode(''); + // @ts-ignore in this case the anchor should always be a comment, + // if not something more fundamental is wrong and throwing here is better to bail out early + anchor.parentElement.insertBefore(node, anchor.nextSibling); + } + return node; } /** diff --git a/packages/svelte/tests/hydration/samples/text-empty/_after.html b/packages/svelte/tests/hydration/samples/text-empty/_after.html new file mode 100644 index 000000000000..5592a725f3bc --- /dev/null +++ b/packages/svelte/tests/hydration/samples/text-empty/_after.html @@ -0,0 +1 @@ +x \ No newline at end of file diff --git a/packages/svelte/tests/hydration/samples/text-empty/_before.html b/packages/svelte/tests/hydration/samples/text-empty/_before.html new file mode 100644 index 000000000000..a8cad39ae7f4 --- /dev/null +++ b/packages/svelte/tests/hydration/samples/text-empty/_before.html @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/svelte/tests/hydration/samples/text-empty/_config.js b/packages/svelte/tests/hydration/samples/text-empty/_config.js new file mode 100644 index 000000000000..f47bee71df87 --- /dev/null +++ b/packages/svelte/tests/hydration/samples/text-empty/_config.js @@ -0,0 +1,3 @@ +import { test } from '../../test'; + +export default test({}); diff --git a/packages/svelte/tests/hydration/samples/text-empty/main.svelte b/packages/svelte/tests/hydration/samples/text-empty/main.svelte new file mode 100644 index 000000000000..d88ec4833c04 --- /dev/null +++ b/packages/svelte/tests/hydration/samples/text-empty/main.svelte @@ -0,0 +1,5 @@ + + +{x} From 88fa7d30d699c9f7a93768be574fc258f2ed015d Mon Sep 17 00:00:00 2001 From: Simon Holthausen Date: Fri, 9 Feb 2024 12:06:24 +0100 Subject: [PATCH 2/4] fix --- packages/svelte/src/internal/client/each.js | 3 +-- .../svelte/src/internal/client/hydration.js | 9 ++++++- .../svelte/src/internal/client/operations.js | 13 +++++++--- packages/svelte/src/internal/client/render.js | 25 ++++++------------- .../svelte/src/internal/client/transitions.js | 3 +-- 5 files changed, 27 insertions(+), 26 deletions(-) diff --git a/packages/svelte/src/internal/client/each.js b/packages/svelte/src/internal/client/each.js index 68b5f446c272..25df8dce7362 100644 --- a/packages/svelte/src/internal/client/each.js +++ b/packages/svelte/src/internal/client/each.js @@ -13,9 +13,8 @@ import { hydrate_block_anchor, set_current_hydration_fragment } from './hydration.js'; -import { clear_text_content, map_get, map_set } from './operations.js'; +import { clear_text_content, empty, map_get, map_set } from './operations.js'; import { insert, remove } from './reconciler.js'; -import { empty } from './render.js'; import { destroy_signal, execute_effect, diff --git a/packages/svelte/src/internal/client/hydration.js b/packages/svelte/src/internal/client/hydration.js index cd2e84e4a08b..a8067ff89f1b 100644 --- a/packages/svelte/src/internal/client/hydration.js +++ b/packages/svelte/src/internal/client/hydration.js @@ -1,5 +1,6 @@ // Handle hydration +import { empty } from './operations.js'; import { schedule_task } from './runtime.js'; /** @type {null | Array} */ @@ -16,9 +17,10 @@ export function set_current_hydration_fragment(fragment) { /** * Returns all nodes between the first `` comment tag pair encountered. * @param {Node | null} node + * @param {boolean} [insert_text] Whether to insert an empty text node if the fragment is empty * @returns {Array | null} */ -export function get_hydration_fragment(node) { +export function get_hydration_fragment(node, insert_text = false) { /** @type {Array} */ const fragment = []; @@ -37,6 +39,11 @@ export function get_hydration_fragment(node) { if (target_depth === null) { target_depth = depth; } else if (depth === target_depth) { + if (insert_text && fragment.length === 0) { + const text = empty(); + fragment.push(text); + /** @type {Node} */ (current_node.parentNode).insertBefore(text, current_node); + } return fragment; } else { fragment.push(/** @type {Text | Comment | Element} */ (current_node)); diff --git a/packages/svelte/src/internal/client/operations.js b/packages/svelte/src/internal/client/operations.js index d74b473440db..3f0541a6099d 100644 --- a/packages/svelte/src/internal/client/operations.js +++ b/packages/svelte/src/internal/client/operations.js @@ -158,6 +158,11 @@ export function clone_node(node, deep) { return /** @type {N} */ (clone_node_method.call(node, deep)); } +/** @returns {Text} */ +export function empty() { + return document.createTextNode(''); +} + /** * @template {Node} N * @param {N} node @@ -169,7 +174,7 @@ export function child(node) { if (current_hydration_fragment !== null) { // Child can be null if we have an element with a single child, like `

{text}

`, where `text` is empty if (child === null) { - const text = document.createTextNode(''); + const text = empty(); node.appendChild(text); return text; } else { @@ -193,7 +198,7 @@ export function child_frag(node, is_text) { // if an {expression} is empty during SSR, there might be no // text node to hydrate — we must therefore create one if (is_text && first_node?.nodeType !== 3) { - const text = document.createTextNode(''); + const text = empty(); current_hydration_fragment.unshift(text); if (first_node) { /** @type {DocumentFragment} */ (first_node.parentNode).insertBefore(text, first_node); @@ -221,8 +226,10 @@ export function child_frag(node, is_text) { export function sibling(node, is_text = false) { const next_sibling = next_sibling_get.call(node); if (current_hydration_fragment !== null) { + // 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 !== 3) { - const text = document.createTextNode(''); + const text = empty(); if (next_sibling) { const index = current_hydration_fragment.indexOf( /** @type {Text | Comment | Element} */ (next_sibling) diff --git a/packages/svelte/src/internal/client/render.js b/packages/svelte/src/internal/client/render.js index b5b42c7ece55..a8a3ff7e2192 100644 --- a/packages/svelte/src/internal/client/render.js +++ b/packages/svelte/src/internal/client/render.js @@ -4,6 +4,7 @@ import { child, clone_node, create_element, + empty, init_operations, map_get, map_set, @@ -75,11 +76,6 @@ const all_registerd_events = new Set(); /** @type {Set<(events: Array) => void>} */ const root_event_handles = new Set(); -/** @returns {Text} */ -export function empty() { - return document.createTextNode(''); -} - /** * @param {string} html * @param {boolean} return_fragment @@ -216,18 +212,7 @@ const comment_template = template('', true); */ /*#__NO_SIDE_EFFECTS__*/ export function space(anchor) { - /** @type {any} */ - var node = open(anchor, true, space_template); - // if an {expression} is empty during SSR, there might be no - // text node to hydrate (or a anchor comment is falsely detected instead) - // — we must therefore create one - if (current_hydration_fragment !== null && node?.nodeType !== 3) { - node = document.createTextNode(''); - // @ts-ignore in this case the anchor should always be a comment, - // if not something more fundamental is wrong and throwing here is better to bail out early - anchor.parentElement.insertBefore(node, anchor.nextSibling); - } - return node; + return open(anchor, true, space_template); } /** @@ -239,6 +224,8 @@ export function comment(anchor) { } /** + * Assign the created (or in hydration mode, traversed) dom elements to the current block + * and insert the elements into the dom (in client mode). * @param {Element | Text} dom * @param {boolean} is_fragment * @param {null | Text | Comment | Element} anchor @@ -2877,7 +2864,9 @@ export function mount(component, options) { const container = options.target; const block = create_root_block(options.intro || false); const first_child = /** @type {ChildNode} */ (container.firstChild); - const hydration_fragment = get_hydration_fragment(first_child); + // Call with insert_text == true to prevent empty {expressions} resulting in an empty + // fragment array, resulting in a hydration error down the line + const hydration_fragment = get_hydration_fragment(first_child, true); const previous_hydration_fragment = current_hydration_fragment; /** @type {Exports} */ diff --git a/packages/svelte/src/internal/client/transitions.js b/packages/svelte/src/internal/client/transitions.js index f9d0133719d3..08d09192d033 100644 --- a/packages/svelte/src/internal/client/transitions.js +++ b/packages/svelte/src/internal/client/transitions.js @@ -10,8 +10,7 @@ import { ROOT_BLOCK } from './block.js'; import { destroy_each_item_block, get_first_element } from './each.js'; -import { append_child } from './operations.js'; -import { empty } from './render.js'; +import { append_child, empty } from './operations.js'; import { current_block, current_effect, From b27e326fc082ef8dd1ff8b2a29ace8b393ec4dde Mon Sep 17 00:00:00 2001 From: Simon Holthausen Date: Fri, 9 Feb 2024 12:20:35 +0100 Subject: [PATCH 3/4] need this, too --- packages/svelte/src/internal/client/render.js | 13 ++++++++++++- .../hydration/samples/if-block-empty/_after.html | 4 ++++ .../hydration/samples/if-block-empty/_before.html | 4 ++++ .../hydration/samples/if-block-empty/_config.js | 3 +++ .../hydration/samples/if-block-empty/main.svelte | 7 +++++++ 5 files changed, 30 insertions(+), 1 deletion(-) create mode 100644 packages/svelte/tests/hydration/samples/if-block-empty/_after.html create mode 100644 packages/svelte/tests/hydration/samples/if-block-empty/_before.html create mode 100644 packages/svelte/tests/hydration/samples/if-block-empty/_config.js create mode 100644 packages/svelte/tests/hydration/samples/if-block-empty/main.svelte diff --git a/packages/svelte/src/internal/client/render.js b/packages/svelte/src/internal/client/render.js index a8a3ff7e2192..458f4ec68434 100644 --- a/packages/svelte/src/internal/client/render.js +++ b/packages/svelte/src/internal/client/render.js @@ -212,7 +212,18 @@ const comment_template = template('', true); */ /*#__NO_SIDE_EFFECTS__*/ export function space(anchor) { - return open(anchor, true, space_template); + /** @type {Node | null} */ + var node = /** @type {any} */ (open(anchor, true, space_template)); + // if an {expression} is empty during SSR, there might be no + // text node to hydrate (or an anchor comment is falsely detected instead) + // — we must therefore create one + if (current_hydration_fragment !== null && node?.nodeType !== 3) { + node = empty(); + // @ts-ignore in this case the anchor should always be a comment, + // if not something more fundamental is wrong and throwing here is better to bail out early + anchor.parentElement.insertBefore(node, anchor); + } + return node; } /** diff --git a/packages/svelte/tests/hydration/samples/if-block-empty/_after.html b/packages/svelte/tests/hydration/samples/if-block-empty/_after.html new file mode 100644 index 000000000000..9c71a6ffeeda --- /dev/null +++ b/packages/svelte/tests/hydration/samples/if-block-empty/_after.html @@ -0,0 +1,4 @@ + + +x + diff --git a/packages/svelte/tests/hydration/samples/if-block-empty/_before.html b/packages/svelte/tests/hydration/samples/if-block-empty/_before.html new file mode 100644 index 000000000000..88c34c8f29f4 --- /dev/null +++ b/packages/svelte/tests/hydration/samples/if-block-empty/_before.html @@ -0,0 +1,4 @@ + + + + diff --git a/packages/svelte/tests/hydration/samples/if-block-empty/_config.js b/packages/svelte/tests/hydration/samples/if-block-empty/_config.js new file mode 100644 index 000000000000..f47bee71df87 --- /dev/null +++ b/packages/svelte/tests/hydration/samples/if-block-empty/_config.js @@ -0,0 +1,3 @@ +import { test } from '../../test'; + +export default test({}); diff --git a/packages/svelte/tests/hydration/samples/if-block-empty/main.svelte b/packages/svelte/tests/hydration/samples/if-block-empty/main.svelte new file mode 100644 index 000000000000..0037e21829a4 --- /dev/null +++ b/packages/svelte/tests/hydration/samples/if-block-empty/main.svelte @@ -0,0 +1,7 @@ + + +{#if true} + {foo} +{/if} From 4ef53cfdcc505df65560122495aee19e66714969 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 9 Feb 2024 14:44:51 -0500 Subject: [PATCH 4/4] Update packages/svelte/src/internal/client/operations.js --- packages/svelte/src/internal/client/operations.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/svelte/src/internal/client/operations.js b/packages/svelte/src/internal/client/operations.js index 3f0541a6099d..5b6b3f93e128 100644 --- a/packages/svelte/src/internal/client/operations.js +++ b/packages/svelte/src/internal/client/operations.js @@ -206,7 +206,7 @@ export function child_frag(node, is_text) { return text; } - if (first_node) { + if (first_node !== null) { return capture_fragment_from_node(first_node); }