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 virtual from '@rollup/plugin-virtual';
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'));
@ -20,31 +48,8 @@ for (const key in pkg.exports) {
if (!pkg.exports[key][type]) continue;
const subpackage = path.join(pkg.name, key);
const resolved = path.resolve(pkg.exports[key][type]);
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();
const code = await bundle_code(`import ${JSON.stringify(resolved)}`);
if (code === '') {
// 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
console.groupEnd();

@ -11,6 +11,7 @@ import {
current_hydration_fragment,
get_hydration_fragment,
hydrate_block_anchor,
hydrating,
set_current_hydration_fragment
} from './hydration.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} */
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;
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
// element in which the each block is rendered, which requires certain handling
// 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
anchor = empty();
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;
if (current_hydration_fragment !== null) {
if (hydrating) {
const is_each_else_comment =
/** @type {Comment} */ (current_hydration_fragment?.[0])?.data === 'ssr:each_else';
// 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.
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);
mismatch = true;
} else if (is_each_else_comment) {
@ -306,22 +310,22 @@ function reconcile_indexed_array(
}
} else {
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);
if (is_hydrating) {
if (hydrating) {
// Hydrate block
var hydration_list = /** @type {import('./types.js').TemplateNode[]} */ (
current_hydration_fragment
);
var hydrating_node = hydration_list[0];
for (; index < length; index++) {
var fragment = /** @type {Array<Text | Comment | Element>} */ (
get_hydration_fragment(hydrating_node)
);
var fragment = get_hydration_fragment(hydrating_node);
set_current_hydration_fragment(fragment);
if (!fragment) {
// 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
mismatch = true;
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
set_current_hydration_fragment([]);
}
@ -425,9 +429,10 @@ function reconcile_tracked_array(
var key;
var item;
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);
if (is_hydrating) {
if (hydrating) {
// Hydrate block
var fragment;
var hydration_list = /** @type {import('./types.js').TemplateNode[]} */ (
@ -435,13 +440,12 @@ function reconcile_tracked_array(
);
var hydrating_node = hydration_list[0];
while (b > 0) {
fragment = /** @type {Array<Text | Comment | Element>} */ (
get_hydration_fragment(hydrating_node)
);
fragment = get_hydration_fragment(hydrating_node);
set_current_hydration_fragment(fragment);
if (!fragment) {
// 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
mismatch = true;
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
set_current_hydration_fragment([]);
}

@ -3,25 +3,37 @@
import { empty } from './operations.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}
*/
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.
* @param {Node | null} node
* @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) {
/** @type {Array<import('./types.js').TemplateNode>} */
/** @type {import('./types.js').TemplateNode[]} */
const fragment = [];
/** @type {null | Node} */
@ -66,9 +78,10 @@ export function get_hydration_fragment(node, insert_text = false) {
* @returns {void}
*/
export function hydrate_block_anchor(anchor_node, is_controlled) {
/** @type {Node} */
let target_node = anchor_node;
if (current_hydration_fragment !== null) {
if (hydrating) {
/** @type {Node} */
let target_node = anchor_node;
if (is_controlled) {
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';
// We cache the Node and Element prototype methods, so that we can avoid doing
@ -171,7 +171,7 @@ export function empty() {
/*#__NO_SIDE_EFFECTS__*/
export function child(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
if (child === null) {
const text = empty();
@ -192,7 +192,7 @@ export function child(node) {
*/
/*#__NO_SIDE_EFFECTS__*/
export function child_frag(node, is_text) {
if (current_hydration_fragment !== null) {
if (hydrating) {
const first_node = /** @type {Node[]} */ (node)[0];
// 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__*/
export function sibling(node, is_text = false) {
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
// text node to hydrate — we must therefore create one
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
* @returns {Node}
*/
@ -283,7 +284,7 @@ function capture_fragment_from_node(node) {
if (
node.nodeType === 8 &&
/** @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 last_child = fragment.at(-1) || node;

@ -1,5 +1,5 @@
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';
/** @param {string} html */
@ -92,7 +92,7 @@ export function remove(current) {
*/
export function reconcile_html(target, value, svg) {
hydrate_block_anchor(target);
if (current_hydration_fragment !== null) {
if (hydrating) {
return current_hydration_fragment;
}
var html = value + '';

@ -54,6 +54,7 @@ import {
current_hydration_fragment,
get_hydration_fragment,
hydrate_block_anchor,
hydrating,
set_current_hydration_fragment
} from './hydration.js';
import {
@ -166,7 +167,7 @@ export function svg_replace(node) {
* @returns {Element | DocumentFragment | Node[]}
*/
function open_template(is_fragment, use_clone_node, anchor, template_element_fn) {
if (current_hydration_fragment !== null) {
if (hydrating) {
if (anchor !== null) {
hydrate_block_anchor(anchor, false);
}
@ -217,7 +218,7 @@ export function space(anchor) {
// if an {expression} is empty during SSR, there might be no
// text node to hydrate (or an anchor comment is falsely detected instead)
// — we must therefore create one
if (current_hydration_fragment !== null && node?.nodeType !== 3) {
if (hydrating && node?.nodeType !== 3) {
node = empty();
// @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
@ -251,10 +252,8 @@ function close_template(dom, is_fragment, anchor) {
? dom
: /** @type {import('./types.js').TemplateNode[]} */ (Array.from(dom.childNodes))
: dom;
if (anchor !== null) {
if (current_hydration_fragment === null) {
insert(current, null, anchor);
}
if (!hydrating && anchor !== null) {
insert(current, null, anchor);
}
block.d = current;
}
@ -415,14 +414,13 @@ export function class_name(dom, value) {
// @ts-expect-error need to add __className to patched prototype
const prev_class_name = dom.__className;
const next_class_name = to_class(value);
const is_hydrating = current_hydration_fragment !== null;
if (is_hydrating && dom.className === next_class_name) {
if (hydrating && dom.className === next_class_name) {
// In case of hydration don't reset the class as it's already correct.
// @ts-expect-error need to add __className to patched prototype
dom.__className = next_class_name;
} else if (
prev_class_name !== next_class_name ||
(is_hydrating && dom.className !== next_class_name)
(hydrating && dom.className !== next_class_name)
) {
if (next_class_name === '') {
dom.removeAttribute('class');
@ -452,7 +450,7 @@ export function text(dom, value) {
// @ts-expect-error need to add __value to patched prototype
const prev_node_value = dom.__nodeValue;
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.
// @ts-expect-error need to add __nodeValue to patched prototype
dom.__nodeValue = next_node_value;
@ -739,7 +737,7 @@ export function bind_playback_rate(media, get_value, update) {
* @param {(paused: boolean) => void} update
*/
export function bind_paused(media, get_value, update) {
let mounted = current_hydration_fragment !== null;
let mounted = hydrating;
let paused = get_value();
const callback = () => {
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) {
const block = create_if_block();
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>} */
let consequent_dom = null;
@ -1495,7 +1494,7 @@ function if_block(anchor_node, condition_fn, consequent_fn, alternate_fn) {
trigger_transitions(alternate_transitions, 'in');
}
}
} else if (current_hydration_fragment !== null) {
} else if (hydrating) {
const comment_text = /** @type {Comment} */ (current_hydration_fragment?.[0])?.data;
if (
!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.
remove(current_hydration_fragment);
set_current_hydration_fragment(null);
mismatch = true;
} else {
// Remove the ssr:if comment node or else it will confuse the subsequent hydration algorithm
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) {
consequent_fn(anchor_node);
if (current_branch_effect === null) {
// Restore previous fragment so that Svelte continues to operate in hydration mode
set_current_hydration_fragment(previous_hydration_fragment);
if (mismatch && current_branch_effect === null) {
// Set fragment so that Svelte continues to operate in hydration mode
set_current_hydration_fragment([]);
}
current_branch_effect = consequent_effect;
consequent_dom = block.d;
@ -1558,9 +1558,9 @@ function if_block(anchor_node, condition_fn, consequent_fn, alternate_fn) {
if (alternate_fn !== null) {
alternate_fn(anchor_node);
}
if (current_branch_effect === null) {
// Restore previous fragment so that Svelte continues to operate in hydration mode
set_current_hydration_fragment(previous_hydration_fragment);
if (mismatch && current_branch_effect === null) {
// Set fragment so that Svelte continues to operate in hydration mode
set_current_hydration_fragment([]);
}
current_branch_effect = alternate_effect;
alternate_dom = block.d;
@ -1593,10 +1593,15 @@ export function head(render_fn) {
const block = create_head_block();
// 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.
const hydration_fragment =
current_hydration_fragment !== null ? get_hydration_fragment(document.head.firstChild) : null;
const previous_hydration_fragment = current_hydration_fragment;
set_current_hydration_fragment(hydration_fragment);
let hydration_fragment = null;
let previous_hydration_fragment = null;
let is_hydrating = hydrating;
if (is_hydrating) {
hydration_fragment = get_hydration_fragment(document.head.firstChild);
previous_hydration_fragment = current_hydration_fragment;
set_current_hydration_fragment(hydration_fragment);
}
try {
const head_effect = render_effect(
() => {
@ -1606,7 +1611,7 @@ export function head(render_fn) {
block.d = null;
}
let anchor = null;
if (current_hydration_fragment === null) {
if (!hydrating) {
anchor = empty();
document.head.appendChild(anchor);
}
@ -1623,7 +1628,9 @@ export function head(render_fn) {
});
block.e = head_effect;
} 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
: anchor_node.parentElement?.namespaceURI ?? null;
const next_element = tag
? current_hydration_fragment !== null
? hydrating
? /** @type {Element} */ (current_hydration_fragment[0])
: ns
? document.createElementNS(ns, tag)
@ -1701,7 +1708,7 @@ export function element(anchor_node, tag_fn, is_svg, render_fn) {
element = next_element;
if (element !== null && render_fn !== undefined) {
let anchor;
if (current_hydration_fragment !== null) {
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
anchor = /** @type {Comment} */ (element.firstChild);
@ -2158,7 +2165,7 @@ export function cssProps(anchor, is_html, props, component) {
/** @type {Text | Comment} */
let component_anchor;
if (current_hydration_fragment !== null) {
if (hydrating) {
// Hydration: css props element is surrounded by a ssr comment ...
tag = /** @type {HTMLElement | SVGElement} */ (current_hydration_fragment[0]);
// ... 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}
*/
export function remove_input_attr_defaults(dom) {
if (current_hydration_fragment !== null) {
if (hydrating) {
attr(dom, 'value', null);
attr(dom, 'checked', null);
}
@ -2336,7 +2343,7 @@ export function remove_input_attr_defaults(dom) {
* @returns {void}
*/
export function remove_textarea_child(dom) {
if (current_hydration_fragment !== null && dom.firstChild !== null) {
if (hydrating && dom.firstChild !== null) {
dom.textContent = '';
}
}
@ -2366,7 +2373,7 @@ export function attr(dom, attribute, value) {
}
if (
current_hydration_fragment === null ||
!hydrating ||
(dom.getAttribute(attribute) !== value &&
// 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
@ -2429,7 +2436,7 @@ export function srcset_url_equal(element, srcset) {
* @param {string | null} 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 === '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);
}
if (
current_hydration_fragment === null ||
!hydrating ||
// @ts-ignore see attr method for an explanation of src/srcset
(dom[name] !== value && name !== 'src' && name !== 'href' && name !== 'srcset')
) {
@ -2828,7 +2835,7 @@ export function spread_props(...props) {
export function createRoot(component, options) {
const props = proxy(/** @type {any} */ (options.props) || {}, false);
let [accessors, $destroy] = mount(component, { ...options, props });
let [accessors, $destroy] = hydrate(component, { ...options, props });
const result =
/** @type {Exports & { $destroy: () => void; $set: (props: Partial<Props>) => void; }} */ ({
@ -2871,71 +2878,58 @@ export function createRoot(component, options) {
* events?: Events;
* context?: Map<any, any>;
* intro?: boolean;
* recover?: false;
* }} options
* @returns {[Exports, () => void]}
*/
export function mount(component, options) {
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 container = options.target;
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} */
// @ts-expect-error will be defined because the render effect runs synchronously
let accessors = undefined;
try {
/** @type {null | Text} */
let anchor = null;
if (hydration_fragment === null) {
anchor = empty();
container.appendChild(anchor);
}
set_current_hydration_fragment(hydration_fragment);
const effect = render_effect(
() => {
if (options.context) {
push({});
/** @type {import('../client/types.js').ComponentContext} */ (
current_component_context
).c = options.context;
}
// @ts-expect-error the public typings are not what the actual function looks like
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 effect = render_effect(
() => {
if (options.context) {
push({});
/** @type {import('../client/types.js').ComponentContext} */ (current_component_context).c =
options.context;
}
// @ts-expect-error the public typings are not what the actual function looks like
accessors = component(options.anchor, options.props || {});
if (options.context) {
pop();
}
},
block,
true
);
block.e = effect;
const bound_event_listener = handle_event_propagation.bind(null, container);
const bound_document_event_listener = handle_event_propagation.bind(null, document);
@ -2985,14 +2979,71 @@ export function mount(component, options) {
if (dom !== null) {
remove(dom);
}
if (hydration_fragment !== null) {
remove(hydration_fragment);
}
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
* @returns {void}

@ -233,4 +233,12 @@ function init_update_callbacks(context) {
// TODO bring implementations in here
// (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,
hasContext,
mount,
hydrate,
setContext,
tick,
untrack

@ -353,6 +353,19 @@ declare module 'svelte' {
events?: Events | undefined;
context?: Map<any, any> | 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;
}): [Exports, () => void];
/**

@ -1,8 +1,8 @@
// @ts-ignore
import { mount } from 'svelte';
import { hydrate } from 'svelte';
// @ts-ignore you need to create this file
import App from './App.svelte';
// @ts-ignore
[window.unmount] = mount(App, {
[window.unmount] = hydrate(App, {
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`.
> 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