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

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

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

@ -272,18 +272,8 @@ export function client_component(analysis, options) {
} }
if (binding?.kind === 'state' || binding?.kind === 'frozen_state') { if (binding?.kind === 'state' || binding?.kind === 'frozen_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;

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

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

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

@ -119,7 +119,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 +128,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_or_freeze(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);

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

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

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

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

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

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

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

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

Loading…
Cancel
Save