feat: single-pass hydration (#12335)

* WIP towards single-pass hydration

* fix

* fixes

* fix

* fix

* fixes

* fix

* fixes

* fix

* fix, tidy up

* update script (it currently fails)

* fix

* fix

* hmm

* fix

* fix

* fix

* fix

* all hydration tests passing

* drive-by fix

* fix

* update snapshot tests

* fix

* recover: false

* fix invalid HTML message

* note to self

* fix

* fix

* update snapshot tests

* fix

* fix

* fix

* update test

* fix

* fix

* fix

* ALL TESTS PASSING THIS IS NOT A DRILL

* optimise each blocks

* changeset

* type stuff

* fix comment

* tidy up

* tidy up

* tidy up

* tidy up

* tidy up

* remove comment, turns out we do need it

* revert

* reinstate standalone optimisation

* improve <svelte:element> SSR

* reset more conservatively

* tweak

* DRY/fix

* revert

* simplify

* add comment

* tweak

* simplify

* simplify

* answer: yes, at least for now, because otherwise empty components are a nuisance

* tweak

* unused

* comment is answered by https://github.com/sveltejs/svelte/pull/12356

* tweak

* handle `<template>` edge case at compile time

* this is no longer a possibility, because of is_text_first

* unused

* tweak

* fix

* move annotations to properties

* Update packages/svelte/src/constants.js

Co-authored-by: Simon H <5968653+dummdidumm@users.noreply.github.com>

* Update packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js

Co-authored-by: Simon H <5968653+dummdidumm@users.noreply.github.com>

* Update packages/svelte/src/internal/client/dom/blocks/each.js

Co-authored-by: Simon H <5968653+dummdidumm@users.noreply.github.com>

* Update packages/svelte/src/internal/client/dom/hydration.js

Co-authored-by: Simon H <5968653+dummdidumm@users.noreply.github.com>

* Update playgrounds/demo/vite.config.js

Co-authored-by: Simon H <5968653+dummdidumm@users.noreply.github.com>

* add a comment

* prettier

* tweak

* tighten up hydration tests, add test for standalone component

* test for standalone snippet

* fix

* add some comments

* tidy up

* avoid mutating `arguments`

---------

Co-authored-by: Simon H <5968653+dummdidumm@users.noreply.github.com>
pull/12361/head
Rich Harris 6 months ago committed by GitHub
parent 3dbb220169
commit 2789a3c0ef
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,5 @@
---
'svelte': patch
---
feat: single-pass hydration

@ -113,7 +113,7 @@ const bundle = await bundle_code(
).js.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 // eslint-disable-next-line no-console
console.error(`✅ Hydration code treeshakeable`); console.error(`✅ Hydration code treeshakeable`);
} else { } else {

@ -980,7 +980,8 @@ function serialize_inline_component(node, component_name, context, anchor = cont
statements.push( statements.push(
b.stmt(b.call('$.css_props', anchor, b.thunk(b.object(custom_css_props)))), 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 { } else {
context.state.template.push('<!>'); context.state.template.push('<!>');
@ -1441,6 +1442,12 @@ function process_children(nodes, expression, is_element, { visit, state }) {
} }
if (sequence.length > 0) { 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); flush_sequence(sequence);
} }
} }
@ -1569,7 +1576,7 @@ export const template_visitors = {
const namespace = infer_namespace(context.state.metadata.namespace, parent, node.nodes); 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, parent,
node.nodes, node.nodes,
context.path, context.path,
@ -1619,6 +1626,11 @@ export const template_visitors = {
context.visit(node, state); 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').Identifier} template_name
* @param {import('estree').Expression[]} args * @param {import('estree').Expression[]} args
@ -1677,11 +1689,7 @@ export const template_visitors = {
state state
}); });
body.push( body.push(b.var(id, b.call('$.text')), ...state.before_init, ...state.init);
b.var(id, b.call('$.text', b.id('$$anchor'))),
...state.before_init,
...state.init
);
close = b.stmt(b.call('$.append', b.id('$$anchor'), id)); close = b.stmt(b.call('$.append', b.id('$$anchor'), id));
} else { } else {
if (is_standalone) { if (is_standalone) {
@ -1689,8 +1697,7 @@ export const template_visitors = {
process_children(trimmed, () => b.id('$$anchor'), false, { ...context, state }); process_children(trimmed, () => b.id('$$anchor'), false, { ...context, state });
} else { } else {
/** @type {(is_text: boolean) => import('estree').Expression} */ /** @type {(is_text: boolean) => import('estree').Expression} */
const expression = (is_text) => const expression = (is_text) => b.call('$.first_child', id, is_text && b.true);
is_text ? b.call('$.first_child', id, b.true) : b.call('$.first_child', id);
process_children(trimmed, expression, false, { ...context, state }); process_children(trimmed, expression, false, { ...context, state });
@ -2180,18 +2187,30 @@ export const template_visitors = {
context.visit(node, child_state); context.visit(node, child_state);
} }
process_children( /** @type {import('estree').Expression} */
trimmed, let arg = context.state.node;
() =>
b.call( // If `hydrate_node` is set inside the element, we need to reset it
'$.child', // after the element has been hydrated
node.name === 'template' let needs_reset = trimmed.some((node) => node.type !== 'Text');
? b.member(context.state.node, b.id('content'))
: context.state.node // The same applies if it's a `<template>` element, since we need to
), // set the value of `hydrate_node` to `node.content`
true, if (node.name === 'template') {
{ ...context, state: child_state } 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) { if (has_declaration) {
context.state.init.push( context.state.init.push(

@ -33,16 +33,21 @@ import {
import { escape_html } from '../../../../escaping.js'; import { escape_html } from '../../../../escaping.js';
import { sanitize_template_string } from '../../../utils/sanitize_template_string.js'; import { sanitize_template_string } from '../../../utils/sanitize_template_string.js';
import { import {
BLOCK_ANCHOR, EMPTY_COMMENT,
BLOCK_CLOSE, BLOCK_CLOSE,
BLOCK_CLOSE_ELSE, BLOCK_OPEN,
BLOCK_OPEN BLOCK_OPEN_ELSE
} from '../../../../internal/server/hydration.js'; } from '../../../../internal/server/hydration.js';
import { filename, locator } from '../../../state.js'; import { filename, locator } from '../../../state.js';
export const block_open = b.literal(BLOCK_OPEN); /** Opens an if/each block, so that we can remove nodes in the case of a mismatch */
export const block_close = b.literal(BLOCK_CLOSE); const block_open = b.literal(BLOCK_OPEN);
export const block_anchor = b.literal(BLOCK_ANCHOR);
/** 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 * @param {import('estree').Node} node
@ -996,22 +1001,32 @@ function serialize_inline_component(node, expression, context) {
statement = b.block([...snippet_declarations, statement]); statement = b.block([...snippet_declarations, statement]);
} }
const dynamic =
node.type === 'SvelteComponent' || (node.type === 'Component' && node.metadata.dynamic);
if (custom_css_props.length > 0) { if (custom_css_props.length > 0) {
statement = b.stmt( context.state.template.push(
b.stmt(
b.call( b.call(
'$.css_props', '$.css_props',
b.id('$$payload'), b.id('$$payload'),
b.literal(context.state.namespace === 'svg' ? false : true), b.literal(context.state.namespace === 'svg' ? false : true),
b.object(custom_css_props), b.object(custom_css_props),
b.thunk(b.block([statement])) b.thunk(b.block([statement])),
dynamic && b.true
)
) )
); );
} else {
if (dynamic) {
context.state.template.push(empty_comment);
}
context.state.template.push(statement); context.state.template.push(statement);
} else if (context.state.skip_hydration_boundaries) {
context.state.template.push(statement); if (!context.state.skip_hydration_boundaries) {
} else { context.state.template.push(empty_comment);
context.state.template.push(block_open, statement, block_close); }
} }
} }
@ -1119,7 +1134,7 @@ const template_visitors = {
const parent = context.path.at(-1) ?? node; const parent = context.path.at(-1) ?? node;
const namespace = infer_namespace(context.state.namespace, parent, node.nodes); 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, parent,
node.nodes, node.nodes,
context.path, context.path,
@ -1142,13 +1157,18 @@ const template_visitors = {
context.visit(node, state); 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 }); process_children(trimmed, { ...context, state });
return b.block([...state.init, ...serialize_template(state.template)]); return b.block([...state.init, ...serialize_template(state.template)]);
}, },
HtmlTag(node, context) { HtmlTag(node, context) {
const expression = /** @type {import('estree').Expression} */ (context.visit(node.expression)); 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 }) { ConstTag(node, { state, visit }) {
const declaration = node.declaration.declarations[0]; const declaration = node.declaration.declarations[0];
@ -1188,10 +1208,6 @@ const template_visitors = {
return /** @type {import('estree').Expression} */ (context.visit(arg)); return /** @type {import('estree').Expression} */ (context.visit(arg));
}); });
if (!context.state.skip_hydration_boundaries) {
context.state.template.push(block_open);
}
context.state.template.push( context.state.template.push(
b.stmt( b.stmt(
(node.expression.type === 'CallExpression' ? b.call : b.maybe_call)( (node.expression.type === 'CallExpression' ? b.call : b.maybe_call)(
@ -1203,7 +1219,7 @@ const template_visitors = {
); );
if (!context.state.skip_hydration_boundaries) { if (!context.state.skip_hydration_boundaries) {
context.state.template.push(block_close); context.state.template.push(empty_comment);
} }
}, },
ClassDirective() { ClassDirective() {
@ -1353,7 +1369,6 @@ const template_visitors = {
}, },
EachBlock(node, context) { EachBlock(node, context) {
const state = context.state; const state = context.state;
state.template.push(block_open);
const each_node_meta = node.metadata; const each_node_meta = node.metadata;
const collection = /** @type {import('estree').Expression} */ (context.visit(node.expression)); 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.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(.../** @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( const for_loop = b.for(
b.let(index, b.literal(0)), b.let(index, b.literal(0)),
b.binary('<', index, b.member(array_id, b.id('length'))), b.binary('<', index, b.member(array_id, b.id('length'))),
@ -1389,26 +1400,27 @@ const template_visitors = {
b.block(each) b.block(each)
); );
const close = b.stmt(b.assignment('+=', b.id('$$payload.out'), b.literal(BLOCK_CLOSE)));
if (node.fallback) { if (node.fallback) {
const open = b.stmt(b.assignment('+=', b.id('$$payload.out'), block_open));
const fallback = /** @type {import('estree').BlockStatement} */ ( const fallback = /** @type {import('estree').BlockStatement} */ (
context.visit(node.fallback) context.visit(node.fallback)
); );
fallback.body.push( fallback.body.unshift(
b.stmt(b.assignment('+=', b.id('$$payload.out'), b.literal(BLOCK_CLOSE_ELSE))) b.stmt(b.assignment('+=', b.id('$$payload.out'), b.literal(BLOCK_OPEN_ELSE)))
); );
state.template.push( state.template.push(
b.if( b.if(
b.binary('!==', b.member(array_id, b.id('length')), b.literal(0)), b.binary('!==', b.member(array_id, b.id('length')), b.literal(0)),
b.block([for_loop, close]), b.block([open, for_loop]),
fallback fallback
) ),
block_close
); );
} else { } else {
state.template.push(for_loop, close); state.template.push(block_open, for_loop, block_close);
} }
}, },
IfBlock(node, context) { IfBlock(node, context) {
@ -1422,16 +1434,17 @@ const template_visitors = {
? /** @type {import('estree').BlockStatement} */ (context.visit(node.alternate)) ? /** @type {import('estree').BlockStatement} */ (context.visit(node.alternate))
: b.block([]); : b.block([]);
consequent.body.push(b.stmt(b.assignment('+=', b.id('$$payload.out'), b.literal(BLOCK_CLOSE)))); consequent.body.unshift(b.stmt(b.assignment('+=', b.id('$$payload.out'), block_open)));
alternate.body.push(
b.stmt(b.assignment('+=', b.id('$$payload.out'), b.literal(BLOCK_CLOSE_ELSE))) 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) { AwaitBlock(node, context) {
context.state.template.push( context.state.template.push(
block_open, empty_comment,
b.stmt( b.stmt(
b.call( b.call(
'$.await', '$.await',
@ -1455,12 +1468,12 @@ const template_visitors = {
) )
) )
), ),
block_close empty_comment
); );
}, },
KeyBlock(node, context) { KeyBlock(node, context) {
const block = /** @type {import('estree').BlockStatement} */ (context.visit(node.fragment)); 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) { SnippetBlock(node, context) {
const fn = b.function_declaration( const fn = b.function_declaration(
@ -1594,7 +1607,7 @@ const template_visitors = {
const slot = b.call('$.slot', b.id('$$payload'), expression, props_expression, fallback); 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) { SvelteHead(node, context) {
const block = /** @type {import('estree').BlockStatement} */ (context.visit(node.fragment)); const block = /** @type {import('estree').BlockStatement} */ (context.visit(node.fragment));

@ -270,21 +270,28 @@ export function clean_nodes(
var first = trimmed[0]; var first = trimmed[0];
return {
hoisted,
trimmed,
/** /**
* In a case like `{#if x}<Foo />{/if}`, we don't need to wrap the child in * 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. * comments we can just use the parent block's anchor for the component.
* TODO extend this optimisation to other cases * TODO extend this optimisation to other cases
*/ */
const is_standalone = is_standalone:
trimmed.length === 1 && trimmed.length === 1 &&
((first.type === 'RenderTag' && !first.metadata.dynamic) || ((first.type === 'RenderTag' && !first.metadata.dynamic) ||
(first.type === 'Component' && (first.type === 'Component' &&
!state.options.hmr && !state.options.hmr &&
!first.attributes.some( !first.attributes.some(
(attribute) => attribute.type === 'Attribute' && attribute.name.startsWith('--') (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 */
return { hoisted, trimmed, is_standalone }; is_text_first:
(parent.type === 'Fragment' || parent.type === 'SnippetBlock') &&
first &&
(first?.type === 'Text' || first?.type === 'ExpressionTag')
};
} }
/** /**

@ -20,9 +20,9 @@ export const TEMPLATE_FRAGMENT = 1;
export const TEMPLATE_USE_IMPORT_NODE = 1 << 1; export const TEMPLATE_USE_IMPORT_NODE = 1 << 1;
export const HYDRATION_START = '['; 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_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 HYDRATION_ERROR = {};
export const ELEMENT_IS_NAMESPACED = 1; export const ELEMENT_IS_NAMESPACED = 1;

@ -1,5 +1,5 @@
/** @import { SourceLocation } from '#shared' */ /** @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'; import { hydrating } from '../dom/hydration.js';
/** /**
@ -47,7 +47,7 @@ function assign_locations(node, filename, locations) {
while (node && i < locations.length) { while (node && i < locations.length) {
if (hydrating && node.nodeType === 8) { if (hydrating && node.nodeType === 8) {
var comment = /** @type {Comment} */ (node); 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; else if (comment.data[0] === HYDRATION_END) depth -= 1;
} }

@ -12,7 +12,7 @@ import {
import { block, branch, pause_effect, resume_effect } from '../../reactivity/effects.js'; import { block, branch, pause_effect, resume_effect } from '../../reactivity/effects.js';
import { DEV } from 'esm-env'; import { DEV } from 'esm-env';
import { queue_micro_task } from '../task.js'; 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'; import { mutable_source, set, source } from '../../reactivity/sources.js';
const PENDING = 0; const PENDING = 0;
@ -21,14 +21,19 @@ const CATCH = 2;
/** /**
* @template V * @template V
* @param {TemplateNode} anchor * @param {TemplateNode} node
* @param {(() => Promise<V>)} get_input * @param {(() => Promise<V>)} get_input
* @param {null | ((anchor: Node) => void)} pending_fn * @param {null | ((anchor: Node) => void)} pending_fn
* @param {null | ((anchor: Node, value: Source<V>) => void)} then_fn * @param {null | ((anchor: Node, value: Source<V>) => void)} then_fn
* @param {null | ((anchor: Node, error: unknown) => void)} catch_fn * @param {null | ((anchor: Node, error: unknown) => void)} catch_fn
* @returns {void} * @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 runes = is_runes();
var component_context = current_component_context; 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 // teardown function is an easy way to ensure that this is not discarded
return noop; return noop;
}); });
if (hydrating) {
anchor = hydrate_node;
}
} }

@ -1,6 +1,6 @@
/** @import { TemplateNode } from '#client' */ /** @import { TemplateNode } from '#client' */
import { hydrating, set_hydrate_nodes } from '../hydration.js';
import { render_effect, teardown } from '../../reactivity/effects.js'; import { render_effect, teardown } from '../../reactivity/effects.js';
import { hydrate_node, hydrating, set_hydrate_node } from '../hydration.js';
/** /**
* @param {HTMLDivElement | SVGGElement} element * @param {HTMLDivElement | SVGGElement} element
@ -9,7 +9,7 @@ import { render_effect, teardown } from '../../reactivity/effects.js';
*/ */
export function css_props(element, get_styles) { export function css_props(element, get_styles) {
if (hydrating) { if (hydrating) {
set_hydrate_nodes(/** @type {TemplateNode[]} */ ([...element.childNodes]).slice(0, -1)); set_hydrate_node(/** @type {TemplateNode} */ (element.firstChild));
} }
render_effect(() => { render_effect(() => {

@ -1,3 +1,4 @@
/** @import { TemplateNode } from '#client' */
import { import {
EACH_INDEX_REACTIVE, EACH_INDEX_REACTIVE,
EACH_IS_ANIMATED, EACH_IS_ANIMATED,
@ -5,18 +6,18 @@ import {
EACH_IS_STRICT_EQUALS, EACH_IS_STRICT_EQUALS,
EACH_ITEM_REACTIVE, EACH_ITEM_REACTIVE,
EACH_KEYED, EACH_KEYED,
HYDRATION_END_ELSE, HYDRATION_END,
HYDRATION_START HYDRATION_START_ELSE
} from '../../../../constants.js'; } from '../../../../constants.js';
import { import {
hydrate_anchor, hydrate_next,
hydrate_nodes, hydrate_node,
hydrate_start,
hydrating, hydrating,
remove_nodes,
set_hydrate_node,
set_hydrating set_hydrating
} from '../hydration.js'; } from '../hydration.js';
import { clear_text_content, empty } from '../operations.js'; import { clear_text_content, empty } from '../operations.js';
import { remove } from '../reconciler.js';
import { import {
block, block,
branch, branch,
@ -96,7 +97,7 @@ function pause_effects(state, items, controlled_anchor, items_map) {
/** /**
* @template V * @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 {number} flags
* @param {() => V[]} get_collection * @param {() => V[]} get_collection
* @param {(value: V, index: number) => any} get_key * @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 * @param {null | ((anchor: Node) => void)} fallback_fn
* @returns {void} * @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} */ /** @type {import('#client').EachState} */
var state = { flags, items: new Map(), first: null }; var state = { flags, items: new Map(), first: null };
var is_controlled = (flags & EACH_IS_CONTROLLED) !== 0; var is_controlled = (flags & EACH_IS_CONTROLLED) !== 0;
if (is_controlled) { if (is_controlled) {
var parent_node = /** @type {Element} */ (anchor); var parent_node = /** @type {Element} */ (node);
anchor = hydrating anchor = hydrating
? /** @type {Comment | Text} */ ( ? set_hydrate_node(/** @type {Comment | Text} */ (parent_node.firstChild))
hydrate_anchor(/** @type {Comment | Text} */ (parent_node.firstChild))
)
: parent_node.appendChild(empty()); : parent_node.appendChild(empty());
} }
if (hydrating) {
hydrate_next();
}
/** @type {import('#client').Effect | null} */ /** @type {import('#client').Effect | null} */
var fallback = null; var fallback = null;
@ -155,11 +160,13 @@ export function each(anchor, flags, get_collection, get_key, render_fn, fallback
let mismatch = false; let mismatch = false;
if (hydrating) { 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 // hydration mismatch — remove the server-rendered DOM and start over
remove(hydrate_nodes); anchor = remove_nodes();
set_hydrate_node(anchor);
set_hydrating(false); set_hydrating(false);
mismatch = true; 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 // this is separate to the previous block because `hydrating` might change
if (hydrating) { if (hydrating) {
/** @type {Node} */
var child_anchor = hydrate_start;
/** @type {import('#client').EachItem | null} */ /** @type {import('#client').EachItem | null} */
var prev = 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++) { for (var i = 0; i < length; i++) {
if ( if (
child_anchor.nodeType !== 8 || hydrate_node.nodeType === 8 &&
/** @type {Comment} */ (child_anchor).data !== HYDRATION_START /** @type {Comment} */ (hydrate_node).data === HYDRATION_END
) { ) {
// If `nodes` is null, then that means that the server rendered fewer items than what // The server rendered fewer items than expected,
// expected, so break out and continue appending non-hydrated items // so break out and continue appending non-hydrated items
anchor = /** @type {Comment} */ (hydrate_node);
mismatch = true; mismatch = true;
set_hydrating(false); set_hydrating(false);
break; break;
} }
child_anchor = hydrate_anchor(child_anchor);
var value = array[i]; var value = array[i];
var key = get_key(value, 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); state.items.set(key, item);
child_anchor = /** @type {Comment} */ (child_anchor.nextSibling);
prev = item; prev = item;
} }
// remove excess nodes // remove excess nodes
if (length > 0) { if (length > 0) {
while (child_anchor !== anchor) { set_hydrate_node(remove_nodes());
var next = /** @type {import('#client').TemplateNode} */ (child_anchor.nextSibling);
/** @type {import('#client').TemplateNode} */ (child_anchor).remove();
child_anchor = next;
}
} }
} }
@ -231,6 +230,10 @@ export function each(anchor, flags, get_collection, get_key, render_fn, fallback
set_hydrating(true); set_hydrating(true);
} }
}); });
if (hydrating) {
anchor = hydrate_node;
}
} }
/** /**

@ -1,17 +1,21 @@
/** @import { Effect, TemplateNode } from '#client' */ /** @import { Effect, TemplateNode } from '#client' */
import { HYDRATION_ERROR } from '../../../../constants.js';
import { block, branch, destroy_effect } from '../../reactivity/effects.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 { create_fragment_from_html } from '../reconciler.js';
import { assign_nodes } from '../template.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 {() => string} get_value
* @param {boolean} svg * @param {boolean} svg
* @param {boolean} mathml * @param {boolean} mathml
* @returns {void} * @returns {void}
*/ */
export function html(anchor, get_value, svg, mathml) { export function html(node, get_value, svg, mathml) {
var anchor = node;
var value = ''; var value = '';
/** @type {Effect | null} */ /** @type {Effect | null} */
@ -29,7 +33,24 @@ export function html(anchor, get_value, svg, mathml) {
effect = branch(() => { effect = branch(() => {
if (hydrating) { 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; return;
} }

@ -1,24 +1,31 @@
/** @import { TemplateNode } from '#client' */
import { EFFECT_TRANSPARENT } from '../../constants.js'; import { EFFECT_TRANSPARENT } from '../../constants.js';
import { hydrate_nodes, hydrating, set_hydrating } from '../hydration.js'; import {
import { remove } from '../reconciler.js'; 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 { 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 {() => boolean} get_condition
* @param {(anchor: Node) => import('#client').Dom} consequent_fn * @param {(anchor: Node) => import('#client').Dom} consequent_fn
* @param {null | ((anchor: Node) => import('#client').Dom)} [alternate_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' * @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} * @returns {void}
*/ */
export function if_block( export function if_block(node, get_condition, consequent_fn, alternate_fn = null, elseif = false) {
anchor, if (hydrating) {
get_condition, hydrate_next();
consequent_fn, }
alternate_fn = null,
elseif = false var anchor = node;
) {
/** @type {import('#client').Effect | null} */ /** @type {import('#client').Effect | null} */
var consequent_effect = null; var consequent_effect = null;
@ -37,12 +44,14 @@ export function if_block(
let mismatch = false; let mismatch = false;
if (hydrating) { if (hydrating) {
const is_else = anchor.data === HYDRATION_END_ELSE; const is_else = /** @type {Comment} */ (anchor).data === HYDRATION_START_ELSE;
if (condition === is_else) { if (condition === is_else) {
// Hydration mismatch: remove everything inside the anchor and start fresh. // Hydration mismatch: remove everything inside the anchor and start fresh.
// This could happen with `{#if browser}...{/if}`, for example // This could happen with `{#if browser}...{/if}`, for example
remove(hydrate_nodes); anchor = remove_nodes();
set_hydrate_node(anchor);
set_hydrating(false); set_hydrating(false);
mismatch = true; mismatch = true;
} }
@ -79,4 +88,8 @@ export function if_block(
set_hydrating(true); set_hydrating(true);
} }
}, flags); }, flags);
if (hydrating) {
anchor = hydrate_node;
}
} }

@ -1,21 +1,28 @@
/** @import { Dom, Effect } from '#client' */ /** @import { Effect, TemplateNode } from '#client' */
import { UNINITIALIZED } from '../../../../constants.js'; import { UNINITIALIZED } from '../../../../constants.js';
import { block, branch, pause_effect } from '../../reactivity/effects.js'; import { block, branch, pause_effect } from '../../reactivity/effects.js';
import { safe_not_equal } from '../../reactivity/equality.js'; import { safe_not_equal } from '../../reactivity/equality.js';
import { hydrate_next, hydrate_node, hydrating } from '../hydration.js';
/** /**
* @template V * @template V
* @param {Comment} anchor * @param {TemplateNode} node
* @param {() => V} get_key * @param {() => V} get_key
* @param {(anchor: Node) => Dom | void} render_fn * @param {(anchor: Node) => TemplateNode | void} render_fn
* @returns {void} * @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} */ /** @type {V | typeof UNINITIALIZED} */
let key = UNINITIALIZED; var key = UNINITIALIZED;
/** @type {Effect} */ /** @type {Effect} */
let effect; var effect;
block(() => { block(() => {
if (safe_not_equal(key, (key = get_key()))) { 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)); effect = branch(() => render_fn(anchor));
} }
}); });
if (hydrating) {
anchor = hydrate_node;
}
} }

@ -1,3 +1,5 @@
import { hydrate_next, hydrating } from '../hydration.js';
/** /**
* @param {Comment} anchor * @param {Comment} anchor
* @param {void | ((anchor: Comment, slot_props: Record<string, unknown>) => void)} slot_fn * @param {void | ((anchor: Comment, slot_props: Record<string, unknown>) => void)} slot_fn
@ -5,6 +7,10 @@
* @param {null | ((anchor: Comment) => void)} fallback_fn * @param {null | ((anchor: Comment) => void)} fallback_fn
*/ */
export function slot(anchor, slot_fn, slot_props, fallback_fn) { export function slot(anchor, slot_fn, slot_props, fallback_fn) {
if (hydrating) {
hydrate_next();
}
if (slot_fn === undefined) { if (slot_fn === undefined) {
if (fallback_fn !== null) { if (fallback_fn !== null) {
fallback_fn(anchor); fallback_fn(anchor);

@ -6,15 +6,18 @@ import {
dev_current_component_function, dev_current_component_function,
set_dev_current_component_function set_dev_current_component_function
} from '../../runtime.js'; } from '../../runtime.js';
import { hydrate_node, hydrating } from '../hydration.js';
/** /**
* @template {(node: TemplateNode, ...args: any[]) => void} SnippetFn * @template {(node: TemplateNode, ...args: any[]) => void} SnippetFn
* @param {TemplateNode} anchor * @param {TemplateNode} node
* @param {() => SnippetFn | null | undefined} get_snippet * @param {() => SnippetFn | null | undefined} get_snippet
* @param {(() => any)[]} args * @param {(() => any)[]} args
* @returns {void} * @returns {void}
*/ */
export function snippet(anchor, get_snippet, ...args) { export function snippet(node, get_snippet, ...args) {
var anchor = node;
/** @type {SnippetFn | null | undefined} */ /** @type {SnippetFn | null | undefined} */
var snippet; var snippet;
@ -33,6 +36,10 @@ export function snippet(anchor, get_snippet, ...args) {
snippet_effect = branch(() => /** @type {SnippetFn} */ (snippet)(anchor, ...args)); snippet_effect = branch(() => /** @type {SnippetFn} */ (snippet)(anchor, ...args));
} }
}, EFFECT_TRANSPARENT); }, EFFECT_TRANSPARENT);
if (hydrating) {
anchor = hydrate_node;
}
} }
/** /**

@ -1,20 +1,27 @@
/** @import { TemplateNode, Dom, Effect } from '#client' */ /** @import { TemplateNode, Dom, Effect } from '#client' */
import { block, branch, pause_effect } from '../../reactivity/effects.js'; import { block, branch, pause_effect } from '../../reactivity/effects.js';
import { hydrate_next, hydrate_node, hydrating } from '../hydration.js';
/** /**
* @template P * @template P
* @template {(props: P) => void} C * @template {(props: P) => void} C
* @param {TemplateNode} anchor * @param {TemplateNode} node
* @param {() => C} get_component * @param {() => C} get_component
* @param {(anchor: TemplateNode, component: C) => Dom | void} render_fn * @param {(anchor: TemplateNode, component: C) => Dom | void} render_fn
* @returns {void} * @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} */ /** @type {C} */
let component; var component;
/** @type {Effect | null} */ /** @type {Effect | null} */
let effect; var effect;
block(() => { block(() => {
if (component === (component = get_component())) return; if (component === (component = get_component())) return;
@ -28,4 +35,8 @@ export function component(anchor, get_component, render_fn) {
effect = branch(() => render_fn(anchor, component)); effect = branch(() => render_fn(anchor, component));
} }
}); });
if (hydrating) {
anchor = hydrate_node;
}
} }

@ -1,5 +1,12 @@
/** @import { Effect, EffectNodes, TemplateNode } from '#client' */
import { namespace_svg } from '../../../../constants.js'; 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 { empty } from '../operations.js';
import { import {
block, block,
@ -10,10 +17,10 @@ import {
} from '../../reactivity/effects.js'; } from '../../reactivity/effects.js';
import { set_should_intro } from '../../render.js'; import { set_should_intro } from '../../render.js';
import { current_each_item, set_current_each_item } from './each.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 { DEV } from 'esm-env';
import { assign_nodes } from '../template.js';
import { EFFECT_TRANSPARENT } from '../../constants.js'; import { EFFECT_TRANSPARENT } from '../../constants.js';
import { assign_nodes } from '../template.js';
/** /**
* @param {Comment | Element} node * @param {Comment | Element} node
@ -25,36 +32,43 @@ import { EFFECT_TRANSPARENT } from '../../constants.js';
* @returns {void} * @returns {void}
*/ */
export function element(node, get_tag, is_svg, render_fn, get_namespace, location) { 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} */ /** @type {string | null} */
let tag; var tag;
/** @type {string | null} */ /** @type {string | null} */
let current_tag; var current_tag;
/** @type {null | Element} */ /** @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} */ /** @type {Effect | null} */
let effect; var effect;
/** /**
* The keyed `{#each ...}` item block, if any, that this element is inside. * 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 * We track this so we can set it when changing the element, allowing any
* `animate:` directive to bind itself to the correct block * `animate:` directive to bind itself to the correct block
*/ */
let each_item_block = current_each_item; var each_item_block = current_each_item;
block(() => { block(() => {
const next_tag = get_tag() || null; const next_tag = get_tag() || null;
const ns = get_namespace var ns = get_namespace ? get_namespace() : is_svg || next_tag === 'svg' ? namespace_svg : null;
? get_namespace()
: is_svg || next_tag === 'svg'
? namespace_svg
: null;
// Assumption: Noone changes the namespace but not the tag (what would that even mean?) // Assumption: Noone changes the namespace but not the tag (what would that even mean?)
if (next_tag === tag) return; 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.createElementNS(ns, next_tag)
: document.createElement(next_tag); : document.createElement(next_tag);
assign_nodes(element, element);
if (DEV && location) { if (DEV && location) {
// @ts-expect-error // @ts-expect-error
element.__svelte_meta = { 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 (render_fn) {
// If hydrating, use the existing ssr comment as the anchor so that the // 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 // inner open and close methods can pick up the existing nodes correctly
var child_anchor = hydrating ? element.lastChild : element.appendChild(empty()); var child_anchor = /** @type {TemplateNode} */ (
hydrating ? element.firstChild : element.appendChild(empty())
if (hydrating && child_anchor) {
set_hydrate_nodes(
/** @type {import('#client').TemplateNode[]} */ ([...element.childNodes]).slice(0, -1)
); );
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 // `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); 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); 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); set_current_each_item(previous_each_item);
}, EFFECT_TRANSPARENT); }, EFFECT_TRANSPARENT);
if (was_hydrating) {
set_hydrating(true);
set_hydrate_node(anchor);
}
} }

@ -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 { empty } from '../operations.js';
import { block } from '../../reactivity/effects.js'; import { block } from '../../reactivity/effects.js';
import { HYDRATION_END, HYDRATION_START } from '../../../../constants.js';
import { HEAD_EFFECT } from '../../constants.js'; import { HEAD_EFFECT } from '../../constants.js';
import { HYDRATION_START } from '../../../../constants.js';
/** /**
* @type {Node | undefined} * @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} * @returns {void}
*/ */
export function head(render_fn) { export function head(render_fn) {
// The head function may be called after the first hydration pass and ssr comment nodes may still be present, // 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. // 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; let was_hydrating = hydrating;
/** @type {Comment | Text} */ /** @type {Comment | Text} */
var anchor; var anchor;
if (hydrating) { 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. // There might be multiple head blocks in our app, so we need to account for each one needing independent hydration.
if (head_anchor === undefined) { if (head_anchor === undefined) {
head_anchor = /** @type {import('#client').TemplateNode} */ (document.head.firstChild); head_anchor = /** @type {TemplateNode} */ (document.head.firstChild);
} }
while ( while (
head_anchor.nodeType !== 8 || head_anchor.nodeType !== 8 ||
/** @type {Comment} */ (head_anchor).data !== HYDRATION_START /** @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 = set_hydrate_node(/** @type {TemplateNode} */ (head_anchor.nextSibling));
head_anchor = /** @type {import('#client').TemplateNode} */ (head_anchor.nextSibling);
} else { } else {
anchor = document.head.appendChild(empty()); anchor = document.head.appendChild(empty());
} }
@ -51,7 +51,7 @@ export function head(render_fn) {
block(() => render_fn(anchor), HEAD_EFFECT); block(() => render_fn(anchor), HEAD_EFFECT);
} finally { } finally {
if (was_hydrating) { if (was_hydrating) {
set_hydrate_nodes(/** @type {import('#client').TemplateNode[]} */ (previous_hydrate_nodes)); set_hydrate_node(/** @type {TemplateNode} */ (previous_hydrate_node));
} }
} }
} }

@ -1,6 +1,6 @@
import { DEV } from 'esm-env'; /** @import { TemplateNode } from '#client' */
import { HYDRATION_END, HYDRATION_START, HYDRATION_ERROR } from '../../../constants.js';
import * as w from '../warnings.js'; 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 * 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 node that is currently being hydrated. This starts out as the first node inside the opening
* the sake of simplicity we're not going to use `null` checks everywhere and instead rely on * <!--[--> comment, and updates each time a component calls `$.child(...)` or `$.sibling(...)`.
* the `hydrating` flag to tell whether or not we're in hydration mode at which point this is set. * When entering a block (e.g. `{#if ...}`), `hydrate_node` is the block opening comment; by the
* @type {import('#client').TemplateNode[]} * 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); export let hydrate_node;
/** @type {import('#client').TemplateNode} */
export let hydrate_start;
/** @param {import('#client').TemplateNode[]} nodes */ /** @param {TemplateNode} node */
export function set_hydrate_nodes(nodes) { export function set_hydrate_node(node) {
hydrate_nodes = nodes; return (hydrate_node = node);
hydrate_start = nodes && nodes[0];
} }
/** export function hydrate_next() {
* When assigning nodes to an effect during hydration, we typically want the hydration boundary comment node return (hydrate_node = /** @type {TemplateNode} */ (hydrate_node.nextSibling));
* 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
);
} }
/** /** @param {TemplateNode} node */
* This function is only called when `hydrating` is true. If passed a `<!--[-->` opening export function reset(node) {
* hydration marker, it finds the corresponding closing marker and sets `hydrate_nodes` if (hydrating) {
* to everything between the markers, before returning the closing marker. hydrate_node = node;
* @param {Node} node }
* @returns {Node}
*/
export function hydrate_anchor(node) {
if (node.nodeType !== 8) {
return node;
} }
var current = /** @type {Node | null} */ (node); export function next() {
if (hydrating) {
// TODO this could have false positives, if a user comment consisted of `[`. need to tighten that up hydrate_next();
if (/** @type {Comment} */ (current).data !== HYDRATION_START) { }
return node;
} }
/** @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 depth = 0;
var node = hydrate_node;
while ((current = /** @type {Node} */ (current).nextSibling) !== null) { while (true) {
if (current.nodeType === 8) { if (node.nodeType === 8) {
var data = /** @type {Comment} */ (current).data; var data = /** @type {Comment} */ (node).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;
}
if (data === HYDRATION_END) {
if (depth === 0) return node;
depth -= 1; 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;
}

@ -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 { DEV } from 'esm-env';
import { init_array_prototype_warnings } from '../dev/equality.js'; import { init_array_prototype_warnings } from '../dev/equality.js';
import { current_effect } from '../runtime.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 // export these for reference in the compiled code, making global name deduplication unnecessary
/** @type {Window} */ /** @type {Window} */
@ -58,19 +58,23 @@ export function empty() {
*/ */
/*#__NO_SIDE_EFFECTS__*/ /*#__NO_SIDE_EFFECTS__*/
export function child(node) { export function child(node) {
const child = node.firstChild; if (!hydrating) {
if (!hydrating) return child; 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 // Child can be null if we have an element with a single child, like `<p>{text}</p>`, where `text` is empty
if (child === null) { if (child === null) {
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 * @param {boolean} is_text
* @returns {Node | null} * @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 // if an {expression} is empty during SSR, there might be no
// text node to hydrate — we must therefore create one // text node to hydrate — we must therefore create one
if (is_text && hydrate_start?.nodeType !== 3) { if (is_text && hydrate_node?.nodeType !== 3) {
var text = empty(); 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 text;
} }
return hydrate_anchor(hydrate_start); return hydrate_node;
} }
/** /**
@ -111,27 +111,25 @@ export function first_child(fragment, is_text) {
*/ */
/*#__NO_SIDE_EFFECTS__*/ /*#__NO_SIDE_EFFECTS__*/
export function sibling(node, is_text = false) { export function sibling(node, is_text = false) {
var next_sibling = /** @type {import('#client').TemplateNode} */ (node.nextSibling);
if (!hydrating) { 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) { var type = next_sibling.nodeType;
return sibling(next_sibling, is_text);
}
// if a sibling {expression} is empty during SSR, there might be no // if a sibling {expression} is empty during SSR, there might be no
// text node to hydrate — we must therefore create one // text node to hydrate — we must therefore create one
if (is_text && type !== 3) { if (is_text && type !== 3) {
var text = empty(); var text = empty();
next_sibling?.before(text); next_sibling?.before(text);
set_hydrate_node(text);
return text; return text;
} }
return hydrate_anchor(/** @type {Node} */ (next_sibling)); set_hydrate_node(next_sibling);
return /** @type {TemplateNode} */ (next_sibling);
} }
/** /**

@ -1,24 +1,6 @@
import { is_array } from '../utils.js';
/** @param {string} html */ /** @param {string} html */
export function create_fragment_from_html(html) { export function create_fragment_from_html(html) {
var elem = document.createElement('template'); var elem = document.createElement('template');
elem.innerHTML = html; elem.innerHTML = html;
return elem.content; 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();
}
}

@ -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 { empty } from './operations.js';
import { create_fragment_from_html } from './reconciler.js'; import { create_fragment_from_html } from './reconciler.js';
import { current_effect } from '../runtime.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'; import { queue_micro_task } from './task.js';
/** /**
* @param {import('#client').TemplateNode} start * @param {TemplateNode} start
* @param {import('#client').TemplateNode} end * @param {TemplateNode | null} end
*/ */
export function assign_nodes(start, 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 () => { return () => {
if (hydrating) { if (hydrating) {
assign_nodes(get_start(), hydrate_nodes[hydrate_nodes.length - 1]); assign_nodes(hydrate_node, null);
return hydrate_node;
return hydrate_start;
} }
if (!node) { if (!node) {
@ -44,13 +44,13 @@ export function template(content, flags) {
if (!is_fragment) node = /** @type {Node} */ (node.firstChild); 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) use_import_node ? document.importNode(node, true) : node.cloneNode(true)
); );
if (is_fragment) { if (is_fragment) {
var start = /** @type {import('#client').TemplateNode} */ (clone.firstChild); var start = /** @type {TemplateNode} */ (clone.firstChild);
var end = /** @type {import('#client').TemplateNode} */ (clone.lastChild); var end = /** @type {TemplateNode} */ (clone.lastChild);
assign_nodes(start, end); assign_nodes(start, end);
} else { } else {
@ -107,9 +107,8 @@ export function ns_template(content, flags, ns = 'svg') {
return () => { return () => {
if (hydrating) { if (hydrating) {
assign_nodes(get_start(), hydrate_nodes[hydrate_nodes.length - 1]); assign_nodes(hydrate_node, null);
return hydrate_node;
return hydrate_start;
} }
if (!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) { if (is_fragment) {
var start = /** @type {import('#client').TemplateNode} */ (clone.firstChild); var start = /** @type {TemplateNode} */ (clone.firstChild);
var end = /** @type {import('#client').TemplateNode} */ (clone.lastChild); var end = /** @type {TemplateNode} */ (clone.lastChild);
assign_nodes(start, end); assign_nodes(start, end);
} else { } else {
@ -195,6 +194,7 @@ function run_scripts(node) {
} }
clone.textContent = script.textContent; clone.textContent = script.textContent;
// If node === script tag, replaceWith will do nothing because there's no parent yet, // 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. // waiting until that's the case using an effect solves this.
// Don't do it in other circumstances or we could accidentally execute scripts // 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__*/ /*#__NO_SIDE_EFFECTS__*/
export function text(anchor) { export function text() {
if (!hydrating) { if (!hydrating) {
var t = empty(); var t = empty();
assign_nodes(t, t); assign_nodes(t, t);
return t; return t;
} }
var node = hydrate_start; var node = hydrate_node;
if (!node) { if (node.nodeType !== 3) {
// if an {expression} is empty during SSR, `hydrate_nodes` will be empty. // if an {expression} is empty during SSR, we need to insert an empty text node
// we need to insert an empty text node node.before((node = empty()));
anchor.before((node = empty())); set_hydrate_node(node);
} }
assign_nodes(node, node); assign_nodes(node, node);
@ -233,9 +230,8 @@ export function text(anchor) {
export function comment() { export function comment() {
// we're not delegating to `template` here for performance reasons // we're not delegating to `template` here for performance reasons
if (hydrating) { if (hydrating) {
assign_nodes(get_start(), hydrate_nodes[hydrate_nodes.length - 1]); assign_nodes(hydrate_node, null);
return hydrate_node;
return hydrate_start;
} }
var frag = document.createDocumentFragment(); var frag = document.createDocumentFragment();
@ -255,10 +251,16 @@ export function comment() {
* @param {DocumentFragment | Element} dom * @param {DocumentFragment | Element} dom
*/ */
export function append(anchor, dom) { export function append(anchor, dom) {
if (hydrating) return; if (hydrating) {
// We intentionally do not assign the `dom` property of the effect here because it's far too /** @type {Effect & { nodes: EffectNodes }} */ (current_effect).nodes.end = hydrate_node;
// late. If we try, we will capture additional DOM elements that we cannot control the lifecycle hydrate_next();
// for and will inevitably cause memory leaks. See https://github.com/sveltejs/svelte/pull/11832 return;
}
if (anchor === null) {
// edge case — void `<svelte:element>` with content
return;
}
anchor.before(/** @type {Node} */ (dom)); anchor.before(/** @type {Node} */ (dom));
} }

@ -63,6 +63,7 @@ export {
bind_focused bind_focused
} from './dom/elements/bindings/universal.js'; } from './dom/elements/bindings/universal.js';
export { bind_window_scroll, bind_window_size } from './dom/elements/bindings/window.js'; export { bind_window_scroll, bind_window_size } from './dom/elements/bindings/window.js';
export { next, reset } from './dom/hydration.js';
export { export {
once, once,
preventDefault, preventDefault,

@ -36,7 +36,7 @@ export interface Derived<V = unknown> extends Value<V>, Reaction {
export interface EffectNodes { export interface EffectNodes {
start: TemplateNode; start: TemplateNode;
end: TemplateNode; end: null | TemplateNode;
} }
export interface Effect extends Reaction { export interface Effect extends Reaction {

@ -1,12 +1,18 @@
import { DEV } from 'esm-env'; import { DEV } from 'esm-env';
import { clear_text_content, empty, init_operations } from './dom/operations.js'; import { clear_text_content, empty, init_operations } from './dom/operations.js';
import { HYDRATION_ERROR, HYDRATION_START, PassiveDelegatedEvents } from '../../constants.js'; import {
import { flush_sync, push, pop, current_component_context } from './runtime.js'; 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 { effect_root, branch } from './reactivity/effects.js';
import { import {
hydrate_anchor, hydrate_next,
hydrate_nodes, hydrate_node,
set_hydrate_nodes, hydrating,
set_hydrate_node,
set_hydrating set_hydrating
} from './dom/hydration.js'; } from './dom/hydration.js';
import { array_from } from './utils.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 w from './warnings.js';
import * as e from './errors.js'; import * as e from './errors.js';
import { validate_component } from '../shared/validate.js'; import { validate_component } from '../shared/validate.js';
import { assign_nodes } from './dom/template.js';
/** @type {Set<string>} */ /** @type {Set<string>} */
export const all_registered_events = new Set(); export const all_registered_events = new Set();
@ -113,28 +120,37 @@ export function hydrate(component, options) {
options.intro = options.intro ?? false; options.intro = options.intro ?? false;
const target = options.target; const target = options.target;
const previous_hydrate_nodes = hydrate_nodes; const was_hydrating = hydrating;
try { try {
// Don't flush previous effects to ensure order of outer effects stays consistent // Don't flush previous effects to ensure order of outer effects stays consistent
return flush_sync(() => { return flush_sync(() => {
set_hydrating(true); var anchor = /** @type {import('#client').TemplateNode} */ (target.firstChild);
var node = target.firstChild;
while ( while (
node && anchor &&
(node.nodeType !== 8 || /** @type {Comment} */ (node).data !== HYDRATION_START) (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; 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 }); 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, // 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 // which don't belong to the hydration phase anymore - therefore reset it here
set_hydrating(false); set_hydrating(false);
@ -143,6 +159,9 @@ export function hydrate(component, options) {
}, false); }, false);
} catch (error) { } catch (error) {
if (error === HYDRATION_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) { if (options.recover === false) {
e.hydration_failed(); e.hydration_failed();
} }
@ -157,8 +176,7 @@ export function hydrate(component, options) {
throw error; throw error;
} finally { } finally {
set_hydrating(!!previous_hydrate_nodes); set_hydrating(was_hydrating);
set_hydrate_nodes(previous_hydrate_nodes);
reset_head_anchor(); reset_head_anchor();
} }
} }
@ -222,11 +240,21 @@ function _mount(Component, { target, anchor, props = {}, events, context, intro
/** @type {any} */ (props).$$events = events; /** @type {any} */ (props).$$events = events;
} }
if (hydrating) {
assign_nodes(/** @type {import('#client').TemplateNode} */ (anchor), null);
}
should_intro = intro; should_intro = intro;
// @ts-expect-error the public typings are not what the actual function looks like // @ts-expect-error the public typings are not what the actual function looks like
component = Component(anchor, props) || {}; component = Component(anchor, props) || {};
should_intro = true; should_intro = true;
if (hydrating) {
/** @type {import('#client').Effect & { nodes: import('#client').EffectNodes }} */ (
current_effect
).nodes.end = hydrate_node;
}
if (context) { if (context) {
pop(); pop();
} }

@ -1,11 +1,6 @@
import { import { HYDRATION_END, HYDRATION_START, HYDRATION_START_ELSE } from '../../constants.js';
HYDRATION_ANCHOR,
HYDRATION_END,
HYDRATION_END_ELSE,
HYDRATION_START
} from '../../constants.js';
export const BLOCK_OPEN = `<!--${HYDRATION_START}-->`; export const BLOCK_OPEN = `<!--${HYDRATION_START}-->`;
export const BLOCK_OPEN_ELSE = `<!--${HYDRATION_START_ELSE}-->`;
export const BLOCK_CLOSE = `<!--${HYDRATION_END}-->`; export const BLOCK_CLOSE = `<!--${HYDRATION_END}-->`;
export const BLOCK_ANCHOR = `<!--${HYDRATION_ANCHOR}-->`; export const EMPTY_COMMENT = `<!---->`;
export const BLOCK_CLOSE_ELSE = `<!--${HYDRATION_END_ELSE}-->`;

@ -10,7 +10,7 @@ import {
import { escape_html } from '../../escaping.js'; import { escape_html } from '../../escaping.js';
import { DEV } from 'esm-env'; import { DEV } from 'esm-env';
import { current_component, pop, push } from './context.js'; 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'; import { validate_store } from '../shared/validate.js';
// https://html.spec.whatwg.org/multipage/syntax.html#attributes-2 // https://html.spec.whatwg.org/multipage/syntax.html#attributes-2
@ -73,6 +73,8 @@ export function assign_payload(p1, p2) {
* @returns {void} * @returns {void}
*/ */
export function element(payload, tag, attributes_fn = noop, children_fn = noop) { export function element(payload, tag, attributes_fn = noop, children_fn = noop) {
payload.out += '<!---->';
if (tag) { if (tag) {
payload.out += `<${tag} `; payload.out += `<${tag} `;
attributes_fn(); attributes_fn();
@ -81,7 +83,7 @@ export function element(payload, tag, attributes_fn = noop, children_fn = noop)
if (!VoidElements.has(tag)) { if (!VoidElements.has(tag)) {
children_fn(); children_fn();
if (!RawTextElements.includes(tag)) { if (!RawTextElements.includes(tag)) {
payload.out += BLOCK_ANCHOR; payload.out += EMPTY_COMMENT;
} }
payload.out += `</${tag}>`; payload.out += `</${tag}>`;
} }
@ -141,9 +143,9 @@ export function render(component, options = {}) {
*/ */
export function head(payload, fn) { export function head(payload, fn) {
const head_payload = payload.head; const head_payload = payload.head;
payload.head.out += BLOCK_OPEN; head_payload.out += BLOCK_OPEN;
fn(head_payload); 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 {boolean} is_html
* @param {Record<string, string>} props * @param {Record<string, string>} props
* @param {() => void} component * @param {() => void} component
* @param {boolean} dynamic
* @returns {void} * @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); const styles = style_object_to_string(props);
if (is_html) { if (is_html) {
payload.out += `<div style="display: contents; ${styles}">`; payload.out += `<div style="display: contents; ${styles}">`;
} else { } else {
payload.out += `<g style="${styles}">`; payload.out += `<g style="${styles}">`;
} }
if (dynamic) {
payload.out += '<!---->';
}
component(); component();
if (is_html) { if (is_html) {
payload.out += `<!----></div>`; payload.out += `<!----></div>`;
} else { } else {

@ -1 +1 @@
<!--ssr:0--><input> <p>Hello world!</p><!--ssr:0--> <!--[--><input> <p>Hello world!</p><!--]-->

@ -1 +1 @@
<!--ssr:0--><h1>Hello everybody!</h1><!--ssr:0--> <!--[--><h1>Hello everybody!</h1><!--]-->

@ -1,9 +1 @@
<!--ssr:0--><ul><!--ssr:1--><!--ssr:7--><li>a</li><!--ssr:7--><!--ssr:1--></ul> <!--[--><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><!--]--><!--]-->
<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>

@ -1,2 +1 @@
<!--ssr:0--><!--ssr:1--><p>a</p><!--ssr:1--> <!--[--><!--[!--><p>a</p><!--]--> <!--[--><p>empty</p><!--]--><!--]-->
<!--ssr:2--><p>empty</p><!--ssr:2--><!--ssr:0-->

@ -1,9 +1 @@
<!--ssr:0--><ul><!--ssr:1--><!--ssr:7--><li>a</li><!--ssr:7--><!--ssr:1--></ul> <!--[--><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><!--]--><!--]-->
<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>

@ -1,9 +1 @@
<!--ssr:0--><ul><!--ssr:1--><!--ssr:7--><li>a</li><!--ssr:7--><li>b</li><!--ssr:1--></ul> <!--[--><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><!--]--><!--]-->
<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>

@ -1 +1 @@
<!--ssr:0--><div class="bar"></div><!--ssr:0--> <!--[--><div class="bar"></div><!--]-->

@ -1 +1 @@
<!--ssr:0--><div class="bar"></div><!--ssr:0--> <!--[--><div class="bar"></div><!--]-->

@ -1 +1 @@
<div></div><div></div><div></div> <!--[--><div></div> <!--[--><div></div> <div></div><!--]--><!--]-->

@ -1,2 +1 @@
<!--ssr:0--><noscript>JavaScript is required for this site.</noscript> <!--[--><noscript>JavaScript is required for this site.</noscript> <h1>Hello!</h1><p>Count: 1</p><!--]-->
<h1>Hello!</h1><p>Count: 1</p><!--ssr:0-->

@ -0,0 +1 @@
<!----><p><p>invalid</p><!----></p><!----> <p><p>invalid</p><!----></p>

@ -0,0 +1,4 @@
import { test } from '../../test';
// Ensure that we don't create additional comment nodes for standalone components
export default test({});

@ -0,0 +1 @@
<!--[--><!--[--><p>child</p><!--]--> <!--[--><p>child</p><p>child</p><p>child</p><!--]--><!--]-->

@ -0,0 +1,11 @@
<script>
import Child from './Child.svelte';
</script>
{#if true}
<Child />
{/if}
{#each [1, 2, 3] as n}
<Child />
{/each}

@ -0,0 +1,4 @@
import { test } from '../../test';
// Ensure that we don't create additional comment nodes for standalone components
export default test({});

@ -0,0 +1 @@
<!--[--><!--[--><p>thing</p><!--]--> <!--[--><p>thing</p><p>thing</p><p>thing</p><!--]--><!--]-->

@ -0,0 +1,11 @@
{#snippet thing()}
<p>thing</p>
{/snippet}
{#if true}
{@render thing()}
{/if}
{#each [1, 2, 3] as n}
{@render thing()}
{/each}

@ -1 +1 @@
<!--ssr:0-->x<!--ssr:0--> <!--[--><!---->x<!--]-->

@ -52,8 +52,10 @@ const { test, run } = suite<HydrationTest>(async (config, cwd) => {
props: config.server_props ?? config.props ?? {} props: config.server_props ?? config.props ?? {}
}); });
const override = read(`${cwd}/_override.html`);
fs.writeFileSync(`${cwd}/_output/body.html`, rendered.html + '\n'); 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) { if (rendered.head) {
fs.writeFileSync(`${cwd}/_output/head.html`, rendered.head + '\n'); 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')}`); throw new Error(`Unexpected errors: ${errors.join('\n')}`);
} }
if (!override) {
const expected = read(`${cwd}/_expected.html`) ?? rendered.html; const expected = read(`${cwd}/_expected.html`) ?? rendered.html;
assert_html_equal(target.innerHTML, expected); assert.equal(target.innerHTML.trim(), expected.trim());
}
if (rendered.head) { if (rendered.head) {
const expected = read(`${cwd}/_expected_head.html`) ?? 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) { if (config.snapshot) {

@ -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>
`
);
}
});

@ -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}

@ -11,7 +11,7 @@ export default test({
if (variant === 'dom') { if (variant === 'dom') {
assert.ok(!span.previousSibling); assert.ok(!span.previousSibling);
} else { } else {
assert.ok(span.previousSibling?.textContent === '['); // ssr commment node assert.ok(span.previousSibling?.textContent === ''); // ssr commment node
} }
component.raw = '<span>bar</span>'; component.raw = '<span>bar</span>';

@ -1,13 +1,17 @@
<!--[--> <!--[-->
<!---->
<title>lorem</title> <title>lorem</title>
<!----> <!---->
<!---->
<style> <style>
.ipsum { .ipsum {
display: block; display: block;
} }
</style> </style>
<!----> <!---->
<!---->
<script> <script>
console.log(true); console.log(true);
</script> </script>
<!----><!--]--> <!---->
<!--]-->

@ -7,6 +7,8 @@ var root = $.template(`<!> `, 1);
export default function Bind_component_snippet($$anchor) { export default function Bind_component_snippet($$anchor) {
const snippet = ($$anchor) => { const snippet = ($$anchor) => {
$.next();
var fragment = root_1(); var fragment = root_1();
$.append($$anchor, fragment); $.append($$anchor, fragment);

@ -6,15 +6,13 @@ export default function Bind_component_snippet($$payload) {
const _snippet = snippet; const _snippet = snippet;
function snippet($$payload) { function snippet($$payload) {
$$payload.out += `Something`; $$payload.out += `<!---->Something`;
} }
let $$settled = true; let $$settled = true;
let $$inner_payload; let $$inner_payload;
function $$render_inner($$payload) { function $$render_inner($$payload) {
$$payload.out += `<!--[-->`;
TextInput($$payload, { TextInput($$payload, {
get value() { get value() {
return 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 { do {

@ -6,7 +6,7 @@ export default function Each_string_template($$anchor) {
var node = $.first_child(fragment); var node = $.first_child(fragment);
$.each(node, 1, () => ['foo', 'bar', 'baz'], $.index, ($$anchor, thing, $$index) => { $.each(node, 1, () => ['foo', 'bar', 'baz'], $.index, ($$anchor, thing, $$index) => {
var text = $.text($$anchor); var text = $.text();
$.template_effect(() => $.set_text(text, `${$.unwrap(thing) ?? ""}, `)); $.template_effect(() => $.set_text(text, `${$.unwrap(thing) ?? ""}, `));
$.append($$anchor, text); $.append($$anchor, text);

@ -8,10 +8,8 @@ export default function Each_string_template($$payload) {
for (let $$index = 0; $$index < each_array.length; $$index++) { for (let $$index = 0; $$index < each_array.length; $$index++) {
const thing = each_array[$$index]; const thing = each_array[$$index];
$$payload.out += "<!--[-->";
$$payload.out += `${$.escape(thing)}, `; $$payload.out += `${$.escape(thing)}, `;
$$payload.out += "<!--]-->";
} }
$$payload.out += "<!--]-->"; $$payload.out += `<!--]-->`;
} }

@ -15,7 +15,7 @@ export default function Function_prop_no_getter($$anchor) {
onmouseup, onmouseup,
onmouseenter: () => $.set(count, $.proxy(plusOne($.get(count)))), onmouseenter: () => $.set(count, $.proxy(plusOne($.get(count)))),
children: ($$anchor, $$slotProps) => { children: ($$anchor, $$slotProps) => {
var text = $.text($$anchor); var text = $.text();
$.template_effect(() => $.set_text(text, `clicks: ${$.get(count) ?? ""}`)); $.template_effect(() => $.set_text(text, `clicks: ${$.get(count) ?? ""}`));
$.append($$anchor, text); $.append($$anchor, text);

Loading…
Cancel
Save