breaking: replace `$state.frozen` with `$state.raw` (#12808)

* breaking: replace `$state.frozen` with `$state.raw`

* regenerate

* rename

* rename

* rename

* rename

* rename

* rename

* rename

* rename

* rename

* typo

* add compiler error for existing `$state.frozen` uses

* regenerate

* rename

* tidy up

* move proxy logic into props function
pull/12813/head
Rich Harris 5 months ago committed by GitHub
parent fa5d3a9002
commit 7cbd188f80
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,5 @@
---
'svelte': patch
---
breaking: replace `$state.frozen` with `$state.raw`

@ -64,10 +64,6 @@
> The `%rune%` rune is only available inside `.svelte` and `.svelte.js/ts` files > 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 ## state_prototype_fixed
> Cannot set prototype of `$state` object > Cannot set prototype of `$state` object

@ -102,12 +102,13 @@ declare namespace $state {
: never; : 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: * Example:
* ```ts * ```ts
* <script> * <script>
* let items = $state.frozen([0]); * let items = $state.raw([0]);
* *
* const addItem = () => { * const addItem = () => {
* items = [...items, items.length]; * items = [...items, items.length];
@ -123,8 +124,8 @@ declare namespace $state {
* *
* @param initial The initial value * @param initial The initial value
*/ */
export function frozen<T>(initial: T): Readonly<T>; export function raw<T>(initial: T): T;
export function frozen<T>(): Readonly<T> | undefined; export function raw<T>(): T | undefined;
/** /**
* To take a static snapshot of a deeply reactive `$state` proxy, use `$state.snapshot`: * To take a static snapshot of a deeply reactive `$state` proxy, use `$state.snapshot`:
* *

@ -34,7 +34,7 @@ export function BindDirective(node, context) {
node.name !== 'this' && // bind:this also works for regular variables node.name !== 'this' && // bind:this also works for regular variables
(!binding || (!binding ||
(binding.kind !== 'state' && (binding.kind !== 'state' &&
binding.kind !== 'frozen_state' && binding.kind !== 'raw_state' &&
binding.kind !== 'prop' && binding.kind !== 'prop' &&
binding.kind !== 'bindable_prop' && binding.kind !== 'bindable_prop' &&
binding.kind !== 'each' && binding.kind !== 'each' &&

@ -73,7 +73,7 @@ export function CallExpression(node, context) {
break; break;
case '$state': case '$state':
case '$state.frozen': case '$state.raw':
case '$derived': case '$derived':
case '$derived.by': case '$derived.by':
if ( if (

@ -32,7 +32,7 @@ export function ExportNamedDeclaration(node, context) {
e.derived_invalid_export(node); 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); e.state_invalid_export(node);
} }
} }

@ -26,7 +26,7 @@ export function ExportSpecifier(node, context) {
if ( if (
binding !== null && binding !== null &&
(binding.kind === 'state' || (binding.kind === 'state' ||
binding.kind === 'frozen_state' || binding.kind === 'raw_state' ||
(binding.kind === 'normal' && (binding.kind === 'normal' &&
(binding.declaration_kind === 'let' || binding.declaration_kind === 'var'))) (binding.declaration_kind === 'let' || binding.declaration_kind === 'var')))
) { ) {
@ -60,7 +60,7 @@ function validate_export(node, scope, name) {
e.derived_invalid_export(node); 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); e.state_invalid_export(node);
} }
} }

@ -1,7 +1,7 @@
/** @import { Expression, Identifier } from 'estree' */ /** @import { Expression, Identifier } from 'estree' */
/** @import { Context } from '../types' */ /** @import { Context } from '../types' */
import is_reference from 'is-reference'; 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 e from '../../../errors.js';
import * as w from '../../../warnings.js'; import * as w from '../../../warnings.js';
import { is_rune } from '../../../../utils.js'; import { is_rune } from '../../../../utils.js';
@ -53,6 +53,10 @@ export function Identifier(node, context) {
e.rune_renamed(parent, '$effect.active', '$effect.tracking'); 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); e.rune_invalid_name(parent, name);
} }
} }
@ -132,8 +136,8 @@ export function Identifier(node, context) {
(binding.initial?.type === 'CallExpression' && (binding.initial?.type === 'CallExpression' &&
binding.initial.arguments.length === 1 && binding.initial.arguments.length === 1 &&
binding.initial.arguments[0].type !== 'SpreadElement' && binding.initial.arguments[0].type !== 'SpreadElement' &&
!should_proxy_or_freeze(binding.initial.arguments[0], context.state.scope)))) || !should_proxy(binding.initial.arguments[0], context.state.scope)))) ||
binding.kind === 'frozen_state' || binding.kind === 'raw_state' ||
binding.kind === 'derived') && binding.kind === 'derived') &&
// We're only concerned with reads here // We're only concerned with reads here
(parent.type !== 'AssignmentExpression' || parent.left !== node) && (parent.type !== 'AssignmentExpression' || parent.left !== node) &&

