Skip to content

Commit 524d229

Browse files
committed
add support for class fields
1 parent b05dbbf commit 524d229

File tree

7 files changed

+117
-12
lines changed

7 files changed

+117
-12
lines changed

documentation/docs/98-reference/.generated/compile-errors.md

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -828,10 +828,35 @@ Cannot export state from a module if it is reassigned. Either export a function
828828
`%rune%(...)` can only be used as a variable declaration initializer or a class field
829829
```
830830

831+
### state_invalidate_invalid_this_property
832+
833+
```
834+
`$state.invalidate` can only be called with an argument referencing `this` in a class using a non-computed property
835+
```
836+
837+
Like how you can't use `$state` or `$derived` when declaring computed class fields, you can't use `$state.invalidate` to invalidate a computed class field. For example, while `count` here is not itself a computed property, you can't invalidate it if you reference it in a computed property:
838+
```js
839+
class Box {
840+
value;
841+
constructor(initial) {
842+
this.value = initial;
843+
}
844+
}
845+
const property = 'count';
846+
class Counter {
847+
count = $state(new Box(0));
848+
increment() {
849+
this.count.value += 1;
850+
$state.invalidate(this[property]); // this doesn't work
851+
$state.invalidate(this.count); // this works
852+
}
853+
}
854+
```
855+
831856
### state_invalidate_nonreactive_argument
832857

833858
```
834-
`$state.invalidate` only takes a variable declared with `$state` or `$state.raw` as its argument
859+
`$state.invalidate` only takes a variable or non-computed class field declared with `$state` or `$state.raw` as its argument
835860
```
836861

837862
### store_invalid_scoped_subscription

packages/svelte/messages/compile-errors/script.md

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -220,9 +220,32 @@ It's possible to export a snippet from a `<script module>` block, but only if it
220220

221221
> `%rune%(...)` can only be used as a variable declaration initializer or a class field
222222
223+
## state_invalidate_invalid_this_property
224+
225+
> `$state.invalidate` can only be called with an argument referencing `this` in a class using a non-computed property
226+
227+
Like how you can't use `$state` or `$derived` when declaring computed class fields, you can't use `$state.invalidate` to invalidate a computed class field. For example, while `count` here is not itself a computed property, you can't invalidate it if you reference it in a computed property:
228+
```js
229+
class Box {
230+
value;
231+
constructor(initial) {
232+
this.value = initial;
233+
}
234+
}
235+
const property = 'count';
236+
class Counter {
237+
count = $state(new Box(0));
238+
increment() {
239+
this.count.value += 1;
240+
$state.invalidate(this[property]); // this doesn't work
241+
$state.invalidate(this.count); // this works
242+
}
243+
}
244+
```
245+
223246
## state_invalidate_nonreactive_argument
224247

225-
> `$state.invalidate` only takes a variable declared with `$state` or `$state.raw` as its argument
248+
> `$state.invalidate` only takes a variable or non-computed class field declared with `$state` or `$state.raw` as its argument
226249
227250
## store_invalid_scoped_subscription
228251

packages/svelte/src/ambient.d.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ declare namespace $state {
9494
: never;
9595

9696
/**
97-
* Forces an update on a `$state` or `$state.raw` variable.
97+
* Forces an update on a `$state` or `$state.raw` variable or class field.
9898
* This is primarily meant as an escape hatch to be able to use external or native classes
9999
* with Svelte's reactivity system.
100100
* If you used Svelte 3 or 4, this is the equivalent of `foo = foo`.

packages/svelte/src/compiler/errors.js

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -481,12 +481,21 @@ export function state_invalid_placement(node, rune) {
481481
}
482482

483483
/**
484-
* `$state.invalidate` only takes a variable declared with `$state` or `$state.raw` as its argument
484+
* `$state.invalidate` can only be called with an argument referencing `this` in a class using a non-computed property
485+
* @param {null | number | NodeLike} node
486+
* @returns {never}
487+
*/
488+
export function state_invalidate_invalid_this_property(node) {
489+
e(node, 'state_invalidate_invalid_this_property', `\`$state.invalidate\` can only be called with an argument referencing \`this\` in a class using a non-computed property\nhttps://svelte.dev/e/state_invalidate_invalid_this_property`);
490+
}
491+
492+
/**
493+
* `$state.invalidate` only takes a variable or non-computed class field declared with `$state` or `$state.raw` as its argument
485494
* @param {null | number | NodeLike} node
486495
* @returns {never}
487496
*/
488497
export function state_invalidate_nonreactive_argument(node) {
489-
e(node, 'state_invalidate_nonreactive_argument', `\`$state.invalidate\` only takes a variable declared with \`$state\` or \`$state.raw\` as its argument\nhttps://svelte.dev/e/state_invalidate_nonreactive_argument`);
498+
e(node, 'state_invalidate_nonreactive_argument', `\`$state.invalidate\` only takes a variable or non-computed class field declared with \`$state\` or \`$state.raw\` as its argument\nhttps://svelte.dev/e/state_invalidate_nonreactive_argument`);
490499
}
491500

