Skip to content
Merged
Changes from all commits
Commits
Show all changes
91 commits
Select commit Hold shift + click to select a range
0c37a40
WIP towards single-pass hydration
Rich-Harris Jul 3, 2024
a7b658a
fix
Rich-Harris Jul 4, 2024
cbeb564
fixes
Rich-Harris Jul 4, 2024
9607f7e
fix
Rich-Harris Jul 4, 2024
bf75fd8
fix
Rich-Harris Jul 4, 2024
e23d992
Merge branch 'main' into single-pass-hydration
Rich-Harris Jul 4, 2024
ee9008b
fixes
Rich-Harris Jul 5, 2024
86492e5
fix
Rich-Harris Jul 5, 2024
6b3a3d6
fixes
Rich-Harris Jul 5, 2024
9a80016
fix
Rich-Harris Jul 5, 2024
eb9824e
fix, tidy up
Rich-Harris Jul 5, 2024
840c572
update script (it currently fails)
Rich-Harris Jul 5, 2024
10f02f4
fix
Rich-Harris Jul 5, 2024
2bfd5b2
fix
Rich-Harris Jul 5, 2024
a761d88
hmm
Rich-Harris Jul 5, 2024
c8bad17
fix
Rich-Harris Jul 5, 2024
75ab257
fix
Rich-Harris Jul 5, 2024
1074bfd
fix
Rich-Harris Jul 5, 2024
64d8acd
fix
Rich-Harris Jul 5, 2024
dc45164
all hydration tests passing
Rich-Harris Jul 5, 2024
cafefce
drive-by fix
Rich-Harris Jul 5, 2024
32bea02
fix
Rich-Harris Jul 5, 2024
f39c0f0
update snapshot tests
Rich-Harris Jul 5, 2024
1dd0575
fix
Rich-Harris Jul 5, 2024
79755e3
recover: false
Rich-Harris Jul 6, 2024
2486b95
fix invalid HTML message
Rich-Harris Jul 6, 2024
0343fc3
note to self
Rich-Harris Jul 6, 2024
c23e5ea
fix
Rich-Harris Jul 7, 2024
16533e1
fix
Rich-Harris Jul 7, 2024
65f8675
update snapshot tests
Rich-Harris Jul 7, 2024
65d2053
fix
Rich-Harris Jul 7, 2024
c440445
fix
Rich-Harris Jul 7, 2024
2aa9c67
fix
Rich-Harris Jul 7, 2024
ee95533
update test
Rich-Harris Jul 7, 2024
f263956
fix
Rich-Harris Jul 7, 2024
32f6c4f
fix
Rich-Harris Jul 7, 2024
8a4883f
fix
Rich-Harris Jul 7, 2024
9f6c032
ALL TESTS PASSING THIS IS NOT A DRILL
Rich-Harris Jul 7, 2024
fa4d373
merge
Rich-Harris Jul 7, 2024
ee76cd7
optimise each blocks
Rich-Harris Jul 7, 2024
7be9add
changeset
Rich-Harris Jul 7, 2024
ce14d06
type stuff
Rich-Harris Jul 7, 2024
19579e1
fix comment
Rich-Harris Jul 7, 2024
4f2b26a
tidy up
Rich-Harris Jul 7, 2024
aee8d0d
tidy up
Rich-Harris Jul 7, 2024
99b2dca
tidy up
Rich-Harris Jul 7, 2024
4de3f2e
tidy up
Rich-Harris Jul 7, 2024
11a9ada
tidy up
Rich-Harris Jul 7, 2024
c7e3fb8
remove comment, turns out we do need it
Rich-Harris Jul 8, 2024
667a1a2
revert
Rich-Harris Jul 8, 2024
412ac8a
merge main
Rich-Harris Jul 8, 2024
7bfc74f
reinstate standalone optimisation
Rich-Harris Jul 8, 2024
0bdb35e
improve <svelte:element> SSR
Rich-Harris Jul 8, 2024
1d9fd22
merge main
Rich-Harris Jul 8, 2024
56d8708
reset more conservatively
Rich-Harris Jul 8, 2024
f5d36b8
tweak
Rich-Harris Jul 8, 2024
2e468eb
DRY/fix
Rich-Harris Jul 8, 2024
aa45aa4
revert
Rich-Harris Jul 8, 2024
f77b10a
simplify
Rich-Harris Jul 8, 2024
4a20760
add comment
Rich-Harris Jul 8, 2024
357c5f5
tweak
Rich-Harris Jul 8, 2024
1de3caa
simplify
Rich-Harris Jul 8, 2024
50d9536
simplify
Rich-Harris Jul 9, 2024
2436cbc
answer: yes, at least for now, because otherwise empty components are…
Rich-Harris Jul 9, 2024
81d6c3f
tweak
Rich-Harris Jul 9, 2024
93076bb
unused
Rich-Harris Jul 9, 2024
6f31b62
comment is answered by https://github.com/sveltejs/svelte/pull/12356
Rich-Harris Jul 9, 2024
09a3537
tweak
Rich-Harris Jul 9, 2024
d71dd5f
handle `<template>` edge case at compile time
Rich-Harris Jul 9, 2024
3079878
this is no longer a possibility, because of is_text_first
Rich-Harris Jul 9, 2024
4a2cb4b
unused
Rich-Harris Jul 9, 2024
bdf8946
merge main
Rich-Harris Jul 9, 2024
b763654
tweak
Rich-Harris Jul 9, 2024
2d41103
merge main
Rich-Harris Jul 9, 2024
e52bf4b
fix
Rich-Harris Jul 9, 2024
5676a84
move annotations to properties
Rich-Harris Jul 9, 2024
d79d503
Update packages/svelte/src/constants.js
Rich-Harris Jul 9, 2024
8bdcee6
Update packages/svelte/src/compiler/phases/3-transform/client/visitor…
Rich-Harris Jul 9, 2024
44efc18
Update packages/svelte/src/internal/client/dom/blocks/each.js
Rich-Harris Jul 9, 2024
360ea32
Update packages/svelte/src/internal/client/dom/hydration.js
Rich-Harris Jul 9, 2024
d2a30c5
Update playgrounds/demo/vite.config.js
Rich-Harris Jul 9, 2024
85ae369
add a comment
Rich-Harris Jul 9, 2024
c023a15
Merge branch 'single-pass-hydration' of github.com:sveltejs/svelte in…
Rich-Harris Jul 9, 2024
e7c8d61
prettier
Rich-Harris Jul 9, 2024
9192bbb
tweak
Rich-Harris Jul 9, 2024
ba62a07
tighten up hydration tests, add test for standalone component
Rich-Harris Jul 9, 2024
b66f8bc
test for standalone snippet
Rich-Harris Jul 9, 2024
fd43c85
fix
Rich-Harris Jul 9, 2024
b62c321
add some comments
Rich-Harris Jul 9, 2024
9268126
tidy up
Rich-Harris Jul 9, 2024
4557344
avoid mutating `arguments`
Rich-Harris Jul 9, 2024
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/spotty-shrimps-hug.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'svelte': patch
---

