fix: handle sole empty expression tags (#10433)

* 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

* fix

* need this, too

* Update packages/svelte/src/internal/client/operations.js

---------

Co-authored-by: Rich Harris <richard.a.harris@gmail.com>
pull/10434/head
Simon H 11 months ago committed by GitHub
parent 456cf843d2
commit 5dd9951cb6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: handle sole empty expression tags

@ -13,9 +13,8 @@ import {
hydrate_block_anchor, hydrate_block_anchor,
set_current_hydration_fragment set_current_hydration_fragment
} from './hydration.js'; } 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 { insert, remove } from './reconciler.js';
import { empty } from './render.js';
import { import {
destroy_signal, destroy_signal,
execute_effect, execute_effect,

@ -1,5 +1,6 @@
// Handle hydration // Handle hydration
import { empty } from './operations.js';
import { schedule_task } from './runtime.js'; import { schedule_task } from './runtime.js';
/** @type {null | Array<import('./types.js').TemplateNode>} */ /** @type {null | Array<import('./types.js').TemplateNode>} */
@ -16,9 +17,10 @@ export function set_current_hydration_fragment(fragment) {
/** /**
* Returns all nodes between the first `<!--ssr:...-->` comment tag pair encountered. * Returns all nodes between the first `<!--ssr:...-->` comment tag pair encountered.
* @param {Node | null} node * @param {Node | null} node
* @param {boolean} [insert_text] Whether to insert an empty text node if the fragment is empty
* @returns {Array<import('./types.js').TemplateNode> | null} * @returns {Array<import('./types.js').TemplateNode> | null}
*/ */
export function get_hydration_fragment(node) { export function get_hydration_fragment(node, insert_text = false) {
/** @type {Array<import('./types.js').TemplateNode>} */ /** @type {Array<import('./types.js').TemplateNode>} */
const fragment = []; const fragment = [];
@ -37,6 +39,11 @@ export function get_hydration_fragment(node) {
if (target_depth === null) { if (target_depth === null) {
target_depth = depth; target_depth = depth;
} else if (depth === target_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; return fragment;
} else { } else {
fragment.push(/** @type {Text | Comment | Element} */ (current_node)); fragment.push(/** @type {Text | Comment | Element} */ (current_node));

@ -158,6 +158,11 @@ export function clone_node(node, deep) {
return /** @type {N} */ (clone_node_method.call(node, deep)); return /** @type {N} */ (clone_node_method.call(node, deep));
} }
/** @returns {Text} */
export function empty() {
return document.createTextNode('');
}
/** /**
* @template {Node} N * @template {Node} N
* @param {N} node * @param {N} node
@ -169,7 +174,7 @@ export function child(node) {
if (current_hydration_fragment !== null) { if (current_hydration_fragment !== null) {
// Child can be null if we have an element with a single child, like `<p>{text}</p>`, where `text` is empty // Child can be null if we have an element with a single child, like `<p>{text}</p>`, where `text` is empty
if (child === null) { if (child === null) {
const text = document.createTextNode(''); const text = empty();
node.appendChild(text); node.appendChild(text);
return text; return text;
} else { } else {
@ -193,7 +198,7 @@ export function child_frag(node, is_text) {
// if an {expression} is empty during SSR, there might be no // if an {expression} is empty during SSR, there might be no
// text node to hydrate — we must therefore create one // text node to hydrate — we must therefore create one
if (is_text && first_node?.nodeType !== 3) { if (is_text && first_node?.nodeType !== 3) {
const text = document.createTextNode(''); const text = empty();
current_hydration_fragment.unshift(text); current_hydration_fragment.unshift(text);
if (first_node) { if (first_node) {
/** @type {DocumentFragment} */ (first_node.parentNode).insertBefore(text, 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) { export function sibling(node, is_text = false) {
const next_sibling = next_sibling_get.call(node); const next_sibling = next_sibling_get.call(node);
if (current_hydration_fragment !== null) { 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) { if (is_text && next_sibling?.nodeType !== 3) {
const text = document.createTextNode(''); const text = empty();
if (next_sibling) { if (next_sibling) {
const index = current_hydration_fragment.indexOf( const index = current_hydration_fragment.indexOf(
/** @type {Text | Comment | Element} */ (next_sibling) /** @type {Text | Comment | Element} */ (next_sibling)

@ -4,6 +4,7 @@ import {
child, child,
clone_node, clone_node,
create_element, create_element,
empty,
init_operations, init_operations,
map_get, map_get,
map_set, map_set,
@ -75,11 +76,6 @@ const all_registerd_events = new Set();
/** @type {Set<(events: Array<string>) => void>} */ /** @type {Set<(events: Array<string>) => void>} */
const root_event_handles = new Set(); const root_event_handles = new Set();
/** @returns {Text} */
export function empty() {
return document.createTextNode('');
}
/** /**
* @param {string} html * @param {string} html
* @param {boolean} return_fragment * @param {boolean} return_fragment
@ -212,11 +208,22 @@ const space_template = template(' ', false);
const comment_template = template('<!>', true); const comment_template = template('<!>', true);
/** /**
* @param {null | Text | Comment | Element} anchor * @param {Text | Comment | Element | null} anchor
*/ */
/*#__NO_SIDE_EFFECTS__*/ /*#__NO_SIDE_EFFECTS__*/
export function space(anchor) { 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;
} }
/** /**
@ -228,6 +235,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 {Element | Text} dom
* @param {boolean} is_fragment * @param {boolean} is_fragment
* @param {null | Text | Comment | Element} anchor * @param {null | Text | Comment | Element} anchor
@ -2866,7 +2875,9 @@ export function mount(component, options) {
const container = options.target; const container = options.target;
const block = create_root_block(options.intro || false); const block = create_root_block(options.intro || false);
const first_child = /** @type {ChildNode} */ (container.firstChild); 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; const previous_hydration_fragment = current_hydration_fragment;
/** @type {Exports} */ /** @type {Exports} */

@ -10,8 +10,7 @@ import {
ROOT_BLOCK ROOT_BLOCK
} from './block.js'; } from './block.js';
import { destroy_each_item_block, get_first_element } from './each.js'; import { destroy_each_item_block, get_first_element } from './each.js';
import { append_child } from './operations.js'; import { append_child, empty } from './operations.js';
import { empty } from './render.js';
import { import {
current_block, current_block,
current_effect, current_effect,

@ -0,0 +1,4 @@
<!--ssr:0-->
<!--ssr:1-->
<!--ssr:if:true-->x<!--ssr:1-->
<!--ssr:0-->

@ -0,0 +1,4 @@
<!--ssr:0-->
<!--ssr:1-->
<!--ssr:if:true--><!--ssr:1-->
<!--ssr:0-->

@ -0,0 +1,3 @@
import { test } from '../../test';
export default test({});

@ -0,0 +1,7 @@
<script>
let foo = typeof window === 'undefined' ? '' : 'x';
</script>
{#if true}
{foo}
{/if}

@ -0,0 +1,3 @@
import { test } from '../../test';
export default test({});

@ -0,0 +1,5 @@
<script>
let x = typeof window === 'undefined' ? '' : 'x'
</script>
{x}
Loading…
Cancel
Save