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
## 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

@ -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];
@ -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`:
*

@ -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' &&

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

@ -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);
}
}

@ -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')))
) {
@ -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);
}
}

@ -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';
@ -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);
}
}
@ -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) &&

@ -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'
@ -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

@ -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)
// );

@ -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;

@ -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;
}

@ -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';
@ -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)
);
}
@ -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;
}
@ -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' ||
@ -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;

@ -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';
/**
@ -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);
}
@ -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)
);
}
@ -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);

@ -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
@ -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'
) {
@ -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',
@ -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
)
: field.kind === 'frozen_state'
? b.call(
'$.source',
should_proxy_or_freeze(init, context.state.scope)
? b.call('$.freeze', init)
: init
should_proxy(init, context.state.scope) ? b.call('$.proxy', init) : init
)
: field.kind === 'raw_state'
? b.call('$.source', init)
: field.kind === 'derived_by'
? b.call('$.derived', 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; }
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))])
);
}

@ -6,7 +6,7 @@ import {
EACH_INDEX_REACTIVE,
EACH_IS_ANIMATED,
EACH_IS_CONTROLLED,
EACH_IS_STRICT_EQUALS,
EACH_ITEM_IMMUTABLE,
EACH_ITEM_REACTIVE,
EACH_KEYED
} from '../../../../../constants.js';
@ -55,6 +55,17 @@ export function EachBlock(node, context) {
node.context.type === 'Identifier' &&
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) {
// 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)`
@ -64,12 +75,16 @@ export function EachBlock(node, context) {
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;
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,
// 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).
@ -87,10 +102,6 @@ export function EachBlock(node, context) {
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.
// This doesn't catch all cases, but all the ones that Svelte 4 catches, too.
let store_to_invalidate = '';

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

@ -1,6 +1,6 @@
/** @import { BinaryOperator, Expression, Identifier } from 'estree' */
/** @import { Identifier } from 'estree' */
/** @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';
/**

@ -11,7 +11,7 @@ export function PropertyDefinition(node, context) {
if (context.state.analysis.runes && node.value != null && node.value.type === 'CallExpression') {
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 {
...node,
value:

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

@ -5,12 +5,13 @@ export const EACH_KEYED = 1 << 2;
/** See EachBlock interface metadata.is_controlled for an explanation what this is */
export const EACH_IS_CONTROLLED = 1 << 3;
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_RUNES = 1 << 1;
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_OUT = 1 << 1;

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

@ -3,9 +3,8 @@ import {
EACH_INDEX_REACTIVE,
EACH_IS_ANIMATED,
EACH_IS_CONTROLLED,
EACH_IS_STRICT_EQUALS,
EACH_ITEM_IMMUTABLE,
EACH_ITEM_REACTIVE,
EACH_KEYED,
HYDRATION_END,
HYDRATION_START_ELSE
} from '../../../../constants.js';
@ -29,7 +28,7 @@ import {
} from '../../reactivity/effects.js';
import { source, mutable_source, set } from '../../reactivity/sources.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 { 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;
// 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 */
let mismatch = false;
@ -470,7 +457,7 @@ function create_item(anchor, state, prev, next, value, key, index, render_fn, fl
try {
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 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
* @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,
text
} from './dom/template.js';
export { freeze } from './freeze.js';
export { derived, derived_safe_equal } from './reactivity/deriveds.js';
export {
effect_tracking,

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

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

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

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

@ -8,9 +8,9 @@ export default test({
async test({ assert, warnings }) {
assert.deepEqual(warnings, [
'`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={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'
]);
}

@ -5,7 +5,7 @@
value: 1
};
let frozen = $state.frozen({
let raw = $state.raw({
value: 2
});
@ -48,9 +48,9 @@
<!-- should warn -->
<input bind:value={pojo.value} />
<input bind:value={frozen.value} />
<input bind:value={raw.value} />
<Child bind:value={pojo.value} />
<Child bind:value={frozen.value} />
<Child bind:value={raw.value} />
{#if value}
<div bind:this={pojo.value}></div>
{/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({
html: `<button>0</button>`,
test({ assert, target, logs }) {
test({ assert, target }) {
const btn = target.querySelector('button');
btn?.click();
@ -14,7 +14,5 @@ export default test({
btn?.click();
flushSync();
assert.htmlEqual(target.innerHTML, `<button>0</button>`);
assert.deepEqual(logs, ['read only', 'read only']);
}
});

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

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

@ -4,7 +4,7 @@ import { test } from '../../test';
export default test({
html: `<button>0</button>`,
test({ assert, target, logs }) {
test({ assert, target }) {
const btn = target.querySelector('button');
btn?.click();
@ -14,7 +14,5 @@ export default test({
btn?.click();
flushSync();
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>
class Counter {
count = $state.frozen(0);
count = $state.raw(0);
}
const counter = new Counter();
</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>
let items = $state.frozen([0]);
let items = $state.raw([0]);
const addItem = () => {
items = [...items, items.length];

@ -1,6 +1,6 @@
<script>
let x = $state.frozen(0);
let y = $state.frozen(0);
let x = $state.raw(0);
let y = $state.raw(0);
$effect(() => {
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'
| 'rest_prop'
| 'state'
| 'frozen_state'
| 'raw_state'
| 'derived'
| 'each'
| 'snippet'
@ -2879,12 +2879,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];
@ -2900,8 +2901,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`:
*

Loading…
Cancel
Save