diff --git a/.changeset/spotty-shrimps-hug.md b/.changeset/spotty-shrimps-hug.md
new file mode 100644
index 000000000000..7ddfdf518bc4
--- /dev/null
+++ b/.changeset/spotty-shrimps-hug.md
@@ -0,0 +1,5 @@
+---
+'svelte': patch
+---
+
+feat: single-pass hydration
diff --git a/packages/svelte/scripts/check-treeshakeability.js b/packages/svelte/scripts/check-treeshakeability.js
index 7b9fb816beb3..90d5ab022c7e 100644
--- a/packages/svelte/scripts/check-treeshakeability.js
+++ b/packages/svelte/scripts/check-treeshakeability.js
@@ -113,7 +113,7 @@ const bundle = await bundle_code(
).js.code
);
-if (!bundle.includes('hydrate_nodes') && !bundle.includes('hydrate_anchor')) {
+if (!bundle.includes('hydrate_node') && !bundle.includes('hydrate_next')) {
// eslint-disable-next-line no-console
console.error(`✅ Hydration code treeshakeable`);
} else {
diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js
index deaf7dd20853..0de47791afb0 100644
--- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js
+++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js
@@ -980,7 +980,8 @@ function serialize_inline_component(node, component_name, context, anchor = cont
statements.push(
b.stmt(b.call('$.css_props', anchor, b.thunk(b.object(custom_css_props)))),
- b.stmt(fn(b.member(anchor, b.id('lastChild'))))
+ b.stmt(fn(b.member(anchor, b.id('lastChild')))),
+ b.stmt(b.call('$.reset', anchor))
);
} else {
context.state.template.push('');
@@ -1441,6 +1442,12 @@ function process_children(nodes, expression, is_element, { visit, state }) {
}
if (sequence.length > 0) {
+ // if the final item in a fragment is static text,
+ // we need to force `hydrate_node` to advance
+ if (sequence.length === 1 && sequence[0].type === 'Text' && nodes.length > 1) {
+ state.init.push(b.stmt(b.call('$.next')));
+ }
+
flush_sequence(sequence);
}
}
@@ -1569,7 +1576,7 @@ export const template_visitors = {
const namespace = infer_namespace(context.state.metadata.namespace, parent, node.nodes);
- const { hoisted, trimmed, is_standalone } = clean_nodes(
+ const { hoisted, trimmed, is_standalone, is_text_first } = clean_nodes(
parent,
node.nodes,
context.path,
@@ -1619,6 +1626,11 @@ export const template_visitors = {
context.visit(node, state);
}
+ if (is_text_first) {
+ // skip over inserted comment
+ body.push(b.stmt(b.call('$.next')));
+ }
+
/**
* @param {import('estree').Identifier} template_name
* @param {import('estree').Expression[]} args
@@ -1677,11 +1689,7 @@ export const template_visitors = {
state
});
- body.push(
- b.var(id, b.call('$.text', b.id('$$anchor'))),
- ...state.before_init,
- ...state.init
- );
+ body.push(b.var(id, b.call('$.text')), ...state.before_init, ...state.init);
close = b.stmt(b.call('$.append', b.id('$$anchor'), id));
} else {
if (is_standalone) {
@@ -1689,8 +1697,7 @@ export const template_visitors = {
process_children(trimmed, () => b.id('$$anchor'), false, { ...context, state });
} else {
/** @type {(is_text: boolean) => import('estree').Expression} */
- const expression = (is_text) =>
- is_text ? b.call('$.first_child', id, b.true) : b.call('$.first_child', id);
+ const expression = (is_text) => b.call('$.first_child', id, is_text && b.true);
process_children(trimmed, expression, false, { ...context, state });
@@ -2180,18 +2187,30 @@ export const template_visitors = {
context.visit(node, child_state);
}
- process_children(
- trimmed,
- () =>
- b.call(
- '$.child',
- node.name === 'template'
- ? b.member(context.state.node, b.id('content'))
- : context.state.node
- ),
- true,
- { ...context, state: child_state }
- );
+ /** @type {import('estree').Expression} */
+ let arg = context.state.node;
+
+ // If `hydrate_node` is set inside the element, we need to reset it
+ // after the element has been hydrated
+ let needs_reset = trimmed.some((node) => node.type !== 'Text');
+
+ // The same applies if it's a `` element, since we need to
+ // set the value of `hydrate_node` to `node.content`
+ if (node.name === 'template') {
+ needs_reset = true;
+
+ arg = b.member(arg, b.id('content'));
+ child_state.init.push(b.stmt(b.call('$.reset', arg)));
+ }
+
+ process_children(trimmed, () => b.call('$.child', arg), true, {
+ ...context,
+ state: child_state
+ });
+
+ if (needs_reset) {
+ child_state.init.push(b.stmt(b.call('$.reset', context.state.node)));
+ }
if (has_declaration) {
context.state.init.push(
diff --git a/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js b/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js
index d5c9c790b710..9359f967dfd2 100644
--- a/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js
+++ b/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js
@@ -33,16 +33,21 @@ import {
import { escape_html } from '../../../../escaping.js';
import { sanitize_template_string } from '../../../utils/sanitize_template_string.js';
import {
- BLOCK_ANCHOR,
+ EMPTY_COMMENT,
BLOCK_CLOSE,
- BLOCK_CLOSE_ELSE,
- BLOCK_OPEN
+ BLOCK_OPEN,
+ BLOCK_OPEN_ELSE
} from '../../../../internal/server/hydration.js';
import { filename, locator } from '../../../state.js';
-export const block_open = b.literal(BLOCK_OPEN);
-export const block_close = b.literal(BLOCK_CLOSE);
-export const block_anchor = b.literal(BLOCK_ANCHOR);
+/** Opens an if/each block, so that we can remove nodes in the case of a mismatch */
+const block_open = b.literal(BLOCK_OPEN);
+
+/** Closes an if/each block, so that we can remove nodes in the case of a mismatch. Also serves as an anchor for these blocks */
+const block_close = b.literal(BLOCK_CLOSE);
+
+/** Empty comment to keep text nodes separate, or provide an anchor node for blocks */
+const empty_comment = b.literal(EMPTY_COMMENT);
/**
* @param {import('estree').Node} node
@@ -996,22 +1001,32 @@ function serialize_inline_component(node, expression, context) {
statement = b.block([...snippet_declarations, statement]);
}
+ const dynamic =
+ node.type === 'SvelteComponent' || (node.type === 'Component' && node.metadata.dynamic);
+
if (custom_css_props.length > 0) {
- statement = b.stmt(
- b.call(
- '$.css_props',
- b.id('$$payload'),
- b.literal(context.state.namespace === 'svg' ? false : true),
- b.object(custom_css_props),
- b.thunk(b.block([statement]))
+ context.state.template.push(
+ b.stmt(
+ b.call(
+ '$.css_props',
+ b.id('$$payload'),
+ b.literal(context.state.namespace === 'svg' ? false : true),
+ b.object(custom_css_props),
+ b.thunk(b.block([statement])),
+ dynamic && b.true
+ )
)
);
+ } else {
+ if (dynamic) {
+ context.state.template.push(empty_comment);
+ }
context.state.template.push(statement);
- } else if (context.state.skip_hydration_boundaries) {
- context.state.template.push(statement);
- } else {
- context.state.template.push(block_open, statement, block_close);
+
+ if (!context.state.skip_hydration_boundaries) {
+ context.state.template.push(empty_comment);
+ }
}
}
@@ -1119,7 +1134,7 @@ const template_visitors = {
const parent = context.path.at(-1) ?? node;
const namespace = infer_namespace(context.state.namespace, parent, node.nodes);
- const { hoisted, trimmed, is_standalone } = clean_nodes(
+ const { hoisted, trimmed, is_standalone, is_text_first } = clean_nodes(
parent,
node.nodes,
context.path,
@@ -1142,13 +1157,18 @@ const template_visitors = {
context.visit(node, state);
}
+ if (is_text_first) {
+ // insert `` to prevent this from being glued to the previous fragment
+ state.template.push(empty_comment);
+ }
+
process_children(trimmed, { ...context, state });
return b.block([...state.init, ...serialize_template(state.template)]);
},
HtmlTag(node, context) {
const expression = /** @type {import('estree').Expression} */ (context.visit(node.expression));
- context.state.template.push(block_open, expression, block_close);
+ context.state.template.push(empty_comment, expression, empty_comment);
},
ConstTag(node, { state, visit }) {
const declaration = node.declaration.declarations[0];
@@ -1188,10 +1208,6 @@ const template_visitors = {
return /** @type {import('estree').Expression} */ (context.visit(arg));
});
- if (!context.state.skip_hydration_boundaries) {
- context.state.template.push(block_open);
- }
-
context.state.template.push(
b.stmt(
(node.expression.type === 'CallExpression' ? b.call : b.maybe_call)(
@@ -1203,7 +1219,7 @@ const template_visitors = {
);
if (!context.state.skip_hydration_boundaries) {
- context.state.template.push(block_close);
+ context.state.template.push(empty_comment);
}
},
ClassDirective() {
@@ -1353,7 +1369,6 @@ const template_visitors = {
},
EachBlock(node, context) {
const state = context.state;
- state.template.push(block_open);
const each_node_meta = node.metadata;
const collection = /** @type {import('estree').Expression} */ (context.visit(node.expression));
@@ -1376,12 +1391,8 @@ const template_visitors = {
each.push(b.let(node.index, index));
}
- each.push(b.stmt(b.assignment('+=', b.id('$$payload.out'), b.literal(BLOCK_OPEN))));
-
each.push(.../** @type {import('estree').BlockStatement} */ (context.visit(node.body)).body);
- each.push(b.stmt(b.assignment('+=', b.id('$$payload.out'), b.literal(BLOCK_CLOSE))));
-
const for_loop = b.for(
b.let(index, b.literal(0)),
b.binary('<', index, b.member(array_id, b.id('length'))),
@@ -1389,26 +1400,27 @@ const template_visitors = {
b.block(each)
);
- const close = b.stmt(b.assignment('+=', b.id('$$payload.out'), b.literal(BLOCK_CLOSE)));
-
if (node.fallback) {
+ const open = b.stmt(b.assignment('+=', b.id('$$payload.out'), block_open));
+
const fallback = /** @type {import('estree').BlockStatement} */ (
context.visit(node.fallback)
);
- fallback.body.push(
- b.stmt(b.assignment('+=', b.id('$$payload.out'), b.literal(BLOCK_CLOSE_ELSE)))
+ fallback.body.unshift(
+ b.stmt(b.assignment('+=', b.id('$$payload.out'), b.literal(BLOCK_OPEN_ELSE)))
);
state.template.push(
b.if(
b.binary('!==', b.member(array_id, b.id('length')), b.literal(0)),
- b.block([for_loop, close]),
+ b.block([open, for_loop]),
fallback
- )
+ ),
+ block_close
);
} else {
- state.template.push(for_loop, close);
+ state.template.push(block_open, for_loop, block_close);
}
},
IfBlock(node, context) {
@@ -1422,16 +1434,17 @@ const template_visitors = {
? /** @type {import('estree').BlockStatement} */ (context.visit(node.alternate))
: b.block([]);
- consequent.body.push(b.stmt(b.assignment('+=', b.id('$$payload.out'), b.literal(BLOCK_CLOSE))));
- alternate.body.push(
- b.stmt(b.assignment('+=', b.id('$$payload.out'), b.literal(BLOCK_CLOSE_ELSE)))
+ consequent.body.unshift(b.stmt(b.assignment('+=', b.id('$$payload.out'), block_open)));
+
+ alternate.body.unshift(
+ b.stmt(b.assignment('+=', b.id('$$payload.out'), b.literal(BLOCK_OPEN_ELSE)))
);
- context.state.template.push(block_open, b.if(test, consequent, alternate));
+ context.state.template.push(b.if(test, consequent, alternate), block_close);
},
AwaitBlock(node, context) {
context.state.template.push(
- block_open,
+ empty_comment,
b.stmt(
b.call(
'$.await',
@@ -1455,12 +1468,12 @@ const template_visitors = {
)
)
),
- block_close
+ empty_comment
);
},
KeyBlock(node, context) {
const block = /** @type {import('estree').BlockStatement} */ (context.visit(node.fragment));
- context.state.template.push(block_open, block, block_close);
+ context.state.template.push(empty_comment, block, empty_comment);
},
SnippetBlock(node, context) {
const fn = b.function_declaration(
@@ -1594,7 +1607,7 @@ const template_visitors = {
const slot = b.call('$.slot', b.id('$$payload'), expression, props_expression, fallback);
- context.state.template.push(block_open, b.stmt(slot), block_close);
+ context.state.template.push(empty_comment, b.stmt(slot), empty_comment);
},
SvelteHead(node, context) {
const block = /** @type {import('estree').BlockStatement} */ (context.visit(node.fragment));
diff --git a/packages/svelte/src/compiler/phases/3-transform/utils.js b/packages/svelte/src/compiler/phases/3-transform/utils.js
index 4eeeeed03895..c396c5d7530e 100644
--- a/packages/svelte/src/compiler/phases/3-transform/utils.js
+++ b/packages/svelte/src/compiler/phases/3-transform/utils.js
@@ -270,21 +270,28 @@ export function clean_nodes(
var first = trimmed[0];
- /**
- * In a case like `{#if x}{/if}`, we don't need to wrap the child in
- * comments — we can just use the parent block's anchor for the component.
- * TODO extend this optimisation to other cases
- */
- const is_standalone =
- trimmed.length === 1 &&
- ((first.type === 'RenderTag' && !first.metadata.dynamic) ||
- (first.type === 'Component' &&
- !state.options.hmr &&
- !first.attributes.some(
- (attribute) => attribute.type === 'Attribute' && attribute.name.startsWith('--')
- )));
-
- return { hoisted, trimmed, is_standalone };
+ return {
+ hoisted,
+ trimmed,
+ /**
+ * In a case like `{#if x}{/if}`, we don't need to wrap the child in
+ * comments — we can just use the parent block's anchor for the component.
+ * TODO extend this optimisation to other cases
+ */
+ is_standalone:
+ trimmed.length === 1 &&
+ ((first.type === 'RenderTag' && !first.metadata.dynamic) ||
+ (first.type === 'Component' &&
+ !state.options.hmr &&
+ !first.attributes.some(
+ (attribute) => attribute.type === 'Attribute' && attribute.name.startsWith('--')
+ ))),
+ /** if a component or snippet starts with text, we need to add an anchor comment so that its text node doesn't get fused with its surroundings */
+ is_text_first:
+ (parent.type === 'Fragment' || parent.type === 'SnippetBlock') &&
+ first &&
+ (first?.type === 'Text' || first?.type === 'ExpressionTag')
+ };
}
/**
diff --git a/packages/svelte/src/constants.js b/packages/svelte/src/constants.js
index 619a83562d58..6f5b03ec38c1 100644
--- a/packages/svelte/src/constants.js
+++ b/packages/svelte/src/constants.js
@@ -20,9 +20,9 @@ export const TEMPLATE_FRAGMENT = 1;
export const TEMPLATE_USE_IMPORT_NODE = 1 << 1;
export const HYDRATION_START = '[';
+/** used to indicate that an `{:else}...` block was rendered */
+export const HYDRATION_START_ELSE = '[!';
export const HYDRATION_END = ']';
-export const HYDRATION_ANCHOR = '';
-export const HYDRATION_END_ELSE = `${HYDRATION_END}!`; // used to indicate that an `{:else}...` block was rendered
export const HYDRATION_ERROR = {};
export const ELEMENT_IS_NAMESPACED = 1;
diff --git a/packages/svelte/src/internal/client/dev/elements.js b/packages/svelte/src/internal/client/dev/elements.js
index 14713736ade3..bf75e6e5ccef 100644
--- a/packages/svelte/src/internal/client/dev/elements.js
+++ b/packages/svelte/src/internal/client/dev/elements.js
@@ -1,5 +1,5 @@
/** @import { SourceLocation } from '#shared' */
-import { HYDRATION_END, HYDRATION_START } from '../../../constants.js';
+import { HYDRATION_END, HYDRATION_START, HYDRATION_START_ELSE } from '../../../constants.js';
import { hydrating } from '../dom/hydration.js';
/**
@@ -47,7 +47,7 @@ function assign_locations(node, filename, locations) {
while (node && i < locations.length) {
if (hydrating && node.nodeType === 8) {
var comment = /** @type {Comment} */ (node);
- if (comment.data === HYDRATION_START) depth += 1;
+ if (comment.data === HYDRATION_START || comment.data === HYDRATION_START_ELSE) depth += 1;
else if (comment.data[0] === HYDRATION_END) depth -= 1;
}
diff --git a/packages/svelte/src/internal/client/dom/blocks/await.js b/packages/svelte/src/internal/client/dom/blocks/await.js
index 22cf23828566..547717666041 100644
--- a/packages/svelte/src/internal/client/dom/blocks/await.js
+++ b/packages/svelte/src/internal/client/dom/blocks/await.js
@@ -12,7 +12,7 @@ import {
import { block, branch, pause_effect, resume_effect } from '../../reactivity/effects.js';
import { DEV } from 'esm-env';
import { queue_micro_task } from '../task.js';
-import { hydrating } from '../hydration.js';
+import { hydrate_next, hydrate_node, hydrating } from '../hydration.js';
import { mutable_source, set, source } from '../../reactivity/sources.js';
const PENDING = 0;
@@ -21,14 +21,19 @@ const CATCH = 2;
/**
* @template V
- * @param {TemplateNode} anchor
+ * @param {TemplateNode} node
* @param {(() => Promise)} get_input
* @param {null | ((anchor: Node) => void)} pending_fn
* @param {null | ((anchor: Node, value: Source) => void)} then_fn
* @param {null | ((anchor: Node, error: unknown) => void)} catch_fn
* @returns {void}
*/
-export function await_block(anchor, get_input, pending_fn, then_fn, catch_fn) {
+export function await_block(node, get_input, pending_fn, then_fn, catch_fn) {
+ if (hydrating) {
+ hydrate_next();
+ }
+
+ var anchor = node;
var runes = is_runes();
var component_context = current_component_context;
@@ -147,4 +152,8 @@ export function await_block(anchor, get_input, pending_fn, then_fn, catch_fn) {
// teardown function is an easy way to ensure that this is not discarded
return noop;
});
+
+ if (hydrating) {
+ anchor = hydrate_node;
+ }
}
diff --git a/packages/svelte/src/internal/client/dom/blocks/css-props.js b/packages/svelte/src/internal/client/dom/blocks/css-props.js
index 8cae0c652636..c5d63fc9a6ae 100644
--- a/packages/svelte/src/internal/client/dom/blocks/css-props.js
+++ b/packages/svelte/src/internal/client/dom/blocks/css-props.js
@@ -1,6 +1,6 @@
/** @import { TemplateNode } from '#client' */
-import { hydrating, set_hydrate_nodes } from '../hydration.js';
import { render_effect, teardown } from '../../reactivity/effects.js';
+import { hydrate_node, hydrating, set_hydrate_node } from '../hydration.js';
/**
* @param {HTMLDivElement | SVGGElement} element
@@ -9,7 +9,7 @@ import { render_effect, teardown } from '../../reactivity/effects.js';
*/
export function css_props(element, get_styles) {
if (hydrating) {
- set_hydrate_nodes(/** @type {TemplateNode[]} */ ([...element.childNodes]).slice(0, -1));
+ set_hydrate_node(/** @type {TemplateNode} */ (element.firstChild));
}
render_effect(() => {
diff --git a/packages/svelte/src/internal/client/dom/blocks/each.js b/packages/svelte/src/internal/client/dom/blocks/each.js
index 2e274d1b03b4..2faca068b4fc 100644
--- a/packages/svelte/src/internal/client/dom/blocks/each.js
+++ b/packages/svelte/src/internal/client/dom/blocks/each.js
@@ -1,3 +1,4 @@
+/** @import { TemplateNode } from '#client' */
import {
EACH_INDEX_REACTIVE,
EACH_IS_ANIMATED,
@@ -5,18 +6,18 @@ import {
EACH_IS_STRICT_EQUALS,
EACH_ITEM_REACTIVE,
EACH_KEYED,
- HYDRATION_END_ELSE,
- HYDRATION_START
+ HYDRATION_END,
+ HYDRATION_START_ELSE
} from '../../../../constants.js';
import {
- hydrate_anchor,
- hydrate_nodes,
- hydrate_start,
+ hydrate_next,
+ hydrate_node,
hydrating,
+ remove_nodes,
+ set_hydrate_node,
set_hydrating
} from '../hydration.js';
import { clear_text_content, empty } from '../operations.js';
-import { remove } from '../reconciler.js';
import {
block,
branch,
@@ -96,7 +97,7 @@ function pause_effects(state, items, controlled_anchor, items_map) {
/**
* @template V
- * @param {Element | Comment} anchor The next sibling node, or the parent node if this is a 'controlled' block
+ * @param {Element | Comment} node The next sibling node, or the parent node if this is a 'controlled' block
* @param {number} flags
* @param {() => V[]} get_collection
* @param {(value: V, index: number) => any} get_key
@@ -104,22 +105,26 @@ function pause_effects(state, items, controlled_anchor, items_map) {
* @param {null | ((anchor: Node) => void)} fallback_fn
* @returns {void}
*/
-export function each(anchor, flags, get_collection, get_key, render_fn, fallback_fn = null) {
+export function each(node, flags, get_collection, get_key, render_fn, fallback_fn = null) {
+ var anchor = node;
+
/** @type {import('#client').EachState} */
var state = { flags, items: new Map(), first: null };
var is_controlled = (flags & EACH_IS_CONTROLLED) !== 0;
if (is_controlled) {
- var parent_node = /** @type {Element} */ (anchor);
+ var parent_node = /** @type {Element} */ (node);
anchor = hydrating
- ? /** @type {Comment | Text} */ (
- hydrate_anchor(/** @type {Comment | Text} */ (parent_node.firstChild))
- )
+ ? set_hydrate_node(/** @type {Comment | Text} */ (parent_node.firstChild))
: parent_node.appendChild(empty());
}
+ if (hydrating) {
+ hydrate_next();
+ }
+
/** @type {import('#client').Effect | null} */
var fallback = null;
@@ -155,11 +160,13 @@ export function each(anchor, flags, get_collection, get_key, render_fn, fallback
let mismatch = false;
if (hydrating) {
- var is_else = /** @type {Comment} */ (anchor).data === HYDRATION_END_ELSE;
+ var is_else = /** @type {Comment} */ (anchor).data === HYDRATION_START_ELSE;
- if (is_else !== (length === 0) || hydrate_start === undefined) {
+ if (is_else !== (length === 0)) {
// hydration mismatch — remove the server-rendered DOM and start over
- remove(hydrate_nodes);
+ anchor = remove_nodes();
+
+ set_hydrate_node(anchor);
set_hydrating(false);
mismatch = true;
}
@@ -167,9 +174,6 @@ export function each(anchor, flags, get_collection, get_key, render_fn, fallback
// this is separate to the previous block because `hydrating` might change
if (hydrating) {
- /** @type {Node} */
- var child_anchor = hydrate_start;
-
/** @type {import('#client').EachItem | null} */
var prev = null;
@@ -178,33 +182,28 @@ export function each(anchor, flags, get_collection, get_key, render_fn, fallback
for (var i = 0; i < length; i++) {
if (
- child_anchor.nodeType !== 8 ||
- /** @type {Comment} */ (child_anchor).data !== HYDRATION_START
+ hydrate_node.nodeType === 8 &&
+ /** @type {Comment} */ (hydrate_node).data === HYDRATION_END
) {
- // If `nodes` is null, then that means that the server rendered fewer items than what
- // expected, so break out and continue appending non-hydrated items
+ // The server rendered fewer items than expected,
+ // so break out and continue appending non-hydrated items
+ anchor = /** @type {Comment} */ (hydrate_node);
mismatch = true;
set_hydrating(false);
break;
}
- child_anchor = hydrate_anchor(child_anchor);
var value = array[i];
var key = get_key(value, i);
- item = create_item(child_anchor, state, prev, null, value, key, i, render_fn, flags);
+ item = create_item(hydrate_node, state, prev, null, value, key, i, render_fn, flags);
state.items.set(key, item);
- child_anchor = /** @type {Comment} */ (child_anchor.nextSibling);
prev = item;
}
// remove excess nodes
if (length > 0) {
- while (child_anchor !== anchor) {
- var next = /** @type {import('#client').TemplateNode} */ (child_anchor.nextSibling);
- /** @type {import('#client').TemplateNode} */ (child_anchor).remove();
- child_anchor = next;
- }
+ set_hydrate_node(remove_nodes());
}
}
@@ -231,6 +230,10 @@ export function each(anchor, flags, get_collection, get_key, render_fn, fallback
set_hydrating(true);
}
});
+
+ if (hydrating) {
+ anchor = hydrate_node;
+ }
}
/**
diff --git a/packages/svelte/src/internal/client/dom/blocks/html.js b/packages/svelte/src/internal/client/dom/blocks/html.js
index 220e44794849..4d0fc38e6796 100644
--- a/packages/svelte/src/internal/client/dom/blocks/html.js
+++ b/packages/svelte/src/internal/client/dom/blocks/html.js
@@ -1,17 +1,21 @@
/** @import { Effect, TemplateNode } from '#client' */
+import { HYDRATION_ERROR } from '../../../../constants.js';
import { block, branch, destroy_effect } from '../../reactivity/effects.js';
-import { get_start, hydrate_nodes, hydrating } from '../hydration.js';
+import { hydrate_next, hydrate_node, hydrating, set_hydrate_node } from '../hydration.js';
import { create_fragment_from_html } from '../reconciler.js';
import { assign_nodes } from '../template.js';
+import * as w from '../../warnings.js';
/**
- * @param {Element | Text | Comment} anchor
+ * @param {Element | Text | Comment} node
* @param {() => string} get_value
* @param {boolean} svg
* @param {boolean} mathml
* @returns {void}
*/
-export function html(anchor, get_value, svg, mathml) {
+export function html(node, get_value, svg, mathml) {
+ var anchor = node;
+
var value = '';
/** @type {Effect | null} */
@@ -29,7 +33,24 @@ export function html(anchor, get_value, svg, mathml) {
effect = branch(() => {
if (hydrating) {
- assign_nodes(get_start(), hydrate_nodes[hydrate_nodes.length - 1]);
+ var next = hydrate_next();
+ var last = next;
+
+ while (
+ next !== null &&
+ (next.nodeType !== 8 || /** @type {Comment} */ (next).data !== '')
+ ) {
+ last = next;
+ next = /** @type {TemplateNode} */ (next.nextSibling);
+ }
+
+ if (next === null) {
+ w.hydration_mismatch();
+ throw HYDRATION_ERROR;
+ }
+
+ assign_nodes(hydrate_node, last);
+ anchor = set_hydrate_node(next);
return;
}
diff --git a/packages/svelte/src/internal/client/dom/blocks/if.js b/packages/svelte/src/internal/client/dom/blocks/if.js
index 80ed8c09f4f6..dc3ffbd72d44 100644
--- a/packages/svelte/src/internal/client/dom/blocks/if.js
+++ b/packages/svelte/src/internal/client/dom/blocks/if.js
@@ -1,24 +1,31 @@
+/** @import { TemplateNode } from '#client' */
import { EFFECT_TRANSPARENT } from '../../constants.js';
-import { hydrate_nodes, hydrating, set_hydrating } from '../hydration.js';
-import { remove } from '../reconciler.js';
+import {
+ hydrate_next,
+ hydrate_node,
+ hydrating,
+ remove_nodes,
+ set_hydrate_node,
+ set_hydrating
+} from '../hydration.js';
import { block, branch, pause_effect, resume_effect } from '../../reactivity/effects.js';
-import { HYDRATION_END_ELSE } from '../../../../constants.js';
+import { HYDRATION_START_ELSE } from '../../../../constants.js';
/**
- * @param {Comment} anchor
+ * @param {TemplateNode} node
* @param {() => boolean} get_condition
* @param {(anchor: Node) => import('#client').Dom} consequent_fn
* @param {null | ((anchor: Node) => import('#client').Dom)} [alternate_fn]
* @param {boolean} [elseif] True if this is an `{:else if ...}` block rather than an `{#if ...}`, as that affects which transitions are considered 'local'
* @returns {void}
*/
-export function if_block(
- anchor,
- get_condition,
- consequent_fn,
- alternate_fn = null,
- elseif = false
-) {
+export function if_block(node, get_condition, consequent_fn, alternate_fn = null, elseif = false) {
+ if (hydrating) {
+ hydrate_next();
+ }
+
+ var anchor = node;
+
/** @type {import('#client').Effect | null} */
var consequent_effect = null;
@@ -37,12 +44,14 @@ export function if_block(
let mismatch = false;
if (hydrating) {
- const is_else = anchor.data === HYDRATION_END_ELSE;
+ const is_else = /** @type {Comment} */ (anchor).data === HYDRATION_START_ELSE;
if (condition === is_else) {
// Hydration mismatch: remove everything inside the anchor and start fresh.
// This could happen with `{#if browser}...{/if}`, for example
- remove(hydrate_nodes);
+ anchor = remove_nodes();
+
+ set_hydrate_node(anchor);
set_hydrating(false);
mismatch = true;
}
@@ -79,4 +88,8 @@ export function if_block(
set_hydrating(true);
}
}, flags);
+
+ if (hydrating) {
+ anchor = hydrate_node;
+ }
}
diff --git a/packages/svelte/src/internal/client/dom/blocks/key.js b/packages/svelte/src/internal/client/dom/blocks/key.js
index 489262b71477..04b8ab403f00 100644
--- a/packages/svelte/src/internal/client/dom/blocks/key.js
+++ b/packages/svelte/src/internal/client/dom/blocks/key.js
@@ -1,21 +1,28 @@
-/** @import { Dom, Effect } from '#client' */
+/** @import { Effect, TemplateNode } from '#client' */
import { UNINITIALIZED } from '../../../../constants.js';
import { block, branch, pause_effect } from '../../reactivity/effects.js';
import { safe_not_equal } from '../../reactivity/equality.js';
+import { hydrate_next, hydrate_node, hydrating } from '../hydration.js';
/**
* @template V
- * @param {Comment} anchor
+ * @param {TemplateNode} node
* @param {() => V} get_key
- * @param {(anchor: Node) => Dom | void} render_fn
+ * @param {(anchor: Node) => TemplateNode | void} render_fn
* @returns {void}
*/
-export function key_block(anchor, get_key, render_fn) {
+export function key_block(node, get_key, render_fn) {
+ if (hydrating) {
+ hydrate_next();
+ }
+
+ var anchor = node;
+
/** @type {V | typeof UNINITIALIZED} */
- let key = UNINITIALIZED;
+ var key = UNINITIALIZED;
/** @type {Effect} */
- let effect;
+ var effect;
block(() => {
if (safe_not_equal(key, (key = get_key()))) {
@@ -26,4 +33,8 @@ export function key_block(anchor, get_key, render_fn) {
effect = branch(() => render_fn(anchor));
}
});
+
+ if (hydrating) {
+ anchor = hydrate_node;
+ }
}
diff --git a/packages/svelte/src/internal/client/dom/blocks/slot.js b/packages/svelte/src/internal/client/dom/blocks/slot.js
index 6ef1885e007c..c4ed14200b52 100644
--- a/packages/svelte/src/internal/client/dom/blocks/slot.js
+++ b/packages/svelte/src/internal/client/dom/blocks/slot.js
@@ -1,3 +1,5 @@
+import { hydrate_next, hydrating } from '../hydration.js';
+
/**
* @param {Comment} anchor
* @param {void | ((anchor: Comment, slot_props: Record) => void)} slot_fn
@@ -5,6 +7,10 @@
* @param {null | ((anchor: Comment) => void)} fallback_fn
*/
export function slot(anchor, slot_fn, slot_props, fallback_fn) {
+ if (hydrating) {
+ hydrate_next();
+ }
+
if (slot_fn === undefined) {
if (fallback_fn !== null) {
fallback_fn(anchor);
diff --git a/packages/svelte/src/internal/client/dom/blocks/snippet.js b/packages/svelte/src/internal/client/dom/blocks/snippet.js
index b962b7b84ce6..c0fc350ce45c 100644
--- a/packages/svelte/src/internal/client/dom/blocks/snippet.js
+++ b/packages/svelte/src/internal/client/dom/blocks/snippet.js
@@ -6,15 +6,18 @@ import {
dev_current_component_function,
set_dev_current_component_function
} from '../../runtime.js';
+import { hydrate_node, hydrating } from '../hydration.js';
/**
* @template {(node: TemplateNode, ...args: any[]) => void} SnippetFn
- * @param {TemplateNode} anchor
+ * @param {TemplateNode} node
* @param {() => SnippetFn | null | undefined} get_snippet
* @param {(() => any)[]} args
* @returns {void}
*/
-export function snippet(anchor, get_snippet, ...args) {
+export function snippet(node, get_snippet, ...args) {
+ var anchor = node;
+
/** @type {SnippetFn | null | undefined} */
var snippet;
@@ -33,6 +36,10 @@ export function snippet(anchor, get_snippet, ...args) {
snippet_effect = branch(() => /** @type {SnippetFn} */ (snippet)(anchor, ...args));
}
}, EFFECT_TRANSPARENT);
+
+ if (hydrating) {
+ anchor = hydrate_node;
+ }
}
/**
diff --git a/packages/svelte/src/internal/client/dom/blocks/svelte-component.js b/packages/svelte/src/internal/client/dom/blocks/svelte-component.js
index 9a4f6db33408..1c7106bb7f5f 100644
--- a/packages/svelte/src/internal/client/dom/blocks/svelte-component.js
+++ b/packages/svelte/src/internal/client/dom/blocks/svelte-component.js
@@ -1,20 +1,27 @@
/** @import { TemplateNode, Dom, Effect } from '#client' */
import { block, branch, pause_effect } from '../../reactivity/effects.js';
+import { hydrate_next, hydrate_node, hydrating } from '../hydration.js';
/**
* @template P
* @template {(props: P) => void} C
- * @param {TemplateNode} anchor
+ * @param {TemplateNode} node
* @param {() => C} get_component
* @param {(anchor: TemplateNode, component: C) => Dom | void} render_fn
* @returns {void}
*/
-export function component(anchor, get_component, render_fn) {
+export function component(node, get_component, render_fn) {
+ if (hydrating) {
+ hydrate_next();
+ }
+
+ var anchor = node;
+
/** @type {C} */
- let component;
+ var component;
/** @type {Effect | null} */
- let effect;
+ var effect;
block(() => {
if (component === (component = get_component())) return;
@@ -28,4 +35,8 @@ export function component(anchor, get_component, render_fn) {
effect = branch(() => render_fn(anchor, component));
}
});
+
+ if (hydrating) {
+ anchor = hydrate_node;
+ }
}
diff --git a/packages/svelte/src/internal/client/dom/blocks/svelte-element.js b/packages/svelte/src/internal/client/dom/blocks/svelte-element.js
index 428f7a5ca48b..1df6c8acabab 100644
--- a/packages/svelte/src/internal/client/dom/blocks/svelte-element.js
+++ b/packages/svelte/src/internal/client/dom/blocks/svelte-element.js
@@ -1,5 +1,12 @@
+/** @import { Effect, EffectNodes, TemplateNode } from '#client' */
import { namespace_svg } from '../../../../constants.js';
-import { hydrating, set_hydrate_nodes } from '../hydration.js';
+import {
+ hydrate_next,
+ hydrate_node,
+ hydrating,
+ set_hydrate_node,
+ set_hydrating
+} from '../hydration.js';
import { empty } from '../operations.js';
import {
block,
@@ -10,10 +17,10 @@ import {
} from '../../reactivity/effects.js';
import { set_should_intro } from '../../render.js';
import { current_each_item, set_current_each_item } from './each.js';
-import { current_component_context } from '../../runtime.js';
+import { current_component_context, current_effect } from '../../runtime.js';
import { DEV } from 'esm-env';
-import { assign_nodes } from '../template.js';
import { EFFECT_TRANSPARENT } from '../../constants.js';
+import { assign_nodes } from '../template.js';
/**
* @param {Comment | Element} node
@@ -25,36 +32,43 @@ import { EFFECT_TRANSPARENT } from '../../constants.js';
* @returns {void}
*/
export function element(node, get_tag, is_svg, render_fn, get_namespace, location) {
- const filename = DEV && location && current_component_context?.function.filename;
+ let was_hydrating = hydrating;
+
+ if (hydrating) {
+ hydrate_next();
+ }
+
+ var filename = DEV && location && current_component_context?.function.filename;
/** @type {string | null} */
- let tag;
+ var tag;
/** @type {string | null} */
- let current_tag;
+ var current_tag;
/** @type {null | Element} */
- let element = hydrating && node.nodeType === 1 ? /** @type {Element} */ (node) : null;
+ var element = null;
+
+ if (hydrating && hydrate_node.nodeType === 1) {
+ element = /** @type {Element} */ (hydrate_node);
+ hydrate_next();
+ }
- let anchor = /** @type {Comment} */ (hydrating && element ? element.nextSibling : node);
+ var anchor = /** @type {TemplateNode} */ (hydrating ? hydrate_node : node);
- /** @type {import('#client').Effect | null} */
- let effect;
+ /** @type {Effect | null} */
+ var effect;
/**
* The keyed `{#each ...}` item block, if any, that this element is inside.
* We track this so we can set it when changing the element, allowing any
* `animate:` directive to bind itself to the correct block
*/
- let each_item_block = current_each_item;
+ var each_item_block = current_each_item;
block(() => {
const next_tag = get_tag() || null;
- const ns = get_namespace
- ? get_namespace()
- : is_svg || next_tag === 'svg'
- ? namespace_svg
- : null;
+ var ns = get_namespace ? get_namespace() : is_svg || next_tag === 'svg' ? namespace_svg : null;
// Assumption: Noone changes the namespace but not the tag (what would that even mean?)
if (next_tag === tag) return;
@@ -88,8 +102,6 @@ export function element(node, get_tag, is_svg, render_fn, get_namespace, locatio
? document.createElementNS(ns, next_tag)
: document.createElement(next_tag);
- assign_nodes(element, element);
-
if (DEV && location) {
// @ts-expect-error
element.__svelte_meta = {
@@ -101,15 +113,21 @@ export function element(node, get_tag, is_svg, render_fn, get_namespace, locatio
};
}
+ assign_nodes(element, element);
+
if (render_fn) {
// If hydrating, use the existing ssr comment as the anchor so that the
// inner open and close methods can pick up the existing nodes correctly
- var child_anchor = hydrating ? element.lastChild : element.appendChild(empty());
-
- if (hydrating && child_anchor) {
- set_hydrate_nodes(
- /** @type {import('#client').TemplateNode[]} */ ([...element.childNodes]).slice(0, -1)
- );
+ var child_anchor = /** @type {TemplateNode} */ (
+ hydrating ? element.firstChild : element.appendChild(empty())
+ );
+
+ if (hydrating) {
+ if (child_anchor === null) {
+ set_hydrating(false);
+ } else {
+ set_hydrate_node(child_anchor);
+ }
}
// `child_anchor` is undefined if this is a void element, but we still
@@ -119,6 +137,9 @@ export function element(node, get_tag, is_svg, render_fn, get_namespace, locatio
render_fn(element, child_anchor);
}
+ // we do this after calling `render_fn` so that child effects don't override `nodes.end`
+ /** @type {Effect & { nodes: EffectNodes }} */ (current_effect).nodes.end = element;
+
anchor.before(element);
});
}
@@ -129,4 +150,9 @@ export function element(node, get_tag, is_svg, render_fn, get_namespace, locatio
set_current_each_item(previous_each_item);
}, EFFECT_TRANSPARENT);
+
+ if (was_hydrating) {
+ set_hydrating(true);
+ set_hydrate_node(anchor);
+ }
}
diff --git a/packages/svelte/src/internal/client/dom/blocks/svelte-head.js b/packages/svelte/src/internal/client/dom/blocks/svelte-head.js
index f25ecefa0334..bfd429172841 100644
--- a/packages/svelte/src/internal/client/dom/blocks/svelte-head.js
+++ b/packages/svelte/src/internal/client/dom/blocks/svelte-head.js
@@ -1,8 +1,9 @@
-import { hydrate_anchor, hydrate_nodes, hydrating, set_hydrate_nodes } from '../hydration.js';
+/** @import { TemplateNode } from '#client' */
+import { hydrate_node, hydrating, set_hydrate_node } from '../hydration.js';
import { empty } from '../operations.js';
import { block } from '../../reactivity/effects.js';
-import { HYDRATION_END, HYDRATION_START } from '../../../../constants.js';
import { HEAD_EFFECT } from '../../constants.js';
+import { HYDRATION_START } from '../../../../constants.js';
/**
* @type {Node | undefined}
@@ -14,35 +15,34 @@ export function reset_head_anchor() {
}
/**
- * @param {(anchor: Node) => import('#client').Dom | void} render_fn
+ * @param {(anchor: Node) => void} render_fn
* @returns {void}
*/
export function head(render_fn) {
// The head function may be called after the first hydration pass and ssr comment nodes may still be present,
// therefore we need to skip that when we detect that we're not in hydration mode.
- let previous_hydrate_nodes = null;
+ let previous_hydrate_node = null;
let was_hydrating = hydrating;
/** @type {Comment | Text} */
var anchor;
if (hydrating) {
- previous_hydrate_nodes = hydrate_nodes;
+ previous_hydrate_node = hydrate_node;
// There might be multiple head blocks in our app, so we need to account for each one needing independent hydration.
if (head_anchor === undefined) {
- head_anchor = /** @type {import('#client').TemplateNode} */ (document.head.firstChild);
+ head_anchor = /** @type {TemplateNode} */ (document.head.firstChild);
}
while (
head_anchor.nodeType !== 8 ||
/** @type {Comment} */ (head_anchor).data !== HYDRATION_START
) {
- head_anchor = /** @type {import('#client').TemplateNode} */ (head_anchor.nextSibling);
+ head_anchor = /** @type {TemplateNode} */ (head_anchor.nextSibling);
}
- head_anchor = /** @type {import('#client').TemplateNode} */ (hydrate_anchor(head_anchor));
- head_anchor = /** @type {import('#client').TemplateNode} */ (head_anchor.nextSibling);
+ head_anchor = set_hydrate_node(/** @type {TemplateNode} */ (head_anchor.nextSibling));
} else {
anchor = document.head.appendChild(empty());
}
@@ -51,7 +51,7 @@ export function head(render_fn) {
block(() => render_fn(anchor), HEAD_EFFECT);
} finally {
if (was_hydrating) {
- set_hydrate_nodes(/** @type {import('#client').TemplateNode[]} */ (previous_hydrate_nodes));
+ set_hydrate_node(/** @type {TemplateNode} */ (previous_hydrate_node));
}
}
}
diff --git a/packages/svelte/src/internal/client/dom/hydration.js b/packages/svelte/src/internal/client/dom/hydration.js
index 02d07be057d1..8b03fe7f9eb6 100644
--- a/packages/svelte/src/internal/client/dom/hydration.js
+++ b/packages/svelte/src/internal/client/dom/hydration.js
@@ -1,6 +1,6 @@
-import { DEV } from 'esm-env';
-import { HYDRATION_END, HYDRATION_START, HYDRATION_ERROR } from '../../../constants.js';
-import * as w from '../warnings.js';
+/** @import { TemplateNode } from '#client' */
+
+import { HYDRATION_END, HYDRATION_START, HYDRATION_START_ELSE } from '../../../constants.js';
/**
* Use this variable to guard everything related to hydration code so it can be treeshaken out
@@ -14,86 +14,57 @@ export function set_hydrating(value) {
}
/**
- * Array of nodes to traverse for hydration. This will be null if we're not hydrating, but for
- * the sake of simplicity we're not going to use `null` checks everywhere and instead rely on
- * the `hydrating` flag to tell whether or not we're in hydration mode at which point this is set.
- * @type {import('#client').TemplateNode[]}
+ * The node that is currently being hydrated. This starts out as the first node inside the opening
+ * comment, and updates each time a component calls `$.child(...)` or `$.sibling(...)`.
+ * When entering a block (e.g. `{#if ...}`), `hydrate_node` is the block opening comment; by the
+ * time we leave the block it is the closing comment, which serves as the block's anchor.
+ * @type {TemplateNode}
*/
-export let hydrate_nodes = /** @type {any} */ (null);
-
-/** @type {import('#client').TemplateNode} */
-export let hydrate_start;
+export let hydrate_node;
-/** @param {import('#client').TemplateNode[]} nodes */
-export function set_hydrate_nodes(nodes) {
- hydrate_nodes = nodes;
- hydrate_start = nodes && nodes[0];
+/** @param {TemplateNode} node */
+export function set_hydrate_node(node) {
+ return (hydrate_node = node);
}
-/**
- * When assigning nodes to an effect during hydration, we typically want the hydration boundary comment node
- * immediately before `hydrate_start`. In some cases, this comment doesn't exist because we optimized it away.
- * TODO it might be worth storing this value separately rather than retrieving it with `previousSibling`
- */
-export function get_start() {
- return /** @type {import('#client').TemplateNode} */ (
- hydrate_start.previousSibling ?? hydrate_start
- );
+export function hydrate_next() {
+ return (hydrate_node = /** @type {TemplateNode} */ (hydrate_node.nextSibling));
}
-/**
- * This function is only called when `hydrating` is true. If passed a `` opening
- * hydration marker, it finds the corresponding closing marker and sets `hydrate_nodes`
- * to everything between the markers, before returning the closing marker.
- * @param {Node} node
- * @returns {Node}
- */
-export function hydrate_anchor(node) {
- if (node.nodeType !== 8) {
- return node;
+/** @param {TemplateNode} node */
+export function reset(node) {
+ if (hydrating) {
+ hydrate_node = node;
}
+}
- var current = /** @type {Node | null} */ (node);
-
- // TODO this could have false positives, if a user comment consisted of `[`. need to tighten that up
- if (/** @type {Comment} */ (current).data !== HYDRATION_START) {
- return node;
+export function next() {
+ if (hydrating) {
+ hydrate_next();
}
+}
- /** @type {Node[]} */
- var nodes = [];
+/**
+ * Removes all nodes starting at `hydrate_node` up until the next hydration end comment
+ */
+export function remove_nodes() {
var depth = 0;
+ var node = hydrate_node;
- while ((current = /** @type {Node} */ (current).nextSibling) !== null) {
- if (current.nodeType === 8) {
- var data = /** @type {Comment} */ (current).data;
-
- if (data === HYDRATION_START) {
- depth += 1;
- } else if (data[0] === HYDRATION_END) {
- if (depth === 0) {
- hydrate_nodes = /** @type {import('#client').TemplateNode[]} */ (nodes);
- hydrate_start = /** @type {import('#client').TemplateNode} */ (nodes[0]);
- return current;
- }
+ while (true) {
+ if (node.nodeType === 8) {
+ var data = /** @type {Comment} */ (node).data;
+ if (data === HYDRATION_END) {
+ if (depth === 0) return node;
depth -= 1;
+ } else if (data === HYDRATION_START || data === HYDRATION_START_ELSE) {
+ depth += 1;
}
}
- nodes.push(current);
+ var next = /** @type {TemplateNode} */ (node.nextSibling);
+ node.remove();
+ node = next;
}
-
- let location;
-
- if (DEV) {
- // @ts-expect-error
- const loc = node.parentNode?.__svelte_meta?.loc;
- if (loc) {
- location = `${loc.file}:${loc.line}:${loc.column}`;
- }
- }
-
- w.hydration_mismatch(location);
- throw HYDRATION_ERROR;
}
diff --git a/packages/svelte/src/internal/client/dom/operations.js b/packages/svelte/src/internal/client/dom/operations.js
index 175719f66c70..c1eb66305349 100644
--- a/packages/svelte/src/internal/client/dom/operations.js
+++ b/packages/svelte/src/internal/client/dom/operations.js
@@ -1,8 +1,8 @@
-import { hydrate_anchor, hydrate_start, hydrating } from './hydration.js';
+/** @import { Effect, TemplateNode } from '#client' */
+import { hydrate_node, hydrating, set_hydrate_node } from './hydration.js';
import { DEV } from 'esm-env';
import { init_array_prototype_warnings } from '../dev/equality.js';
import { current_effect } from '../runtime.js';
-import { HYDRATION_ANCHOR } from '../../../constants.js';
// export these for reference in the compiled code, making global name deduplication unnecessary
/** @type {Window} */
@@ -58,19 +58,23 @@ export function empty() {
*/
/*#__NO_SIDE_EFFECTS__*/
export function child(node) {
- const child = node.firstChild;
- if (!hydrating) return child;
+ if (!hydrating) {
+ return node.firstChild;
+ }
+
+ var child = /** @type {TemplateNode} */ (hydrate_node.firstChild);
// Child can be null if we have an element with a single child, like `{text}
`, where `text` is empty
if (child === null) {
- return node.appendChild(empty());
+ child = hydrate_node.appendChild(empty());
}
- return hydrate_anchor(child);
+ set_hydrate_node(child);
+ return child;
}
/**
- * @param {DocumentFragment | import('#client').TemplateNode[]} fragment
+ * @param {DocumentFragment | TemplateNode[]} fragment
* @param {boolean} is_text
* @returns {Node | null}
*/
@@ -88,19 +92,15 @@ export function first_child(fragment, 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 && hydrate_start?.nodeType !== 3) {
+ if (is_text && hydrate_node?.nodeType !== 3) {
var text = empty();
- var effect = /** @type {import('#client').Effect} */ (current_effect);
-
- if (effect.nodes?.start === hydrate_start) {
- effect.nodes.start = text;
- }
- hydrate_start?.before(text);
+ hydrate_node?.before(text);
+ set_hydrate_node(text);
return text;
}
- return hydrate_anchor(hydrate_start);
+ return hydrate_node;
}
/**
@@ -111,27 +111,25 @@ export function first_child(fragment, is_text) {
*/
/*#__NO_SIDE_EFFECTS__*/
export function sibling(node, is_text = false) {
- var next_sibling = /** @type {import('#client').TemplateNode} */ (node.nextSibling);
-
if (!hydrating) {
- return next_sibling;
+ return /** @type {TemplateNode} */ (node.nextSibling);
}
- var type = next_sibling.nodeType;
+ var next_sibling = /** @type {TemplateNode} */ (hydrate_node.nextSibling);
- if (type === 8 && /** @type {Comment} */ (next_sibling).data === HYDRATION_ANCHOR) {
- return sibling(next_sibling, is_text);
- }
+ var type = next_sibling.nodeType;
// if a sibling {expression} is empty during SSR, there might be no
// text node to hydrate — we must therefore create one
if (is_text && type !== 3) {
var text = empty();
next_sibling?.before(text);
+ set_hydrate_node(text);
return text;
}
- return hydrate_anchor(/** @type {Node} */ (next_sibling));
+ set_hydrate_node(next_sibling);
+ return /** @type {TemplateNode} */ (next_sibling);
}
/**
diff --git a/packages/svelte/src/internal/client/dom/reconciler.js b/packages/svelte/src/internal/client/dom/reconciler.js
index 5b9f246ed338..9897e08d5314 100644
--- a/packages/svelte/src/internal/client/dom/reconciler.js
+++ b/packages/svelte/src/internal/client/dom/reconciler.js
@@ -1,24 +1,6 @@
-import { is_array } from '../utils.js';
-
/** @param {string} html */
export function create_fragment_from_html(html) {
var elem = document.createElement('template');
elem.innerHTML = html;
return elem.content;
}
-
-/**
- * @param {import('#client').Dom} current
- */
-export function remove(current) {
- if (is_array(current)) {
- for (var i = 0; i < current.length; i++) {
- var node = current[i];
- if (node.isConnected) {
- node.remove();
- }
- }
- } else if (current.isConnected) {
- current.remove();
- }
-}
diff --git a/packages/svelte/src/internal/client/dom/template.js b/packages/svelte/src/internal/client/dom/template.js
index ab7c57fa71dc..3ef0b181b7b5 100644
--- a/packages/svelte/src/internal/client/dom/template.js
+++ b/packages/svelte/src/internal/client/dom/template.js
@@ -1,4 +1,5 @@
-import { get_start, hydrate_nodes, hydrate_start, hydrating } from './hydration.js';
+/** @import { Effect, EffectNodes, TemplateNode } from '#client' */
+import { hydrate_next, hydrate_node, hydrating, set_hydrate_node } from './hydration.js';
import { empty } from './operations.js';
import { create_fragment_from_html } from './reconciler.js';
import { current_effect } from '../runtime.js';
@@ -6,11 +7,11 @@ import { TEMPLATE_FRAGMENT, TEMPLATE_USE_IMPORT_NODE } from '../../../constants.
import { queue_micro_task } from './task.js';
/**
- * @param {import('#client').TemplateNode} start
- * @param {import('#client').TemplateNode} end
+ * @param {TemplateNode} start
+ * @param {TemplateNode | null} end
*/
export function assign_nodes(start, end) {
- /** @type {import('#client').Effect} */ (current_effect).nodes ??= { start, end };
+ /** @type {Effect} */ (current_effect).nodes ??= { start, end };
}
/**
@@ -34,9 +35,8 @@ export function template(content, flags) {
return () => {
if (hydrating) {
- assign_nodes(get_start(), hydrate_nodes[hydrate_nodes.length - 1]);
-
- return hydrate_start;
+ assign_nodes(hydrate_node, null);
+ return hydrate_node;
}
if (!node) {
@@ -44,13 +44,13 @@ export function template(content, flags) {
if (!is_fragment) node = /** @type {Node} */ (node.firstChild);
}
- var clone = /** @type {import('#client').TemplateNode} */ (
+ var clone = /** @type {TemplateNode} */ (
use_import_node ? document.importNode(node, true) : node.cloneNode(true)
);
if (is_fragment) {
- var start = /** @type {import('#client').TemplateNode} */ (clone.firstChild);
- var end = /** @type {import('#client').TemplateNode} */ (clone.lastChild);
+ var start = /** @type {TemplateNode} */ (clone.firstChild);
+ var end = /** @type {TemplateNode} */ (clone.lastChild);
assign_nodes(start, end);
} else {
@@ -107,9 +107,8 @@ export function ns_template(content, flags, ns = 'svg') {
return () => {
if (hydrating) {
- assign_nodes(get_start(), hydrate_nodes[hydrate_nodes.length - 1]);
-
- return hydrate_start;
+ assign_nodes(hydrate_node, null);
+ return hydrate_node;
}
if (!node) {
@@ -126,11 +125,11 @@ export function ns_template(content, flags, ns = 'svg') {
}
}
- var clone = /** @type {import('#client').TemplateNode} */ (node.cloneNode(true));
+ var clone = /** @type {TemplateNode} */ (node.cloneNode(true));
if (is_fragment) {
- var start = /** @type {import('#client').TemplateNode} */ (clone.firstChild);
- var end = /** @type {import('#client').TemplateNode} */ (clone.lastChild);
+ var start = /** @type {TemplateNode} */ (clone.firstChild);
+ var end = /** @type {TemplateNode} */ (clone.lastChild);
assign_nodes(start, end);
} else {
@@ -195,6 +194,7 @@ function run_scripts(node) {
}
clone.textContent = script.textContent;
+
// If node === script tag, replaceWith will do nothing because there's no parent yet,
// waiting until that's the case using an effect solves this.
// Don't do it in other circumstances or we could accidentally execute scripts
@@ -207,23 +207,20 @@ function run_scripts(node) {
}
}
-/**
- * @param {Text | Comment | Element} anchor
- */
/*#__NO_SIDE_EFFECTS__*/
-export function text(anchor) {
+export function text() {
if (!hydrating) {
var t = empty();
assign_nodes(t, t);
return t;
}
- var node = hydrate_start;
+ var node = hydrate_node;
- if (!node) {
- // if an {expression} is empty during SSR, `hydrate_nodes` will be empty.
- // we need to insert an empty text node
- anchor.before((node = empty()));
+ if (node.nodeType !== 3) {
+ // if an {expression} is empty during SSR, we need to insert an empty text node
+ node.before((node = empty()));
+ set_hydrate_node(node);
}
assign_nodes(node, node);
@@ -233,9 +230,8 @@ export function text(anchor) {
export function comment() {
// we're not delegating to `template` here for performance reasons
if (hydrating) {
- assign_nodes(get_start(), hydrate_nodes[hydrate_nodes.length - 1]);
-
- return hydrate_start;
+ assign_nodes(hydrate_node, null);
+ return hydrate_node;
}
var frag = document.createDocumentFragment();
@@ -255,10 +251,16 @@ export function comment() {
* @param {DocumentFragment | Element} dom
*/
export function append(anchor, dom) {
- if (hydrating) return;
- // We intentionally do not assign the `dom` property of the effect here because it's far too
- // late. If we try, we will capture additional DOM elements that we cannot control the lifecycle
- // for and will inevitably cause memory leaks. See https://github.com/sveltejs/svelte/pull/11832
+ if (hydrating) {
+ /** @type {Effect & { nodes: EffectNodes }} */ (current_effect).nodes.end = hydrate_node;
+ hydrate_next();
+ return;
+ }
+
+ if (anchor === null) {
+ // edge case — void `` with content
+ return;
+ }
anchor.before(/** @type {Node} */ (dom));
}
diff --git a/packages/svelte/src/internal/client/index.js b/packages/svelte/src/internal/client/index.js
index 4575f75f0dc8..3231b5f5d1dd 100644
--- a/packages/svelte/src/internal/client/index.js
+++ b/packages/svelte/src/internal/client/index.js
@@ -63,6 +63,7 @@ export {
bind_focused
} from './dom/elements/bindings/universal.js';
export { bind_window_scroll, bind_window_size } from './dom/elements/bindings/window.js';
+export { next, reset } from './dom/hydration.js';
export {
once,
preventDefault,
diff --git a/packages/svelte/src/internal/client/reactivity/types.d.ts b/packages/svelte/src/internal/client/reactivity/types.d.ts
index abc2be90154c..1e347c011f0f 100644
--- a/packages/svelte/src/internal/client/reactivity/types.d.ts
+++ b/packages/svelte/src/internal/client/reactivity/types.d.ts
@@ -36,7 +36,7 @@ export interface Derived extends Value, Reaction {
export interface EffectNodes {
start: TemplateNode;
- end: TemplateNode;
+ end: null | TemplateNode;
}
export interface Effect extends Reaction {
diff --git a/packages/svelte/src/internal/client/render.js b/packages/svelte/src/internal/client/render.js
index 7dfa69585162..b6cdd8303c45 100644
--- a/packages/svelte/src/internal/client/render.js
+++ b/packages/svelte/src/internal/client/render.js
@@ -1,12 +1,18 @@
import { DEV } from 'esm-env';
import { clear_text_content, empty, init_operations } from './dom/operations.js';
-import { HYDRATION_ERROR, HYDRATION_START, PassiveDelegatedEvents } from '../../constants.js';
-import { flush_sync, push, pop, current_component_context } from './runtime.js';
+import {
+ HYDRATION_END,
+ HYDRATION_ERROR,
+ HYDRATION_START,
+ PassiveDelegatedEvents
+} from '../../constants.js';
+import { flush_sync, push, pop, current_component_context, current_effect } from './runtime.js';
import { effect_root, branch } from './reactivity/effects.js';
import {
- hydrate_anchor,
- hydrate_nodes,
- set_hydrate_nodes,
+ hydrate_next,
+ hydrate_node,
+ hydrating,
+ set_hydrate_node,
set_hydrating
} from './dom/hydration.js';
import { array_from } from './utils.js';
@@ -15,6 +21,7 @@ import { reset_head_anchor } from './dom/blocks/svelte-head.js';
import * as w from './warnings.js';
import * as e from './errors.js';
import { validate_component } from '../shared/validate.js';
+import { assign_nodes } from './dom/template.js';
/** @type {Set} */
export const all_registered_events = new Set();
@@ -113,28 +120,37 @@ export function hydrate(component, options) {
options.intro = options.intro ?? false;
const target = options.target;
- const previous_hydrate_nodes = hydrate_nodes;
+ const was_hydrating = hydrating;
try {
// Don't flush previous effects to ensure order of outer effects stays consistent
return flush_sync(() => {
- set_hydrating(true);
-
- var node = target.firstChild;
+ var anchor = /** @type {import('#client').TemplateNode} */ (target.firstChild);
while (
- node &&
- (node.nodeType !== 8 || /** @type {Comment} */ (node).data !== HYDRATION_START)
+ anchor &&
+ (anchor.nodeType !== 8 || /** @type {Comment} */ (anchor).data !== HYDRATION_START)
) {
- node = node.nextSibling;
+ anchor = /** @type {import('#client').TemplateNode} */ (anchor.nextSibling);
}
- if (!node) {
+ if (!anchor) {
throw HYDRATION_ERROR;
}
- const anchor = hydrate_anchor(node);
+ set_hydrating(true);
+ set_hydrate_node(/** @type {Comment} */ (anchor));
+ hydrate_next();
+
const instance = _mount(component, { ...options, anchor });
+ if (
+ hydrate_node.nodeType !== 8 ||
+ /** @type {Comment} */ (hydrate_node).data !== HYDRATION_END
+ ) {
+ w.hydration_mismatch();
+ throw HYDRATION_ERROR;
+ }
+
// flush_sync will run this callback and then synchronously run any pending effects,
// which don't belong to the hydration phase anymore - therefore reset it here
set_hydrating(false);
@@ -143,6 +159,9 @@ export function hydrate(component, options) {
}, false);
} catch (error) {
if (error === HYDRATION_ERROR) {
+ // TODO it's possible for event listeners to have been added and
+ // not removed, e.g. with `` or ``
+
if (options.recover === false) {
e.hydration_failed();
}
@@ -157,8 +176,7 @@ export function hydrate(component, options) {
throw error;
} finally {
- set_hydrating(!!previous_hydrate_nodes);
- set_hydrate_nodes(previous_hydrate_nodes);
+ set_hydrating(was_hydrating);
reset_head_anchor();
}
}
@@ -222,11 +240,21 @@ function _mount(Component, { target, anchor, props = {}, events, context, intro
/** @type {any} */ (props).$$events = events;
}
+ if (hydrating) {
+ assign_nodes(/** @type {import('#client').TemplateNode} */ (anchor), null);
+ }
+
should_intro = intro;
// @ts-expect-error the public typings are not what the actual function looks like
component = Component(anchor, props) || {};
should_intro = true;
+ if (hydrating) {
+ /** @type {import('#client').Effect & { nodes: import('#client').EffectNodes }} */ (
+ current_effect
+ ).nodes.end = hydrate_node;
+ }
+
if (context) {
pop();
}
diff --git a/packages/svelte/src/internal/server/hydration.js b/packages/svelte/src/internal/server/hydration.js
index d860d759d24e..94fc61b89ca1 100644
--- a/packages/svelte/src/internal/server/hydration.js
+++ b/packages/svelte/src/internal/server/hydration.js
@@ -1,11 +1,6 @@
-import {
- HYDRATION_ANCHOR,
- HYDRATION_END,
- HYDRATION_END_ELSE,
- HYDRATION_START
-} from '../../constants.js';
+import { HYDRATION_END, HYDRATION_START, HYDRATION_START_ELSE } from '../../constants.js';
export const BLOCK_OPEN = ``;
+export const BLOCK_OPEN_ELSE = ``;
export const BLOCK_CLOSE = ``;
-export const BLOCK_ANCHOR = ``;
-export const BLOCK_CLOSE_ELSE = ``;
+export const EMPTY_COMMENT = ``;
diff --git a/packages/svelte/src/internal/server/index.js b/packages/svelte/src/internal/server/index.js
index fc3ad10d30f2..8238e469cfdb 100644
--- a/packages/svelte/src/internal/server/index.js
+++ b/packages/svelte/src/internal/server/index.js
@@ -10,7 +10,7 @@ import {
import { escape_html } from '../../escaping.js';
import { DEV } from 'esm-env';
import { current_component, pop, push } from './context.js';
-import { BLOCK_ANCHOR, BLOCK_CLOSE, BLOCK_OPEN } from './hydration.js';
+import { EMPTY_COMMENT, BLOCK_CLOSE, BLOCK_OPEN } from './hydration.js';
import { validate_store } from '../shared/validate.js';
// https://html.spec.whatwg.org/multipage/syntax.html#attributes-2
@@ -73,6 +73,8 @@ export function assign_payload(p1, p2) {
* @returns {void}
*/
export function element(payload, tag, attributes_fn = noop, children_fn = noop) {
+ payload.out += '';
+
if (tag) {
payload.out += `<${tag} `;
attributes_fn();
@@ -81,7 +83,7 @@ export function element(payload, tag, attributes_fn = noop, children_fn = noop)
if (!VoidElements.has(tag)) {
children_fn();
if (!RawTextElements.includes(tag)) {
- payload.out += BLOCK_ANCHOR;
+ payload.out += EMPTY_COMMENT;
}
payload.out += `${tag}>`;
}
@@ -141,9 +143,9 @@ export function render(component, options = {}) {
*/
export function head(payload, fn) {
const head_payload = payload.head;
- payload.head.out += BLOCK_OPEN;
+ head_payload.out += BLOCK_OPEN;
fn(head_payload);
- payload.head.out += BLOCK_CLOSE;
+ head_payload.out += BLOCK_CLOSE;
}
/**
@@ -164,16 +166,24 @@ export function attr(name, value, is_boolean = false) {
* @param {boolean} is_html
* @param {Record} props
* @param {() => void} component
+ * @param {boolean} dynamic
* @returns {void}
*/
-export function css_props(payload, is_html, props, component) {
+export function css_props(payload, is_html, props, component, dynamic = false) {
const styles = style_object_to_string(props);
+
if (is_html) {
payload.out += ``;
} else {
payload.out += ``;
}
+
+ if (dynamic) {
+ payload.out += '';
+ }
+
component();
+
if (is_html) {
payload.out += `
`;
} else {
diff --git a/packages/svelte/tests/hydration/samples/binding-input/_expected.html b/packages/svelte/tests/hydration/samples/binding-input/_expected.html
index b5bc6af1614a..e36735dc6b9d 100644
--- a/packages/svelte/tests/hydration/samples/binding-input/_expected.html
+++ b/packages/svelte/tests/hydration/samples/binding-input/_expected.html
@@ -1 +1 @@
- Hello world!
+ Hello world!
diff --git a/packages/svelte/tests/hydration/samples/dynamic-text-changed/_expected.html b/packages/svelte/tests/hydration/samples/dynamic-text-changed/_expected.html
index b163603869b5..11e3f55d6b56 100644
--- a/packages/svelte/tests/hydration/samples/dynamic-text-changed/_expected.html
+++ b/packages/svelte/tests/hydration/samples/dynamic-text-changed/_expected.html
@@ -1 +1 @@
-Hello everybody!
+Hello everybody!
diff --git a/packages/svelte/tests/hydration/samples/each-block-0-on-server-more-on-client/_expected.html b/packages/svelte/tests/hydration/samples/each-block-0-on-server-more-on-client/_expected.html
index 5de1a42d70a9..2b2a31b92633 100644
--- a/packages/svelte/tests/hydration/samples/each-block-0-on-server-more-on-client/_expected.html
+++ b/packages/svelte/tests/hydration/samples/each-block-0-on-server-more-on-client/_expected.html
@@ -1,9 +1 @@
-
-
-
-a
-a
-a
-a
-a
-a
+ a a a a a a
diff --git a/packages/svelte/tests/hydration/samples/each-block-fallback-mismatch/_expected.html b/packages/svelte/tests/hydration/samples/each-block-fallback-mismatch/_expected.html
index fcfd9fe9efe2..af66d98b25b6 100644
--- a/packages/svelte/tests/hydration/samples/each-block-fallback-mismatch/_expected.html
+++ b/packages/svelte/tests/hydration/samples/each-block-fallback-mismatch/_expected.html
@@ -1,2 +1 @@
-a
-empty
+a
empty
diff --git a/packages/svelte/tests/hydration/samples/each-block-less-nodes-on-client/_expected.html b/packages/svelte/tests/hydration/samples/each-block-less-nodes-on-client/_expected.html
index 5de1a42d70a9..2b2a31b92633 100644
--- a/packages/svelte/tests/hydration/samples/each-block-less-nodes-on-client/_expected.html
+++ b/packages/svelte/tests/hydration/samples/each-block-less-nodes-on-client/_expected.html
@@ -1,9 +1 @@
-
-
-
-a
-a
-a
-a
-a
-a
+ a a a a a a
diff --git a/packages/svelte/tests/hydration/samples/each-block-more-nodes-on-client/_expected.html b/packages/svelte/tests/hydration/samples/each-block-more-nodes-on-client/_expected.html
index 453cee352ba8..fa72a80dcda0 100644
--- a/packages/svelte/tests/hydration/samples/each-block-more-nodes-on-client/_expected.html
+++ b/packages/svelte/tests/hydration/samples/each-block-more-nodes-on-client/_expected.html
@@ -1,9 +1 @@
-
-
-
-a
-abb
-a
-abb
-a
-abb
+ a ab b a ab b a ab b
diff --git a/packages/svelte/tests/hydration/samples/element-attribute-added/_expected.html b/packages/svelte/tests/hydration/samples/element-attribute-added/_expected.html
index 73cf4a84b646..7d8e91c757e1 100644
--- a/packages/svelte/tests/hydration/samples/element-attribute-added/_expected.html
+++ b/packages/svelte/tests/hydration/samples/element-attribute-added/_expected.html
@@ -1 +1 @@
-
+
diff --git a/packages/svelte/tests/hydration/samples/element-attribute-changed/_expected.html b/packages/svelte/tests/hydration/samples/element-attribute-changed/_expected.html
index 73cf4a84b646..7d8e91c757e1 100644
--- a/packages/svelte/tests/hydration/samples/element-attribute-changed/_expected.html
+++ b/packages/svelte/tests/hydration/samples/element-attribute-changed/_expected.html
@@ -1 +1 @@
-
+
diff --git a/packages/svelte/tests/hydration/samples/element-attribute-removed/_expected.html b/packages/svelte/tests/hydration/samples/element-attribute-removed/_expected.html
index bbd534112d67..a6c1be8b4906 100644
--- a/packages/svelte/tests/hydration/samples/element-attribute-removed/_expected.html
+++ b/packages/svelte/tests/hydration/samples/element-attribute-removed/_expected.html
@@ -1 +1 @@
-
+
diff --git a/packages/svelte/tests/hydration/samples/noscript/_expected.html b/packages/svelte/tests/hydration/samples/noscript/_expected.html
index be193b8c989a..990ce87303d2 100644
--- a/packages/svelte/tests/hydration/samples/noscript/_expected.html
+++ b/packages/svelte/tests/hydration/samples/noscript/_expected.html
@@ -1,2 +1 @@
-
-Hello!
Count: 1
+ Hello!
Count: 1
diff --git a/packages/svelte/tests/hydration/samples/raw-repair/_expected.html b/packages/svelte/tests/hydration/samples/raw-repair/_expected.html
new file mode 100644
index 000000000000..39714f0fd3cd
--- /dev/null
+++ b/packages/svelte/tests/hydration/samples/raw-repair/_expected.html
@@ -0,0 +1 @@
+invalid
invalid
diff --git a/packages/svelte/tests/hydration/samples/standalone-component/Child.svelte b/packages/svelte/tests/hydration/samples/standalone-component/Child.svelte
new file mode 100644
index 000000000000..b69380af2b5e
--- /dev/null
+++ b/packages/svelte/tests/hydration/samples/standalone-component/Child.svelte
@@ -0,0 +1 @@
+child
diff --git a/packages/svelte/tests/hydration/samples/standalone-component/_config.js b/packages/svelte/tests/hydration/samples/standalone-component/_config.js
new file mode 100644
index 000000000000..5e08315e29cf
--- /dev/null
+++ b/packages/svelte/tests/hydration/samples/standalone-component/_config.js
@@ -0,0 +1,4 @@
+import { test } from '../../test';
+
+// Ensure that we don't create additional comment nodes for standalone components
+export default test({});
diff --git a/packages/svelte/tests/hydration/samples/standalone-component/_expected.html b/packages/svelte/tests/hydration/samples/standalone-component/_expected.html
new file mode 100644
index 000000000000..96850f7bdf73
--- /dev/null
+++ b/packages/svelte/tests/hydration/samples/standalone-component/_expected.html
@@ -0,0 +1 @@
+child
child
child
child
diff --git a/packages/svelte/tests/hydration/samples/standalone-component/main.svelte b/packages/svelte/tests/hydration/samples/standalone-component/main.svelte
new file mode 100644
index 000000000000..8d6342246d9f
--- /dev/null
+++ b/packages/svelte/tests/hydration/samples/standalone-component/main.svelte
@@ -0,0 +1,11 @@
+
+
+{#if true}
+
+{/if}
+
+{#each [1, 2, 3] as n}
+
+{/each}
diff --git a/packages/svelte/tests/hydration/samples/standalone-snippet/_config.js b/packages/svelte/tests/hydration/samples/standalone-snippet/_config.js
new file mode 100644
index 000000000000..5e08315e29cf
--- /dev/null
+++ b/packages/svelte/tests/hydration/samples/standalone-snippet/_config.js
@@ -0,0 +1,4 @@
+import { test } from '../../test';
+
+// Ensure that we don't create additional comment nodes for standalone components
+export default test({});
diff --git a/packages/svelte/tests/hydration/samples/standalone-snippet/_expected.html b/packages/svelte/tests/hydration/samples/standalone-snippet/_expected.html
new file mode 100644
index 000000000000..acb14818c2c9
--- /dev/null
+++ b/packages/svelte/tests/hydration/samples/standalone-snippet/_expected.html
@@ -0,0 +1 @@
+thing
thing
thing
thing
diff --git a/packages/svelte/tests/hydration/samples/standalone-snippet/main.svelte b/packages/svelte/tests/hydration/samples/standalone-snippet/main.svelte
new file mode 100644
index 000000000000..9ce34a0baa7e
--- /dev/null
+++ b/packages/svelte/tests/hydration/samples/standalone-snippet/main.svelte
@@ -0,0 +1,11 @@
+{#snippet thing()}
+ thing
+{/snippet}
+
+{#if true}
+ {@render thing()}
+{/if}
+
+{#each [1, 2, 3] as n}
+ {@render thing()}
+{/each}
diff --git a/packages/svelte/tests/hydration/samples/text-empty/_expected.html b/packages/svelte/tests/hydration/samples/text-empty/_expected.html
index 5592a725f3bc..2e03580f422c 100644
--- a/packages/svelte/tests/hydration/samples/text-empty/_expected.html
+++ b/packages/svelte/tests/hydration/samples/text-empty/_expected.html
@@ -1 +1 @@
-x
\ No newline at end of file
+x
diff --git a/packages/svelte/tests/hydration/test.ts b/packages/svelte/tests/hydration/test.ts
index 0fcdc6f1b4c9..c18680271172 100644
--- a/packages/svelte/tests/hydration/test.ts
+++ b/packages/svelte/tests/hydration/test.ts
@@ -52,8 +52,10 @@ const { test, run } = suite(async (config, cwd) => {
props: config.server_props ?? config.props ?? {}
});
+ const override = read(`${cwd}/_override.html`);
+
fs.writeFileSync(`${cwd}/_output/body.html`, rendered.html + '\n');
- target.innerHTML = read(`${cwd}/_override.html`) ?? rendered.html;
+ target.innerHTML = override ?? rendered.html;
if (rendered.head) {
fs.writeFileSync(`${cwd}/_output/head.html`, rendered.head + '\n');
@@ -109,12 +111,14 @@ const { test, run } = suite(async (config, cwd) => {
throw new Error(`Unexpected errors: ${errors.join('\n')}`);
}
- const expected = read(`${cwd}/_expected.html`) ?? rendered.html;
- assert_html_equal(target.innerHTML, expected);
+ if (!override) {
+ const expected = read(`${cwd}/_expected.html`) ?? rendered.html;
+ assert.equal(target.innerHTML.trim(), expected.trim());
+ }
if (rendered.head) {
const expected = read(`${cwd}/_expected_head.html`) ?? rendered.head;
- assert_html_equal(head.innerHTML, expected);
+ assert.equal(head.innerHTML.trim(), expected.trim());
}
if (config.snapshot) {
diff --git a/packages/svelte/tests/runtime-legacy/samples/dynamic-component-in-if-initial-falsy/Foo.svelte b/packages/svelte/tests/runtime-legacy/samples/dynamic-component-in-if-initial-falsy/Foo.svelte
new file mode 100644
index 000000000000..c62ee08e19f5
--- /dev/null
+++ b/packages/svelte/tests/runtime-legacy/samples/dynamic-component-in-if-initial-falsy/Foo.svelte
@@ -0,0 +1 @@
+Foo
\ No newline at end of file
diff --git a/packages/svelte/tests/runtime-legacy/samples/dynamic-component-in-if-initial-falsy/_config.js b/packages/svelte/tests/runtime-legacy/samples/dynamic-component-in-if-initial-falsy/_config.js
new file mode 100644
index 000000000000..1acbf9b07379
--- /dev/null
+++ b/packages/svelte/tests/runtime-legacy/samples/dynamic-component-in-if-initial-falsy/_config.js
@@ -0,0 +1,32 @@
+import { flushSync } from 'svelte';
+import { test } from '../../test';
+
+export default test({
+ html: `
+
+
+ `,
+
+ test({ assert, component, target }) {
+ const [btn1, btn2] = target.querySelectorAll('button');
+
+ flushSync(() => btn1.click());
+ assert.htmlEqual(
+ target.innerHTML,
+ `
+
+
+ Foo
+ `
+ );
+
+ flushSync(() => btn2.click());
+ assert.htmlEqual(
+ target.innerHTML,
+ `
+
+
+ `
+ );
+ }
+});
diff --git a/packages/svelte/tests/runtime-legacy/samples/dynamic-component-in-if-initial-falsy/main.svelte b/packages/svelte/tests/runtime-legacy/samples/dynamic-component-in-if-initial-falsy/main.svelte
new file mode 100644
index 000000000000..c275f6b4fd16
--- /dev/null
+++ b/packages/svelte/tests/runtime-legacy/samples/dynamic-component-in-if-initial-falsy/main.svelte
@@ -0,0 +1,15 @@
+
+
+
+
+
+{#if show}
+
+{/if}
diff --git a/packages/svelte/tests/runtime-legacy/samples/raw-anchor-first-child/_config.js b/packages/svelte/tests/runtime-legacy/samples/raw-anchor-first-child/_config.js
index f575b3274e15..0f47f3c1143c 100644
--- a/packages/svelte/tests/runtime-legacy/samples/raw-anchor-first-child/_config.js
+++ b/packages/svelte/tests/runtime-legacy/samples/raw-anchor-first-child/_config.js
@@ -11,7 +11,7 @@ export default test({
if (variant === 'dom') {
assert.ok(!span.previousSibling);
} else {
- assert.ok(span.previousSibling?.textContent === '['); // ssr commment node
+ assert.ok(span.previousSibling?.textContent === ''); // ssr commment node
}
component.raw = 'bar';
diff --git a/packages/svelte/tests/server-side-rendering/samples/head-svelte-components-raw-content/_expected.html b/packages/svelte/tests/server-side-rendering/samples/head-svelte-components-raw-content/_expected.html
index 48da62b4de5a..290179a702bb 100644
--- a/packages/svelte/tests/server-side-rendering/samples/head-svelte-components-raw-content/_expected.html
+++ b/packages/svelte/tests/server-side-rendering/samples/head-svelte-components-raw-content/_expected.html
@@ -1,13 +1,17 @@
+
lorem
+
+
+
-
+
+
diff --git a/packages/svelte/tests/snapshot/samples/bind-component-snippet/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/bind-component-snippet/_expected/client/index.svelte.js
index ba4352d57b80..38f60bacdffc 100644
--- a/packages/svelte/tests/snapshot/samples/bind-component-snippet/_expected/client/index.svelte.js
+++ b/packages/svelte/tests/snapshot/samples/bind-component-snippet/_expected/client/index.svelte.js
@@ -7,6 +7,8 @@ var root = $.template(` `, 1);
export default function Bind_component_snippet($$anchor) {
const snippet = ($$anchor) => {
+ $.next();
+
var fragment = root_1();
$.append($$anchor, fragment);
diff --git a/packages/svelte/tests/snapshot/samples/bind-component-snippet/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/bind-component-snippet/_expected/server/index.svelte.js
index 5c9a37cfdbf2..b645d055702c 100644
--- a/packages/svelte/tests/snapshot/samples/bind-component-snippet/_expected/server/index.svelte.js
+++ b/packages/svelte/tests/snapshot/samples/bind-component-snippet/_expected/server/index.svelte.js
@@ -6,15 +6,13 @@ export default function Bind_component_snippet($$payload) {
const _snippet = snippet;
function snippet($$payload) {
- $$payload.out += `Something`;
+ $$payload.out += `Something`;
}
let $$settled = true;
let $$inner_payload;
function $$render_inner($$payload) {
- $$payload.out += ``;
-
TextInput($$payload, {
get value() {
return value;
@@ -25,7 +23,7 @@ export default function Bind_component_snippet($$payload) {
}
});
- $$payload.out += ` value: ${$.escape(value)}`;
+ $$payload.out += ` value: ${$.escape(value)}`;
};
do {
diff --git a/packages/svelte/tests/snapshot/samples/each-string-template/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/each-string-template/_expected/client/index.svelte.js
index 5d5e47faf2d6..c63bbfe561c3 100644
--- a/packages/svelte/tests/snapshot/samples/each-string-template/_expected/client/index.svelte.js
+++ b/packages/svelte/tests/snapshot/samples/each-string-template/_expected/client/index.svelte.js
@@ -6,7 +6,7 @@ export default function Each_string_template($$anchor) {
var node = $.first_child(fragment);
$.each(node, 1, () => ['foo', 'bar', 'baz'], $.index, ($$anchor, thing, $$index) => {
- var text = $.text($$anchor);
+ var text = $.text();
$.template_effect(() => $.set_text(text, `${$.unwrap(thing) ?? ""}, `));
$.append($$anchor, text);
diff --git a/packages/svelte/tests/snapshot/samples/each-string-template/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/each-string-template/_expected/server/index.svelte.js
index fb143a56c9dd..9258fb774783 100644
--- a/packages/svelte/tests/snapshot/samples/each-string-template/_expected/server/index.svelte.js
+++ b/packages/svelte/tests/snapshot/samples/each-string-template/_expected/server/index.svelte.js
@@ -8,10 +8,8 @@ export default function Each_string_template($$payload) {
for (let $$index = 0; $$index < each_array.length; $$index++) {
const thing = each_array[$$index];
- $$payload.out += "";
$$payload.out += `${$.escape(thing)}, `;
- $$payload.out += "";
}
- $$payload.out += "";
+ $$payload.out += ``;
}
\ No newline at end of file
diff --git a/packages/svelte/tests/snapshot/samples/function-prop-no-getter/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/function-prop-no-getter/_expected/client/index.svelte.js
index 8fb6d075cd7c..0515f1aa331c 100644
--- a/packages/svelte/tests/snapshot/samples/function-prop-no-getter/_expected/client/index.svelte.js
+++ b/packages/svelte/tests/snapshot/samples/function-prop-no-getter/_expected/client/index.svelte.js
@@ -15,7 +15,7 @@ export default function Function_prop_no_getter($$anchor) {
onmouseup,
onmouseenter: () => $.set(count, $.proxy(plusOne($.get(count)))),
children: ($$anchor, $$slotProps) => {
- var text = $.text($$anchor);
+ var text = $.text();
$.template_effect(() => $.set_text(text, `clicks: ${$.get(count) ?? ""}`));
$.append($$anchor, text);