diff --git a/packages/svelte/src/internal/client/dom/blocks/each.js b/packages/svelte/src/internal/client/dom/blocks/each.js index 672575e7a3..32babbf0b6 100644 --- a/packages/svelte/src/internal/client/dom/blocks/each.js +++ b/packages/svelte/src/internal/client/dom/blocks/each.js @@ -8,11 +8,10 @@ import { } from '../../../../constants.js'; import { current_hydration_fragment, - get_hydration_fragment, hydrate_block_anchor, hydrating, - set_current_hydration_fragment, - set_hydrating + set_hydrating, + update_hydration_fragment } from '../hydration.js'; import { empty } from '../operations.js'; import { insert, remove } from '../reconciler.js'; @@ -124,11 +123,11 @@ function each(anchor, get_collection, flags, get_key, render_fn, fallback_fn, re var hydrating_node = hydration_list[0]; for (var i = 0; i < length; i++) { - 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 + var fragment = update_hydration_fragment(hydrating_node); + + if (fragment === null) { + // If fragment is null, then that means that the server rendered fewer items than what + // expected, so break out and continue appending non-hydrated items mismatch = true; set_hydrating(false); break; diff --git a/packages/svelte/src/internal/client/dom/blocks/svelte-head.js b/packages/svelte/src/internal/client/dom/blocks/svelte-head.js index 3c6bfbcf68..ea7f50a434 100644 --- a/packages/svelte/src/internal/client/dom/blocks/svelte-head.js +++ b/packages/svelte/src/internal/client/dom/blocks/svelte-head.js @@ -1,8 +1,8 @@ import { current_hydration_fragment, - get_hydration_fragment, hydrating, - set_current_hydration_fragment + set_current_hydration_fragment, + update_hydration_fragment } from '../hydration.js'; import { empty } from '../operations.js'; import { render_effect } from '../../reactivity/effects.js'; @@ -15,14 +15,12 @@ import { remove } from '../reconciler.js'; export function head(render_fn) { // The head function may be called after the first hydration pass and ssr comment nodes may still be present, // therefore we need to skip that when we detect that we're not in hydration mode. - let hydration_fragment = null; let previous_hydration_fragment = null; + let was_hydrating = hydrating; - let is_hydrating = hydrating; - if (is_hydrating) { - hydration_fragment = get_hydration_fragment(document.head.firstChild); + if (hydrating) { previous_hydration_fragment = current_hydration_fragment; - set_current_hydration_fragment(hydration_fragment); + update_hydration_fragment(document.head.firstChild); } try { @@ -50,7 +48,7 @@ export function head(render_fn) { } }; } finally { - if (is_hydrating) { + if (was_hydrating) { set_current_hydration_fragment(previous_hydration_fragment); } } diff --git a/packages/svelte/src/internal/client/dom/hydration.js b/packages/svelte/src/internal/client/dom/hydration.js index 1466f91f86..a85d47d141 100644 --- a/packages/svelte/src/internal/client/dom/hydration.js +++ b/packages/svelte/src/internal/client/dom/hydration.js @@ -27,17 +27,26 @@ export let current_hydration_fragment = /** @type {any} */ (null); * @returns {void} */ export function set_current_hydration_fragment(fragment) { - // hydrating = fragment !== null; current_hydration_fragment = /** @type {import('#client').TemplateNode[]} */ (fragment); } +/** + * @param {Node | null} first + * @param {boolean} [insert_text] Whether to insert an empty text node if the fragment is empty + */ +export function update_hydration_fragment(first, insert_text) { + const fragment = get_hydration_fragment(first, insert_text); + set_current_hydration_fragment(fragment); + return 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 {import('#client').TemplateNode[] | null} */ -export function get_hydration_fragment(node, insert_text = false) { +function get_hydration_fragment(node, insert_text = false) { /** @type {import('#client').TemplateNode[]} */ const fragment = []; @@ -46,13 +55,17 @@ export function get_hydration_fragment(node, insert_text = false) { /** @type {null | string} */ let target_depth = null; + while (current_node !== null) { const node_type = current_node.nodeType; const next_sibling = current_node.nextSibling; + if (node_type === 8) { const data = /** @type {Comment} */ (current_node).data; + if (data.startsWith('ssr:')) { const depth = data.slice(4); + if (target_depth === null) { target_depth = depth; } else if (depth === target_depth) { @@ -65,15 +78,19 @@ export function get_hydration_fragment(node, insert_text = false) { } else { fragment.push(/** @type {Text | Comment | Element} */ (current_node)); } + current_node = next_sibling; continue; } } + if (target_depth !== null) { fragment.push(/** @type {Text | Comment | Element} */ (current_node)); } + current_node = next_sibling; } + return null; } @@ -101,3 +118,24 @@ export function hydrate_block_anchor(node) { set_current_hydration_fragment(first_child === null ? [] : [first_child]); } } + +/** + * Expects to only be called in hydration mode + * @param {Node} node + * @returns {Node} + */ +export function capture_fragment_from_node(node) { + if ( + node.nodeType === 8 && + /** @type {Comment} */ (node).data.startsWith('ssr:') && + current_hydration_fragment[current_hydration_fragment.length - 1] !== node + ) { + const fragment = /** @type {Array} */ (get_hydration_fragment(node)); + const last_child = fragment[fragment.length - 1] || node; + const target = /** @type {Node} */ (last_child.nextSibling); + // @ts-ignore + target.$$fragment = fragment; + return target; + } + return node; +} diff --git a/packages/svelte/src/internal/client/dom/operations.js b/packages/svelte/src/internal/client/dom/operations.js index 8b8746cb3f..e3532cbf12 100644 --- a/packages/svelte/src/internal/client/dom/operations.js +++ b/packages/svelte/src/internal/client/dom/operations.js @@ -1,4 +1,4 @@ -import { current_hydration_fragment, get_hydration_fragment, hydrating } from './hydration.js'; +import { capture_fragment_from_node, current_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 @@ -226,24 +226,3 @@ export function clear_text_content(node) { export function create_element(name) { return document.createElement(name); } - -/** - * Expects to only be called in hydration mode - * @param {Node} node - * @returns {Node} - */ -function capture_fragment_from_node(node) { - if ( - node.nodeType === 8 && - /** @type {Comment} */ (node).data.startsWith('ssr:') && - current_hydration_fragment[current_hydration_fragment.length - 1] !== node - ) { - const fragment = /** @type {Array} */ (get_hydration_fragment(node)); - const last_child = fragment[fragment.length - 1] || node; - const target = /** @type {Node} */ (last_child.nextSibling); - // @ts-ignore - target.$$fragment = fragment; - return target; - } - return node; -} diff --git a/packages/svelte/src/internal/client/render.js b/packages/svelte/src/internal/client/render.js index 4259b4436d..f775a7d682 100644 --- a/packages/svelte/src/internal/client/render.js +++ b/packages/svelte/src/internal/client/render.js @@ -6,11 +6,11 @@ import { flush_sync, push, pop, current_component_context } from './runtime.js'; import { render_effect, destroy_effect } from './reactivity/effects.js'; import { current_hydration_fragment, - get_hydration_fragment, hydrate_block_anchor, hydrating, set_current_hydration_fragment, - set_hydrating + set_hydrating, + update_hydration_fragment } from './dom/hydration.js'; import { array_from } from './utils.js'; import { handle_event_propagation } from './dom/elements/events.js'; @@ -142,11 +142,12 @@ export function hydrate(component, options) { init_operations(); const container = options.target; const first_child = /** @type {ChildNode} */ (container.firstChild); + const previous_hydration_fragment = current_hydration_fragment; + // 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; - set_current_hydration_fragment(hydration_fragment); + // TODO is both this and the `container.appendChild(anchor)` below necessary? + const hydration_fragment = update_hydration_fragment(first_child, true); set_hydrating(true); /** @type {null | Text} */