diff --git a/.changeset/kind-rings-flash.md b/.changeset/kind-rings-flash.md
new file mode 100644
index 0000000000..525a5dd54b
--- /dev/null
+++ b/.changeset/kind-rings-flash.md
@@ -0,0 +1,5 @@
+---
+"svelte": patch
+---
+
+fix: handle deep assignments to `$state()` class properties correctly
diff --git a/packages/svelte/src/compiler/phases/3-transform/client/utils.js b/packages/svelte/src/compiler/phases/3-transform/client/utils.js
index 49ca618ada..3b4d5a6f85 100644
--- a/packages/svelte/src/compiler/phases/3-transform/client/utils.js
+++ b/packages/svelte/src/compiler/phases/3-transform/client/utils.js
@@ -1,5 +1,10 @@
import * as b from '../../../utils/builders.js';
-import { extract_paths, is_simple_expression, unwrap_ts_expression } from '../../../utils/ast.js';
+import {
+ extract_paths,
+ is_simple_expression,
+ object,
+ unwrap_ts_expression
+} from '../../../utils/ast.js';
import { error } from '../../../errors.js';
import {
PROPS_IS_LAZY_INITIAL,
@@ -280,12 +285,13 @@ export function serialize_set_binding(node, context, fallback, options) {
error(node, 'INTERNAL', `Unexpected assignment type ${assignee.type}`);
}
- let left = assignee;
-
// Handle class private/public state assignment cases
- while (left.type === 'MemberExpression') {
- if (left.object.type === 'ThisExpression' && left.property.type === 'PrivateIdentifier') {
- const private_state = context.state.private_state.get(left.property.name);
+ if (assignee.type === 'MemberExpression') {
+ if (
+ assignee.object.type === 'ThisExpression' &&
+ assignee.property.type === 'PrivateIdentifier'
+ ) {
+ const private_state = context.state.private_state.get(assignee.property.name);
const value = get_assignment_value(node, context);
if (private_state !== undefined) {
if (state.in_constructor) {
@@ -307,7 +313,7 @@ export function serialize_set_binding(node, context, fallback, options) {
} else {
return b.call(
'$.set',
- left,
+ assignee,
context.state.analysis.runes &&
!options?.skip_proxy_and_freeze &&
should_proxy_or_freeze(value, context.state.scope)
@@ -319,11 +325,11 @@ export function serialize_set_binding(node, context, fallback, options) {
}
}
} else if (
- left.object.type === 'ThisExpression' &&
- left.property.type === 'Identifier' &&
+ assignee.object.type === 'ThisExpression' &&
+ assignee.property.type === 'Identifier' &&
state.in_constructor
) {
- const public_state = context.state.public_state.get(left.property.name);
+ const public_state = context.state.public_state.get(assignee.property.name);
const value = get_assignment_value(node, context);
// See if we should wrap value in $.proxy
if (
@@ -342,11 +348,11 @@ export function serialize_set_binding(node, context, fallback, options) {
}
}
}
- // @ts-expect-error
- left = unwrap_ts_expression(left.object);
}
- if (left.type !== 'Identifier') {
+ const left = object(assignee);
+
+ if (left === null) {
return fallback();
}
diff --git a/packages/svelte/tests/runtime-runes/samples/class-state-deep-update/_config.js b/packages/svelte/tests/runtime-runes/samples/class-state-deep-update/_config.js
new file mode 100644
index 0000000000..e9ef53f95f
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/class-state-deep-update/_config.js
@@ -0,0 +1,15 @@
+import { test } from '../../test';
+
+export default test({
+ html: ``,
+
+ async test({ assert, target }) {
+ const [btn1, btn2] = target.querySelectorAll('button');
+
+ await btn1?.click();
+ assert.htmlEqual(target.innerHTML, ``);
+
+ await btn2?.click();
+ assert.htmlEqual(target.innerHTML, ``);
+ }
+});
diff --git a/packages/svelte/tests/runtime-runes/samples/class-state-deep-update/main.svelte b/packages/svelte/tests/runtime-runes/samples/class-state-deep-update/main.svelte
new file mode 100644
index 0000000000..22f814174b
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/class-state-deep-update/main.svelte
@@ -0,0 +1,24 @@
+
+
+
+