Skip to content

breaking: replace $state.frozen with $state.raw #12808

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 17 commits into from
Aug 12, 2024
Merged
5 changes: 5 additions & 0 deletions .changeset/four-yaks-boil.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'svelte': patch
---

breaking: replace `$state.frozen` with `$state.raw`
4 changes: 0 additions & 4 deletions packages/svelte/messages/client-errors/errors.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,10 +64,6 @@

> The `%rune%` rune is only available inside `.svelte` and `.svelte.js/ts` files

## state_frozen_invalid_argument

> The argument to `$state.frozen(...)` cannot be an object created with `$state(...)`. You should create a copy of it first, for example with `$state.snapshot`

## state_prototype_fixed

> Cannot set prototype of `$state` object
Expand Down
9 changes: 5 additions & 4 deletions packages/svelte/src/ambient.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,12 +102,13 @@ declare namespace $state {
: never;

/**
* Declares reactive read-only state that is shallowly immutable.
* Declares state that is _not_ made deeply reactive — instead of mutating it,
* you must reassign it.
*
* Example:
* ```ts
* <script>
* let items = $state.frozen([0]);
* let items = $state.raw([0]);
*
* const addItem = () => {
* items = [...items, items.length];
Expand All @@ -123,8 +124,8 @@ declare namespace $state {
*
* @param initial The initial value
*/
export function frozen<T>(initial: T): Readonly<T>;
export function frozen<T>(): Readonly<T> | undefined;
export function raw<T>(initial: T): T;
export function raw<T>(): T | undefined;
/**
* To take a static snapshot of a deeply reactive `$state` proxy, use `$state.snapshot`:
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ export function BindDirective(node, context) {
node.name !== 'this' && // bind:this also works for regular variables
(!binding ||
(binding.kind !== 'state' &&
binding.kind !== 'frozen_state' &&
binding.kind !== 'raw_state' &&
binding.kind !== 'prop' &&
binding.kind !== 'bindable_prop' &&
binding.kind !== 'each' &&
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ export function CallExpression(node, context) {
break;

case '$state':
case '$state.frozen':
case '$state.raw':
case '$derived':
case '$derived.by':
if (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export function ExportNamedDeclaration(node, context) {
e.derived_invalid_export(node);
}

if ((binding.kind === 'state' || binding.kind === 'frozen_state') && binding.reassigned) {
if ((binding.kind === 'state' || binding.kind === 'raw_state') && binding.reassigned) {
e.state_invalid_export(node);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export function ExportSpecifier(node, context) {
if (
binding !== null &&
(binding.kind === 'state' ||
binding.kind === 'frozen_state' ||
binding.kind === 'raw_state' ||
(binding.kind === 'normal' &&
(binding.declaration_kind === 'let' || binding.declaration_kind === 'var')))
) {
Expand Down Expand Up @@ -60,7 +60,7 @@ function validate_export(node, scope, name) {
e.derived_invalid_export(node);
}

if ((binding.kind === 'state' || binding.kind === 'frozen_state') && binding.reassigned) {
if ((binding.kind === 'state' || binding.kind === 'raw_state') && binding.reassigned) {
e.state_invalid_export(node);
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/** @import { Expression, Identifier } from 'estree' */
/** @import { Context } from '../types' */
import is_reference from 'is-reference';
import { should_proxy_or_freeze } from '../../3-transform/client/utils.js';
import { should_proxy } from '../../3-transform/client/utils.js';
import * as e from '../../../errors.js';
import * as w from '../../../warnings.js';
import { is_rune } from '../../../../utils.js';
Expand Down Expand Up @@ -53,6 +53,10 @@ export function Identifier(node, context) {
e.rune_renamed(parent, '$effect.active', '$effect.tracking');
}

if (name === '$state.frozen') {
e.rune_renamed(parent, '$state.frozen', '$state.raw');
}

e.rune_invalid_name(parent, name);
}
}
Expand Down Expand Up @@ -132,8 +136,8 @@ export function Identifier(node, context) {
(binding.initial?.type === 'CallExpression' &&
binding.initial.arguments.length === 1 &&
binding.initial.arguments[0].type !== 'SpreadElement' &&
!should_proxy_or_freeze(binding.initial.arguments[0], context.state.scope)))) ||
binding.kind === 'frozen_state' ||
!should_proxy(binding.initial.arguments[0], context.state.scope)))) ||
binding.kind === 'raw_state' ||
binding.kind === 'derived') &&
// We're only concerned with reads here
(parent.type !== 'AssignmentExpression' || parent.left !== node) &&
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export function VariableDeclarator(node, context) {
// TODO feels like this should happen during scope creation?
if (
rune === '$state' ||
rune === '$state.frozen' ||
rune === '$state.raw' ||
rune === '$derived' ||
rune === '$derived.by' ||
rune === '$props'
Expand All @@ -32,8 +32,8 @@ export function VariableDeclarator(node, context) {
binding.kind =
rune === '$state'
? 'state'
: rune === '$state.frozen'
? 'frozen_state'
: rune === '$state.raw'
? 'raw_state'
: rune === '$derived' || rune === '$derived.by'
? 'derived'
: path.is_rest
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ export 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 !== 'frozen_state' &&
// binding.kind !== 'raw_state' &&
// (binding.kind !== 'normal' || !binding.initial)
// );

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -271,19 +271,9 @@ export function client_component(analysis, options) {
}
}

if (binding?.kind === 'state' || binding?.kind === 'frozen_state') {
return [
getter,
b.set(alias ?? name, [
b.stmt(
b.call(
'$.set',
b.id(name),
b.call(binding.kind === 'state' ? '$.proxy' : '$.freeze', b.id('$$value'))
)
)
])
];
if (binding?.kind === 'state' || binding?.kind === 'raw_state') {
const value = binding.kind === 'state' ? b.call('$.proxy', b.id('$$value')) : b.id('$$value');
return [getter, b.set(alias ?? name, [b.stmt(b.call('$.set', b.id(name), value))])];
}

return getter;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ export interface ComponentClientTransformState extends ClientTransformState {
}

export interface StateField {
kind: 'state' | 'frozen_state' | 'derived' | 'derived_by';
kind: 'state' | 'raw_state' | 'derived' | 'derived_by';
id: PrivateIdentifier;
}

Expand Down
13 changes: 9 additions & 4 deletions packages/svelte/src/compiler/phases/3-transform/client/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ import {
PROPS_IS_LAZY_INITIAL,
PROPS_IS_IMMUTABLE,
PROPS_IS_RUNES,
PROPS_IS_UPDATED
PROPS_IS_UPDATED,
PROPS_IS_BINDABLE
} from '../../../../constants.js';
import { dev } from '../../../state.js';
import { get_value } from './visitors/shared/declarations.js';
Expand All @@ -20,7 +21,7 @@ import { get_value } from './visitors/shared/declarations.js';
*/
export function is_state_source(binding, state) {
return (
(binding.kind === 'state' || binding.kind === 'frozen_state') &&
(binding.kind === 'state' || binding.kind === 'raw_state') &&
(!state.analysis.immutable || binding.reassigned || state.analysis.accessors)
);
}
Expand Down Expand Up @@ -168,6 +169,10 @@ export function get_prop_source(binding, state, name, initial) {

let flags = 0;

if (binding.kind === 'bindable_prop') {
flags |= PROPS_IS_BINDABLE;
}

if (state.analysis.immutable) {
flags |= PROPS_IS_IMMUTABLE;
}
Expand Down Expand Up @@ -238,7 +243,7 @@ export function is_prop_source(binding, state) {
* @param {Expression} node
* @param {Scope | null} scope
*/
export function should_proxy_or_freeze(node, scope) {
export function should_proxy(node, scope) {
if (
!node ||
node.type === 'Literal' ||
Expand All @@ -263,7 +268,7 @@ export function should_proxy_or_freeze(node, scope) {
binding.initial.type !== 'ImportDeclaration' &&
binding.initial.type !== 'EachBlock'
) {
return should_proxy_or_freeze(binding.initial, null);
return should_proxy(binding.initial, null);
}
}
return true;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import * as b from '../../../../utils/builders.js';
import { build_assignment_value } from '../../../../utils/ast.js';
import { is_ignored } from '../../../../state.js';
import { build_proxy_reassignment, should_proxy_or_freeze } from '../utils.js';
import { build_proxy_reassignment, should_proxy } from '../utils.js';
import { visit_assignment_expression } from '../../shared/assignments.js';

/**
Expand Down Expand Up @@ -37,11 +37,11 @@ export function build_assignment(operator, left, right, context) {
context.visit(build_assignment_value(operator, left, right))
);

if (should_proxy_or_freeze(value, context.state.scope)) {
if (should_proxy(value, context.state.scope)) {
transformed = true;
value =
private_state.kind === 'frozen_state'
? b.call('$.freeze', value)
private_state.kind === 'raw_state'
? value
: build_proxy_reassignment(value, private_state.id);
}

Expand All @@ -54,14 +54,14 @@ export function build_assignment(operator, left, right, context) {
} else if (left.property.type === 'Identifier' && context.state.in_constructor) {
const public_state = context.state.public_state.get(left.property.name);

if (public_state !== undefined && should_proxy_or_freeze(right, context.state.scope)) {
if (public_state !== undefined && should_proxy(right, context.state.scope)) {
const value = /** @type {Expression} */ (context.visit(right));

return b.assignment(
operator,
/** @type {Pattern} */ (context.visit(left)),
public_state.kind === 'frozen_state'
? b.call('$.freeze', value)
public_state.kind === 'raw_state'
? value
: build_proxy_reassignment(value, public_state.id)
);
}
Expand Down Expand Up @@ -99,13 +99,11 @@ export function build_assignment(operator, left, right, context) {
if (
!is_primitive &&
binding.kind !== 'prop' &&
binding.kind !== 'bindable_prop' &&
context.state.analysis.runes &&
should_proxy_or_freeze(value, context.state.scope)
should_proxy(value, context.state.scope)
) {
value =
binding.kind === 'frozen_state'
? b.call('$.freeze', value)
: build_proxy_reassignment(value, object.name);
value = binding.kind === 'raw_state' ? value : build_proxy_reassignment(value, object.name);
}

return transform.assign(object, value);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { dev, is_ignored } from '../../../../state.js';
import * as b from '../../../../utils/builders.js';
import { regex_invalid_identifier_chars } from '../../../patterns.js';
import { get_rune } from '../../../scope.js';
import { build_proxy_reassignment, should_proxy_or_freeze } from '../utils.js';
import { build_proxy_reassignment, should_proxy } from '../utils.js';

/**
* @param {ClassBody} node
Expand Down Expand Up @@ -44,7 +44,7 @@ export function ClassBody(node, context) {
const rune = get_rune(definition.value, context.state.scope);
if (
rune === '$state' ||
rune === '$state.frozen' ||
rune === '$state.raw' ||
rune === '$derived' ||
rune === '$derived.by'
) {
Expand All @@ -53,8 +53,8 @@ export function ClassBody(node, context) {
kind:
rune === '$state'
? 'state'
: rune === '$state.frozen'
? 'frozen_state'
: rune === '$state.raw'
? 'raw_state'
: rune === '$derived.by'
? 'derived_by'
: 'derived',
Expand Down Expand Up @@ -114,15 +114,10 @@ export function ClassBody(node, context) {
field.kind === 'state'
? b.call(
'$.source',
should_proxy_or_freeze(init, context.state.scope) ? b.call('$.proxy', init) : init
should_proxy(init, context.state.scope) ? b.call('$.proxy', init) : init
)
: field.kind === 'frozen_state'
? b.call(
'$.source',
should_proxy_or_freeze(init, context.state.scope)
? b.call('$.freeze', init)
: init
)
: field.kind === 'raw_state'
? b.call('$.source', init)
: field.kind === 'derived_by'
? b.call('$.derived', init)
: b.call('$.derived', b.thunk(init));
Expand Down Expand Up @@ -154,16 +149,11 @@ export function ClassBody(node, context) {
);
}

if (field.kind === 'frozen_state') {
if (field.kind === 'raw_state') {
// set foo(value) { this.#foo = value; }
const value = b.id('value');
body.push(
b.method(
'set',
definition.key,
[value],
[b.stmt(b.call('$.set', member, b.call('$.freeze', value)))]
)
b.method('set', definition.key, [value], [b.stmt(b.call('$.set', member, value))])
);
}

Expand Down
Loading