diff --git a/.changeset/fast-donkeys-pay.md b/.changeset/fast-donkeys-pay.md new file mode 100644 index 0000000000..29913f5e52 --- /dev/null +++ b/.changeset/fast-donkeys-pay.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +feat: simpler ` hydration diff --git a/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js b/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js index 78f677b8ef..6deb02385f 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js @@ -28,6 +28,7 @@ import { DOMBooleanAttributes, ELEMENT_IS_NAMESPACED, ELEMENT_PRESERVE_ATTRIBUTE_CASE, + HYDRATION_ANCHOR, HYDRATION_END, HYDRATION_START } from '../../../../constants.js'; @@ -38,6 +39,7 @@ import { filename, locator } from '../../../state.js'; export const block_open = t_string(``); export const block_close = t_string(``); +export const block_anchor = t_string(``); /** * @param {string} value @@ -1477,8 +1479,6 @@ const template_visitors = { } }; - context.state.template.push(block_open); - const main = /** @type {import('estree').BlockStatement} */ ( context.visit(node.fragment, { ...context.state, @@ -1515,7 +1515,7 @@ const template_visitors = { ) ) ), - block_close + block_anchor ); if (context.state.options.dev) { context.state.template.push(t_statement(b.stmt(b.call('$.pop_element')))); diff --git a/packages/svelte/src/constants.js b/packages/svelte/src/constants.js index 63949f7e99..a8963d6854 100644 --- a/packages/svelte/src/constants.js +++ b/packages/svelte/src/constants.js @@ -21,6 +21,7 @@ export const TEMPLATE_USE_IMPORT_NODE = 1 << 1; export const HYDRATION_START = '['; export const HYDRATION_END = ']'; +export const HYDRATION_ANCHOR = ''; export const HYDRATION_END_ELSE = `${HYDRATION_END}!`; // used to indicate that an `{:else}...` block was rendered export const HYDRATION_ERROR = {}; diff --git a/packages/svelte/src/internal/client/dom/blocks/svelte-element.js b/packages/svelte/src/internal/client/dom/blocks/svelte-element.js index df607fe54a..8e5dd9e5a8 100644 --- a/packages/svelte/src/internal/client/dom/blocks/svelte-element.js +++ b/packages/svelte/src/internal/client/dom/blocks/svelte-element.js @@ -1,5 +1,5 @@ import { namespace_svg } from '../../../../constants.js'; -import { hydrate_anchor, hydrate_start, hydrating } from '../hydration.js'; +import { hydrating, set_hydrate_nodes } from '../hydration.js'; import { empty } from '../operations.js'; import { block, @@ -37,7 +37,7 @@ function swap_block_dom(effect, from, to) { } /** - * @param {Comment} anchor + * @param {Comment | Element} node * @param {() => string} get_tag * @param {boolean} is_svg * @param {undefined | ((element: Element, anchor: Node | null) => void)} render_fn, @@ -45,7 +45,7 @@ function swap_block_dom(effect, from, to) { * @param {undefined | [number, number]} location * @returns {void} */ -export function element(anchor, get_tag, is_svg, render_fn, get_namespace, location) { +export function element(node, get_tag, is_svg, render_fn, get_namespace, location) { const parent_effect = /** @type {import('#client').Effect} */ (current_effect); const filename = DEV && location && current_component_context?.function.filename; @@ -56,7 +56,9 @@ export function element(anchor, get_tag, is_svg, render_fn, get_namespace, locat let current_tag; /** @type {null | Element} */ - let element = null; + let element = hydrating && node.nodeType === 1 ? /** @type {Element} */ (node) : null; + + let anchor = /** @type {Comment} */ (hydrating && element ? element.nextSibling : node); /** @type {import('#client').Effect | null} */ let effect; @@ -75,6 +77,7 @@ export function element(anchor, get_tag, is_svg, render_fn, get_namespace, locat : is_svg || next_tag === 'svg' ? namespace_svg : null; + // Assumption: Noone changes the namespace but not the tag (what would that even mean?) if (next_tag === tag) return; @@ -104,7 +107,7 @@ export function element(anchor, get_tag, is_svg, render_fn, get_namespace, locat effect = branch(() => { const prev_element = element; element = hydrating - ? /** @type {Element} */ (hydrate_start) + ? /** @type {Element} */ (element) : ns ? document.createElementNS(ns, next_tag) : document.createElement(next_tag); @@ -123,9 +126,13 @@ export function element(anchor, get_tag, is_svg, render_fn, get_namespace, locat if (render_fn) { // If hydrating, use the existing ssr comment as the anchor so that the // inner open and close methods can pick up the existing nodes correctly - var child_anchor = hydrating - ? element.firstChild && hydrate_anchor(/** @type {Comment} */ (element.firstChild)) - : element.appendChild(empty()); + var child_anchor = hydrating ? element.lastChild : element.appendChild(empty()); + + if (hydrating && child_anchor) { + set_hydrate_nodes( + /** @type {import('#client').TemplateNode[]} */ ([...element.childNodes]).slice(0, -1) + ); + } // `child_anchor` is undefined if this is a void element, but we still // need to call `render_fn` in order to run actions etc. If the element @@ -136,11 +143,13 @@ export function element(anchor, get_tag, is_svg, render_fn, get_namespace, locat anchor.before(element); - if (prev_element) { - swap_block_dom(parent_effect, prev_element, element); - prev_element.remove(); - } else if (!hydrating) { - push_template_node(element, parent_effect); + if (!hydrating) { + if (prev_element) { + swap_block_dom(parent_effect, prev_element, element); + prev_element.remove(); + } else { + push_template_node(element, parent_effect); + } } }); } diff --git a/packages/svelte/src/internal/client/dom/operations.js b/packages/svelte/src/internal/client/dom/operations.js index 508ea47388..012c0b22dd 100644 --- a/packages/svelte/src/internal/client/dom/operations.js +++ b/packages/svelte/src/internal/client/dom/operations.js @@ -2,6 +2,7 @@ import { hydrate_anchor, hydrate_start, hydrating } from './hydration.js'; import { DEV } from 'esm-env'; import { init_array_prototype_warnings } from '../dev/equality.js'; import { current_effect } from '../runtime.js'; +import { HYDRATION_ANCHOR } from '../../../constants.js'; // export these for reference in the compiled code, making global name deduplication unnecessary /** @type {Window} */ @@ -105,15 +106,21 @@ export function first_child(fragment, is_text) { */ /*#__NO_SIDE_EFFECTS__*/ export function sibling(node, is_text = false) { - const next_sibling = node.nextSibling; + var next_sibling = /** @type {import('#client').TemplateNode} */ (node.nextSibling); if (!hydrating) { return next_sibling; } + var type = next_sibling.nodeType; + + if (type === 8 && /** @type {Comment} */ (next_sibling).data === HYDRATION_ANCHOR) { + return sibling(next_sibling, is_text); + } + // 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) { + if (is_text && type !== 3) { var text = empty(); var dom = /** @type {import('#client').TemplateNode[]} */ ( /** @type {import('#client').Effect} */ (current_effect).dom diff --git a/packages/svelte/src/internal/server/hydration.js b/packages/svelte/src/internal/server/hydration.js index 20913cd54c..d860d759d2 100644 --- a/packages/svelte/src/internal/server/hydration.js +++ b/packages/svelte/src/internal/server/hydration.js @@ -1,5 +1,11 @@ -import { HYDRATION_END, HYDRATION_END_ELSE, HYDRATION_START } from '../../constants.js'; +import { + HYDRATION_ANCHOR, + HYDRATION_END, + HYDRATION_END_ELSE, + HYDRATION_START +} from '../../constants.js'; export const BLOCK_OPEN = ``; export const BLOCK_CLOSE = ``; +export const BLOCK_ANCHOR = ``; export const BLOCK_CLOSE_ELSE = ``; diff --git a/packages/svelte/src/internal/server/index.js b/packages/svelte/src/internal/server/index.js index d9ca957949..3b6a0158b3 100644 --- a/packages/svelte/src/internal/server/index.js +++ b/packages/svelte/src/internal/server/index.js @@ -10,7 +10,7 @@ import { import { escape_html } from '../../escaping.js'; import { DEV } from 'esm-env'; import { current_component, pop, push } from './context.js'; -import { BLOCK_CLOSE, BLOCK_OPEN } from './hydration.js'; +import { BLOCK_ANCHOR, BLOCK_CLOSE, BLOCK_OPEN } from './hydration.js'; import { validate_store } from '../shared/validate.js'; // https://html.spec.whatwg.org/multipage/syntax.html#attributes-2 @@ -78,12 +78,9 @@ export function element(payload, tag, attributes_fn, children_fn) { payload.out += `>`; if (!VoidElements.has(tag)) { - if (!RawTextElements.includes(tag)) { - payload.out += BLOCK_OPEN; - } children_fn(); if (!RawTextElements.includes(tag)) { - payload.out += BLOCK_CLOSE; + payload.out += BLOCK_ANCHOR; } payload.out += ``; } diff --git a/packages/svelte/tests/server-side-rendering/samples/head-svelte-components-raw-content/_expected.html b/packages/svelte/tests/server-side-rendering/samples/head-svelte-components-raw-content/_expected.html index 3eca221c36..48da62b4de 100644 --- a/packages/svelte/tests/server-side-rendering/samples/head-svelte-components-raw-content/_expected.html +++ b/packages/svelte/tests/server-side-rendering/samples/head-svelte-components-raw-content/_expected.html @@ -1,16 +1,13 @@ - lorem - - + - - + - + diff --git a/packages/svelte/tests/snapshot/samples/svelte-element/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/svelte-element/_expected/server/index.svelte.js index 2513822d91..6560589772 100644 --- a/packages/svelte/tests/snapshot/samples/svelte-element/_expected/server/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/svelte-element/_expected/server/index.svelte.js @@ -3,7 +3,6 @@ import * as $ from "svelte/internal/server"; export default function Svelte_element($$payload, $$props) { let { tag = 'hr' } = $$props; - $$payload.out += ``; if (tag) $.element($$payload, tag, () => {}, () => {}); - $$payload.out += ``; + $$payload.out += ``; } \ No newline at end of file