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
);
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 {

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

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

@ -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')
};
}
/**

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

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

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

@ -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(() => {

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

@ -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 = `<!---->`;

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

@ -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><!--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><!--]--><!--]-->

@ -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><!--]--><!--]-->

@ -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><!--]--><!--]-->

@ -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><!--]--><!--]-->

@ -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>
<h1>Hello!</h1><p>Count: 1</p><!--ssr:0-->
<!--[--><noscript>JavaScript is required for this site.</noscript> <h1>Hello!</h1><p>Count: 1</p><!--]-->

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

@ -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') {
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>';

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

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

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

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

@ -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 += `<!--]-->`;
}

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

Loading…
Cancel
Save