feat: add hydrate method, make hydration treeshakeable (#10497)

* feat: add hydrate method, make hydration treeshakeable

Introduces a new `hydrate` method which does hydration. Refactors code so that hydration-related code is treeshaken out when not using that method.
closes #9533
part of #9827

* get docs building

* ugh

* one more

* Update packages/svelte/scripts/check-treeshakeability.js

Co-authored-by: Ben McCann <322311+benmccann@users.noreply.github.com>

* warn

* Update sites/svelte-5-preview/src/routes/docs/content/01-api/05-functions.md

---------

Co-authored-by: Rich Harris <richard.a.harris@gmail.com>
Co-authored-by: Ben McCann <322311+benmccann@users.noreply.github.com>
Co-authored-by: Rich Harris <rich.harris@vercel.com>
pull/10499/head
Simon H 10 months ago committed by GitHub
parent a2014809ec
commit 72ff5366de
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,5 @@
---
"svelte": patch
---
feat: add hydrate method, make hydration treeshakeable

@ -3,6 +3,34 @@ import path from 'node:path';
import { rollup } from 'rollup'; import { rollup } from 'rollup';
import virtual from '@rollup/plugin-virtual'; import virtual from '@rollup/plugin-virtual';
import { nodeResolve } from '@rollup/plugin-node-resolve'; import { nodeResolve } from '@rollup/plugin-node-resolve';
import { compile } from 'svelte/compiler';
async function bundle_code(entry) {
const bundle = await rollup({
input: '__entry__',
plugins: [
virtual({
__entry__: entry
}),
nodeResolve({
exportConditions: ['production', 'import', 'browser', 'default']
})
],
onwarn: (warning, handle) => {
if (warning.code !== 'EMPTY_BUNDLE' && warning.code !== 'CIRCULAR_DEPENDENCY') {
handle(warning);
}
}
});
const { output } = await bundle.generate({});
if (output.length > 1) {
throw new Error('errr what');
}
return output[0].code.trim();
}
const pkg = JSON.parse(fs.readFileSync('package.json', 'utf8')); const pkg = JSON.parse(fs.readFileSync('package.json', 'utf8'));
@ -20,31 +48,8 @@ for (const key in pkg.exports) {
if (!pkg.exports[key][type]) continue; if (!pkg.exports[key][type]) continue;
const subpackage = path.join(pkg.name, key); const subpackage = path.join(pkg.name, key);
const resolved = path.resolve(pkg.exports[key][type]); const resolved = path.resolve(pkg.exports[key][type]);
const code = await bundle_code(`import ${JSON.stringify(resolved)}`);
const bundle = await rollup({
input: '__entry__',
plugins: [
virtual({
__entry__: `import ${JSON.stringify(resolved)}`
}),
nodeResolve({
exportConditions: ['production', 'import', 'browser', 'default']
})
],
onwarn: (warning, handle) => {
// if (warning.code !== 'EMPTY_BUNDLE') handle(warning);
}
});
const { output } = await bundle.generate({});
if (output.length > 1) {
throw new Error('errr what');
}
const code = output[0].code.trim();
if (code === '') { if (code === '') {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
@ -59,6 +64,52 @@ for (const key in pkg.exports) {
} }
} }
const client_main = path.resolve(pkg.exports['.'].browser);
const without_hydration = await bundle_code(
// Use all features which contain hydration code to ensure it's treeshakeable
compile(
`
<script>
import { mount } from ${JSON.stringify(client_main)}; mount();
let foo;
</script>
<svelte:head><title>hi</title></svelte:head>
<a href={foo} class={foo}>a</a>
<a {...foo}>a</a>
<svelte:component this={foo} />
<svelte:element this={foo} />
<C {foo} />
{#if foo}
{/if}
{#each foo as bar}
{/each}
{#await foo}
{/await}
{#key foo}
{/key}
{#snippet x()}
{/snippet}
{@render x()}
{@html foo}
`,
{ filename: 'App.svelte' }
).js.code
);
if (!without_hydration.includes('current_hydration_fragment')) {
// eslint-disable-next-line no-console
console.error(`✅ Hydration code treeshakeable`);
} else {
// eslint-disable-next-line no-console
console.error(without_hydration);
// eslint-disable-next-line no-console
console.error(`❌ Hydration code not treeshakeable`);
failed = true;
}
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.groupEnd(); console.groupEnd();

@ -11,6 +11,7 @@ import {
current_hydration_fragment, current_hydration_fragment,
get_hydration_fragment, get_hydration_fragment,
hydrate_block_anchor, hydrate_block_anchor,
hydrating,
set_current_hydration_fragment set_current_hydration_fragment
} from './hydration.js'; } from './hydration.js';
import { clear_text_content, empty, map_get, map_set } from './operations.js'; import { clear_text_content, empty, map_get, map_set } from './operations.js';
@ -61,7 +62,10 @@ function each(anchor_node, collection, flags, key_fn, render_fn, fallback_fn, re
/** @type {null | import('./types.js').EffectSignal} */ /** @type {null | import('./types.js').EffectSignal} */
let render = null; let render = null;
/** Whether or not there was a "rendered fallback but want to render items" (or vice versa) hydration mismatch */ /**
* Whether or not there was a "rendered fallback but want to render items" (or vice versa) hydration mismatch.
* Needs to be a `let` or else it isn't treeshaken out
*/
let mismatch = false; let mismatch = false;
block.r = block.r =
@ -107,7 +111,7 @@ function each(anchor_node, collection, flags, key_fn, render_fn, fallback_fn, re
// If the each block is controlled, then the anchor node will be the surrounding // If the each block is controlled, then the anchor node will be the surrounding
// element in which the each block is rendered, which requires certain handling // element in which the each block is rendered, which requires certain handling
// depending on whether we're in hydration mode or not // depending on whether we're in hydration mode or not
if (current_hydration_fragment === null) { if (!hydrating) {
// Create a new anchor on the fly because there's none due to the optimization // Create a new anchor on the fly because there's none due to the optimization
anchor = empty(); anchor = empty();
block.a.appendChild(anchor); block.a.appendChild(anchor);
@ -153,13 +157,13 @@ function each(anchor_node, collection, flags, key_fn, render_fn, fallback_fn, re
const length = array.length; const length = array.length;
if (current_hydration_fragment !== null) { if (hydrating) {
const is_each_else_comment = const is_each_else_comment =
/** @type {Comment} */ (current_hydration_fragment?.[0])?.data === 'ssr:each_else'; /** @type {Comment} */ (current_hydration_fragment?.[0])?.data === 'ssr:each_else';
// Check for hydration mismatch which can happen if the server renders the each fallback // Check for hydration mismatch which can happen if the server renders the each fallback
// but the client has items, or vice versa. If so, remove everything inside the anchor and start fresh. // but the client has items, or vice versa. If so, remove everything inside the anchor and start fresh.
if ((is_each_else_comment && length) || (!is_each_else_comment && !length)) { if ((is_each_else_comment && length) || (!is_each_else_comment && !length)) {
remove(/** @type {import('./types.js').TemplateNode[]} */ (current_hydration_fragment)); remove(current_hydration_fragment);
set_current_hydration_fragment(null); set_current_hydration_fragment(null);
mismatch = true; mismatch = true;
} else if (is_each_else_comment) { } else if (is_each_else_comment) {
@ -306,22 +310,22 @@ function reconcile_indexed_array(
} }
} else { } else {
var item; var item;
var is_hydrating = current_hydration_fragment !== null; /** `true` if there was a hydration mismatch. Needs to be a `let` or else it isn't treeshaken out */
let mismatch = false;
b_blocks = Array(b); b_blocks = Array(b);
if (is_hydrating) { if (hydrating) {
// Hydrate block // Hydrate block
var hydration_list = /** @type {import('./types.js').TemplateNode[]} */ ( var hydration_list = /** @type {import('./types.js').TemplateNode[]} */ (
current_hydration_fragment current_hydration_fragment
); );
var hydrating_node = hydration_list[0]; var hydrating_node = hydration_list[0];
for (; index < length; index++) { for (; index < length; index++) {
var fragment = /** @type {Array<Text | Comment | Element>} */ ( var fragment = get_hydration_fragment(hydrating_node);
get_hydration_fragment(hydrating_node)
);
set_current_hydration_fragment(fragment); set_current_hydration_fragment(fragment);
if (!fragment) { if (!fragment) {
// If fragment is null, then that means that the server rendered less items than what // If fragment is null, then that means that the server rendered less items than what
// the client code specifies -> break out and continue with client-side node creation // the client code specifies -> break out and continue with client-side node creation
mismatch = true;
break; break;
} }
@ -357,7 +361,7 @@ function reconcile_indexed_array(
} }
} }
if (is_hydrating && current_hydration_fragment === null) { if (mismatch) {
// Server rendered less nodes than the client -> set empty array so that Svelte continues to operate in hydration mode // Server rendered less nodes than the client -> set empty array so that Svelte continues to operate in hydration mode
set_current_hydration_fragment([]); set_current_hydration_fragment([]);
} }
@ -425,9 +429,10 @@ function reconcile_tracked_array(
var key; var key;
var item; var item;
var idx; var idx;
var is_hydrating = current_hydration_fragment !== null; /** `true` if there was a hydration mismatch. Needs to be a `let` or else it isn't treeshaken out */
let mismatch = false;
b_blocks = Array(b); b_blocks = Array(b);
if (is_hydrating) { if (hydrating) {
// Hydrate block // Hydrate block
var fragment; var fragment;
var hydration_list = /** @type {import('./types.js').TemplateNode[]} */ ( var hydration_list = /** @type {import('./types.js').TemplateNode[]} */ (
@ -435,13 +440,12 @@ function reconcile_tracked_array(
); );
var hydrating_node = hydration_list[0]; var hydrating_node = hydration_list[0];
while (b > 0) { while (b > 0) {
fragment = /** @type {Array<Text | Comment | Element>} */ ( fragment = get_hydration_fragment(hydrating_node);
get_hydration_fragment(hydrating_node)
);
set_current_hydration_fragment(fragment); set_current_hydration_fragment(fragment);
if (!fragment) { if (!fragment) {
// If fragment is null, then that means that the server rendered less items than what // If fragment is null, then that means that the server rendered less items than what
// the client code specifies -> break out and continue with client-side node creation // the client code specifies -> break out and continue with client-side node creation
mismatch = true;
break; break;
} }
@ -594,7 +598,7 @@ function reconcile_tracked_array(
} }
} }
if (is_hydrating && current_hydration_fragment === null) { if (mismatch) {
// Server rendered less nodes than the client -> set empty array so that Svelte continues to operate in hydration mode // Server rendered less nodes than the client -> set empty array so that Svelte continues to operate in hydration mode
set_current_hydration_fragment([]); set_current_hydration_fragment([]);
} }

@ -3,25 +3,37 @@
import { empty } from './operations.js'; import { empty } from './operations.js';
import { schedule_task } from './runtime.js'; import { schedule_task } from './runtime.js';
/** @type {null | Array<import('./types.js').TemplateNode>} */ /**
export let current_hydration_fragment = null; * Use this variable to guard everything related to hydration code so it can be treeshaken out
* if the user doesn't use the `hydrate` method and these code paths are therefore not needed.
*/
export let hydrating = false;
/**
* 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('./types.js').TemplateNode[]}
*/
export let current_hydration_fragment = /** @type {any} */ (null);
/** /**
* @param {null | Array<import('./types.js').TemplateNode>} fragment * @param {null | import('./types.js').TemplateNode[]} fragment
* @returns {void} * @returns {void}
*/ */
export function set_current_hydration_fragment(fragment) { export function set_current_hydration_fragment(fragment) {
current_hydration_fragment = fragment; hydrating = fragment !== null;
current_hydration_fragment = /** @type {import('./types.js').TemplateNode[]} */ (fragment);
} }
/** /**
* Returns all nodes between the first `<!--ssr:...-->` comment tag pair encountered. * Returns all nodes between the first `<!--ssr:...-->` comment tag pair encountered.
* @param {Node | null} node * @param {Node | null} node
* @param {boolean} [insert_text] Whether to insert an empty text node if the fragment is empty * @param {boolean} [insert_text] Whether to insert an empty text node if the fragment is empty
* @returns {Array<import('./types.js').TemplateNode> | null} * @returns {import('./types.js').TemplateNode[] | null}
*/ */
export function get_hydration_fragment(node, insert_text = false) { export function get_hydration_fragment(node, insert_text = false) {
/** @type {Array<import('./types.js').TemplateNode>} */ /** @type {import('./types.js').TemplateNode[]} */
const fragment = []; const fragment = [];
/** @type {null | Node} */ /** @type {null | Node} */
@ -66,9 +78,10 @@ export function get_hydration_fragment(node, insert_text = false) {
* @returns {void} * @returns {void}
*/ */
export function hydrate_block_anchor(anchor_node, is_controlled) { export function hydrate_block_anchor(anchor_node, is_controlled) {
/** @type {Node} */ if (hydrating) {
let target_node = anchor_node; /** @type {Node} */
if (current_hydration_fragment !== null) { let target_node = anchor_node;
if (is_controlled) { if (is_controlled) {
target_node = /** @type {Node} */ (target_node.firstChild); target_node = /** @type {Node} */ (target_node.firstChild);
} }

@ -1,4 +1,4 @@
import { current_hydration_fragment, get_hydration_fragment } from './hydration.js'; import { current_hydration_fragment, get_hydration_fragment, hydrating } from './hydration.js';
import { get_descriptor } from './utils.js'; import { get_descriptor } from './utils.js';
// We cache the Node and Element prototype methods, so that we can avoid doing // We cache the Node and Element prototype methods, so that we can avoid doing
@ -171,7 +171,7 @@ export function empty() {
/*#__NO_SIDE_EFFECTS__*/ /*#__NO_SIDE_EFFECTS__*/
export function child(node) { export function child(node) {
const child = first_child_get.call(node); const child = first_child_get.call(node);
if (current_hydration_fragment !== null) { if (hydrating) {
// 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) {
const text = empty(); const text = empty();
@ -192,7 +192,7 @@ export function child(node) {
*/ */
/*#__NO_SIDE_EFFECTS__*/ /*#__NO_SIDE_EFFECTS__*/
export function child_frag(node, is_text) { export function child_frag(node, is_text) {
if (current_hydration_fragment !== null) { if (hydrating) {
const first_node = /** @type {Node[]} */ (node)[0]; const first_node = /** @type {Node[]} */ (node)[0];
// if an {expression} is empty during SSR, there might be no // if an {expression} is empty during SSR, there might be no
@ -225,7 +225,7 @@ export function child_frag(node, is_text) {
/*#__NO_SIDE_EFFECTS__*/ /*#__NO_SIDE_EFFECTS__*/
export function sibling(node, is_text = false) { export function sibling(node, is_text = false) {
const next_sibling = next_sibling_get.call(node); const next_sibling = next_sibling_get.call(node);
if (current_hydration_fragment !== null) { if (hydrating) {
// 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 && next_sibling?.nodeType !== 3) { if (is_text && next_sibling?.nodeType !== 3) {
@ -276,6 +276,7 @@ export function create_element(name) {
} }
/** /**
* Expects to only be called in hydration mode
* @param {Node} node * @param {Node} node
* @returns {Node} * @returns {Node}
*/ */
@ -283,7 +284,7 @@ function capture_fragment_from_node(node) {
if ( if (
node.nodeType === 8 && node.nodeType === 8 &&
/** @type {Comment} */ (node).data.startsWith('ssr:') && /** @type {Comment} */ (node).data.startsWith('ssr:') &&
/** @type {Array<Element | Text | Comment>} */ (current_hydration_fragment).at(-1) !== node current_hydration_fragment.at(-1) !== node
) { ) {
const fragment = /** @type {Array<Element | Text | Comment>} */ (get_hydration_fragment(node)); const fragment = /** @type {Array<Element | Text | Comment>} */ (get_hydration_fragment(node));
const last_child = fragment.at(-1) || node; const last_child = fragment.at(-1) || node;

@ -1,5 +1,5 @@
import { append_child } from './operations.js'; import { append_child } from './operations.js';
import { current_hydration_fragment, hydrate_block_anchor } from './hydration.js'; import { current_hydration_fragment, hydrate_block_anchor, hydrating } from './hydration.js';
import { is_array } from './utils.js'; import { is_array } from './utils.js';
/** @param {string} html */ /** @param {string} html */
@ -92,7 +92,7 @@ export function remove(current) {
*/ */
export function reconcile_html(target, value, svg) { export function reconcile_html(target, value, svg) {
hydrate_block_anchor(target); hydrate_block_anchor(target);
if (current_hydration_fragment !== null) { if (hydrating) {
return current_hydration_fragment; return current_hydration_fragment;
} }
var html = value + ''; var html = value + '';

@ -54,6 +54,7 @@ import {
current_hydration_fragment, current_hydration_fragment,
get_hydration_fragment, get_hydration_fragment,
hydrate_block_anchor, hydrate_block_anchor,
hydrating,
set_current_hydration_fragment set_current_hydration_fragment
} from './hydration.js'; } from './hydration.js';
import { import {
@ -166,7 +167,7 @@ export function svg_replace(node) {
* @returns {Element | DocumentFragment | Node[]} * @returns {Element | DocumentFragment | Node[]}
*/ */
function open_template(is_fragment, use_clone_node, anchor, template_element_fn) { function open_template(is_fragment, use_clone_node, anchor, template_element_fn) {
if (current_hydration_fragment !== null) { if (hydrating) {
if (anchor !== null) { if (anchor !== null) {
hydrate_block_anchor(anchor, false); hydrate_block_anchor(anchor, false);
} }
@ -217,7 +218,7 @@ export function space(anchor) {
// 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 (or an anchor comment is falsely detected instead) // text node to hydrate (or an anchor comment is falsely detected instead)
// — we must therefore create one // — we must therefore create one
if (current_hydration_fragment !== null && node?.nodeType !== 3) { if (hydrating && node?.nodeType !== 3) {
node = empty(); node = empty();
// @ts-ignore in this case the anchor should always be a comment, // @ts-ignore in this case the anchor should always be a comment,
// if not something more fundamental is wrong and throwing here is better to bail out early // if not something more fundamental is wrong and throwing here is better to bail out early
@ -251,10 +252,8 @@ function close_template(dom, is_fragment, anchor) {
? dom ? dom
: /** @type {import('./types.js').TemplateNode[]} */ (Array.from(dom.childNodes)) : /** @type {import('./types.js').TemplateNode[]} */ (Array.from(dom.childNodes))
: dom; : dom;
if (anchor !== null) { if (!hydrating && anchor !== null) {
if (current_hydration_fragment === null) { insert(current, null, anchor);
insert(current, null, anchor);
}
} }
block.d = current; block.d = current;
} }
@ -415,14 +414,13 @@ export function class_name(dom, value) {
// @ts-expect-error need to add __className to patched prototype // @ts-expect-error need to add __className to patched prototype
const prev_class_name = dom.__className; const prev_class_name = dom.__className;
const next_class_name = to_class(value); const next_class_name = to_class(value);
const is_hydrating = current_hydration_fragment !== null; if (hydrating && dom.className === next_class_name) {
if (is_hydrating && dom.className === next_class_name) {
// In case of hydration don't reset the class as it's already correct. // In case of hydration don't reset the class as it's already correct.
// @ts-expect-error need to add __className to patched prototype // @ts-expect-error need to add __className to patched prototype
dom.__className = next_class_name; dom.__className = next_class_name;
} else if ( } else if (
prev_class_name !== next_class_name || prev_class_name !== next_class_name ||
(is_hydrating && dom.className !== next_class_name) (hydrating && dom.className !== next_class_name)
) { ) {
if (next_class_name === '') { if (next_class_name === '') {
dom.removeAttribute('class'); dom.removeAttribute('class');
@ -452,7 +450,7 @@ export function text(dom, value) {
// @ts-expect-error need to add __value to patched prototype // @ts-expect-error need to add __value to patched prototype
const prev_node_value = dom.__nodeValue; const prev_node_value = dom.__nodeValue;
const next_node_value = stringify(value); const next_node_value = stringify(value);
if (current_hydration_fragment !== null && dom.nodeValue === next_node_value) { if (hydrating && dom.nodeValue === next_node_value) {
// In case of hydration don't reset the nodeValue as it's already correct. // In case of hydration don't reset the nodeValue as it's already correct.
// @ts-expect-error need to add __nodeValue to patched prototype // @ts-expect-error need to add __nodeValue to patched prototype
dom.__nodeValue = next_node_value; dom.__nodeValue = next_node_value;
@ -739,7 +737,7 @@ export function bind_playback_rate(media, get_value, update) {
* @param {(paused: boolean) => void} update * @param {(paused: boolean) => void} update
*/ */
export function bind_paused(media, get_value, update) { export function bind_paused(media, get_value, update) {
let mounted = current_hydration_fragment !== null; let mounted = hydrating;
let paused = get_value(); let paused = get_value();
const callback = () => { const callback = () => {
if (paused !== media.paused) { if (paused !== media.paused) {
@ -1452,7 +1450,8 @@ export function slot(anchor_node, slot_fn, slot_props, fallback_fn) {
function if_block(anchor_node, condition_fn, consequent_fn, alternate_fn) { function if_block(anchor_node, condition_fn, consequent_fn, alternate_fn) {
const block = create_if_block(); const block = create_if_block();
hydrate_block_anchor(anchor_node); hydrate_block_anchor(anchor_node);
const previous_hydration_fragment = current_hydration_fragment; /** Whether or not there was a hydration mismatch. Needs to be a `let` or else it isn't treeshaken out */
let mismatch = false;
/** @type {null | import('./types.js').TemplateNode | Array<import('./types.js').TemplateNode>} */ /** @type {null | import('./types.js').TemplateNode | Array<import('./types.js').TemplateNode>} */
let consequent_dom = null; let consequent_dom = null;
@ -1495,7 +1494,7 @@ function if_block(anchor_node, condition_fn, consequent_fn, alternate_fn) {
trigger_transitions(alternate_transitions, 'in'); trigger_transitions(alternate_transitions, 'in');
} }
} }
} else if (current_hydration_fragment !== null) { } else if (hydrating) {
const comment_text = /** @type {Comment} */ (current_hydration_fragment?.[0])?.data; const comment_text = /** @type {Comment} */ (current_hydration_fragment?.[0])?.data;
if ( if (
!comment_text || !comment_text ||
@ -1506,6 +1505,7 @@ function if_block(anchor_node, condition_fn, consequent_fn, alternate_fn) {
// This could happen using when `{#if browser} .. {/if}` in SvelteKit. // This could happen using when `{#if browser} .. {/if}` in SvelteKit.
remove(current_hydration_fragment); remove(current_hydration_fragment);
set_current_hydration_fragment(null); set_current_hydration_fragment(null);
mismatch = true;
} else { } else {
// Remove the ssr:if comment node or else it will confuse the subsequent hydration algorithm // Remove the ssr:if comment node or else it will confuse the subsequent hydration algorithm
current_hydration_fragment.shift(); current_hydration_fragment.shift();
@ -1530,9 +1530,9 @@ function if_block(anchor_node, condition_fn, consequent_fn, alternate_fn) {
} }
if (result && current_branch_effect !== consequent_effect) { if (result && current_branch_effect !== consequent_effect) {
consequent_fn(anchor_node); consequent_fn(anchor_node);
if (current_branch_effect === null) { if (mismatch && current_branch_effect === null) {
// Restore previous fragment so that Svelte continues to operate in hydration mode // Set fragment so that Svelte continues to operate in hydration mode
set_current_hydration_fragment(previous_hydration_fragment); set_current_hydration_fragment([]);
} }
current_branch_effect = consequent_effect; current_branch_effect = consequent_effect;
consequent_dom = block.d; consequent_dom = block.d;
@ -1558,9 +1558,9 @@ function if_block(anchor_node, condition_fn, consequent_fn, alternate_fn) {
if (alternate_fn !== null) { if (alternate_fn !== null) {
alternate_fn(anchor_node); alternate_fn(anchor_node);
} }
if (current_branch_effect === null) { if (mismatch && current_branch_effect === null) {
// Restore previous fragment so that Svelte continues to operate in hydration mode // Set fragment so that Svelte continues to operate in hydration mode
set_current_hydration_fragment(previous_hydration_fragment); set_current_hydration_fragment([]);
} }
current_branch_effect = alternate_effect; current_branch_effect = alternate_effect;
alternate_dom = block.d; alternate_dom = block.d;
@ -1593,10 +1593,15 @@ export function head(render_fn) {
const block = create_head_block(); const block = create_head_block();
// 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.
const hydration_fragment = let hydration_fragment = null;
current_hydration_fragment !== null ? get_hydration_fragment(document.head.firstChild) : null; let previous_hydration_fragment = null;
const previous_hydration_fragment = current_hydration_fragment; let is_hydrating = hydrating;
set_current_hydration_fragment(hydration_fragment); if (is_hydrating) {
hydration_fragment = get_hydration_fragment(document.head.firstChild);
previous_hydration_fragment = current_hydration_fragment;
set_current_hydration_fragment(hydration_fragment);
}
try { try {
const head_effect = render_effect( const head_effect = render_effect(
() => { () => {
@ -1606,7 +1611,7 @@ export function head(render_fn) {
block.d = null; block.d = null;
} }
let anchor = null; let anchor = null;
if (current_hydration_fragment === null) { if (!hydrating) {
anchor = empty(); anchor = empty();
document.head.appendChild(anchor); document.head.appendChild(anchor);
} }
@ -1623,7 +1628,9 @@ export function head(render_fn) {
}); });
block.e = head_effect; block.e = head_effect;
} finally { } finally {
set_current_hydration_fragment(previous_hydration_fragment); if (is_hydrating) {
set_current_hydration_fragment(previous_hydration_fragment);
}
} }
} }
@ -1688,7 +1695,7 @@ export function element(anchor_node, tag_fn, is_svg, render_fn) {
? null ? null
: anchor_node.parentElement?.namespaceURI ?? null; : anchor_node.parentElement?.namespaceURI ?? null;
const next_element = tag const next_element = tag
? current_hydration_fragment !== null ? hydrating
? /** @type {Element} */ (current_hydration_fragment[0]) ? /** @type {Element} */ (current_hydration_fragment[0])
: ns : ns
? document.createElementNS(ns, tag) ? document.createElementNS(ns, tag)
@ -1701,7 +1708,7 @@ export function element(anchor_node, tag_fn, is_svg, render_fn) {
element = next_element; element = next_element;
if (element !== null && render_fn !== undefined) { if (element !== null && render_fn !== undefined) {
let anchor; let anchor;
if (current_hydration_fragment !== null) { if (hydrating) {
// Use the existing ssr comment as the anchor so that the inner open and close // Use the existing ssr comment as the anchor so that the inner open and close
// methods can pick up the existing nodes correctly // methods can pick up the existing nodes correctly
anchor = /** @type {Comment} */ (element.firstChild); anchor = /** @type {Comment} */ (element.firstChild);
@ -2158,7 +2165,7 @@ export function cssProps(anchor, is_html, props, component) {
/** @type {Text | Comment} */ /** @type {Text | Comment} */
let component_anchor; let component_anchor;
if (current_hydration_fragment !== null) { if (hydrating) {
// Hydration: css props element is surrounded by a ssr comment ... // Hydration: css props element is surrounded by a ssr comment ...
tag = /** @type {HTMLElement | SVGElement} */ (current_hydration_fragment[0]); tag = /** @type {HTMLElement | SVGElement} */ (current_hydration_fragment[0]);
// ... and the child(ren) of the css props element is also surround by a ssr comment // ... and the child(ren) of the css props element is also surround by a ssr comment
@ -2324,7 +2331,7 @@ export function action(dom, action, value_fn) {
* @returns {void} * @returns {void}
*/ */
export function remove_input_attr_defaults(dom) { export function remove_input_attr_defaults(dom) {
if (current_hydration_fragment !== null) { if (hydrating) {
attr(dom, 'value', null); attr(dom, 'value', null);
attr(dom, 'checked', null); attr(dom, 'checked', null);
} }
@ -2336,7 +2343,7 @@ export function remove_input_attr_defaults(dom) {
* @returns {void} * @returns {void}
*/ */
export function remove_textarea_child(dom) { export function remove_textarea_child(dom) {
if (current_hydration_fragment !== null && dom.firstChild !== null) { if (hydrating && dom.firstChild !== null) {
dom.textContent = ''; dom.textContent = '';
} }
} }
@ -2366,7 +2373,7 @@ export function attr(dom, attribute, value) {
} }
if ( if (
current_hydration_fragment === null || !hydrating ||
(dom.getAttribute(attribute) !== value && (dom.getAttribute(attribute) !== value &&
// If we reset those, they would result in another network request, which we want to avoid. // If we reset those, they would result in another network request, which we want to avoid.
// We assume they are the same between client and server as checking if they are equal is expensive // We assume they are the same between client and server as checking if they are equal is expensive
@ -2429,7 +2436,7 @@ export function srcset_url_equal(element, srcset) {
* @param {string | null} value * @param {string | null} value
*/ */
function check_src_in_dev_hydration(dom, attribute, value) { function check_src_in_dev_hydration(dom, attribute, value) {
if (!current_hydration_fragment) return; if (!hydrating) return;
if (attribute !== 'src' && attribute !== 'href' && attribute !== 'srcset') return; if (attribute !== 'src' && attribute !== 'href' && attribute !== 'srcset') return;
if (attribute === 'srcset' && srcset_url_equal(dom, value)) return; if (attribute === 'srcset' && srcset_url_equal(dom, value)) return;
@ -2642,7 +2649,7 @@ export function spread_attributes(dom, prev, attrs, lowercase_attributes, css_ha
check_src_in_dev_hydration(dom, name, value); check_src_in_dev_hydration(dom, name, value);
} }
if ( if (
current_hydration_fragment === null || !hydrating ||
// @ts-ignore see attr method for an explanation of src/srcset // @ts-ignore see attr method for an explanation of src/srcset
(dom[name] !== value && name !== 'src' && name !== 'href' && name !== 'srcset') (dom[name] !== value && name !== 'src' && name !== 'href' && name !== 'srcset')
) { ) {
@ -2828,7 +2835,7 @@ export function spread_props(...props) {
export function createRoot(component, options) { export function createRoot(component, options) {
const props = proxy(/** @type {any} */ (options.props) || {}, false); const props = proxy(/** @type {any} */ (options.props) || {}, false);
let [accessors, $destroy] = mount(component, { ...options, props }); let [accessors, $destroy] = hydrate(component, { ...options, props });
const result = const result =
/** @type {Exports & { $destroy: () => void; $set: (props: Partial<Props>) => void; }} */ ({ /** @type {Exports & { $destroy: () => void; $set: (props: Partial<Props>) => void; }} */ ({
@ -2871,71 +2878,58 @@ export function createRoot(component, options) {
* events?: Events; * events?: Events;
* context?: Map<any, any>; * context?: Map<any, any>;
* intro?: boolean; * intro?: boolean;
* recover?: false;
* }} options * }} options
* @returns {[Exports, () => void]} * @returns {[Exports, () => void]}
*/ */
export function mount(component, options) { export function mount(component, options) {
init_operations(); init_operations();
const anchor = empty();
options.target.appendChild(anchor);
return _mount(component, { ...options, anchor });
}
/**
* @template {Record<string, any>} Props
* @template {Record<string, any> | undefined} Exports
* @template {Record<string, any>} Events
* @param {import('../../main/public.js').ComponentType<import('../../main/public.js').SvelteComponent<Props, Events>>} component
* @param {{
* target: Node;
* anchor: null | Text;
* props?: Props;
* events?: Events;
* context?: Map<any, any>;
* intro?: boolean;
* recover?: false;
* }} options
* @returns {[Exports, () => void]}
*/
function _mount(component, options) {
const registered_events = new Set(); const registered_events = new Set();
const container = options.target; const container = options.target;
const block = create_root_block(options.intro || false); const block = create_root_block(options.intro || false);
const first_child = /** @type {ChildNode} */ (container.firstChild);
// Call with insert_text == true to prevent empty {expressions} resulting in an empty
// fragment array, resulting in a hydration error down the line
const hydration_fragment = get_hydration_fragment(first_child, true);
const previous_hydration_fragment = current_hydration_fragment;
/** @type {Exports} */ /** @type {Exports} */
// @ts-expect-error will be defined because the render effect runs synchronously // @ts-expect-error will be defined because the render effect runs synchronously
let accessors = undefined; let accessors = undefined;
try { const effect = render_effect(
/** @type {null | Text} */ () => {
let anchor = null; if (options.context) {
if (hydration_fragment === null) { push({});
anchor = empty(); /** @type {import('../client/types.js').ComponentContext} */ (current_component_context).c =
container.appendChild(anchor); options.context;
} }
set_current_hydration_fragment(hydration_fragment); // @ts-expect-error the public typings are not what the actual function looks like
const effect = render_effect( accessors = component(options.anchor, options.props || {});
() => { if (options.context) {
if (options.context) { pop();
push({}); }
/** @type {import('../client/types.js').ComponentContext} */ ( },
current_component_context block,
).c = options.context; true
} );
// @ts-expect-error the public typings are not what the actual function looks like block.e = effect;
accessors = component(anchor, options.props || {});
if (options.context) {
pop();
}
},
block,
true
);
block.e = effect;
} catch (error) {
if (options.recover !== false && hydration_fragment !== null) {
// eslint-disable-next-line no-console
console.error(
'ERR_SVELTE_HYDRATION_MISMATCH' +
(DEV
? ': Hydration failed because the initial UI does not match what was rendered on the server.'
: ''),
error
);
remove(hydration_fragment);
first_child.remove();
hydration_fragment.at(-1)?.nextSibling?.remove();
return mount(component, options);
} else {
throw error;
}
} finally {
set_current_hydration_fragment(previous_hydration_fragment);
}
const bound_event_listener = handle_event_propagation.bind(null, container); const bound_event_listener = handle_event_propagation.bind(null, container);
const bound_document_event_listener = handle_event_propagation.bind(null, document); const bound_document_event_listener = handle_event_propagation.bind(null, document);
@ -2985,14 +2979,71 @@ export function mount(component, options) {
if (dom !== null) { if (dom !== null) {
remove(dom); remove(dom);
} }
if (hydration_fragment !== null) {
remove(hydration_fragment);
}
destroy_signal(/** @type {import('./types.js').EffectSignal} */ (block.e)); destroy_signal(/** @type {import('./types.js').EffectSignal} */ (block.e));
} }
]; ];
} }
/**
* Hydrates the given component to the given target and returns the accessors of the component and a function to destroy it.
*
* If you need to interact with the component after hydrating, use `createRoot` instead.
*
* @template {Record<string, any>} Props
* @template {Record<string, any> | undefined} Exports
* @template {Record<string, any>} Events
* @param {import('../../main/public.js').ComponentType<import('../../main/public.js').SvelteComponent<Props, Events>>} component
* @param {{
* target: Node;
* props?: Props;
* events?: Events;
* context?: Map<any, any>;
* intro?: boolean;
* recover?: false;
* }} options
* @returns {[Exports, () => void]}
*/
export function hydrate(component, options) {
init_operations();
const container = options.target;
const first_child = /** @type {ChildNode} */ (container.firstChild);
// Call with insert_text == true to prevent empty {expressions} resulting in an empty
// fragment array, resulting in a hydration error down the line
const hydration_fragment = get_hydration_fragment(first_child, true);
const previous_hydration_fragment = current_hydration_fragment;
try {
/** @type {null | Text} */
let anchor = null;
if (hydration_fragment === null) {
anchor = empty();
container.appendChild(anchor);
}
set_current_hydration_fragment(hydration_fragment);
return _mount(component, { ...options, anchor });
} catch (error) {
if (options.recover !== false && hydration_fragment !== null) {
// eslint-disable-next-line no-console
console.error(
'ERR_SVELTE_HYDRATION_MISMATCH' +
(DEV
? ': Hydration failed because the initial UI does not match what was rendered on the server.'
: ''),
error
);
remove(hydration_fragment);
first_child.remove();
hydration_fragment.at(-1)?.nextSibling?.remove();
set_current_hydration_fragment(null);
return mount(component, options);
} else {
throw error;
}
} finally {
set_current_hydration_fragment(previous_hydration_fragment);
}
}
/** /**
* @param {Record<string, unknown>} props * @param {Record<string, unknown>} props
* @returns {void} * @returns {void}

@ -233,4 +233,12 @@ function init_update_callbacks(context) {
// TODO bring implementations in here // TODO bring implementations in here
// (except probably untrack — do we want to expose that, if there's also a rune?) // (except probably untrack — do we want to expose that, if there's also a rune?)
export { flushSync, createRoot, mount, tick, untrack, unstate } from '../internal/index.js'; export {
flushSync,
createRoot,
mount,
hydrate,
tick,
untrack,
unstate
} from '../internal/index.js';

@ -8,6 +8,7 @@ export {
getContext, getContext,
hasContext, hasContext,
mount, mount,
hydrate,
setContext, setContext,
tick, tick,
untrack untrack

@ -353,6 +353,19 @@ declare module 'svelte' {
events?: Events | undefined; events?: Events | undefined;
context?: Map<any, any> | undefined; context?: Map<any, any> | undefined;
intro?: boolean | undefined; intro?: boolean | undefined;
}): [Exports, () => void];
/**
* Hydrates the given component to the given target and returns the accessors of the component and a function to destroy it.
*
* If you need to interact with the component after hydrating, use `createRoot` instead.
*
* */
export function hydrate<Props extends Record<string, any>, Exports extends Record<string, any> | undefined, Events extends Record<string, any>>(component: ComponentType<SvelteComponent<Props, Events, any>>, options: {
target: Node;
props?: Props | undefined;
events?: Events | undefined;
context?: Map<any, any> | undefined;
intro?: boolean | undefined;
recover?: false | undefined; recover?: false | undefined;
}): [Exports, () => void]; }): [Exports, () => void];
/** /**

@ -1,8 +1,8 @@
// @ts-ignore // @ts-ignore
import { mount } from 'svelte'; import { hydrate } from 'svelte';
// @ts-ignore you need to create this file // @ts-ignore you need to create this file
import App from './App.svelte'; import App from './App.svelte';
// @ts-ignore // @ts-ignore
[window.unmount] = mount(App, { [window.unmount] = hydrate(App, {
target: document.getElementById('root')! target: document.getElementById('root')!
}); });

@ -43,3 +43,47 @@ To remove reactivity from objects and arrays created with `$state`, use `unstate
This is handy when you want to pass some state to an external library or API that doesn't expect a reactive object such as `structuredClone`. This is handy when you want to pass some state to an external library or API that doesn't expect a reactive object such as `structuredClone`.
> Note that `unstate` will return a new object from the input when removing reactivity. If the object passed isn't reactive, it will be returned as is. > Note that `unstate` will return a new object from the input when removing reactivity. If the object passed isn't reactive, it will be returned as is.
## `mount`
Instantiates a component and mounts it to the given target:
```js
// @errors: 2724 2305
import { mount } from 'svelte';
import App from './App.svelte';
const app = mount(App, {
target: document.querySelector('#app'),
props: { some: 'property' }
});
```
## `hydrate`
Like `mount`, but will pick up any HTML rendered by Svelte's SSR output (from the `render` function) inside the target and make it interactive:
```js
// @errors: 2724 2305
import { hydrate } from 'svelte';
import App from './App.svelte';
const app = hydrate(App, {
target: document.querySelector('#app'),
props: { some: 'property' }
});
```
## `render`
Only available on the server and when compiling with the `server` option. Takes a component and returns an object with `html` and `head` properties on it, which you can use to populate the HTML when server-rendering your app:
```js
// @errors: 2724 2305 2307
import { render } from 'svelte/server';
import App from './App.svelte';
const result = render(App, {
props: { some: 'property' }
});
```

Loading…
Cancel
Save