feat: single-pass hydration
2 changes: 1 addition & 1 deletion packages/svelte/scripts/check-treeshakeability.js
Original file line number Diff line number Diff line change
@@ -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 {
Original file line number Diff line number Diff line change
@@ -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,20 +1689,15 @@ 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) {
// no need to create a template, we can just use the existing block's anchor
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 `<template>` 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(
Original file line number Diff line number Diff line change
@@ -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,39 +1391,36 @@ 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'))),
b.update('++', index, false),
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));
37 changes: 22 additions & 15 deletions packages/svelte/src/compiler/phases/3-transform/utils.js
Original file line number Diff line number Diff line change
@@ -270,21 +270,28 @@ export function clean_nodes(

var first = trimmed[0];

/**
* In a case like `{#if x}<Foo />{/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}<Foo />{/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')
};
}

/**
4 changes: 2 additions & 2 deletions packages/svelte/src/constants.js
Original file line number Diff line number Diff line change
@@ -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;
4 changes: 2 additions & 2 deletions packages/svelte/src/internal/client/dev/elements.js
Original file line number Diff line number Diff line change
@@ -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;
}

15 changes: 12 additions & 3 deletions packages/svelte/src/internal/client/dom/blocks/await.js
Original file line number Diff line number Diff line change
@@ -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<V>)} get_input
* @param {null | ((anchor: Node) => void)} pending_fn
* @param {null | ((anchor: Node, value: Source<V>) => 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;
}
}
4 changes: 2 additions & 2 deletions packages/svelte/src/internal/client/dom/blocks/css-props.js
Original file line number Diff line number Diff line change
@@ -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(() => {
63 changes: 33 additions & 30 deletions packages/svelte/src/internal/client/dom/blocks/each.js
Original file line number Diff line number Diff line change
@@ -1,22 +1,23 @@
/** @import { TemplateNode } from '#client' */
import {
EACH_INDEX_REACTIVE,
EACH_IS_ANIMATED,
EACH_IS_CONTROLLED,
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,30 +97,34 @@ 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
* @param {(anchor: Node, item: import('#client').MaybeSource<V>, index: import('#client').MaybeSource<number>) => void} render_fn
* @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,21 +160,20 @@ 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;
}
}

