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

pull/12808/head
Rich Harris 2 years ago
parent b462c8d2e9
commit ef4b6facb6

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

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

@ -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,7 +32,7 @@ export function VariableDeclarator(node, context) {
binding.kind =
rune === '$state'
? 'state'
: rune === '$state.frozen'
: rune === '$state.raw'
? 'frozen_state'
: rune === '$derived' || rune === '$derived.by'
? 'derived'

@ -272,18 +272,8 @@ 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'))
)
)
])
];
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;

@ -41,7 +41,7 @@ export function build_assignment(operator, left, right, context) {
transformed = true;
value =
private_state.kind === 'frozen_state'
? b.call('$.freeze', value)
? value
: build_proxy_reassignment(value, private_state.id);
}
@ -61,7 +61,7 @@ export function build_assignment(operator, left, right, context) {
operator,
/** @type {Pattern} */ (context.visit(left)),
public_state.kind === 'frozen_state'
? b.call('$.freeze', value)
? value
: build_proxy_reassignment(value, public_state.id)
);
}
@ -103,9 +103,7 @@ export function build_assignment(operator, left, right, context) {
should_proxy_or_freeze(value, context.state.scope)
) {
value =
binding.kind === 'frozen_state'
? b.call('$.freeze', value)
: build_proxy_reassignment(value, object.name);
binding.kind === 'frozen_state' ? value : build_proxy_reassignment(value, object.name);
}
return transform.assign(object, value);

@ -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,7 +53,7 @@ export function ClassBody(node, context) {
kind:
rune === '$state'
? 'state'
: rune === '$state.frozen'
: rune === '$state.raw'
? 'frozen_state'
: rune === '$derived.by'
? 'derived_by'
@ -117,12 +117,7 @@ export function ClassBody(node, context) {
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
)
? b.call('$.source', init)
: field.kind === 'derived_by'
? b.call('$.derived', init)
: b.call('$.derived', b.thunk(init));
@ -158,12 +153,7 @@ export function ClassBody(node, context) {
// 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))])
);
}

@ -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 us 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_IS_STRICT_EQUALS;
}
// 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 = '';

@ -119,7 +119,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 +128,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_or_freeze(value, context.state.scope)) {
value = b.call('$.proxy', value);
}
if (is_state_source(binding, context.state)) {
value = b.call('$.source', value);

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

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

@ -5,7 +5,6 @@ import {
EACH_IS_CONTROLLED,
EACH_IS_STRICT_EQUALS,
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;

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

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

@ -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,14 +1,12 @@
<script>
class Counter {
count = $state.frozen({ a: 0 });
count = $state.raw({ a: 0 });
}
const counter = new Counter();
</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);
}
const counter = new Counter();
</script>

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

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

Loading…
Cancel
Save