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 1800479a681b..7aadabeffb11 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 @@ -38,6 +38,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 @@ -959,9 +960,12 @@ function serialize_element_spread_attributes( * @param {import('#compiler').Component | import('#compiler').SvelteComponent | import('#compiler').SvelteSelf} node * @param {string | import('estree').Expression} component_name * @param {import('./types').ComponentContext} context - * @returns {import('estree').Statement} + * @returns {import('estree').Statement[]} */ function serialize_inline_component(node, component_name, context) { + /** @type {import('./types').Template[]} */ + const parts = []; + /** @type {Array} */ const props_and_spreads = []; @@ -1123,37 +1127,43 @@ function serialize_inline_component(node, component_name, context) { b.array(props_and_spreads.map((p) => (Array.isArray(p) ? b.object(p) : p))) ); - /** @type {import('estree').Statement} */ - let statement = b.stmt( - (typeof component_name === 'string' ? b.call : b.maybe_call)( - context.state.options.dev - ? b.call( - '$.validate_component', - typeof component_name === 'string' ? b.id(component_name) : component_name - ) - : component_name, - b.id('$$payload'), - props_expression - ) - ); + /** @type {import('estree').Statement[]} */ + let statements = [ + b.stmt(b.assignment('+=', b.id('$$payload.out'), b.literal(block_open.value))), + b.stmt( + (typeof component_name === 'string' ? b.call : b.maybe_call)( + context.state.options.dev + ? b.call( + '$.validate_component', + typeof component_name === 'string' ? b.id(component_name) : component_name + ) + : component_name, + b.id('$$payload'), + props_expression + ) + ), + b.stmt(b.assignment('+=', b.id('$$payload.out'), b.literal(block_close.value))) + ]; if (custom_css_props.length > 0) { - statement = b.stmt( - b.call( - '$.css_props', - b.id('$$payload'), - b.literal(context.state.metadata.namespace === 'svg' ? false : true), - b.object(custom_css_props), - b.thunk(b.block([statement])) + statements = [ + b.stmt( + b.call( + '$.css_props', + b.id('$$payload'), + b.literal(context.state.metadata.namespace === 'svg' ? false : true), + b.object(custom_css_props), + b.thunk(b.block(statements)) + ) ) - ); + ]; } if (snippet_declarations.length > 0) { - statement = b.block([...snippet_declarations, statement]); + statements = [b.block([...snippet_declarations, ...statements])]; } - return statement; + return statements; } /** @@ -1470,8 +1480,6 @@ const template_visitors = { } }; - context.state.template.push(block_open); - const main = create_block(node, node.fragment.nodes, { ...context, state: { ...context.state, metadata } @@ -1506,7 +1514,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')))); @@ -1654,29 +1662,27 @@ const template_visitors = { } }, Component(node, context) { - const state = context.state; - state.template.push(block_open); - const call = serialize_inline_component(node, node.name, context); - state.template.push(t_statement(call)); - state.template.push(block_close); + context.state.template.push( + ...serialize_inline_component(node, node.name, context).map((statement) => + t_statement(statement) + ) + ); }, SvelteSelf(node, context) { - const state = context.state; - state.template.push(block_open); - const call = serialize_inline_component(node, context.state.analysis.name, context); - state.template.push(t_statement(call)); - state.template.push(block_close); + context.state.template.push( + ...serialize_inline_component(node, context.state.analysis.name, context).map((statement) => + t_statement(statement) + ) + ); }, SvelteComponent(node, context) { - const state = context.state; - state.template.push(block_open); - const call = serialize_inline_component( - node, - /** @type {import('estree').Expression} */ (context.visit(node.expression)), - context + context.state.template.push( + ...serialize_inline_component( + node, + /** @type {import('estree').Expression} */ (context.visit(node.expression)), + context + ).map((statement) => t_statement(statement)) ); - state.template.push(t_statement(call)); - state.template.push(block_close); }, LetDirective(node, { state }) { if (node.expression && node.expression.type !== 'Identifier') { diff --git a/packages/svelte/src/internal/client/dom/blocks/css-props.js b/packages/svelte/src/internal/client/dom/blocks/css-props.js index 042b6aa9e58f..613718258fe3 100644 --- a/packages/svelte/src/internal/client/dom/blocks/css-props.js +++ b/packages/svelte/src/internal/client/dom/blocks/css-props.js @@ -1,16 +1,16 @@ import { namespace_svg } from '../../../../constants.js'; -import { hydrate_anchor, hydrate_start, hydrating } from '../hydration.js'; +import { hydrate_anchor, hydrating } from '../hydration.js'; import { empty } from '../operations.js'; import { render_effect } from '../../reactivity/effects.js'; /** - * @param {Element | Text | Comment} anchor + * @param {HTMLElement | SVGElement | Comment} node * @param {boolean} is_html * @param {() => Record} props * @param {(anchor: Element | Text | Comment) => any} component * @returns {void} */ -export function css_props(anchor, is_html, props, component) { +export function css_props(node, is_html, props, component) { /** @type {HTMLElement | SVGElement} */ let element; @@ -19,7 +19,8 @@ export function css_props(anchor, is_html, props, component) { if (hydrating) { // Hydration: css props element is surrounded by a ssr comment ... - element = /** @type {HTMLElement | SVGElement} */ (hydrate_start); + element = /** @type {HTMLElement | SVGElement} */ (node); + // ... and the child(ren) of the css props element is also surround by a ssr comment component_anchor = /** @type {Comment} */ ( hydrate_anchor(/** @type {Comment} */ (element.firstChild)) @@ -32,7 +33,7 @@ export function css_props(anchor, is_html, props, component) { element = document.createElementNS(namespace_svg, 'g'); } - anchor.before(element); + node.before(element); // TODO do we even need an anchor? Can we just pass in the correct element directly from the template? component_anchor = element.appendChild(empty()); } diff --git a/packages/svelte/src/internal/client/dom/blocks/each.js b/packages/svelte/src/internal/client/dom/blocks/each.js index 8c59e3aebab3..baf86a2c9516 100644 --- a/packages/svelte/src/internal/client/dom/blocks/each.js +++ b/packages/svelte/src/internal/client/dom/blocks/each.js @@ -10,13 +10,12 @@ import { } from '../../../../constants.js'; import { hydrate_anchor, - hydrate_nodes, hydrate_start, hydrating, + remove_hydrate_nodes, set_hydrating } from '../hydration.js'; import { clear_text_content, empty } from '../operations.js'; -import { remove } from '../reconciler.js'; import { untrack } from '../../runtime.js'; import { block, @@ -145,7 +144,7 @@ export function each(anchor, flags, get_collection, get_key, render_fn, fallback if (is_else !== (length === 0)) { // hydration mismatch — remove the server-rendered DOM and start over - remove(hydrate_nodes); + remove_hydrate_nodes(); set_hydrating(false); mismatch = true; } diff --git a/packages/svelte/src/internal/client/dom/blocks/html.js b/packages/svelte/src/internal/client/dom/blocks/html.js index 103bf7c046db..98fb91c41bfd 100644 --- a/packages/svelte/src/internal/client/dom/blocks/html.js +++ b/packages/svelte/src/internal/client/dom/blocks/html.js @@ -1,29 +1,9 @@ import { derived } from '../../reactivity/deriveds.js'; import { render_effect } from '../../reactivity/effects.js'; -import { current_effect, get } from '../../runtime.js'; -import { is_array } from '../../utils.js'; -import { hydrate_nodes, hydrating } from '../hydration.js'; -import { create_fragment_from_html, remove } from '../reconciler.js'; - -/** - * @param {import('#client').Effect} effect - * @param {(Element | Comment | Text)[]} to_remove - * @returns {void} - */ -function remove_from_parent_effect(effect, to_remove) { - const dom = effect.dom; - - if (is_array(dom)) { - for (let i = dom.length - 1; i >= 0; i--) { - if (to_remove.includes(dom[i])) { - dom.splice(i, 1); - break; - } - } - } else if (dom !== null && to_remove.includes(dom)) { - effect.dom = null; - } -} +import { get } from '../../runtime.js'; +import { hydrate_start, hydrating } from '../hydration.js'; +import { remove_nodes } from '../operations.js'; +import { create_fragment_from_html } from '../reconciler.js'; /** * @param {Element | Text | Comment} anchor @@ -33,20 +13,14 @@ function remove_from_parent_effect(effect, to_remove) { * @returns {void} */ export function html(anchor, get_value, svg, mathml) { - const parent_effect = anchor.parentNode !== current_effect?.dom ? current_effect : null; let value = derived(get_value); render_effect(() => { - var dom = html_to_dom(anchor, get(value), svg, mathml); + var [start, end] = html_to_dom(anchor, get(value), svg, mathml); - if (dom) { - return () => { - if (parent_effect !== null) { - remove_from_parent_effect(parent_effect, is_array(dom) ? dom : [dom]); - } - remove(dom); - }; - } + return () => { + remove_nodes(start, end); + }; }); } @@ -58,10 +32,12 @@ export function html(anchor, get_value, svg, mathml) { * @param {V} value * @param {boolean} svg * @param {boolean} mathml - * @returns {Element | Comment | (Element | Comment | Text)[]} + * @returns {[import('#client').TemplateNode, import('#client').TemplateNode]} */ function html_to_dom(target, value, svg, mathml) { - if (hydrating) return hydrate_nodes; + if (hydrating) { + return [hydrate_start, hydrate_start]; + } var html = value + ''; if (svg) html = `${html}`; @@ -79,10 +55,11 @@ function html_to_dom(target, value, svg, mathml) { if (node.childNodes.length === 1) { var child = /** @type {Text | Element | Comment} */ (node.firstChild); target.before(child); - return child; + return [child, child]; } - var nodes = /** @type {Array} */ ([...node.childNodes]); + var first = /** @type {import('#client').TemplateNode} */ (node.firstChild); + var last = /** @type {import('#client').TemplateNode} */ (node.lastChild); if (svg || mathml) { while (node.firstChild) { @@ -92,5 +69,5 @@ function html_to_dom(target, value, svg, mathml) { target.before(node); } - return nodes; + return [first, last]; } diff --git a/packages/svelte/src/internal/client/dom/blocks/if.js b/packages/svelte/src/internal/client/dom/blocks/if.js index 80ed8c09f4f6..41c26bbfd8b0 100644 --- a/packages/svelte/src/internal/client/dom/blocks/if.js +++ b/packages/svelte/src/internal/client/dom/blocks/if.js @@ -1,6 +1,5 @@ import { EFFECT_TRANSPARENT } from '../../constants.js'; -import { hydrate_nodes, hydrating, set_hydrating } from '../hydration.js'; -import { remove } from '../reconciler.js'; +import { hydrating, remove_hydrate_nodes, set_hydrating } from '../hydration.js'; import { block, branch, pause_effect, resume_effect } from '../../reactivity/effects.js'; import { HYDRATION_END_ELSE } from '../../../../constants.js'; @@ -42,7 +41,7 @@ export function if_block( if (condition === is_else) { // Hydration mismatch: remove everything inside the anchor and start fresh. // This could happen with `{#if browser}...{/if}`, for example - remove(hydrate_nodes); + remove_hydrate_nodes(); set_hydrating(false); mismatch = true; } 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 49d6ddeff7df..d01610d0f889 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 { hydrate_anchor, hydrating } from '../hydration.js'; import { empty } from '../operations.js'; import { block, @@ -14,7 +14,7 @@ import { current_component_context } from '../../runtime.js'; import { DEV } from 'esm-env'; /** - * @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, @@ -22,7 +22,7 @@ import { DEV } from 'esm-env'; * @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 filename = DEV && location && current_component_context?.function.filename; /** @type {string | null} */ @@ -32,7 +32,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; @@ -51,6 +53,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; @@ -79,7 +82,7 @@ export function element(anchor, get_tag, is_svg, render_fn, get_namespace, locat if (next_tag && next_tag !== current_tag) { effect = branch(() => { element = hydrating - ? /** @type {Element} */ (hydrate_start) + ? /** @type {Element} */ (element) : ns ? document.createElementNS(ns, next_tag) : document.createElement(next_tag); 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 b00a3a242b1e..e41dfc120b57 100644 --- a/packages/svelte/src/internal/client/dom/blocks/svelte-head.js +++ b/packages/svelte/src/internal/client/dom/blocks/svelte-head.js @@ -1,7 +1,7 @@ -import { hydrate_anchor, hydrate_nodes, hydrating, set_hydrate_nodes } from '../hydration.js'; +import { hydrate_anchor, hydrate_start, hydrating, set_hydrate_nodes } from '../hydration.js'; import { empty } from '../operations.js'; import { block } from '../../reactivity/effects.js'; -import { HYDRATION_END, HYDRATION_START } from '../../../../constants.js'; +import { HYDRATION_START } from '../../../../constants.js'; /** * @type {Node | undefined} @@ -19,14 +19,14 @@ export function reset_head_anchor() { 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 previous_hydrate_nodes = null; + let previous_hydrate_start = null; let was_hydrating = hydrating; /** @type {Comment | Text} */ var anchor; if (hydrating) { - previous_hydrate_nodes = hydrate_nodes; + previous_hydrate_start = hydrate_start; // There might be multiple head blocks in our app, so we need to account for each one needing independent hydration. if (head_anchor === undefined) { @@ -50,7 +50,7 @@ export function head(render_fn) { block(() => render_fn(anchor)); } finally { if (was_hydrating) { - set_hydrate_nodes(/** @type {import('#client').TemplateNode[]} */ (previous_hydrate_nodes)); + set_hydrate_nodes(/** @type {import('#client').TemplateNode} */ (previous_hydrate_start)); } } } diff --git a/packages/svelte/src/internal/client/dom/hydration.js b/packages/svelte/src/internal/client/dom/hydration.js index 06113ba784da..d51ae78ca362 100644 --- a/packages/svelte/src/internal/client/dom/hydration.js +++ b/packages/svelte/src/internal/client/dom/hydration.js @@ -1,6 +1,7 @@ import { DEV } from 'esm-env'; import { HYDRATION_END, HYDRATION_START, HYDRATION_ERROR } from '../../../constants.js'; import * as w from '../warnings.js'; +import { remove_nodes } from './operations.js'; /** * Use this variable to guard everything related to hydration code so it can be treeshaken out @@ -13,47 +14,36 @@ export function set_hydrating(value) { hydrating = value; } -/** - * 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('#client').TemplateNode[]} - */ -export let hydrate_nodes = /** @type {any} */ (null); - /** @type {import('#client').TemplateNode} */ -export let hydrate_start; +export let hydrate_start = /** @type {any} */ (null); -/** @param {import('#client').TemplateNode[]} nodes */ -export function set_hydrate_nodes(nodes) { - hydrate_nodes = nodes; - hydrate_start = nodes && nodes[0]; +/** + * @param {import('#client').TemplateNode} start + */ +export function set_hydrate_nodes(start) { + hydrate_start = start; } /** * This function is only called when `hydrating` is true. If passed a `` opening - * hydration marker, it finds the corresponding closing marker and sets `hydrate_nodes` - * to everything between the markers, before returning the closing marker. + * hydration marker, it sets `hydrate_start` to be the next node and returns the closing marker * @param {Node} node * @returns {Node} */ export function hydrate_anchor(node) { - if (node.nodeType !== 8) { - return node; - } - - var current = /** @type {Node | null} */ (node); - // TODO this could have false positives, if a user comment consisted of `[`. need to tighten that up - if (/** @type {Comment} */ (current).data !== HYDRATION_START) { + if (node.nodeType !== 8 || /** @type {Comment} */ (node).data !== HYDRATION_START) { return node; } - /** @type {Node[]} */ - var nodes = []; + hydrate_start = /** @type {import('#client').TemplateNode} */ ( + /** @type {Comment} */ (node).nextSibling + ); + + var current = hydrate_start; var depth = 0; - while ((current = /** @type {Node} */ (current).nextSibling) !== null) { + while (current !== null) { if (current.nodeType === 8) { var data = /** @type {Comment} */ (current).data; @@ -61,8 +51,6 @@ export function hydrate_anchor(node) { depth += 1; } else if (data[0] === HYDRATION_END) { if (depth === 0) { - hydrate_nodes = /** @type {import('#client').TemplateNode[]} */ (nodes); - hydrate_start = /** @type {import('#client').TemplateNode} */ (nodes[0]); return current; } @@ -70,7 +58,7 @@ export function hydrate_anchor(node) { } } - nodes.push(current); + current = /** @type {import('#client').TemplateNode} */ (current.nextSibling); } let location; @@ -86,3 +74,26 @@ export function hydrate_anchor(node) { w.hydration_mismatch(location); throw HYDRATION_ERROR; } + +export function remove_hydrate_nodes() { + /** @type {import('#client').TemplateNode | null} */ + var node = hydrate_start; + var depth = 0; + + while (node) { + if (node.nodeType === 8) { + var data = /** @type {Comment} */ (node).data; + + if (data === HYDRATION_START) { + depth += 1; + } else if (data[0] === HYDRATION_END) { + if (depth === 0) return; + depth -= 1; + } + } + + var next = /** @type {import('#client').TemplateNode | null} */ (node.nextSibling); + node.remove(); + node = next; + } +} diff --git a/packages/svelte/src/internal/client/dom/operations.js b/packages/svelte/src/internal/client/dom/operations.js index 508ea47388e1..0d3159a06359 100644 --- a/packages/svelte/src/internal/client/dom/operations.js +++ b/packages/svelte/src/internal/client/dom/operations.js @@ -1,7 +1,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_END } from '../../../constants.js'; // export these for reference in the compiled code, making global name deduplication unnecessary /** @type {Window} */ @@ -69,7 +69,7 @@ export function child(node) { } /** - * @param {DocumentFragment | import('#client').TemplateNode[]} fragment + * @param {DocumentFragment} fragment * @param {boolean} is_text * @returns {Node | null} */ @@ -83,14 +83,8 @@ export function first_child(fragment, is_text) { // if an {expression} is empty during SSR, there might be no // text node to hydrate — we must therefore create one if (is_text && hydrate_start?.nodeType !== 3) { - var text = empty(); - var dom = /** @type {import('#client').TemplateNode[]} */ ( - /** @type {import('#client').Effect} */ (current_effect).dom - ); - - dom.unshift(text); + const text = empty(); hydrate_start?.before(text); - return text; } @@ -105,23 +99,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; } + if (next_sibling.nodeType === 8 && /** @type {Comment} */ (next_sibling).data === '') { + 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) { - var text = empty(); - var dom = /** @type {import('#client').TemplateNode[]} */ ( - /** @type {import('#client').Effect} */ (current_effect).dom - ); - - dom.unshift(text); + const text = empty(); next_sibling?.before(text); - return text; } @@ -142,3 +134,21 @@ export function clear_text_content(node) { export function create_element(name) { return document.createElement(name); } + +/** + * Remove all nodes between `from` and `to`, inclusive + * @param {import('#client').TemplateNode} from + * @param {import('#client').TemplateNode} to + */ +export function remove_nodes(from, to) { + var node = from; + + while (node) { + var next = node.nextSibling; + + node.remove(); + if (node === to) break; + + node = /** @type {import('#client').TemplateNode} */ (next); + } +} diff --git a/packages/svelte/src/internal/client/dom/reconciler.js b/packages/svelte/src/internal/client/dom/reconciler.js index 5b9f246ed338..de9b2db49d83 100644 --- a/packages/svelte/src/internal/client/dom/reconciler.js +++ b/packages/svelte/src/internal/client/dom/reconciler.js @@ -6,19 +6,3 @@ export function create_fragment_from_html(html) { elem.innerHTML = html; return elem.content; } - -/** - * @param {import('#client').Dom} current - */ -export function remove(current) { - if (is_array(current)) { - for (var i = 0; i < current.length; i++) { - var node = current[i]; - if (node.isConnected) { - node.remove(); - } - } - } else if (current.isConnected) { - current.remove(); - } -} diff --git a/packages/svelte/src/internal/client/dom/template.js b/packages/svelte/src/internal/client/dom/template.js index 6458dc65cddf..73c4e98be9bb 100644 --- a/packages/svelte/src/internal/client/dom/template.js +++ b/packages/svelte/src/internal/client/dom/template.js @@ -1,4 +1,4 @@ -import { hydrate_nodes, hydrate_start, hydrating } from './hydration.js'; +import { hydrate_start, hydrating } from './hydration.js'; import { empty } from './operations.js'; import { create_fragment_from_html } from './reconciler.js'; import { current_effect } from '../runtime.js'; @@ -6,14 +6,14 @@ import { TEMPLATE_FRAGMENT, TEMPLATE_USE_IMPORT_NODE } from '../../../constants. import { effect } from '../reactivity/effects.js'; /** - * @template {import("#client").TemplateNode | import("#client").TemplateNode[]} T - * @param {T} dom + * @template {import("#client").TemplateNode} T + * @param {T} d1 */ -function push_template_node(dom) { +function push_template_node(d1) { var effect = /** @type {import('#client').Effect} */ (current_effect); - if (effect.dom === null) { - effect.dom = dom; + if (effect.d1 === null) { + effect.d1 = d1; } } @@ -32,7 +32,7 @@ export function template(content, flags) { return () => { if (hydrating) { - push_template_node(is_fragment ? hydrate_nodes : hydrate_start); + push_template_node(hydrate_start); return hydrate_start; } @@ -77,7 +77,6 @@ export function template_with_script(content, flags) { */ /*#__NO_SIDE_EFFECTS__*/ export function ns_template(content, flags, ns = 'svg') { - var is_fragment = (flags & TEMPLATE_FRAGMENT) !== 0; var fn = template(`<${ns}>${content}`, 0); // we don't need to worry about using importNode for namespaced elements /** @type {Element | DocumentFragment} */ @@ -85,7 +84,7 @@ export function ns_template(content, flags, ns = 'svg') { return () => { if (hydrating) { - push_template_node(is_fragment ? hydrate_nodes : hydrate_start); + push_template_node(hydrate_start); return hydrate_start; } @@ -182,7 +181,7 @@ export function text(anchor) { var node = hydrate_start; if (!node) { - // if an {expression} is empty during SSR, `hydrate_nodes` will be empty. + // if an {expression} is empty during SSR, `hydrate_start` will be missing. // we need to insert an empty text node anchor.before((node = empty())); } @@ -194,7 +193,7 @@ export function text(anchor) { export function comment() { // we're not delegating to `template` here for performance reasons if (hydrating) { - push_template_node(hydrate_nodes); + push_template_node(hydrate_start); return hydrate_start; } @@ -212,14 +211,21 @@ export function comment() { * @param {DocumentFragment | Element} dom */ export function append(anchor, dom) { - if (hydrating) return; - var effect = /** @type {import('#client').Effect} */ (current_effect); - effect.dom = - dom.nodeType === 11 - ? /** @type {import('#client').TemplateNode[]} */ ([...dom.childNodes]) - : /** @type {Element | Comment} */ (dom); + if (!hydrating) { + if (dom.nodeType === 11) { + // prepend an empty text node + var d1 = empty(); + + /** @type {import('#client').TemplateNode} */ (dom.firstChild).before(d1); + effect.d1 = d1; + } else { + effect.d1 = /** @type {Element} */ (dom); + } + + anchor.before(/** @type {Node} */ (dom)); + } - anchor.before(/** @type {Node} */ (dom)); + effect.d2 = anchor?.previousSibling; } diff --git a/packages/svelte/src/internal/client/reactivity/effects.js b/packages/svelte/src/internal/client/reactivity/effects.js index 6411e8109af7..0554271fc44b 100644 --- a/packages/svelte/src/internal/client/reactivity/effects.js +++ b/packages/svelte/src/internal/client/reactivity/effects.js @@ -33,10 +33,10 @@ import { UNOWNED } from '../constants.js'; import { set } from './sources.js'; -import { remove } from '../dom/reconciler.js'; import * as e from '../errors.js'; import { DEV } from 'esm-env'; import { define_property } from '../utils.js'; +import { remove_nodes } from '../dom/operations.js'; /** * @param {'$effect' | '$effect.pre' | '$inspect'} rune @@ -79,7 +79,8 @@ function create_effect(type, fn, sync) { var effect = { ctx: current_component_context, deps: null, - dom: null, + d1: null, + d2: null, f: type | DIRTY, first: null, fn, @@ -314,10 +315,11 @@ export function execute_effect_teardown(effect) { * @returns {void} */ export function destroy_effect(effect) { - var dom = effect.dom; + var d1 = effect.d1; + var d2 = effect.d2; - if (dom !== null) { - remove(dom); + if (d1 !== null && d2 !== null) { + remove_nodes(d1, d2); } destroy_effect_children(effect); @@ -360,7 +362,8 @@ export function destroy_effect(effect) { effect.prev = effect.teardown = effect.ctx = - effect.dom = + effect.d1 = + effect.d2 = effect.deps = effect.parent = // @ts-expect-error diff --git a/packages/svelte/src/internal/client/reactivity/types.d.ts b/packages/svelte/src/internal/client/reactivity/types.d.ts index 7502443bd10c..eef3c3a2e3e2 100644 --- a/packages/svelte/src/internal/client/reactivity/types.d.ts +++ b/packages/svelte/src/internal/client/reactivity/types.d.ts @@ -1,4 +1,4 @@ -import type { ComponentContext, Dom, Equals, TransitionManager } from '#client'; +import type { ComponentContext, Dom, Equals, TemplateNode, TransitionManager } from '#client'; export interface Signal { /** Flags bitmask */ @@ -36,7 +36,8 @@ export interface Derived extends Value, Reaction { export interface Effect extends Reaction { parent: Effect | null; - dom: Dom | null; + d1: TemplateNode | null; + d2: TemplateNode | null; /** The associated component context */ ctx: null | ComponentContext; /** The effect function */ diff --git a/packages/svelte/src/internal/client/render.js b/packages/svelte/src/internal/client/render.js index 101ba6f1afd3..2117113c555d 100644 --- a/packages/svelte/src/internal/client/render.js +++ b/packages/svelte/src/internal/client/render.js @@ -5,7 +5,7 @@ import { flush_sync, push, pop, current_component_context } from './runtime.js'; import { effect_root, branch } from './reactivity/effects.js'; import { hydrate_anchor, - hydrate_nodes, + hydrate_start, hydrating, set_hydrate_nodes, set_hydrating @@ -129,7 +129,7 @@ export function hydrate(component, options) { } const target = options.target; - const previous_hydrate_nodes = hydrate_nodes; + const previous_hydrate_start = hydrate_start; try { // Don't flush previous effects to ensure order of outer effects stays consistent @@ -173,8 +173,8 @@ export function hydrate(component, options) { throw error; } finally { - set_hydrating(!!previous_hydrate_nodes); - set_hydrate_nodes(previous_hydrate_nodes); + set_hydrating(!!previous_hydrate_start); + set_hydrate_nodes(previous_hydrate_start); reset_head_anchor(); } } diff --git a/packages/svelte/src/internal/server/index.js b/packages/svelte/src/internal/server/index.js index bf9d5d1c57b8..c480b8055d7e 100644 --- a/packages/svelte/src/internal/server/index.js +++ b/packages/svelte/src/internal/server/index.js @@ -171,15 +171,15 @@ export function attr(name, value, boolean) { export function css_props(payload, is_html, props, component) { const styles = style_object_to_string(props); if (is_html) { - payload.out += `
`; + payload.out += `
`; } else { - payload.out += ``; + payload.out += ``; } component(); if (is_html) { - payload.out += `
`; + payload.out += `
`; } else { - payload.out += ``; + 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 3eca221c361a..48da62b4de5a 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/bind-component-snippet/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/bind-component-snippet/_expected/server/index.svelte.js index c4d2da79d1cd..ebb55389af5e 100644 --- a/packages/svelte/tests/snapshot/samples/bind-component-snippet/_expected/server/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/bind-component-snippet/_expected/server/index.svelte.js @@ -15,7 +15,7 @@ export default function Bind_component_snippet($$payload, $$props) { let $$inner_payload; function $$render_inner($$payload) { - $$payload.out += ``; + $$payload.out += ""; TextInput($$payload, { get value() { @@ -27,7 +27,8 @@ export default function Bind_component_snippet($$payload, $$props) { } }); - $$payload.out += ` value: ${$.escape(value)}`; + $$payload.out += ""; + $$payload.out += ` value: ${$.escape(value)}`; }; do { diff --git a/packages/svelte/tests/snapshot/samples/bind-this/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/bind-this/_expected/server/index.svelte.js index 9f00d97ad4b0..825b750e1064 100644 --- a/packages/svelte/tests/snapshot/samples/bind-this/_expected/server/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/bind-this/_expected/server/index.svelte.js @@ -2,8 +2,8 @@ import * as $ from "svelte/internal/server"; export default function Bind_this($$payload, $$props) { $.push(); - $$payload.out += ``; + $$payload.out += ""; Foo($$payload, {}); - $$payload.out += ``; + $$payload.out += ""; $.pop(); } \ No newline at end of file diff --git a/packages/svelte/tests/snapshot/samples/function-prop-no-getter/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/function-prop-no-getter/_expected/server/index.svelte.js index b096c4b6c0d7..7bb70ec5cf5f 100644 --- a/packages/svelte/tests/snapshot/samples/function-prop-no-getter/_expected/server/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/function-prop-no-getter/_expected/server/index.svelte.js @@ -11,7 +11,7 @@ export default function Function_prop_no_getter($$payload, $$props) { const plusOne = (num) => num + 1; - $$payload.out += ``; + $$payload.out += ""; Button($$payload, { onmousedown: () => count += 1, @@ -23,6 +23,6 @@ export default function Function_prop_no_getter($$payload, $$props) { $$slots: { default: true } }); - $$payload.out += ``; + $$payload.out += ""; $.pop(); } \ No newline at end of file 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 2cf8edd1ee9f..3be11f26105e 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 @@ -5,8 +5,7 @@ export default function Svelte_element($$payload, $$props) { let { tag = 'hr' } = $$props; - $$payload.out += ``; if (tag) $.element($$payload, tag, () => {}, () => {}); - $$payload.out += ``; + $$payload.out += ``; $.pop(); } \ No newline at end of file