Skip to content

Commit 3e222fc

Browse files
committed
feat: add state.opqaue rune
1 parent 1a0b822 commit 3e222fc

File tree

18 files changed

+222
-12
lines changed

18 files changed

+222
-12
lines changed

.changeset/large-papayas-serve.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'svelte': minor
3+
---
4+
5+
feat: add $state.opqaue rune

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -748,6 +748,12 @@ This snippet is shadowing the prop `%prop%` with the same name
748748
Cannot export state from a module if it is reassigned. Either export a function returning the state value or only mutate the state value's properties
749749
```
750750

751+
### state_invalid_opaque_declaration
752+
753+
```
754+
`$state.opaque` must be declared with an array destructuring pattern (e.g. `let [state, invalidate] = $state.opaque(data);`)
755+
```
756+
751757
### state_invalid_placement
752758

753759
```

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,10 @@ It's possible to export a snippet from a `<script module>` block, but only if it
168168

169169
> Cannot export state from a module if it is reassigned. Either export a function returning the state value or only mutate the state value's properties
170170
171+
## state_invalid_opaque_declaration
172+
173+
> `$state.opaque` must be declared with an array destructuring pattern (e.g. `let [state, invalidate] = $state.opaque(data);`)
174+
171175
## state_invalid_placement
172176

173177
> `%rune%(...)` can only be used as a variable declaration initializer or a class field

packages/svelte/src/ambient.d.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,35 @@ declare namespace $state {
118118
*/
119119
export function raw<T>(initial: T): T;
120120
export function raw<T>(): T | undefined;
121+
122+
/**
123+
* Declares state that is _not_ known to Svelte and thus is completely opaque to
124+
* reassignments and mutations. To let Svelte know that the value has changed,
125+
* you must invoke its invalidate function manually.
126+
*
127+
* Example:
128+
* ```ts
129+
* <script>
130+
* let [items, invalidate] = $state.opaque([0]);
131+
*
132+
* const addItem = () => {
133+
* items.push(items.length);
134+
* invalidate();
135+
* };
136+
* </script>
137+
*
138+
* <button on:click={addItem}>
139+
* {items.join(', ')}
140+
* </button>
141+
* ```
142+
*
143+
* https://svelte.dev/docs/svelte/$state#$state.opaque
144+
*
145+
* @param initial The initial value
146+
*/
147+
export function opaque<T>(initial: T): [T, () => void];
148+
export function opaque<T>(): [T | undefined, () => void];
149+
121150
/**
122151
* To take a static snapshot of a deeply reactive `$state` proxy, use `$state.snapshot`:
123152
*

packages/svelte/src/compiler/errors.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -432,6 +432,15 @@ export function state_invalid_export(node) {
432432
e(node, "state_invalid_export", "Cannot export state from a module if it is reassigned. Either export a function returning the state value or only mutate the state value's properties");
433433
}
434434

435+
/**
436+
* `$state.opaque` must be declared with an array destructuring pattern (e.g. `let [state, invalidate] = $state.opaque(data);`)
437+
* @param {null | number | NodeLike} node
438+
* @returns {never}
439+
*/
440+
export function state_invalid_opaque_declaration(node) {
441+
e(node, "state_invalid_opaque_declaration", "`$state.opaque` must be declared with an array destructuring pattern (e.g. `let [state, invalidate] = $state.opaque(data);`)");
442+
}
443+
435444
/**
436445
* `%rune%(...)` can only be used as a variable declaration initializer or a class field
437446
* @param {null | number | NodeLike} node

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

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ export function CallExpression(node, context) {
7575

7676
case '$state':
7777
case '$state.raw':
78+
case '$state.opaque':
7879
case '$derived':
7980
case '$derived.by':
8081
if (
@@ -86,9 +87,21 @@ export function CallExpression(node, context) {
8687

8788
if ((rune === '$derived' || rune === '$derived.by') && node.arguments.length !== 1) {
8889
e.rune_invalid_arguments_length(node, rune, 'exactly one argument');
89-
} else if (rune === '$state' && node.arguments.length > 1) {
90+
} else if (
91+
(rune === '$state' || rune === '$state.raw' || rune === '$state.opaque') &&
92+
node.arguments.length > 1
93+
) {
9094
e.rune_invalid_arguments_length(node, rune, 'zero or one arguments');
9195
}
96+
if (
97+
rune === '$state.opaque' &&
98+
(parent.type !== 'VariableDeclarator' ||
99+
parent.id.type !== 'ArrayPattern' ||
100+
parent.id.elements.length !== 2 ||
101+
parent.id.elements[0]?.type !== 'Identifier')
102+
) {
103+
e.state_invalid_opaque_declaration(node);
104+
}
92105

93106
break;
94107

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

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -27,23 +27,29 @@ export function VariableDeclarator(node, context) {
2727
if (
2828
rune === '$state' ||
2929
rune === '$state.raw' ||
30+
rune === '$state.opaque' ||
3031
rune === '$derived' ||
3132
rune === '$derived.by' ||
3233
rune === '$props'
3334
) {
34-
for (const path of paths) {
35+
for (let i = 0; i < paths.length; i++) {
36+
if (rune === '$state.opaque' && i === 1) continue;
37+
38+
const path = paths[i];
3539
// @ts-ignore this fails in CI for some insane reason
3640
const binding = /** @type {Binding} */ (context.state.scope.get(path.node.name));
3741
binding.kind =
3842
rune === '$state'
3943
? 'state'
4044
: rune === '$state.raw'
4145
? 'raw_state'
42-
: rune === '$derived' || rune === '$derived.by'
43-
? 'derived'
44-
: path.is_rest
45-
? 'rest_prop'
46-
: 'prop';
46+
: rune === '$state.opaque'
47+
? 'opaque_state'
48+
: rune === '$derived' || rune === '$derived.by'
49+
? 'derived'
50+
: path.is_rest
51+
? 'rest_prop'
52+
: 'prop';
4753
}
4854
}
4955

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

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
/** @import { CallExpression, Expression, Identifier, Literal, VariableDeclaration, VariableDeclarator } from 'estree' */
1+
/** @import { ArrayPattern, CallExpression, Expression, Identifier, Literal, VariableDeclaration, VariableDeclarator } from 'estree' */
22
/** @import { Binding } from '#compiler' */
33
/** @import { ComponentClientTransformState, ComponentContext } from '../types' */
44
import { dev } from '../../../../state.js';
@@ -156,6 +156,20 @@ export function VariableDeclaration(node, context) {
156156
continue;
157157
}
158158

