From 48bfd817ca15f97cd650a0fb0a97c0ab5c42e15c Mon Sep 17 00:00:00 2001 From: paoloricciuti Date: Mon, 9 Jun 2025 20:22:58 +0200 Subject: [PATCH] fix: properly hydrate dynamic css props components and remove element removal --- .changeset/twelve-foxes-smell.md | 5 ++++ .../3-transform/client/visitors/Component.js | 29 +++++-------------- .../client/visitors/shared/component.js | 17 +++++++---- .../internal/client/dom/blocks/css-props.js | 4 --- .../css-props-dynamic-component/A.svelte | 7 +++++ .../css-props-dynamic-component/B.svelte | 7 +++++ .../css-props-dynamic-component/_config.js | 16 ++++++++++ .../css-props-dynamic-component/main.svelte | 11 +++++++ 8 files changed, 66 insertions(+), 30 deletions(-) create mode 100644 .changeset/twelve-foxes-smell.md create mode 100644 packages/svelte/tests/runtime-browser/samples/css-props-dynamic-component/A.svelte create mode 100644 packages/svelte/tests/runtime-browser/samples/css-props-dynamic-component/B.svelte create mode 100644 packages/svelte/tests/runtime-browser/samples/css-props-dynamic-component/_config.js create mode 100644 packages/svelte/tests/runtime-browser/samples/css-props-dynamic-component/main.svelte diff --git a/.changeset/twelve-foxes-smell.md b/.changeset/twelve-foxes-smell.md new file mode 100644 index 000000000000..1f3be888d856 --- /dev/null +++ b/.changeset/twelve-foxes-smell.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: properly hydrate dynamic css props components and remove element removal diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/Component.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/Component.js index 783bc38e3c45..d58a24b45559 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/Component.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/Component.js @@ -1,7 +1,6 @@ -/** @import { Expression } from 'estree' */ /** @import { AST } from '#compiler' */ /** @import { ComponentContext } from '../types' */ -import * as b from '#compiler/builders'; +import { regex_is_valid_identifier } from '../../../patterns.js'; import { build_component } from './shared/component.js'; /** @@ -9,24 +8,12 @@ import { build_component } from './shared/component.js'; * @param {ComponentContext} context */ export function Component(node, context) { - if (node.metadata.dynamic) { - // Handle dynamic references to what seems like static inline components - const component = build_component(node, '$$component', context, b.id('$$anchor')); - context.state.init.push( - b.stmt( - b.call( - '$.component', - context.state.node, - // TODO use untrack here to not update when binding changes? - // Would align with Svelte 4 behavior, but it's arguably nicer/expected to update this - b.thunk(/** @type {Expression} */ (context.visit(b.member_id(node.name)))), - b.arrow([b.id('$$anchor'), b.id('$$component')], b.block([component])) - ) - ) - ); - return; - } - - const component = build_component(node, node.name, context); + const component = build_component( + node, + // if it's not dynamic we will just use the node name, if it is dynamic we will use the node name + // only if it's a valid identifier, otherwise we will use a default name + !node.metadata.dynamic || regex_is_valid_identifier.test(node.name) ? node.name : '$$component', + context + ); context.state.init.push(component); } diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js index ff98d6d3787b..a1c4025d6023 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js @@ -13,10 +13,13 @@ import { determine_slot } from '../../../../../utils/slot.js'; * @param {AST.Component | AST.SvelteComponent | AST.SvelteSelf} node * @param {string} component_name * @param {ComponentContext} context - * @param {Expression} anchor * @returns {Statement} */ -export function build_component(node, component_name, context, anchor = context.state.node) { +export function build_component(node, component_name, context) { + /** + * @type {Expression} + */ + const anchor = context.state.node; /** @type {Array} */ const props_and_spreads = []; /** @type {Array<() => void>} */ @@ -411,7 +414,7 @@ export function build_component(node, component_name, context, anchor = context. // TODO We can remove this ternary once we remove legacy mode, since in runes mode dynamic components // will be handled separately through the `$.component` function, and then the component name will // always be referenced through just the identifier here. - node.type === 'SvelteComponent' + node.type === 'SvelteComponent' || (node.type === 'Component' && node.metadata.dynamic) ? component_name : /** @type {Expression} */ (context.visit(b.member_id(component_name))), node_id, @@ -429,14 +432,18 @@ export function build_component(node, component_name, context, anchor = context. const statements = [...snippet_declarations]; - if (node.type === 'SvelteComponent') { + if (node.type === 'SvelteComponent' || (node.type === 'Component' && node.metadata.dynamic)) { const prev = fn; fn = (node_id) => { return b.call( '$.component', node_id, - b.thunk(/** @type {Expression} */ (context.visit(node.expression))), + b.thunk( + /** @type {Expression} */ ( + context.visit(node.type === 'Component' ? b.member_id(node.name) : node.expression) + ) + ), b.arrow( [b.id('$$anchor'), b.id(component_name)], b.block([...binding_initializers, b.stmt(prev(b.id('$$anchor')))]) 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 ecbcfd3e8301..ef361987539f 100644 --- a/packages/svelte/src/internal/client/dom/blocks/css-props.js +++ b/packages/svelte/src/internal/client/dom/blocks/css-props.js @@ -26,8 +26,4 @@ export function css_props(element, get_styles) { } } }); - - teardown(() => { - element.remove(); - }); } diff --git a/packages/svelte/tests/runtime-browser/samples/css-props-dynamic-component/A.svelte b/packages/svelte/tests/runtime-browser/samples/css-props-dynamic-component/A.svelte new file mode 100644 index 000000000000..694b26f231c6 --- /dev/null +++ b/packages/svelte/tests/runtime-browser/samples/css-props-dynamic-component/A.svelte @@ -0,0 +1,7 @@ +
a
+ + \ No newline at end of file diff --git a/packages/svelte/tests/runtime-browser/samples/css-props-dynamic-component/B.svelte b/packages/svelte/tests/runtime-browser/samples/css-props-dynamic-component/B.svelte new file mode 100644 index 000000000000..06f28c4f75df --- /dev/null +++ b/packages/svelte/tests/runtime-browser/samples/css-props-dynamic-component/B.svelte @@ -0,0 +1,7 @@ +
b
+ + \ No newline at end of file diff --git a/packages/svelte/tests/runtime-browser/samples/css-props-dynamic-component/_config.js b/packages/svelte/tests/runtime-browser/samples/css-props-dynamic-component/_config.js new file mode 100644 index 000000000000..762963383532 --- /dev/null +++ b/packages/svelte/tests/runtime-browser/samples/css-props-dynamic-component/_config.js @@ -0,0 +1,16 @@ +import { test } from '../../assert'; +import { flushSync } from 'svelte'; + +export default test({ + warnings: [], + async test({ assert, target }) { + const btn = target.querySelector('button'); + let div = /** @type {HTMLElement} */ (target.querySelector('div')); + assert.equal(getComputedStyle(div).color, 'rgb(255, 0, 0)'); + flushSync(() => { + btn?.click(); + }); + div = /** @type {HTMLElement} */ (target.querySelector('div')); + assert.equal(getComputedStyle(div).color, 'rgb(255, 0, 0)'); + } +}); diff --git a/packages/svelte/tests/runtime-browser/samples/css-props-dynamic-component/main.svelte b/packages/svelte/tests/runtime-browser/samples/css-props-dynamic-component/main.svelte new file mode 100644 index 000000000000..055ce57da51a --- /dev/null +++ b/packages/svelte/tests/runtime-browser/samples/css-props-dynamic-component/main.svelte @@ -0,0 +1,11 @@ + + + + + \ No newline at end of file