// 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;
}
}

/**
29 changes: 25 additions & 4 deletions packages/svelte/src/internal/client/dom/blocks/html.js
Original file line number Diff line number Diff line change
@@ -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;
}

39 changes: 26 additions & 13 deletions packages/svelte/src/internal/client/dom/blocks/if.js
Original file line number Diff line number Diff line change
@@ -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;
}
}
23 changes: 17 additions & 6 deletions packages/svelte/src/internal/client/dom/blocks/key.js
Original file line number Diff line number Diff line change
@@ -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;
}
}
6 changes: 6 additions & 0 deletions packages/svelte/src/internal/client/dom/blocks/slot.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
import { hydrate_next, hydrating } from '../hydration.js';

/**
* @param {Comment} anchor
* @param {void | ((anchor: Comment, slot_props: Record<string, unknown>) => void)} slot_fn
* @param {Record<string, unknown>} slot_props
* @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);
11 changes: 9 additions & 2 deletions packages/svelte/src/internal/client/dom/blocks/snippet.js
Original file line number Diff line number Diff line change
@@ -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;
}
}

/**
19 changes: 15 additions & 4 deletions packages/svelte/src/internal/client/dom/blocks/svelte-component.js
Original file line number Diff line number Diff line change
@@ -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;
}
}
74 changes: 50 additions & 24 deletions packages/svelte/src/internal/client/dom/blocks/svelte-element.js
Original file line number Diff line number Diff line change
@@ -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);
}
}
20 changes: 10 additions & 10 deletions packages/svelte/src/internal/client/dom/blocks/svelte-head.js
Original file line number Diff line number Diff line change
@@ -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));
}
}
}
105 changes: 38 additions & 67 deletions packages/svelte/src/internal/client/dom/hydration.js
Original file line number Diff line number Diff line change
@@ -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;
}
44 changes: 21 additions & 23 deletions packages/svelte/src/internal/client/dom/operations.js
Original file line number Diff line number Diff line change
@@ -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 `<p>{text}</p>`, 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);
}

/**
18 changes: 0 additions & 18 deletions packages/svelte/src/internal/client/dom/reconciler.js
Original file line number Diff line number Diff line change
@@ -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();
}
}
66 changes: 34 additions & 32 deletions packages/svelte/src/internal/client/dom/template.js
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
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';
import { TEMPLATE_FRAGMENT, TEMPLATE_USE_IMPORT_NODE } from '../../../constants.js';
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,23 +35,22 @@ 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) {
node = create_fragment_from_html(has_start ? content : '<!>' + content);
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 `<svelte:element>` with content
return;
}

anchor.before(/** @type {Node} */ (dom));
}
1 change: 1 addition & 0 deletions packages/svelte/src/internal/client/index.js
Original file line number Diff line number Diff line change
@@ -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,
2 changes: 1 addition & 1 deletion packages/svelte/src/internal/client/reactivity/types.d.ts
Original file line number Diff line number Diff line change
@@ -36,7 +36,7 @@ export interface Derived<V = unknown> extends Value<V>, Reaction {

export interface EffectNodes {
start: TemplateNode;
end: TemplateNode;
end: null | TemplateNode;
}

export interface Effect extends Reaction {
60 changes: 44 additions & 16 deletions packages/svelte/src/internal/client/render.js
Original file line number Diff line number Diff line change
@@ -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<string>} */
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 `<svelte:window>` or `<svelte:document>`

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();
}
11 changes: 3 additions & 8 deletions packages/svelte/src/internal/server/hydration.js
Original file line number Diff line number Diff line change
@@ -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 = `<!--${HYDRATION_START}-->`;
export const BLOCK_OPEN_ELSE = `<!--${HYDRATION_START_ELSE}-->`;
export const BLOCK_CLOSE = `<!--${HYDRATION_END}-->`;
export const BLOCK_ANCHOR = `<!--${HYDRATION_ANCHOR}-->`;
export const BLOCK_CLOSE_ELSE = `<!--${HYDRATION_END_ELSE}-->`;
export const EMPTY_COMMENT = `<!---->`;
20 changes: 15 additions & 5 deletions packages/svelte/src/internal/server/index.js
Original file line number Diff line number Diff line change
@@ -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<string, string>} 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 += `<div style="display: contents; ${styles}">`;
} else {
payload.out += `<g style="${styles}">`;
}