159+
if (rune === '$state.opaque') {
160+
const pattern = /** @type {ArrayPattern} */ (declarator.id);
161+
const state_id = /** @type {Identifier} */ (pattern.elements[0]);
162+
const invalidation_id = /** @type {Identifier} */ (pattern.elements[1]);
163+
declarations.push(
164+
b.declarator(state_id, b.call('$.opaque_state', value)),
165+
b.declarator(
166+
invalidation_id,
167+
b.thunk(b.call('$.set', state_id, b.member(state_id, b.id('v'))))
168+
)
169+
);
170+
continue;
171+
}
172+
159173
if (rune === '$derived' || rune === '$derived.by') {
160174
if (declarator.id.type === 'Identifier') {
161175
declarations.push(

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

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
/** @import { Identifier } from 'estree' */
1+
/** @import { Expression, Identifier } from 'estree' */
22
/** @import { ComponentContext, Context } from '../../types' */
33
import { is_state_source } from '../../utils.js';
44
import * as b from '../../../../../utils/builders.js';
@@ -48,6 +48,16 @@ export function add_state_transformers(context) {
4848
);
4949
}
5050
};
51+
} else if (binding.kind === 'opaque_state') {
52+
context.state.transform[name] = {
53+
read: binding.declaration_kind === 'var' ? (node) => b.call('$.safe_get', node) : get_value,
54+
assign: (node, value) => {
55+
return b.assignment('=', b.member(node, b.id('v')), /** @type {Expression} */ (value));
56+
},
57+
update: (node) => {
58+
return b.update(node.operator, b.member(node.argument, b.id('v')), node.prefix);
59+
}
60+
};
5161
}
5262
}
5363
}

packages/svelte/src/compiler/phases/3-transform/server/visitors/VariableDeclaration.js

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
/** @import { VariableDeclaration, VariableDeclarator, Expression, CallExpression, Pattern, Identifier } from 'estree' */
1+
/** @import { ArrayPattern, VariableDeclaration, VariableDeclarator, Expression, CallExpression, Pattern, Identifier } from 'estree' */
22
/** @import { Binding } from '#compiler' */
33
/** @import { Context } from '../types.js' */
44
/** @import { Scope } from '../../../scope.js' */
@@ -92,6 +92,17 @@ export function VariableDeclaration(node, context) {
9292
continue;
9393
}
9494

95+
if (rune === '$state.opaque') {
96+
const pattern = /** @type {ArrayPattern} */ (declarator.id);
97+
const state_id = /** @type {Identifier} */ (pattern.elements[0]);
98+
const invalidation_id = /** @type {Identifier} */ (pattern.elements[1]);
99+
declarations.push(
100+
b.declarator(state_id, value),
101+
b.declarator(invalidation_id, b.thunk(b.block([])))
102+
);
103+
continue;
104+
}
105+
95106
declarations.push(...create_state_declarators(declarator, context.state.scope, value));
96107
}
97108
} else {

0 commit comments

Comments
 (0)