Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/silent-apes-report.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"svelte": patch
---

fix: handle sole empty expression tags
3 changes: 1 addition & 2 deletions packages/svelte/src/internal/client/each.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
9 changes: 8 additions & 1 deletion packages/svelte/src/internal/client/hydration.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
// Handle hydration

import { empty } from './operations.js';
import { schedule_task } from './runtime.js';

/** @type {null | Array<import('./types.js').TemplateNode>} */
Expand All @@ -16,9 +17,10 @@ export function set_current_hydration_fragment(fragment) {
/**
* Returns all nodes between the first `<!--ssr:...-->` 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<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>} */
const fragment = [];

Expand All @@ -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));
Expand Down
15 changes: 11 additions & 4 deletions packages/svelte/src/internal/client/operations.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 `<p>{text}</p>`, where `text` is empty
if (child === null) {
const text = document.createTextNode('');
const text = empty();
node.appendChild(text);
return text;
} else {
Expand All @@ -193,15 +198,15 @@ 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);
}
return text;
}

if (first_node !== null) {
if (first_node) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe the explicit comparison is deliberate, since it results in more efficient bytecode

Suggested change
if (first_node) {
if (first_node !== null) {

Copy link
Member Author

@dummdidumm dummdidumm Feb 9, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did change it because the comparison will be wrongfully false when first_node is undefined when the array is empty.

return capture_fragment_from_node(first_node);
}

Expand All @@ -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)
Expand Down
27 changes: 19 additions & 8 deletions packages/svelte/src/internal/client/render.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
child,
clone_node,
create_element,
empty,
init_operations,
map_get,
map_set,
Expand Down Expand Up @@ -75,11 +76,6 @@ const all_registerd_events = new Set();
/** @type {Set<(events: Array<string>) => void>} */
const root_event_handles = new Set();

/** @returns {Text} */
export function empty() {
return document.createTextNode('');
}

/**
* @param {string} html
* @param {boolean} return_fragment
Expand Down Expand Up @@ -212,11 +208,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 {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;
}

/**
Expand All @@ -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 {boolean} is_fragment
* @param {null | Text | Comment | Element} anchor
Expand Down Expand Up @@ -2866,7 +2875,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} */
Expand Down
3 changes: 1 addition & 2 deletions packages/svelte/src/internal/client/transitions.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<!--ssr:0-->
<!--ssr:1-->
<!--ssr:if:true-->x<!--ssr:1-->
<!--ssr:0-->
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<!--ssr:0-->
<!--ssr:1-->
<!--ssr:if:true--><!--ssr:1-->
<!--ssr:0-->
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { test } from '../../test';

export default test({});
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<script>
let foo = typeof window === 'undefined' ? '' : 'x';
</script>

{#if true}
{foo}
{/if}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<!--ssr:0-->x<!--ssr:0-->
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<!--ssr:0--><!--ssr:0-->
3 changes: 3 additions & 0 deletions packages/svelte/tests/hydration/samples/text-empty/_config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { test } from '../../test';

export default test({});
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<script>
let x = typeof window === 'undefined' ? '' : 'x'
</script>

{x}