@ -21,7 +21,7 @@ export function VariableDeclarator(node, context) {
// TODO feels like this should happen during scope creation? // TODO feels like this should happen during scope creation?
if ( if (
rune === '$state' || rune === '$state' ||
rune === '$state.frozen' || rune === '$state.raw' ||
rune === '$derived' || rune === '$derived' ||
rune === '$derived.by' || rune === '$derived.by' ||
rune === '$props' rune === '$props'
@ -32,8 +32,8 @@ export function VariableDeclarator(node, context) {
binding.kind = binding.kind =
rune === '$state' rune === '$state'
? 'state' ? 'state'
: rune === '$state.frozen' : rune === '$state.raw'
? 'frozen_state' ? 'raw_state'
: rune === '$derived' || rune === '$derived.by' : rune === '$derived' || rune === '$derived.by'
? 'derived' ? 'derived'
: path.is_rest : path.is_rest

@ -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. // // 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. // // 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 !== 'state' &&
// binding.kind !== 'frozen_state' && // binding.kind !== 'raw_state' &&
// (binding.kind !== 'normal' || !binding.initial) // (binding.kind !== 'normal' || !binding.initial)
// ); // );

@ -271,19 +271,9 @@ export function client_component(analysis, options) {
} }
} }
if (binding?.kind === 'state' || binding?.kind === 'frozen_state') { if (binding?.kind === 'state' || binding?.kind === 'raw_state') {
return [ const value = binding.kind === 'state' ? b.call('$.proxy', b.id('$$value')) : b.id('$$value');
getter, return [getter, b.set(alias ?? name, [b.stmt(b.call('$.set', b.id(name), value))])];
b.set(alias ?? name, [
b.stmt(
b.call(
'$.set',
b.id(name),
b.call(binding.kind === 'state' ? '$.proxy' : '$.freeze', b.id('$$value'))
)
)
])
];
} }
return getter; return getter;

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

@ -8,7 +8,8 @@ import {
PROPS_IS_LAZY_INITIAL, PROPS_IS_LAZY_INITIAL,
PROPS_IS_IMMUTABLE, PROPS_IS_IMMUTABLE,
PROPS_IS_RUNES, PROPS_IS_RUNES,
PROPS_IS_UPDATED PROPS_IS_UPDATED,
PROPS_IS_BINDABLE
} from '../../../../constants.js'; } from '../../../../constants.js';
import { dev } from '../../../state.js'; import { dev } from '../../../state.js';
import { get_value } from './visitors/shared/declarations.js'; import { get_value } from './visitors/shared/declarations.js';
@ -20,7 +21,7 @@ import { get_value } from './visitors/shared/declarations.js';
*/ */
export function is_state_source(binding, state) { export function is_state_source(binding, state) {
return ( return (
(binding.kind === 'state' || binding.kind === 'frozen_state') && (binding.kind === 'state' || binding.kind === 'raw_state') &&
(!state.analysis.immutable || binding.reassigned || state.analysis.accessors) (!state.analysis.immutable || binding.reassigned || state.analysis.accessors)
); );
} }
@ -168,6 +169,10 @@ export function get_prop_source(binding, state, name, initial) {
let flags = 0; let flags = 0;
if (binding.kind === 'bindable_prop') {
flags |= PROPS_IS_BINDABLE;
}
if (state.analysis.immutable) { if (state.analysis.immutable) {
flags |= PROPS_IS_IMMUTABLE; flags |= PROPS_IS_IMMUTABLE;
} }
@ -238,7 +243,7 @@ export function is_prop_source(binding, state) {
* @param {Expression} node * @param {Expression} node
* @param {Scope | null} scope * @param {Scope | null} scope
*/ */
export function should_proxy_or_freeze(node, scope) { export function should_proxy(node, scope) {
if ( if (
!node || !node ||
node.type === 'Literal' || node.type === 'Literal' ||
@ -263,7 +268,7 @@ export function should_proxy_or_freeze(node, scope) {
binding.initial.type !== 'ImportDeclaration' && binding.initial.type !== 'ImportDeclaration' &&
binding.initial.type !== 'EachBlock' binding.initial.type !== 'EachBlock'
) { ) {
return should_proxy_or_freeze(binding.initial, null); return should_proxy(binding.initial, null);
} }
} }
return true; return true;

@ -3,7 +3,7 @@
import * as b from '../../../../utils/builders.js'; import * as b from '../../../../utils/builders.js';
import { build_assignment_value } from '../../../../utils/ast.js'; import { build_assignment_value } from '../../../../utils/ast.js';
import { is_ignored } from '../../../../state.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'; import { visit_assignment_expression } from '../../shared/assignments.js';
/** /**
@ -37,11 +37,11 @@ export function build_assignment(operator, left, right, context) {
context.visit(build_assignment_value(operator, left, right)) 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; transformed = true;
value = value =
private_state.kind === 'frozen_state' private_state.kind === 'raw_state'
? b.call('$.freeze', value) ? value
: build_proxy_reassignment(value, private_state.id); : build_proxy_reassignment(value, private_state.id);
} }
@ -54,14 +54,14 @@ export function build_assignment(operator, left, right, context) {
} else if (left.property.type === 'Identifier' && context.state.in_constructor) { } else if (left.property.type === 'Identifier' && context.state.in_constructor) {
const public_state = context.state.public_state.get(left.property.name); 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)); const value = /** @type {Expression} */ (context.visit(right));
return b.assignment( return b.assignment(
operator, operator,
/** @type {Pattern} */ (context.visit(left)), /** @type {Pattern} */ (context.visit(left)),
public_state.kind === 'frozen_state' public_state.kind === 'raw_state'
? b.call('$.freeze', value) ? value
: build_proxy_reassignment(value, public_state.id) : build_proxy_reassignment(value, public_state.id)
); );
} }
@ -99,13 +99,11 @@ export function build_assignment(operator, left, right, context) {
if ( if (
!is_primitive && !is_primitive &&
binding.kind !== 'prop' && binding.kind !== 'prop' &&
binding.kind !== 'bindable_prop' &&
context.state.analysis.runes && context.state.analysis.runes &&
should_proxy_or_freeze(value, context.state.scope) should_proxy(value, context.state.scope)
) { ) {
value = value = binding.kind === 'raw_state' ? value : build_proxy_reassignment(value, object.name);
binding.kind === 'frozen_state'
? b.call('$.freeze', value)
: build_proxy_reassignment(value, object.name);
} }
return transform.assign(object, value); return transform.assign(object, value);

@ -5,7 +5,7 @@ import { dev, is_ignored } from '../../../../state.js';
import * as b from '../../../../utils/builders.js'; import * as b from '../../../../utils/builders.js';
import { regex_invalid_identifier_chars } from '../../../patterns.js'; import { regex_invalid_identifier_chars } from '../../../patterns.js';
import { get_rune } from '../../../scope.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 * @param {ClassBody} node
@ -44,7 +44,7 @@ export function ClassBody(node, context) {
const rune = get_rune(definition.value, context.state.scope); const rune = get_rune(definition.value, context.state.scope);
if ( if (
rune === '$state' || rune === '$state' ||
rune === '$state.frozen' || rune === '$state.raw' ||
rune === '$derived' || rune === '$derived' ||
rune === '$derived.by' rune === '$derived.by'
) { ) {
@ -53,8 +53,8 @@ export function ClassBody(node, context) {
kind: kind:
rune === '$state' rune === '$state'
? 'state' ? 'state'
: rune === '$state.frozen' : rune === '$state.raw'
? 'frozen_state' ? 'raw_state'
: rune === '$derived.by' : rune === '$derived.by'
? 'derived_by' ? 'derived_by'
: 'derived', : 'derived',
@ -114,15 +114,10 @@ export function ClassBody(node, context) {
field.kind === 'state' field.kind === 'state'
? b.call( ? b.call(
'$.source', '$.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' : field.kind === 'derived_by'
? b.call('$.derived', init) ? b.call('$.derived', init)
: b.call('$.derived', b.thunk(init)); : b.call('$.derived', b.thunk(init));
@ -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; } // set foo(value) { this.#foo = value; }
const value = b.id('value'); const value = b.id('value');
body.push( body.push(
b.method( b.method('set', definition.key, [value], [b.stmt(b.call('$.set', member, value))])
'set',
definition.key,
[value],
[b.stmt(b.call('$.set', member, b.call('$.freeze', value)))]
)
); );
} }

@ -6,7 +6,7 @@ import {
EACH_INDEX_REACTIVE, EACH_INDEX_REACTIVE,
EACH_IS_ANIMATED, EACH_IS_ANIMATED,
EACH_IS_CONTROLLED, EACH_IS_CONTROLLED,
EACH_IS_STRICT_EQUALS, EACH_ITEM_IMMUTABLE,
EACH_ITEM_REACTIVE, EACH_ITEM_REACTIVE,
EACH_KEYED EACH_KEYED
} from '../../../../../constants.js'; } from '../../../../../constants.js';
@ -55,6 +55,17 @@ export function EachBlock(node, context) {
node.context.type === 'Identifier' && node.context.type === 'Identifier' &&
node.context.name === node.key.name; node.context.name === node.key.name;
// if the each block expression references a store subscription, we need
// to use mutable stores internally
let uses_store;
for (const binding of node.metadata.expression.dependencies) {
if (binding.kind === 'store_sub') {
uses_store = true;
break;
}
}
for (const binding of node.metadata.expression.dependencies) { for (const binding of node.metadata.expression.dependencies) {
// if the expression doesn't reference any external state, we don't need to // if the expression doesn't reference any external state, we don't need to
// create a source for the item. TODO cover more cases (e.g. `x.filter(y)` // create a source for the item. TODO cover more cases (e.g. `x.filter(y)`
@ -64,12 +75,16 @@ export function EachBlock(node, context) {
continue; continue;
} }
if (!context.state.analysis.runes || !key_is_item || binding.kind === 'store_sub') { if (!context.state.analysis.runes || !key_is_item || uses_store) {
flags |= EACH_ITEM_REACTIVE; flags |= EACH_ITEM_REACTIVE;
break; break;
} }
} }
if (context.state.analysis.runes && !uses_store) {
flags |= EACH_ITEM_IMMUTABLE;
}
// Since `animate:` can only appear on elements that are the sole child of a keyed each block, // Since `animate:` can only appear on elements that are the sole child of a keyed each block,
// we can determine at compile time whether the each block is animated or not (in which // we can determine at compile time whether the each block is animated or not (in which
// case it should measure animated elements before and after reconciliation). // case it should measure animated elements before and after reconciliation).
@ -87,10 +102,6 @@ export function EachBlock(node, context) {
flags |= EACH_IS_CONTROLLED; flags |= EACH_IS_CONTROLLED;
} }
if (context.state.analysis.runes) {
flags |= EACH_IS_STRICT_EQUALS;
}
// If the array is a store expression, we need to invalidate it when the array is changed. // If the array is a store expression, we need to invalidate it when the array is changed.
// This doesn't catch all cases, but all the ones that Svelte 4 catches, too. // This doesn't catch all cases, but all the ones that Svelte 4 catches, too.
let store_to_invalidate = ''; let store_to_invalidate = '';

@ -7,12 +7,7 @@ import { extract_paths } from '../../../../utils/ast.js';
import * as b from '../../../../utils/builders.js'; import * as b from '../../../../utils/builders.js';
import * as assert from '../../../../utils/assert.js'; import * as assert from '../../../../utils/assert.js';
import { get_rune } from '../../../scope.js'; import { get_rune } from '../../../scope.js';
import { import { get_prop_source, is_prop_source, is_state_source, should_proxy } from '../utils.js';
get_prop_source,
is_prop_source,
is_state_source,
should_proxy_or_freeze
} from '../utils.js';
import { is_hoisted_function } from '../../utils.js'; import { is_hoisted_function } from '../../utils.js';
/** /**
@ -86,7 +81,7 @@ export function VariableDeclaration(node, context) {
if ( if (
initial && initial &&
binding.kind === 'bindable_prop' && binding.kind === 'bindable_prop' &&
should_proxy_or_freeze(initial, context.state.scope) should_proxy(initial, context.state.scope)
) { ) {
initial = b.call('$.proxy', initial); initial = b.call('$.proxy', initial);
} }
@ -119,7 +114,7 @@ export function VariableDeclaration(node, context) {
const value = const value =
args.length === 0 ? b.id('undefined') : /** @type {Expression} */ (context.visit(args[0])); args.length === 0 ? b.id('undefined') : /** @type {Expression} */ (context.visit(args[0]));
if (rune === '$state' || rune === '$state.frozen') { if (rune === '$state' || rune === '$state.raw') {
/** /**
* @param {Identifier} id * @param {Identifier} id
* @param {Expression} value * @param {Expression} value
@ -128,8 +123,8 @@ export function VariableDeclaration(node, context) {
const binding = /** @type {import('#compiler').Binding} */ ( const binding = /** @type {import('#compiler').Binding} */ (
context.state.scope.get(id.name) context.state.scope.get(id.name)
); );
if (should_proxy_or_freeze(value, context.state.scope)) { if (rune === '$state' && should_proxy(value, context.state.scope)) {
value = b.call(rune === '$state' ? '$.proxy' : '$.freeze', value); value = b.call('$.proxy', value);
} }
if (is_state_source(binding, context.state)) { if (is_state_source(binding, context.state)) {
value = b.call('$.source', value); value = b.call('$.source', value);
@ -151,7 +146,7 @@ export function VariableDeclaration(node, context) {
const binding = context.state.scope.get(/** @type {Identifier} */ (path.node).name); const binding = context.state.scope.get(/** @type {Identifier} */ (path.node).name);
return b.declarator( return b.declarator(
path.node, path.node,
binding?.kind === 'state' || binding?.kind === 'frozen_state' binding?.kind === 'state' || binding?.kind === 'raw_state'
? create_state_declarator(binding.node, value) ? create_state_declarator(binding.node, value)
: value : value
); );

@ -1,6 +1,6 @@
/** @import { BinaryOperator, Expression, Identifier } from 'estree' */ /** @import { Identifier } from 'estree' */
/** @import { ComponentContext, Context } from '../../types' */ /** @import { ComponentContext, Context } from '../../types' */
import { build_proxy_reassignment, is_state_source, should_proxy_or_freeze } from '../../utils.js'; import { is_state_source } from '../../utils.js';
import * as b from '../../../../../utils/builders.js'; import * as b from '../../../../../utils/builders.js';
/** /**

@ -11,7 +11,7 @@ export function PropertyDefinition(node, context) {
if (context.state.analysis.runes && node.value != null && node.value.type === 'CallExpression') { if (context.state.analysis.runes && node.value != null && node.value.type === 'CallExpression') {
const rune = get_rune(node.value, context.state.scope); const rune = get_rune(node.value, context.state.scope);
if (rune === '$state' || rune === '$state.frozen' || rune === '$derived') { if (rune === '$state' || rune === '$state.raw' || rune === '$derived') {
return { return {
...node, ...node,
value: value:

@ -278,7 +278,7 @@ export interface Binding {
| 'bindable_prop' | 'bindable_prop'
| 'rest_prop' | 'rest_prop'
| 'state' | 'state'
| 'frozen_state' | 'raw_state'
| 'derived' | 'derived'
| 'each' | 'each'
| 'snippet' | 'snippet'

@ -5,12 +5,13 @@ export const EACH_KEYED = 1 << 2;
/** See EachBlock interface metadata.is_controlled for an explanation what this is */ /** See EachBlock interface metadata.is_controlled for an explanation what this is */
export const EACH_IS_CONTROLLED = 1 << 3; export const EACH_IS_CONTROLLED = 1 << 3;
export const EACH_IS_ANIMATED = 1 << 4; export const EACH_IS_ANIMATED = 1 << 4;
export const EACH_IS_STRICT_EQUALS = 1 << 6; export const EACH_ITEM_IMMUTABLE = 1 << 5;
export const PROPS_IS_IMMUTABLE = 1; export const PROPS_IS_IMMUTABLE = 1;
export const PROPS_IS_RUNES = 1 << 1; export const PROPS_IS_RUNES = 1 << 1;
export const PROPS_IS_UPDATED = 1 << 2; export const PROPS_IS_UPDATED = 1 << 2;
export const PROPS_IS_LAZY_INITIAL = 1 << 3; export const PROPS_IS_BINDABLE = 1 << 3;
export const PROPS_IS_LAZY_INITIAL = 1 << 4;
export const TRANSITION_IN = 1; export const TRANSITION_IN = 1;
export const TRANSITION_OUT = 1 << 1; export const TRANSITION_OUT = 1 << 1;

@ -20,5 +20,4 @@ export const INSPECT_EFFECT = 1 << 17;
export const HEAD_EFFECT = 1 << 18; export const HEAD_EFFECT = 1 << 18;
export const STATE_SYMBOL = Symbol('$state'); export const STATE_SYMBOL = Symbol('$state');
export const STATE_FROZEN_SYMBOL = Symbol('$state.frozen');
export const LOADING_ATTR_SYMBOL = Symbol(''); export const LOADING_ATTR_SYMBOL = Symbol('');

@ -3,9 +3,8 @@ import {
EACH_INDEX_REACTIVE, EACH_INDEX_REACTIVE,
EACH_IS_ANIMATED, EACH_IS_ANIMATED,
EACH_IS_CONTROLLED, EACH_IS_CONTROLLED,
EACH_IS_STRICT_EQUALS, EACH_ITEM_IMMUTABLE,
EACH_ITEM_REACTIVE, EACH_ITEM_REACTIVE,
EACH_KEYED,
HYDRATION_END, HYDRATION_END,
HYDRATION_START_ELSE HYDRATION_START_ELSE
} from '../../../../constants.js'; } from '../../../../constants.js';
@ -29,7 +28,7 @@ import {
} from '../../reactivity/effects.js'; } from '../../reactivity/effects.js';
import { source, mutable_source, set } from '../../reactivity/sources.js'; import { source, mutable_source, set } from '../../reactivity/sources.js';
import { is_array, is_frozen } from '../../../shared/utils.js'; import { is_array, is_frozen } from '../../../shared/utils.js';
import { INERT, STATE_FROZEN_SYMBOL, STATE_SYMBOL } from '../../constants.js'; import { INERT, STATE_SYMBOL } from '../../constants.js';
import { queue_micro_task } from '../task.js'; import { queue_micro_task } from '../task.js';
import { current_effect } from '../../runtime.js'; import { current_effect } from '../../runtime.js';
@ -139,18 +138,6 @@ export function each(node, flags, get_collection, get_key, render_fn, fallback_f
var length = array.length; var length = array.length;
// If we are working with an array that isn't proxied or frozen, then remove strict equality and ensure the items
// are treated as reactive, so they get wrapped in a signal.
var flags = state.flags;
if (
(flags & EACH_IS_STRICT_EQUALS) !== 0 &&
!is_frozen(array) &&
!(STATE_FROZEN_SYMBOL in array) &&
!(STATE_SYMBOL in array)
) {
flags ^= EACH_IS_STRICT_EQUALS;
}
/** `true` if there was a hydration mismatch. Needs to be a `let` or else it isn't treeshaken out */ /** `true` if there was a hydration mismatch. Needs to be a `let` or else it isn't treeshaken out */
let mismatch = false; let mismatch = false;
@ -470,7 +457,7 @@ function create_item(anchor, state, prev, next, value, key, index, render_fn, fl
try { try {
var reactive = (flags & EACH_ITEM_REACTIVE) !== 0; var reactive = (flags & EACH_ITEM_REACTIVE) !== 0;
var mutable = (flags & EACH_IS_STRICT_EQUALS) === 0; var mutable = (flags & EACH_ITEM_IMMUTABLE) === 0;
var v = reactive ? (mutable ? mutable_source(value) : source(value)) : value; var v = reactive ? (mutable ? mutable_source(value) : source(value)) : value;
var i = (flags & EACH_INDEX_REACTIVE) === 0 ? index : source(index); var i = (flags & EACH_INDEX_REACTIVE) === 0 ? index : source(index);

@ -278,22 +278,6 @@ export function rune_outside_svelte(rune) {
} }
} }
/**
* 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`
* @returns {never}
*/
export function state_frozen_invalid_argument() {
if (DEV) {
const error = new Error(`state_frozen_invalid_argument\nThe argument to \`$state.frozen(...)\` cannot be an object created with \`$state(...)\`. You should create a copy of it first, for example with \`$state.snapshot\``);
error.name = 'Svelte error';
throw error;
} else {
// TODO print a link to the documentation
throw new Error("state_frozen_invalid_argument");
}
}
/** /**
* Cannot set prototype of `$state` object * Cannot set prototype of `$state` object
* @returns {never} * @returns {never}

@ -1,40 +0,0 @@
import { DEV } from 'esm-env';
import { define_property, is_array, is_frozen, object_freeze } from '../shared/utils.js';
import { STATE_FROZEN_SYMBOL, STATE_SYMBOL } from './constants.js';
import * as e from './errors.js';
/**
* Expects a value that was wrapped with `freeze` and makes it frozen in DEV.
* @template T
* @param {T} value
* @returns {Readonly<T>}
*/
export function freeze(value) {
if (
typeof value === 'object' &&
value !== null &&
!is_frozen(value) &&
!(STATE_FROZEN_SYMBOL in value)
) {
var copy = /** @type {T} */ (value);
if (STATE_SYMBOL in value) {
e.state_frozen_invalid_argument();
}
define_property(copy, STATE_FROZEN_SYMBOL, {
value: true,
writable: true,
enumerable: false
});
// Freeze the object in DEV
if (DEV) {
object_freeze(copy);
}
return /** @type {Readonly<T>} */ (copy);
}
return value;
}

@ -1,16 +0,0 @@
import { freeze } from './freeze.js';
import { assert, test } from 'vitest';
import { proxy } from './proxy.js';
test('freezes an object', () => {
const frozen = freeze({ a: 1 });
assert.throws(() => {
// @ts-expect-error
frozen.a += 1;
}, /Cannot assign to read only property/);
});
test('throws if argument is a state proxy', () => {
assert.throws(() => freeze(proxy({})), /state_frozen_invalid_argument/);
});

@ -93,7 +93,6 @@ export {
template_with_script, template_with_script,
text text
} from './dom/template.js'; } from './dom/template.js';
export { freeze } from './freeze.js';
export { derived, derived_safe_equal } from './reactivity/deriveds.js'; export { derived, derived_safe_equal } from './reactivity/deriveds.js';
export { export {
effect_tracking, effect_tracking,

@ -12,7 +12,7 @@ import {
} from '../shared/utils.js'; } from '../shared/utils.js';
import { check_ownership, widen_ownership } from './dev/ownership.js'; import { check_ownership, widen_ownership } from './dev/ownership.js';
import { source, set } from './reactivity/sources.js'; import { source, set } from './reactivity/sources.js';
import { STATE_FROZEN_SYMBOL, STATE_SYMBOL } from './constants.js'; import { STATE_SYMBOL } from './constants.js';
import { UNINITIALIZED } from '../../constants.js'; import { UNINITIALIZED } from '../../constants.js';
import * as e from './errors.js'; import * as e from './errors.js';
@ -24,12 +24,7 @@ import * as e from './errors.js';
* @returns {ProxyStateObject<T> | T} * @returns {ProxyStateObject<T> | T}
*/ */
export function proxy(value, parent = null, prev) { export function proxy(value, parent = null, prev) {
if ( if (typeof value === 'object' && value != null && !is_frozen(value)) {
typeof value === 'object' &&
value != null &&
!is_frozen(value) &&
!(STATE_FROZEN_SYMBOL in value)
) {
// If we have an existing proxy, return it... // If we have an existing proxy, return it...
if (STATE_SYMBOL in value) { if (STATE_SYMBOL in value) {
const metadata = /** @type {ProxyMetadata<T>} */ (value[STATE_SYMBOL]); const metadata = /** @type {ProxyMetadata<T>} */ (value[STATE_SYMBOL]);

@ -1,6 +1,7 @@
/** @import { Source } from './types.js' */ /** @import { Source } from './types.js' */
import { DEV } from 'esm-env'; import { DEV } from 'esm-env';
import { import {
PROPS_IS_BINDABLE,
PROPS_IS_IMMUTABLE, PROPS_IS_IMMUTABLE,
PROPS_IS_LAZY_INITIAL, PROPS_IS_LAZY_INITIAL,
PROPS_IS_RUNES, PROPS_IS_RUNES,
@ -13,6 +14,7 @@ import { get, is_signals_recorded, untrack, update } from '../runtime.js';
import { safe_equals } from './equality.js'; import { safe_equals } from './equality.js';
import * as e from '../errors.js'; import * as e from '../errors.js';
import { LEGACY_DERIVED_PROP } from '../constants.js'; import { LEGACY_DERIVED_PROP } from '../constants.js';
import { proxy } from '../proxy.js';
/** /**
* @param {((value?: number) => number)} fn * @param {((value?: number) => number)} fn
@ -228,6 +230,7 @@ export function spread_props(...props) {
export function prop(props, key, flags, fallback) { export function prop(props, key, flags, fallback) {
var immutable = (flags & PROPS_IS_IMMUTABLE) !== 0; var immutable = (flags & PROPS_IS_IMMUTABLE) !== 0;
var runes = (flags & PROPS_IS_RUNES) !== 0; var runes = (flags & PROPS_IS_RUNES) !== 0;
var bindable = (flags & PROPS_IS_BINDABLE) !== 0;
var lazy = (flags & PROPS_IS_LAZY_INITIAL) !== 0; var lazy = (flags & PROPS_IS_LAZY_INITIAL) !== 0;
var prop_value = /** @type {V} */ (props[key]); var prop_value = /** @type {V} */ (props[key]);
@ -343,7 +346,7 @@ export function prop(props, key, flags, fallback) {
} }
if (arguments.length > 0) { if (arguments.length > 0) {
const new_value = mutation ? get(current_value) : value; const new_value = mutation ? get(current_value) : bindable ? proxy(value) : value;
if (!current_value.equals(new_value)) { if (!current_value.equals(new_value)) {
from_child = true; from_child = true;

@ -393,7 +393,7 @@ export function is_mathml(name) {
const RUNES = /** @type {const} */ ([ const RUNES = /** @type {const} */ ([
'$state', '$state',
'$state.frozen', '$state.raw',
'$state.snapshot', '$state.snapshot',
'$state.is', '$state.is',
'$props', '$props',

@ -3,7 +3,7 @@
import ComponentB from './ComponentB.svelte'; import ComponentB from './ComponentB.svelte';
let type = $state(ComponentA); let type = $state(ComponentA);
let elem = $state.frozen(); let elem = $state.raw();
$effect(() => { $effect(() => {
console.log(elem); console.log(elem);

@ -8,9 +8,9 @@ export default test({
async test({ assert, warnings }) { async test({ assert, warnings }) {
assert.deepEqual(warnings, [ assert.deepEqual(warnings, [
'`bind:value={pojo.value}` (main.svelte:50:7) is binding to a non-reactive property', '`bind:value={pojo.value}` (main.svelte:50:7) is binding to a non-reactive property',
'`bind:value={frozen.value}` (main.svelte:51:7) is binding to a non-reactive property', '`bind:value={raw.value}` (main.svelte:51:7) is binding to a non-reactive property',
'`bind:value={pojo.value}` (main.svelte:52:7) is binding to a non-reactive property', '`bind:value={pojo.value}` (main.svelte:52:7) is binding to a non-reactive property',
'`bind:value={frozen.value}` (main.svelte:53:7) is binding to a non-reactive property', '`bind:value={raw.value}` (main.svelte:53:7) is binding to a non-reactive property',
'`bind:this={pojo.value}` (main.svelte:55:6) is binding to a non-reactive property' '`bind:this={pojo.value}` (main.svelte:55:6) is binding to a non-reactive property'
]); ]);
} }

@ -5,7 +5,7 @@
value: 1 value: 1
}; };
let frozen = $state.frozen({ let raw = $state.raw({
value: 2 value: 2
}); });
@ -48,9 +48,9 @@
<!-- should warn --> <!-- should warn -->
<input bind:value={pojo.value} /> <input bind:value={pojo.value} />
<input bind:value={frozen.value} /> <input bind:value={raw.value} />
<Child bind:value={pojo.value} /> <Child bind:value={pojo.value} />
<Child bind:value={frozen.value} /> <Child bind:value={raw.value} />
{#if value} {#if value}
<div bind:this={pojo.value}></div> <div bind:this={pojo.value}></div>
{/if} {/if}

@ -1,14 +0,0 @@
<script>
class Counter {
count = $state.frozen({ a: 0 });
}
const counter = new Counter();
</script>
<button on:click={() => {
try {
counter.count.a++
} catch (e) {
console.log('read only')
}
}}>{counter.count.a}</button>

@ -4,7 +4,7 @@ import { test } from '../../test';
export default test({ export default test({
html: `<button>0</button>`, html: `<button>0</button>`,
test({ assert, target, logs }) { test({ assert, target }) {
const btn = target.querySelector('button'); const btn = target.querySelector('button');
btn?.click(); btn?.click();
@ -14,7 +14,5 @@ export default test({
btn?.click(); btn?.click();
flushSync(); flushSync();
assert.htmlEqual(target.innerHTML, `<button>0</button>`); assert.htmlEqual(target.innerHTML, `<button>0</button>`);
assert.deepEqual(logs, ['read only', 'read only']);
} }
}); });

@ -1,6 +1,6 @@
<script> <script>
class Counter { class Counter {
#count = $state.frozen(); #count = $state.raw();
constructor(initial_count) { constructor(initial_count) {
this.#count = { a: initial_count }; this.#count = { a: initial_count };
@ -16,10 +16,8 @@
const counter = new Counter(0); const counter = new Counter(0);
</script> </script>
<button on:click={() => { <button
try { on:click={() => {
counter.count.a++ counter.count.a++;
} catch (e) { }}>{counter.count.a}</button
console.log('read only') >
}
}}>{counter.count.a}</button>

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

@ -4,7 +4,7 @@ import { test } from '../../test';
export default test({ export default test({
html: `<button>0</button>`, html: `<button>0</button>`,
test({ assert, target, logs }) { test({ assert, target }) {
const btn = target.querySelector('button'); const btn = target.querySelector('button');
btn?.click(); btn?.click();
@ -14,7 +14,5 @@ export default test({
btn?.click(); btn?.click();
flushSync(); flushSync();
assert.htmlEqual(target.innerHTML, `<button>0</button>`); assert.htmlEqual(target.innerHTML, `<button>0</button>`);
assert.deepEqual(logs, ['read only', 'read only']);
} }
}); });

@ -0,0 +1,12 @@
<script>
class Counter {
count = $state.raw({ a: 0 });
}
const counter = new Counter();
</script>
<button
on:click={() => {
counter.count.a++;
}}>{counter.count.a}</button
>

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

@ -1,16 +0,0 @@
<script>
let frozen_items = $state.frozen([
{id: 0, text: 'a'},
{id: 1, text: 'b'},
{id: 2, text: 'c'}
])
</script>
{#each frozen_items as item (item.id)}
{console.log(item.text)}
{item.text}
{/each}
<button onclick={() => {
frozen_items = [...frozen_items, {id: 3, text: 'd'}]
}}></button>

@ -0,0 +1,18 @@
<script>
let raw_items = $state.raw([
{ id: 0, text: 'a' },
{ id: 1, text: 'b' },
{ id: 2, text: 'c' }
]);
</script>
{#each raw_items as item (item.id)}
{console.log(item.text)}
{item.text}
{/each}
<button
onclick={() => {
raw_items = [...raw_items, { id: 3, text: 'd' }];
}}
></button>

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

@ -1,6 +1,6 @@
<script> <script>
let x = $state.frozen(0); let x = $state.raw(0);
let y = $state.frozen(0); let y = $state.raw(0);
$effect(() => { $effect(() => {
console.log(x); console.log(x);

@ -0,0 +1,6 @@
<script>
let { object = $bindable() } = $props();
</script>
<button onclick={() => (object = { count: object.count + 1 })}>reassign</button>
<button onclick={() => (object.count += 1)}>mutate</button>

@ -0,0 +1,20 @@
import { flushSync } from 'svelte';
import { ok, test } from '../../test';
export default test({
html: '<button>reassign</button> <button>mutate</button> <p>0</p>',
test({ assert, target }) {
const [reassign, mutate] = target.querySelectorAll('button');
const output = target.querySelector('p');
ok(output);
flushSync(() => mutate.click());
assert.htmlEqual(output.innerHTML, '0');
flushSync(() => reassign.click());
assert.htmlEqual(output.innerHTML, '2');
flushSync(() => mutate.click());
assert.htmlEqual(output.innerHTML, '2');
}
});

@ -0,0 +1,8 @@
<script>
import Child from './Child.svelte';
let object = $state.raw({ count: 0 });
</script>
<Child bind:object />
<p>{object.count}</p>

@ -927,7 +927,7 @@ declare module 'svelte/compiler' {
| 'bindable_prop' | 'bindable_prop'
| 'rest_prop' | 'rest_prop'
| 'state' | 'state'
| 'frozen_state' | 'raw_state'
| 'derived' | 'derived'
| 'each' | 'each'
| 'snippet' | 'snippet'
@ -2879,12 +2879,13 @@ declare namespace $state {
: never; : 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: * Example:
* ```ts * ```ts
* <script> * <script>
* let items = $state.frozen([0]); * let items = $state.raw([0]);
* *
* const addItem = () => { * const addItem = () => {
* items = [...items, items.length]; * items = [...items, items.length];
@ -2900,8 +2901,8 @@ declare namespace $state {
* *
* @param initial The initial value * @param initial The initial value
*/ */
export function frozen<T>(initial: T): Readonly<T>; export function raw<T>(initial: T): T;
export function frozen<T>(): Readonly<T> | undefined; export function raw<T>(): T | undefined;
/** /**
* To take a static snapshot of a deeply reactive `$state` proxy, use `$state.snapshot`: * To take a static snapshot of a deeply reactive `$state` proxy, use `$state.snapshot`:
* *

Loading…
Cancel
Save