diff --git a/.changeset/fresh-beds-wash.md b/.changeset/fresh-beds-wash.md
new file mode 100644
index 0000000000..a0cb72d402
--- /dev/null
+++ b/.changeset/fresh-beds-wash.md
@@ -0,0 +1,5 @@
+---
+"svelte": patch
+---
+
+fix: use coarse-grained updates for derived expressions passed to props in legacy mode
diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js
index e6f80d0000..4cc0c836bf 100644
--- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js
+++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js
@@ -632,6 +632,15 @@ function collect_parent_each_blocks(context) {
);
}
+/**
+ * Svelte legacy mode should use safe equals in most places, runes mode shouldn't
+ * @param {import('../types.js').ComponentClientTransformState} state
+ * @param {import('estree').Expression} arg
+ */
+function create_derived(state, arg) {
+ return b.call(state.analysis.runes ? '$.derived' : '$.derived_safe_equal', arg);
+}
+
/**
* @param {import('#compiler').Component | import('#compiler').SvelteComponent | import('#compiler').SvelteSelf} node
* @param {string} component_name
@@ -745,7 +754,7 @@ function serialize_inline_component(node, component_name, context) {
if (should_wrap_in_derived) {
const id = b.id(context.state.scope.generate(attribute.name));
- context.state.init.push(b.var(id, b.call('$.derived', b.thunk(value))));
+ context.state.init.push(b.var(id, create_derived(context.state, b.thunk(value))));
arg = b.call('$.get', id);
}
@@ -1649,9 +1658,8 @@ function serialize_template_literal(values, visit, state) {
state.init.push(
b.const(
id,
- b.call(
- // In runes mode, we want things to be fine-grained - but not in legacy mode
- state.analysis.runes ? '$.derived' : '$.derived_safe_equal',
+ create_derived(
+ state,
b.thunk(/** @type {import('estree').Expression} */ (visit(node.expression)))
)
)
@@ -1701,9 +1709,8 @@ export const template_visitors = {
state.init.push(
b.const(
declaration.id,
- b.call(
- // In runes mode, we want things to be fine-grained - but not in legacy mode
- state.analysis.runes ? '$.derived' : '$.derived_safe_equal',
+ create_derived(
+ state,
b.thunk(/** @type {import('estree').Expression} */ (visit(declaration.init)))
)
)
@@ -1738,10 +1745,7 @@ export const template_visitors = {
])
);
- state.init.push(
- // In runes mode, we want things to be fine-grained - but not in legacy mode
- b.const(tmp, b.call(state.analysis.runes ? '$.derived' : '$.derived_safe_equal', fn))
- );
+ state.init.push(b.const(tmp, create_derived(state, fn)));
// we need to eagerly evaluate the expression in order to hit any
// 'Cannot access x before initialization' errors
@@ -2995,12 +2999,7 @@ export const template_visitors = {
const name = node.expression === null ? node.name : node.expression.name;
return b.const(
name,
- b.call(
- // in legacy mode, sources can be mutated but they're not fine-grained.
- // Using the safe-equal derived version ensures the slot is still updated
- state.analysis.runes ? '$.derived' : '$.derived_safe_equal',
- b.thunk(b.member(b.id('$$slotProps'), b.id(node.name)))
- )
+ create_derived(state, b.thunk(b.member(b.id('$$slotProps'), b.id(node.name))))
);
}
},
diff --git a/packages/svelte/tests/runtime-legacy/samples/component-prop-object/Child.svelte b/packages/svelte/tests/runtime-legacy/samples/component-prop-object/Child.svelte
new file mode 100644
index 0000000000..fd191fa695
--- /dev/null
+++ b/packages/svelte/tests/runtime-legacy/samples/component-prop-object/Child.svelte
@@ -0,0 +1,5 @@
+
+
+child: {x.y}
\ No newline at end of file
diff --git a/packages/svelte/tests/runtime-legacy/samples/component-prop-object/_config.js b/packages/svelte/tests/runtime-legacy/samples/component-prop-object/_config.js
new file mode 100644
index 0000000000..d37a72a2ee
--- /dev/null
+++ b/packages/svelte/tests/runtime-legacy/samples/component-prop-object/_config.js
@@ -0,0 +1,10 @@
+import { test } from '../../test';
+
+export default test({
+ html: `child: 0 parent: 0 `,
+
+ async test({ assert, target }) {
+ await target.querySelector('button')?.click();
+ assert.htmlEqual(target.innerHTML, `child: 1 parent: 1 `);
+ }
+});
diff --git a/packages/svelte/tests/runtime-legacy/samples/component-prop-object/main.svelte b/packages/svelte/tests/runtime-legacy/samples/component-prop-object/main.svelte
new file mode 100644
index 0000000000..82aa5864e6
--- /dev/null
+++ b/packages/svelte/tests/runtime-legacy/samples/component-prop-object/main.svelte
@@ -0,0 +1,11 @@
+
+
+
+
+parent: {x.y}
+
\ No newline at end of file