if (dynamic) {
payload.out += '<!---->';
}

component();

if (is_html) {
payload.out += `<!----></div>`;
} else {
Original file line number Diff line number Diff line change
@@ -1 +1 @@
<!--ssr:0--><input> <p>Hello world!</p><!--ssr:0-->
<!--[--><input> <p>Hello world!</p><!--]-->
Original file line number Diff line number Diff line change
@@ -1 +1 @@
<!--ssr:0--><h1>Hello everybody!</h1><!--ssr:0-->
<!--[--><h1>Hello everybody!</h1><!--]-->
Original file line number Diff line number Diff line change
@@ -1,9 +1 @@
<!--ssr:0--><ul><!--ssr:1--><!--ssr:7--><li>a</li><!--ssr:7--><!--ssr:1--></ul>
<ul><!--ssr:2--><!--ssr:9--><li>a</li><!--ssr:9--><!--ssr:2--></ul>
<ul><!--ssr:3--><!--ssr:11--><li>a</li><!--ssr:11--><!--ssr:3--></ul>
<!--ssr:4--><!--ssr:13--><li>a</li>
<li>a</li><!--ssr:13--><!--ssr:4-->
<!--ssr:5--><!--ssr:15--><li>a</li>
<li>a</li><!--ssr:15--><!--ssr:5-->
<!--ssr:6--><!--ssr:17--><li>a</li>
<li>a</li><!--ssr:17--><!--ssr:6--><!--ssr:0--></div>
<!--[--><ul><!--[--><li>a</li><!--]--></ul> <ul><!--[--><li>a</li><!--]--></ul> <ul><!--[--><li>a</li><!--]--></ul> <!--[--><li>a</li> <li>a</li><!--]--> <!--[--><li>a</li> <li>a</li><!--]--> <!--[--><li>a</li> <li>a</li><!--]--><!--]-->
Original file line number Diff line number Diff line change
@@ -1,2 +1 @@
<!--ssr:0--><!--ssr:1--><p>a</p><!--ssr:1-->
<!--ssr:2--><p>empty</p><!--ssr:2--><!--ssr:0-->
<!--[--><!--[!--><p>a</p><!--]--> <!--[--><p>empty</p><!--]--><!--]-->
Original file line number Diff line number Diff line change
@@ -1,9 +1 @@
<!--ssr:0--><ul><!--ssr:1--><!--ssr:7--><li>a</li><!--ssr:7--><!--ssr:1--></ul>
<ul><!--ssr:2--><!--ssr:9--><li>a</li><!--ssr:9--><!--ssr:2--></ul>
<ul><!--ssr:3--><!--ssr:11--><li>a</li><!--ssr:11--><!--ssr:3--></ul>
<!--ssr:4--><!--ssr:13--><li>a</li>
<li>a</li><!--ssr:13--><!--ssr:4-->
<!--ssr:5--><!--ssr:15--><li>a</li>
<li>a</li><!--ssr:15--><!--ssr:5-->
<!--ssr:6--><!--ssr:17--><li>a</li>
<li>a</li><!--ssr:17--><!--ssr:6--><!--ssr:0--></div>
<!--[--><ul><!--[--><li>a</li><!--]--></ul> <ul><!--[--><li>a</li><!--]--></ul> <ul><!--[--><li>a</li><!--]--></ul> <!--[--><li>a</li> <li>a</li><!--]--> <!--[--><li>a</li> <li>a</li><!--]--> <!--[--><li>a</li> <li>a</li><!--]--><!--]-->
Original file line number Diff line number Diff line change
@@ -1,9 +1 @@
<!--ssr:0--><ul><!--ssr:1--><!--ssr:7--><li>a</li><!--ssr:7--><li>b</li><!--ssr:1--></ul>
<ul><!--ssr:2--><!--ssr:8--><li>a</li><!--ssr:8--><li>b</li><!--ssr:2--></ul>
<ul><!--ssr:3--><!--ssr:9--><li>a</li><!--ssr:9--><li>b</li><!--ssr:3--></ul>
<!--ssr:4--><!--ssr:10--><li>a</li>
<li>a</li><!--ssr:10--><li>b</li><li>b</li><!--ssr:4-->
<!--ssr:5--><!--ssr:11--><li>a</li>
<li>a</li><!--ssr:11--><li>b</li><li>b</li><!--ssr:5-->
<!--ssr:6--><!--ssr:12--><li>a</li>
<li>a</li><!--ssr:12--><li>b</li><li>b</li><!--ssr:6--><!--ssr:0--></div>
<!--[--><ul><!--[--><li>a</li><li>b</li><!--]--></ul> <ul><!--[--><li>a</li><li>b</li><!--]--></ul> <ul><!--[--><li>a</li><li>b</li><!--]--></ul> <!--[--><li>a</li> <li>a</li><li>b</li> <li>b</li><!--]--> <!--[--><li>a</li> <li>a</li><li>b</li> <li>b</li><!--]--> <!--[--><li>a</li> <li>a</li><li>b</li> <li>b</li><!--]--><!--]-->
Original file line number Diff line number Diff line change
@@ -1 +1 @@
<!--ssr:0--><div class="bar"></div><!--ssr:0-->
<!--[--><div class="bar"></div><!--]-->
Original file line number Diff line number Diff line change
@@ -1 +1 @@
<!--ssr:0--><div class="bar"></div><!--ssr:0-->
<!--[--><div class="bar"></div><!--]-->
Original file line number Diff line number Diff line change
@@ -1 +1 @@
<div></div><div></div><div></div>
<!--[--><div></div> <!--[--><div></div> <div></div><!--]--><!--]-->
Original file line number Diff line number Diff line change
@@ -1,2 +1 @@
<!--ssr:0--><noscript>JavaScript is required for this site.</noscript>
<h1>Hello!</h1><p>Count: 1</p><!--ssr:0-->
<!--[--><noscript>JavaScript is required for this site.</noscript> <h1>Hello!</h1><p>Count: 1</p><!--]-->
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<!----><p><p>invalid</p><!----></p><!----> <p><p>invalid</p><!----></p>
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<p>child</p>
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { test } from '../../test';

// Ensure that we don't create additional comment nodes for standalone components
export default test({});
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<!--[--><!--[--><p>child</p><!--]--> <!--[--><p>child</p><p>child</p><p>child</p><!--]--><!--]-->
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<script>
import Child from './Child.svelte';
</script>

{#if true}
<Child />
{/if}

{#each [1, 2, 3] as n}
<Child />
{/each}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { test } from '../../test';

// Ensure that we don't create additional comment nodes for standalone components
export default test({});
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<!--[--><!--[--><p>thing</p><!--]--> <!--[--><p>thing</p><p>thing</p><p>thing</p><!--]--><!--]-->
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{#snippet thing()}
<p>thing</p>
{/snippet}

{#if true}
{@render thing()}
{/if}

{#each [1, 2, 3] as n}
{@render thing()}
{/each}
Original file line number Diff line number Diff line change
@@ -1 +1 @@
<!--ssr:0-->x<!--ssr:0-->
<!--[--><!---->x<!--]-->
12 changes: 8 additions & 4 deletions packages/svelte/tests/hydration/test.ts
Original file line number Diff line number Diff line change
@@ -52,8 +52,10 @@ const { test, run } = suite<HydrationTest>(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<HydrationTest>(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) {
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<p>Foo</p>
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { flushSync } from 'svelte';
import { test } from '../../test';

export default test({
html: `
<button>toggle component</button>
<button>toggle show</button>
`,

test({ assert, component, target }) {
const [btn1, btn2] = target.querySelectorAll('button');

flushSync(() => btn1.click());
assert.htmlEqual(
target.innerHTML,
`
<button>toggle component</button>
<button>toggle show</button>
<p>Foo</p>
`
);

flushSync(() => btn2.click());
assert.htmlEqual(
target.innerHTML,
`
<button>toggle component</button>
<button>toggle show</button>
`
);
}
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<script>
import Foo from './Foo.svelte';
/** @type {typeof Foo | null} */
let component = null;
let show = true;
</script>

<button on:click={() => (component = component ? null : Foo)}>toggle component</button>
<button on:click={() => (show = !show)}>toggle show</button>

{#if show}
<svelte:component this={component} />
{/if}
Original file line number Diff line number Diff line change
@@ -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 = '<span>bar</span>';
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
<!--[-->
<!---->
<title>lorem</title>
<!---->
<!---->
<style>
.ipsum {
display: block;
}
</style>
</style>
<!---->
<!---->
<script>
console.log(true);
</script>
<!----><!--]-->
<!---->
<!--]-->
Original file line number Diff line number Diff line change
@@ -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);
Original file line number Diff line number Diff line change
@@ -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 {
Original file line number Diff line number Diff line change
@@ -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);
Original file line number Diff line number Diff line change
@@ -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 += `<!--]-->`;
}
Original file line number Diff line number Diff line change
@@ -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);