diff --git a/.changeset/gorgeous-singers-rest.md b/.changeset/gorgeous-singers-rest.md
new file mode 100644
index 000000000000..5625c2764a33
--- /dev/null
+++ b/.changeset/gorgeous-singers-rest.md
@@ -0,0 +1,5 @@
+---
+"svelte": patch
+---
+
+feat: add hydrate method, make hydration treeshakeable
diff --git a/packages/svelte/scripts/check-treeshakeability.js b/packages/svelte/scripts/check-treeshakeability.js
index fd55ce096eca..a6164c462b29 100644
--- a/packages/svelte/scripts/check-treeshakeability.js
+++ b/packages/svelte/scripts/check-treeshakeability.js
@@ -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(
+ `
+
+
+hi
+
+a
+a
+
+
+
+
+{#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();
diff --git a/packages/svelte/src/internal/client/each.js b/packages/svelte/src/internal/client/each.js
index 25df8dce7362..2d81a0b070b1 100644
--- a/packages/svelte/src/internal/client/each.js
+++ b/packages/svelte/src/internal/client/each.js
@@ -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} */ (
- 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} */ (
- 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([]);
}
diff --git a/packages/svelte/src/internal/client/hydration.js b/packages/svelte/src/internal/client/hydration.js
index a8067ff89f1b..3d37f88ec1f6 100644
--- a/packages/svelte/src/internal/client/hydration.js
+++ b/packages/svelte/src/internal/client/hydration.js
@@ -3,25 +3,37 @@
import { empty } from './operations.js';
import { schedule_task } from './runtime.js';
-/** @type {null | Array} */
-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} 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 `` 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 | null}
+ * @returns {import('./types.js').TemplateNode[] | null}
*/
export function get_hydration_fragment(node, insert_text = false) {
- /** @type {Array} */
+ /** @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);
}
diff --git a/packages/svelte/src/internal/client/operations.js b/packages/svelte/src/internal/client/operations.js
index 5b6b3f93e128..ce9ea3c8f5f6 100644
--- a/packages/svelte/src/internal/client/operations.js
+++ b/packages/svelte/src/internal/client/operations.js
@@ -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 `{text}
`, 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} */ (current_hydration_fragment).at(-1) !== node
+ current_hydration_fragment.at(-1) !== node
) {
const fragment = /** @type {Array} */ (get_hydration_fragment(node));
const last_child = fragment.at(-1) || node;
diff --git a/packages/svelte/src/internal/client/reconciler.js b/packages/svelte/src/internal/client/reconciler.js
index 848e7205a3f4..7f0aa36a33dc 100644
--- a/packages/svelte/src/internal/client/reconciler.js
+++ b/packages/svelte/src/internal/client/reconciler.js
@@ -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 + '';
diff --git a/packages/svelte/src/internal/client/render.js b/packages/svelte/src/internal/client/render.js
index f9be124243f3..51807c014317 100644
--- a/packages/svelte/src/internal/client/render.js
+++ b/packages/svelte/src/internal/client/render.js
@@ -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} */
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) => void; }} */ ({
@@ -2871,71 +2878,58 @@ export function createRoot(component, options) {
* events?: Events;
* context?: Map;
* 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} Props
+ * @template {Record | undefined} Exports
+ * @template {Record} Events
+ * @param {import('../../main/public.js').ComponentType>} component
+ * @param {{
+ * target: Node;
+ * anchor: null | Text;
+ * props?: Props;
+ * events?: Events;
+ * context?: Map;
+ * 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} Props
+ * @template {Record | undefined} Exports
+ * @template {Record} Events
+ * @param {import('../../main/public.js').ComponentType>} component
+ * @param {{
+ * target: Node;
+ * props?: Props;
+ * events?: Events;
+ * context?: Map;
+ * 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} props
* @returns {void}
diff --git a/packages/svelte/src/main/main-client.js b/packages/svelte/src/main/main-client.js
index 2c6702671030..13c7872e6223 100644
--- a/packages/svelte/src/main/main-client.js
+++ b/packages/svelte/src/main/main-client.js
@@ -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';
diff --git a/packages/svelte/src/main/main-server.js b/packages/svelte/src/main/main-server.js
index a9e1148cc3e5..3b02eb33cfcb 100644
--- a/packages/svelte/src/main/main-server.js
+++ b/packages/svelte/src/main/main-server.js
@@ -8,6 +8,7 @@ export {
getContext,
hasContext,
mount,
+ hydrate,
setContext,
tick,
untrack
diff --git a/packages/svelte/types/index.d.ts b/packages/svelte/types/index.d.ts
index caf1bf6d7326..bd382e28db8f 100644
--- a/packages/svelte/types/index.d.ts
+++ b/packages/svelte/types/index.d.ts
@@ -353,6 +353,19 @@ declare module 'svelte' {
events?: Events | undefined;
context?: Map | 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, Exports extends Record | undefined, Events extends Record>(component: ComponentType>, options: {
+ target: Node;
+ props?: Props | undefined;
+ events?: Events | undefined;
+ context?: Map | undefined;
+ intro?: boolean | undefined;
recover?: false | undefined;
}): [Exports, () => void];
/**
diff --git a/playgrounds/demo/src/entry-client.ts b/playgrounds/demo/src/entry-client.ts
index b29d004b1711..c996ffc02fcc 100644
--- a/playgrounds/demo/src/entry-client.ts
+++ b/playgrounds/demo/src/entry-client.ts
@@ -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')!
});
diff --git a/sites/svelte-5-preview/src/routes/docs/content/01-api/05-functions.md b/sites/svelte-5-preview/src/routes/docs/content/01-api/05-functions.md
index 26410e1774a2..278a71db7fb4 100644
--- a/sites/svelte-5-preview/src/routes/docs/content/01-api/05-functions.md
+++ b/sites/svelte-5-preview/src/routes/docs/content/01-api/05-functions.md
@@ -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' }
+});
+```