fix: handle deep assignments to `$state()` class properties correctly (#10289)

fixes #10276
pull/10291/head
Simon H 2 years ago committed by GitHub
parent c8da99646a
commit 107ec1c848
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: handle deep assignments to `$state()` class properties correctly

@ -1,5 +1,10 @@
import * as b from '../../../utils/builders.js'; 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 { error } from '../../../errors.js';
import { import {
PROPS_IS_LAZY_INITIAL, 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}`); error(node, 'INTERNAL', `Unexpected assignment type ${assignee.type}`);
} }
let left = assignee;
// Handle class private/public state assignment cases // Handle class private/public state assignment cases
while (left.type === 'MemberExpression') { if (assignee.type === 'MemberExpression') {
if (left.object.type === 'ThisExpression' && left.property.type === 'PrivateIdentifier') { if (
const private_state = context.state.private_state.get(left.property.name); 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); const value = get_assignment_value(node, context);
if (private_state !== undefined) { if (private_state !== undefined) {
if (state.in_constructor) { if (state.in_constructor) {
@ -307,7 +313,7 @@ export function serialize_set_binding(node, context, fallback, options) {
} else { } else {
return b.call( return b.call(
'$.set', '$.set',
left, assignee,
context.state.analysis.runes && context.state.analysis.runes &&
!options?.skip_proxy_and_freeze && !options?.skip_proxy_and_freeze &&
should_proxy_or_freeze(value, context.state.scope) should_proxy_or_freeze(value, context.state.scope)
@ -319,11 +325,11 @@ export function serialize_set_binding(node, context, fallback, options) {
} }
} }
} else if ( } else if (
left.object.type === 'ThisExpression' && assignee.object.type === 'ThisExpression' &&
left.property.type === 'Identifier' && assignee.property.type === 'Identifier' &&
state.in_constructor 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); const value = get_assignment_value(node, context);
// See if we should wrap value in $.proxy // See if we should wrap value in $.proxy
if ( 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(); return fallback();
} }

@ -0,0 +1,15 @@
import { test } from '../../test';
export default test({
html: `<button>0 / 0</button><button>0 / 0</button>`,
async test({ assert, target }) {
const [btn1, btn2] = target.querySelectorAll('button');
await btn1?.click();
assert.htmlEqual(target.innerHTML, `<button>1 / 0</button><button>1 / 0</button>`);
await btn2?.click();
assert.htmlEqual(target.innerHTML, `<button>2 / 1</button><button>2 / 1</button>`);
}
});

@ -0,0 +1,24 @@
<script>
class Counter {
container = $state({ count: -1 });
#private = $state({ count: -1 });
constructor(initial_count) {
this.container.count = initial_count;
this.#private.count = initial_count;
}
increment() {
this.container.count += 1;
this.#private.count += 1;
}
get private_count() {
return this.#private.count;
}
}
const counter = new Counter(0);
</script>
<button on:click={() => counter.container.count++}>{counter.container.count} / {counter.private_count}</button>
<button on:click={() => counter.increment()}>{counter.container.count} / {counter.private_count}</button>
Loading…
Cancel
Save