switch to $state.frozen

pull/9851/head
Dominic Gannaway 3 years ago
parent da6ac07226
commit 3077662a77

@ -2,4 +2,4 @@
'svelte': patch
---
feat: add $state.readonly rune
feat: add $state.frozen rune

@ -585,7 +585,7 @@ const legacy_scope_tweaker = {
);
if (
binding.kind === 'state' ||
binding.kind === 'readonly_state' ||
binding.kind === 'frozen_state' ||
(binding.kind === 'normal' && binding.declaration_kind === 'let')
) {
binding.kind = 'prop';
@ -643,13 +643,13 @@ const runes_scope_js_tweaker = {
const callee = node.init.callee;
if (callee.type !== 'Identifier' && callee.type !== 'MemberExpression') return;
if (rune !== '$state' && rune !== '$state.readonly' && rune !== '$derived') return;
if (rune !== '$state' && rune !== '$state.frozen' && rune !== '$derived') return;
for (const path of extract_paths(node.id)) {
// @ts-ignore this fails in CI for some insane reason
const binding = /** @type {import('#compiler').Binding} */ (state.scope.get(path.node.name));
binding.kind =
rune === '$state' ? 'state' : rune === '$state.readonly' ? 'readonly_state' : 'derived';
rune === '$state' ? 'state' : rune === '$state.frozen' ? 'frozen_state' : 'derived';
}
}
};
@ -673,7 +673,7 @@ const runes_scope_tweaker = {
const callee = init.callee;
if (callee.type !== 'Identifier' && callee.type !== 'MemberExpression') return;
if (rune !== '$state' && rune !== '$state.readonly' && rune !== '$derived' && rune !== '$props')
if (rune !== '$state' && rune !== '$state.frozen' && rune !== '$derived' && rune !== '$props')
return;
for (const path of extract_paths(node.id)) {
@ -682,8 +682,8 @@ const runes_scope_tweaker = {
binding.kind =
rune === '$state'
? 'state'
: rune === '$state.readonly'
? 'readonly_state'
: rune === '$state.frozen'
? 'frozen_state'
: rune === '$derived'
? 'derived'
: path.is_rest
@ -904,7 +904,7 @@ const common_visitors = {
if (
node !== binding.node &&
(binding.kind === 'state' ||
binding.kind === 'readonly_state' ||
binding.kind === 'frozen_state' ||
binding.kind === 'derived') &&
context.state.function_depth === binding.scope.function_depth
) {

@ -349,7 +349,7 @@ export const validation = {
if (
!binding ||
(binding.kind !== 'state' &&
binding.kind !== 'readonly_state' &&
binding.kind !== 'frozen_state' &&
binding.kind !== 'prop' &&
binding.kind !== 'each' &&
binding.kind !== 'store_sub' &&
@ -662,7 +662,7 @@ function validate_export(node, scope, name) {
error(node, 'invalid-derived-export');
}
if ((binding.kind === 'state' || binding.kind === 'readonly_state') && binding.reassigned) {
if ((binding.kind === 'state' || binding.kind === 'frozen_state') && binding.reassigned) {
error(node, 'invalid-state-export');
}
}
@ -837,7 +837,7 @@ function validate_no_const_assignment(node, argument, scope, is_binding) {
// This takes advantage of the fact that we don't assign initial for let directives and then/catch variables.
// If we start doing that, we need another property on the binding to differentiate, or give up on the more precise error message.
binding.kind !== 'state' &&
binding.kind !== 'readonly_state' &&
binding.kind !== 'frozen_state' &&
(binding.kind !== 'normal' || !binding.initial)
);
}

@ -233,7 +233,7 @@ export function client_component(source, analysis, options) {
'$.bind_prop',
b.id('$$props'),
b.literal(alias ?? name),
binding?.kind === 'state' || binding?.kind === 'readonly_state'
binding?.kind === 'state' || binding?.kind === 'frozen_state'
? b.call('$.get', b.id(name))
: b.id(name)
)
@ -243,7 +243,7 @@ export function client_component(source, analysis, options) {
const properties = analysis.exports.map(({ name, alias }) => {
const binding = analysis.instance.scope.get(name);
const is_source =
(binding?.kind === 'state' || binding?.kind === 'readonly_state') &&
(binding?.kind === 'state' || binding?.kind === 'frozen_state') &&
(!state.analysis.immutable || binding.reassigned);
// TODO This is always a getter because the `renamed-instance-exports` test wants it that way.

@ -59,7 +59,7 @@ export interface ComponentClientTransformState extends ClientTransformState {
}
export interface StateField {
kind: 'state' | 'readonly_state' | 'derived';
kind: 'state' | 'frozen_state' | 'derived';
id: PrivateIdentifier;
}

@ -92,7 +92,7 @@ export function serialize_get_binding(node, state) {
}
if (
((binding.kind === 'state' || binding.kind === 'readonly_state') &&
((binding.kind === 'state' || binding.kind === 'frozen_state') &&
(!state.analysis.immutable || state.analysis.accessors || binding.reassigned)) ||
binding.kind === 'derived' ||
binding.kind === 'legacy_reactive'
@ -172,7 +172,7 @@ export function serialize_set_binding(node, context, fallback) {
const assignment = fallback();
if (assignment.type === 'AssignmentExpression') {
assignment.right =
private_state.kind === 'readonly_state'
private_state.kind === 'frozen_state'
? b.call('$.freeze', value)
: b.call('$.proxy', value);
return assignment;
@ -183,7 +183,7 @@ export function serialize_set_binding(node, context, fallback) {
'$.set',
left,
context.state.analysis.runes && should_proxy_or_freeze(value)
? private_state.kind === 'readonly_state'
? private_state.kind === 'frozen_state'
? b.call('$.freeze', value)
: b.call('$.proxy', value)
: value
@ -206,7 +206,7 @@ export function serialize_set_binding(node, context, fallback) {
const assignment = fallback();
if (assignment.type === 'AssignmentExpression') {
assignment.right =
public_state.kind === 'readonly_state'
public_state.kind === 'frozen_state'
? b.call('$.freeze', value)
: b.call('$.proxy', value);
return assignment;
@ -245,7 +245,7 @@ export function serialize_set_binding(node, context, fallback) {
if (
binding.kind !== 'state' &&
binding.kind !== 'readonly_state' &&
binding.kind !== 'frozen_state' &&
binding.kind !== 'prop' &&
binding.kind !== 'each' &&
binding.kind !== 'legacy_reactive' &&
@ -271,7 +271,7 @@ export function serialize_set_binding(node, context, fallback) {
? b.call('$.proxy', value)
: value
);
} else if (binding.kind === 'readonly_state') {
} else if (binding.kind === 'frozen_state') {
return b.call(
'$.set',
b.id(left_name),

@ -49,7 +49,7 @@ export const global_visitors = {
// use runtime functions for smaller output
if (
binding?.kind === 'state' ||
binding?.kind === 'readonly_state' ||
binding?.kind === 'frozen_state' ||
binding?.kind === 'each' ||
binding?.kind === 'legacy_reactive' ||
binding?.kind === 'prop' ||

@ -29,15 +29,11 @@ export const javascript_visitors_runes = {
if (definition.value?.type === 'CallExpression') {
const rune = get_rune(definition.value, state.scope);
if (rune === '$state' || rune === '$state.readonly' || rune === '$derived') {
if (rune === '$state' || rune === '$state.frozen' || rune === '$derived') {
/** @type {import('../types.js').StateField} */
const field = {
kind:
rune === '$state'
? 'state'
: rune === '$state.readonly'
? 'readonly_state'
: 'derived',
rune === '$state' ? 'state' : rune === '$state.frozen' ? 'frozen_state' : 'derived',
// @ts-expect-error this is set in the next pass
id: is_private ? definition.key : null
};
@ -90,7 +86,7 @@ export const javascript_visitors_runes = {
value =
field.kind === 'state'
? b.call('$.source', should_proxy_or_freeze(init) ? b.call('$.proxy', init) : init)
: field.kind === 'readonly_state'
: field.kind === 'frozen_state'
? b.call('$.source', should_proxy_or_freeze(init) ? b.call('$.freeze', init) : init)
: b.call('$.derived', b.thunk(init));
} else {
@ -121,7 +117,7 @@ export const javascript_visitors_runes = {
);
}
if (field.kind === 'readonly_state') {
if (field.kind === 'frozen_state') {
// set foo(value) { this.#foo = value; }
const value = b.id('value');
body.push(
@ -244,7 +240,7 @@ export const javascript_visitors_runes = {
if (!state.analysis.immutable || state.analysis.accessors || binding.reassigned) {
value = b.call('$.source', value);
}
} else if (rune === '$state.readonly') {
} else if (rune === '$state.frozen') {
const binding = /** @type {import('#compiler').Binding} */ (
state.scope.get(declarator.id.name)
);

@ -1245,7 +1245,7 @@ function serialize_event_handler(node, { state, visit }) {
if (
binding !== null &&
(binding.kind === 'state' ||
binding.kind === 'readonly_state' ||
binding.kind === 'frozen_state' ||
binding.kind === 'legacy_reactive' ||
binding.kind === 'derived' ||
binding.kind === 'prop' ||

@ -446,7 +446,7 @@ function serialize_set_binding(node, context, fallback) {
if (
binding.kind !== 'state' &&
binding.kind !== 'readonly_state' &&
binding.kind !== 'frozen_state' &&
binding.kind !== 'prop' &&
binding.kind !== 'each' &&
binding.kind !== 'legacy_reactive' &&
@ -559,7 +559,7 @@ const javascript_visitors_runes = {
if (node.value != null && node.value.type === 'CallExpression') {
const rune = get_rune(node.value, state.scope);
if (rune === '$state' || rune === '$state.readonly' || rune === '$derived') {
if (rune === '$state' || rune === '$state.frozen' || rune === '$derived') {
return {
...node,
value:

@ -72,7 +72,7 @@ export const ElementBindings = [
export const Runes = /** @type {const} */ ([
'$state',
'$state.readonly',
'$state.frozen',
'$props',
'$derived',
'$effect',

@ -258,7 +258,7 @@ export interface Binding {
| 'prop'
| 'rest_prop'
| 'state'
| 'readonly_state'
| 'frozen_state'
| 'derived'
| 'each'
| 'store_sub'

@ -19,12 +19,12 @@ declare function $state<T>(): T | undefined;
declare namespace $state {
/**
* Declares reactive read-only state.
* Declares reactive read-only state that is shallowly immutable.
*
* Example:
* ```ts
* <script>
* let items = $state.readonly([0]);
* let items = $state.frozen([0]);
*
* const addItem = () => {
* items = [...items, items.length];
@ -40,8 +40,8 @@ declare namespace $state {
*
* @param initial The initial value
*/
export function readonly<T>(initial: T): Readonly<T>;
export function readonly<T>(): Readonly<T> | undefined;
export function frozen<T>(initial: T): Readonly<T>;
export function frozen<T>(): Readonly<T> | undefined;
}
/**

@ -2,7 +2,7 @@
import { log } from './log.js';
class Counter {
count = $state.readonly({ a: 0 });
count = $state.frozen({ a: 0 });
}
const counter = new Counter();
</script>

@ -1,6 +1,6 @@
<script>
class Counter {
count = $state.readonly(0);
count = $state.frozen(0);
}
const counter = new Counter();
</script>

@ -2,7 +2,7 @@
import { log } from './log.js';
class Counter {
#count = $state.readonly();
#count = $state.frozen();
constructor(initial_count) {
this.#count = { a: initial_count };

@ -1,6 +1,6 @@
<script>
class Counter {
#count = $state.readonly(0);
#count = $state.frozen(0);
constructor(initial_count) {
this.#count = initial_count;

@ -1,5 +1,5 @@
<script>
let items = $state.readonly([0]);
let items = $state.frozen([0]);
const addItem = () => {
items = [...items, items.length];

@ -1,8 +1,8 @@
<script>
import { log } from './log.js';
let x = $state.readonly(0);
let y = $state.readonly(0);
let x = $state.frozen(0);
let y = $state.frozen(0);
$effect(() => {
log.push(x);

@ -214,7 +214,7 @@
type: 'keyword',
boost: 5
}),
{ label: '$state.readonly', type: 'keyword', boost: 4 },
{ label: '$state.frozen', type: 'keyword', boost: 4 },
snip('$effect.root(() => {\n\t${}\n});', {
label: '$effect.root',
type: 'keyword',

@ -64,15 +64,15 @@ Objects and arrays [are made reactive](/#H4sIAAAAAAAAE42QwWrDMBBEf2URhUhUNEl7c21
In non-runes mode, a `let` declaration is treated as reactive state if it is updated at some point. Unlike `$state(...)`, which works anywhere in your app, `let` only behaves this way at the top level of a component.
## `$state.readonly`
## `$state.frozen`
Similar to `$state`, `$state.readonly` is also declared and can be used in many of the same ways (including on classes). However, the properties of any object and arrays are treated as read-only, and cannot be mutated. So if you intend to use objects as state and you want to mutate their properties and have reactivity work by default, it's recommended you use `$state` instead.
Similar to `$state`, `$state.frozen` is also declared and can be used in many of the same ways (including on classes). However, the properties of any object and arrays are treated as read-only, and cannot be mutated. So if you intend to use objects as state and you want to mutate their properties and have reactivity work by default, it's recommended you use `$state` instead.
For the cases where you don't want Svelte's reactivity to apply deeply to state, and for those who might want to have more control over their data structures, you might find `$state.readonly` useful. Furthermore, `$state.readonly` is ideal for those who want to work with data using immutable patterns rather than mutable patterns.
For the cases where you don't want Svelte's reactivity to apply deeply to state, and for those who might want to have more control over their data structures, you might find `$state` useful. Furthermore, `$state.frozen` is ideal for those who want to work with data using immutable patterns rather than mutable patterns.
```svelte
<script>
let items = $state.readonly([0]);
let items = $state.frozen([0]);
const addItem = () => {
items = [...items, items.length];
@ -84,7 +84,7 @@ For the cases where you don't want Svelte's reactivity to apply deeply to state,
</button>
```
> Objects and arrays passed to `$state.readonly` will be shallowly frozen using `Object.freeze()`.
> Objects and arrays passed to `$state.frozen` will be shallowly frozen using `Object.freeze()`.
## `$derived`

Loading…
Cancel
Save