492501
/**

packages/svelte/src/compiler/phases/2-analyze/visitors/CallExpression.js

Lines changed: 35 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -116,15 +116,45 @@ export function CallExpression(node, context) {
116116
e.rune_invalid_arguments_length(node, rune, 'exactly one argument');
117117
} else {
118118
let arg = node.arguments[0];
119-
if (arg.type !== 'Identifier') {
119+
if (arg.type !== 'Identifier' && arg.type !== 'MemberExpression') {
120120
e.state_invalidate_nonreactive_argument(node);
121121
}
122-
let binding = context.state.scope.get(arg.name);
123-
if (binding) {
124-
if (binding.kind === 'raw_state' || binding.kind === 'state') {
125-
binding.reassigned = true;
122+
if (arg.type === 'MemberExpression') {
123+
if (arg.object.type !== 'ThisExpression') {
124+
e.state_invalidate_nonreactive_argument(node);
125+
}
126+
const class_body = context.path.findLast((parent) => parent.type === 'ClassBody');
127+
if (arg.computed || !class_body) {
128+
e.state_invalidate_invalid_this_property(node);
129+
}
130+
const possible_this_bindings = context.path.filter((parent, index) => {
131+
return (
132+
parent.type === 'FunctionDeclaration' ||
133+
(parent.type === 'FunctionExpression' &&
134+
context.path[index - 1]?.type !== 'MethodDefinition')
135+
);
136+
});
137+
if (possible_this_bindings.length === 0) {
126138
break;
127139
}
140+
const class_index = context.path.indexOf(class_body);
141+
const last_possible_this_index = context.path.indexOf(
142+
/** @type {AST.SvelteNode} */ (possible_this_bindings.at(-1))
143+
);
144+
if (class_index < last_possible_this_index) {
145+
e.state_invalidate_invalid_this_property(node);
146+
}
147+
// we can't really do anything else yet, so we just wait for the transformation phase
148+
// where we know which class fields are reactive (and what their private aliases are)
149+
break;
150+
} else {
151+
let binding = context.state.scope.get(arg.name);
152+
if (binding) {
153+
if (binding.kind === 'raw_state' || binding.kind === 'state') {
154+
binding.reassigned = true;
155+
break;
156+
}
157+
}
128158
}
129159
e.state_invalidate_nonreactive_argument(node);
130160
}

packages/svelte/src/compiler/phases/3-transform/client/visitors/CallExpression.js

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { dev, is_ignored } from '../../../../state.js';
44
import * as b from '../../../../utils/builders.js';
55
import { get_rune } from '../../../scope.js';
66
import { transform_inspect_rune } from '../../utils.js';
7+
import * as e from '../../../../errors.js';
78

89
/**
910
* @param {CallExpression} node
@@ -24,7 +25,24 @@ export function CallExpression(node, context) {
2425
is_ignored(node, 'state_snapshot_uncloneable') && b.true
2526
);
2627
case '$state.invalidate':
27-
return b.call('$.invalidate', node.arguments[0]);
28+
if (node.arguments[0].type === 'Identifier') {
29+
return b.call('$.invalidate', node.arguments[0]);
30+
} else if (node.arguments[0].type === 'MemberExpression') {
31+
const { property } = node.arguments[0];
32+
let field;
33+
switch (property.type) {
34+
case 'Identifier':
35+
field = context.state.public_state.get(property.name);
36+
break;
37+
case 'PrivateIdentifier':
38+
field = context.state.private_state.get(property.name);
39+
break;
40+
}
41+
if (!field || (field.kind !== 'state' && field.kind !== 'raw_state')) {
42+
e.state_invalidate_nonreactive_argument(node);
43+
}
44+
return b.call('$.invalidate', b.member(b.this, field.id));
45+
}
2846

2947
case '$effect.root':
3048
return b.call(

packages/svelte/types/index.d.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2761,7 +2761,7 @@ declare namespace $state {
27612761
: never;
27622762

27632763
/**
2764-
* Forces an update on a `$state` or `$state.raw` variable.
2764+
* Forces an update on a `$state` or `$state.raw` variable or class field.
27652765
* This is primarily meant as an escape hatch to be able to use external or native classes
27662766
* with Svelte's reactivity system.
27672767
* If you used Svelte 3 or 4, this is the equivalent of `foo = foo`.

0 commit comments

Comments
 (0)