Skip to content
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