Skip to content

Commit 5dd9951

Browse files
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 <[email protected]>
1 parent 456cf84 commit 5dd9951

File tree

14 files changed

+72
-16
lines changed

14 files changed

+72
-16
lines changed

.changeset/silent-apes-report.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"svelte": patch
3+
---
4+
5+
fix: handle sole empty expression tags

packages/svelte/src/internal/client/each.js

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,8 @@ import {
1313
hydrate_block_anchor,
1414
set_current_hydration_fragment
1515
} from './hydration.js';
16-
import { clear_text_content, map_get, map_set } from './operations.js';
16+
import { clear_text_content, empty, map_get, map_set } from './operations.js';
1717
import { insert, remove } from './reconciler.js';
18-
import { empty } from './render.js';
1918
import {
2019
destroy_signal,
2120
execute_effect,

packages/svelte/src/internal/client/hydration.js

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
// Handle hydration
22

3+
import { empty } from './operations.js';
34
import { schedule_task } from './runtime.js';
45

56
/** @type {null | Array<import('./types.js').TemplateNode>} */
@@ -16,9 +17,10 @@ export function set_current_hydration_fragment(fragment) {
1617
/**
1718
* Returns all nodes between the first `<!--ssr:...-->` comment tag pair encountered.
1819
* @param {Node | null} node
20+
* @param {boolean} [insert_text] Whether to insert an empty text node if the fragment is empty
1921
* @returns {Array<import('./types.js').TemplateNode> | null}
2022
*/
21-
export function get_hydration_fragment(node) {
23+
export function get_hydration_fragment(node, insert_text = false) {
2224
/** @type {Array<import('./types.js').TemplateNode>} */
2325
const fragment = [];
2426

@@ -37,6 +39,11 @@ export function get_hydration_fragment(node) {
3739
if (target_depth === null) {
3840
target_depth = depth;
3941
} else if (depth === target_depth) {
42+
if (insert_text && fragment.length === 0) {
43+
const text = empty();
44+
fragment.push(text);
45+
/** @type {Node} */ (current_node.parentNode).insertBefore(text, current_node);
46+
}
4047
return fragment;
4148
} else {
4249
fragment.push(/** @type {Text | Comment | Element} */ (current_node));

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

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,11 @@ export function clone_node(node, deep) {
158158
return /** @type {N} */ (clone_node_method.call(node, deep));
159159
}
160160

161+
/** @returns {Text} */
162+
export function empty() {
163+
return document.createTextNode('');
164+
}
165+
161166
/**
162167
* @template {Node} N
163168
* @param {N} node
@@ -169,7 +174,7 @@ export function child(node) {
169174
if (current_hydration_fragment !== null) {
170175
// Child can be null if we have an element with a single child, like `<p>{text}</p>`, where `text` is empty
171176
if (child === null) {
172-
const text = document.createTextNode('');
177+
const text = empty();
173178
node.appendChild(text);
174179
return text;
175180
} else {
@@ -193,7 +198,7 @@ export function child_frag(node, is_text) {
193198
// if an {expression} is empty during SSR, there might be no
194199
// text node to hydrate — we must therefore create one
195200
if (is_text && first_node?.nodeType !== 3) {
196-
const text = document.createTextNode('');
201+
const text = empty();
197202
current_hydration_fragment.unshift(text);
198203
if (first_node) {
199204
/** @type {DocumentFragment} */ (first_node.parentNode).insertBefore(text, first_node);
@@ -221,8 +226,10 @@ export function child_frag(node, is_text) {
221226
export function sibling(node, is_text = false) {
222227
const next_sibling = next_sibling_get.call(node);
223228
if (current_hydration_fragment !== null) {
229+
// if a sibling {expression} is empty during SSR, there might be no
230+
// text node to hydrate — we must therefore create one
224231
if (is_text && next_sibling?.nodeType !== 3) {
225-
const text = document.createTextNode('');
232+
const text = empty();
226233
if (next_sibling) {
227234
const index = current_hydration_fragment.indexOf(
228235
/** @type {Text | Comment | Element} */ (next_sibling)

packages/svelte/src/internal/client/render.js

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
child,
55
clone_node,
66
create_element,
7+
empty,
78
init_operations,
89
map_get,
910
map_set,
@@ -75,11 +76,6 @@ const all_registerd_events = new Set();
7576
/** @type {Set<(events: Array<string>) => void>} */
7677
const root_event_handles = new Set();
7778

78-
/** @returns {Text} */
79-
export function empty() {
80-
return document.createTextNode('');
81-
}
82-
8379
/**
8480
* @param {string} html
8581
* @param {boolean} return_fragment
@@ -212,11 +208,22 @@ const space_template = template(' ', false);
212208
const comment_template = template('<!>', true);
213209

214210
/**
215-
* @param {null | Text | Comment | Element} anchor
211+
* @param {Text | Comment | Element | null} anchor
216212
*/
217213
/*#__NO_SIDE_EFFECTS__*/
218214
export function space(anchor) {
219-
return open(anchor, true, space_template);
215+
/** @type {Node | null} */
216+
var node = /** @type {any} */ (open(anchor, true, space_template));
217+
// if an {expression} is empty during SSR, there might be no
218+
// text node to hydrate (or an anchor comment is falsely detected instead)
219+
// — we must therefore create one
220+
if (current_hydration_fragment !== null && node?.nodeType !== 3) {
221+
node = empty();
222+
// @ts-ignore in this case the anchor should always be a comment,
223+
// if not something more fundamental is wrong and throwing here is better to bail out early
224+
anchor.parentElement.insertBefore(node, anchor);
225+
}
226+
return node;
220227
}
221228

222229
/**
@@ -228,6 +235,8 @@ export function comment(anchor) {
228235
}
229236

230237
/**
238+
* Assign the created (or in hydration mode, traversed) dom elements to the current block
239+
* and insert the elements into the dom (in client mode).
231240
* @param {Element | Text} dom
232241
* @param {boolean} is_fragment
233242
* @param {null | Text | Comment | Element} anchor
@@ -2866,7 +2875,9 @@ export function mount(component, options) {
28662875
const container = options.target;
28672876
const block = create_root_block(options.intro || false);
28682877
const first_child = /** @type {ChildNode} */ (container.firstChild);
2869-
const hydration_fragment = get_hydration_fragment(first_child);
2878+
// Call with insert_text == true to prevent empty {expressions} resulting in an empty
2879+
// fragment array, resulting in a hydration error down the line
2880+
const hydration_fragment = get_hydration_fragment(first_child, true);
28702881
const previous_hydration_fragment = current_hydration_fragment;
28712882

28722883
/** @type {Exports} */

packages/svelte/src/internal/client/transitions.js

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,7 @@ import {
1010
ROOT_BLOCK
1111
} from './block.js';
1212
import { destroy_each_item_block, get_first_element } from './each.js';
13-
import { append_child } from './operations.js';
14-
import { empty } from './render.js';
13+
import { append_child, empty } from './operations.js';
1514
import {
1615
current_block,
1716
current_effect,
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
<!--ssr:0-->
2+
<!--ssr:1-->
3+
<!--ssr:if:true-->x<!--ssr:1-->
4+
<!--ssr:0-->
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
<!--ssr:0-->
2+
<!--ssr:1-->
3+
<!--ssr:if:true--><!--ssr:1-->
4+
<!--ssr:0-->
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import { test } from '../../test';
2+
3+
export default test({});
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<script>
2+
let foo = typeof window === 'undefined' ? '' : 'x';
3+
</script>
4+
5+
{#if true}
6+
{foo}
7+
{/if}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
<!--ssr:0-->x<!--ssr:0-->
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
<!--ssr:0--><!--ssr:0-->
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import { test } from '../../test';
2+
3+
export default test({});
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<script>
2+
let x = typeof window === 'undefined' ? '' : 'x'
3+
</script>
4+
5+
{x}

0 commit comments

Comments
 (0)