feat: add $state.raw rune

fix typo

fix typo
pull/9851/head
Dominic Gannaway 3 years ago
parent 436a6c3dc4
commit 605f0baa9d

@ -0,0 +1,5 @@
---
'svelte': patch
---
feat: add $state.raw rune

@ -585,6 +585,7 @@ const legacy_scope_tweaker = {
); );
if ( if (
binding.kind === 'state' || binding.kind === 'state' ||
binding.kind === 'raw_state' ||
(binding.kind === 'normal' && binding.declaration_kind === 'let') (binding.kind === 'normal' && binding.declaration_kind === 'let')
) { ) {
binding.kind = 'prop'; binding.kind = 'prop';
@ -636,18 +637,18 @@ const legacy_scope_tweaker = {
const runes_scope_js_tweaker = { const runes_scope_js_tweaker = {
VariableDeclarator(node, { state }) { VariableDeclarator(node, { state }) {
if (node.init?.type !== 'CallExpression') return; if (node.init?.type !== 'CallExpression') return;
if (get_rune(node.init, state.scope) === null) return; const rune = get_rune(node.init, state.scope);
if (rune === null) return;
const callee = node.init.callee; const callee = node.init.callee;
if (callee.type !== 'Identifier') return; if (callee.type !== 'Identifier' && callee.type !== 'MemberExpression') return;
const name = callee.name; if (rune !== '$state' && rune !== '$state.raw' && rune !== '$derived') return;
if (name !== '$state' && name !== '$derived') return;
for (const path of extract_paths(node.id)) { for (const path of extract_paths(node.id)) {
// @ts-ignore this fails in CI for some insane reason // @ts-ignore this fails in CI for some insane reason
const binding = /** @type {import('#compiler').Binding} */ (state.scope.get(path.node.name)); const binding = /** @type {import('#compiler').Binding} */ (state.scope.get(path.node.name));
binding.kind = name === '$state' ? 'state' : 'derived'; binding.kind = rune === '$state' ? 'state' : rune === '$state.raw' ? 'raw_state' : 'derived';
} }
} }
}; };
@ -665,28 +666,31 @@ const runes_scope_tweaker = {
VariableDeclarator(node, { state }) { VariableDeclarator(node, { state }) {
const init = unwrap_ts_expression(node.init); const init = unwrap_ts_expression(node.init);
if (!init || init.type !== 'CallExpression') return; if (!init || init.type !== 'CallExpression') return;
if (get_rune(init, state.scope) === null) return; const rune = get_rune(init, state.scope);
if (rune === null) return;
const callee = init.callee; const callee = init.callee;
if (callee.type !== 'Identifier') return; if (callee.type !== 'Identifier' && callee.type !== 'MemberExpression') return;
const name = callee.name; if (rune !== '$state' && rune !== '$state.raw' && rune !== '$derived' && rune !== '$props')
if (name !== '$state' && name !== '$derived' && name !== '$props') return; return;
for (const path of extract_paths(node.id)) { for (const path of extract_paths(node.id)) {
// @ts-ignore this fails in CI for some insane reason // @ts-ignore this fails in CI for some insane reason
const binding = /** @type {import('#compiler').Binding} */ (state.scope.get(path.node.name)); const binding = /** @type {import('#compiler').Binding} */ (state.scope.get(path.node.name));
binding.kind = binding.kind =
name === '$state' rune === '$state'
? 'state' ? 'state'
: name === '$derived' : rune === '$state.raw'
? 'raw_state'
: rune === '$derived'
? 'derived' ? 'derived'
: path.is_rest : path.is_rest
? 'rest_prop' ? 'rest_prop'
: 'prop'; : 'prop';
} }
if (name === '$props') { if (rune === '$props') {
for (const property of /** @type {import('estree').ObjectPattern} */ (node.id).properties) { for (const property of /** @type {import('estree').ObjectPattern} */ (node.id).properties) {
if (property.type !== 'Property') continue; if (property.type !== 'Property') continue;
@ -898,7 +902,7 @@ const common_visitors = {
if ( if (
node !== binding.node && node !== binding.node &&
(binding.kind === 'state' || binding.kind === 'derived') && (binding.kind === 'state' || binding.kind === 'raw_state' || binding.kind === 'derived') &&
context.state.function_depth === binding.scope.function_depth context.state.function_depth === binding.scope.function_depth
) { ) {
warn(context.state.analysis.warnings, node, context.path, 'static-state-reference'); warn(context.state.analysis.warnings, node, context.path, 'static-state-reference');

@ -349,6 +349,7 @@ export const validation = {
if ( if (
!binding || !binding ||
(binding.kind !== 'state' && (binding.kind !== 'state' &&
binding.kind !== 'raw_state' &&
binding.kind !== 'prop' && binding.kind !== 'prop' &&
binding.kind !== 'each' && binding.kind !== 'each' &&
binding.kind !== 'store_sub' && binding.kind !== 'store_sub' &&
@ -660,7 +661,7 @@ function validate_export(node, scope, name) {
error(node, 'invalid-derived-export'); error(node, 'invalid-derived-export');
} }
if (binding.kind === 'state' && binding.reassigned) { if ((binding.kind === 'state' || binding.kind === 'raw_state') && binding.reassigned) {
error(node, 'invalid-state-export'); error(node, 'invalid-state-export');
} }
} }
@ -834,7 +835,9 @@ function validate_no_const_assignment(node, argument, scope, is_binding) {
is_binding, 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 !== 'normal' || !binding.initial) binding.kind !== 'state' &&
binding.kind !== 'raw_state' &&
(binding.kind !== 'normal' || !binding.initial)
); );
} }
} }

@ -233,7 +233,9 @@ export function client_component(source, analysis, options) {
'$.bind_prop', '$.bind_prop',
b.id('$$props'), b.id('$$props'),
b.literal(alias ?? name), b.literal(alias ?? name),
binding?.kind === 'state' ? b.call('$.get', b.id(name)) : b.id(name) binding?.kind === 'state' || binding?.kind === 'raw_state'
? b.call('$.get', b.id(name))
: b.id(name)
) )
); );
}); });
@ -241,7 +243,8 @@ export function client_component(source, analysis, options) {
const properties = analysis.exports.map(({ name, alias }) => { const properties = analysis.exports.map(({ name, alias }) => {
const binding = analysis.instance.scope.get(name); const binding = analysis.instance.scope.get(name);
const is_source = const is_source =
binding?.kind === 'state' && (!state.analysis.immutable || binding.reassigned); (binding?.kind === 'state' || binding?.kind === 'raw_state') &&
(!state.analysis.immutable || binding.reassigned);
// TODO This is always a getter because the `renamed-instance-exports` test wants it that way. // TODO This is always a getter because the `renamed-instance-exports` test wants it that way.
// Should we for code size reasons make it an init in runes mode and/or non-dev mode? // Should we for code size reasons make it an init in runes mode and/or non-dev mode?

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

@ -92,7 +92,7 @@ export function serialize_get_binding(node, state) {
} }
if ( if (
(binding.kind === 'state' && ((binding.kind === 'state' || binding.kind === 'raw_state') &&
(!state.analysis.immutable || state.analysis.accessors || binding.reassigned)) || (!state.analysis.immutable || state.analysis.accessors || binding.reassigned)) ||
binding.kind === 'derived' || binding.kind === 'derived' ||
binding.kind === 'legacy_reactive' binding.kind === 'legacy_reactive'
@ -232,6 +232,7 @@ export function serialize_set_binding(node, context, fallback) {
if ( if (
binding.kind !== 'state' && binding.kind !== 'state' &&
binding.kind !== 'raw_state' &&
binding.kind !== 'prop' && binding.kind !== 'prop' &&
binding.kind !== 'each' && binding.kind !== 'each' &&
binding.kind !== 'legacy_reactive' && binding.kind !== 'legacy_reactive' &&
@ -249,12 +250,14 @@ export function serialize_set_binding(node, context, fallback) {
return b.call(left, value); return b.call(left, value);
} else if (is_store) { } else if (is_store) {
return b.call('$.store_set', serialize_get_binding(b.id(left_name), state), value); return b.call('$.store_set', serialize_get_binding(b.id(left_name), state), value);
} else { } else if (binding.kind === 'state') {
return b.call( return b.call(
'$.set', '$.set',
b.id(left_name), b.id(left_name),
context.state.analysis.runes && should_proxy(value) ? b.call('$.proxy', value) : value context.state.analysis.runes && should_proxy(value) ? b.call('$.proxy', value) : value
); );
} else {
return b.call('$.set', b.id(left_name), value);
} }
} else { } else {
if (is_store) { if (is_store) {

@ -49,6 +49,7 @@ export const global_visitors = {
// use runtime functions for smaller output // use runtime functions for smaller output
if ( if (
binding?.kind === 'state' || binding?.kind === 'state' ||
binding?.kind === 'raw_state' ||
binding?.kind === 'each' || binding?.kind === 'each' ||
binding?.kind === 'legacy_reactive' || binding?.kind === 'legacy_reactive' ||
binding?.kind === 'prop' || binding?.kind === 'prop' ||

@ -29,10 +29,10 @@ export const javascript_visitors_runes = {
if (definition.value?.type === 'CallExpression') { if (definition.value?.type === 'CallExpression') {
const rune = get_rune(definition.value, state.scope); const rune = get_rune(definition.value, state.scope);
if (rune === '$state' || rune === '$derived') { if (rune === '$state' || rune === '$state.raw' || rune === '$derived') {
/** @type {import('../types.js').StateField} */ /** @type {import('../types.js').StateField} */
const field = { const field = {
kind: rune === '$state' ? 'state' : 'derived', kind: rune === '$state' ? 'state' : rune === '$state.raw' ? 'raw_state' : 'derived',
// @ts-expect-error this is set in the next pass // @ts-expect-error this is set in the next pass
id: is_private ? definition.key : null id: is_private ? definition.key : null
}; };
@ -85,6 +85,8 @@ export const javascript_visitors_runes = {
value = value =
field.kind === 'state' field.kind === 'state'
? b.call('$.source', should_proxy(init) ? b.call('$.proxy', init) : init) ? b.call('$.source', should_proxy(init) ? b.call('$.proxy', init) : init)
: field.kind === 'raw_state'
? b.call('$.source', init)
: b.call('$.derived', b.thunk(init)); : b.call('$.derived', b.thunk(init));
} else { } else {
// if no arguments, we know it's state as `$derived()` is a compile error // if no arguments, we know it's state as `$derived()` is a compile error
@ -114,6 +116,14 @@ export const javascript_visitors_runes = {
); );
} }
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, value))])
);
}
if (field.kind === 'derived' && state.options.dev) { if (field.kind === 'derived' && state.options.dev) {
body.push( body.push(
b.method( b.method(
@ -224,6 +234,13 @@ export const javascript_visitors_runes = {
if (!state.analysis.immutable || state.analysis.accessors || binding.reassigned) { if (!state.analysis.immutable || state.analysis.accessors || binding.reassigned) {
value = b.call('$.source', value); value = b.call('$.source', value);
} }
} else if (rune === '$state.raw') {
const binding = /** @type {import('#compiler').Binding} */ (
state.scope.get(declarator.id.name)
);
if (binding.reassigned) {
value = b.call('$.source', value);
}
} else { } else {
value = b.call('$.derived', b.thunk(value)); value = b.call('$.derived', b.thunk(value));
} }

@ -1227,6 +1227,7 @@ function serialize_event_handler(node, { state, visit }) {
if ( if (
binding !== null && binding !== null &&
(binding.kind === 'state' || (binding.kind === 'state' ||
binding.kind === 'raw_state' ||
binding.kind === 'legacy_reactive' || binding.kind === 'legacy_reactive' ||
binding.kind === 'derived' || binding.kind === 'derived' ||
binding.kind === 'prop' || binding.kind === 'prop' ||

@ -446,6 +446,7 @@ function serialize_set_binding(node, context, fallback) {
if ( if (
binding.kind !== 'state' && binding.kind !== 'state' &&
binding.kind !== 'raw_state' &&
binding.kind !== 'prop' && binding.kind !== 'prop' &&
binding.kind !== 'each' && binding.kind !== 'each' &&
binding.kind !== 'legacy_reactive' && binding.kind !== 'legacy_reactive' &&
@ -558,7 +559,7 @@ const javascript_visitors_runes = {
if (node.value != null && node.value.type === 'CallExpression') { if (node.value != null && node.value.type === 'CallExpression') {
const rune = get_rune(node.value, state.scope); const rune = get_rune(node.value, state.scope);
if (rune === '$state' || rune === '$derived') { if (rune === '$state' || rune === '$state.raw' || rune === '$derived') {
return { return {
...node, ...node,
value: value:

@ -72,6 +72,7 @@ export const ElementBindings = [
export const Runes = /** @type {const} */ ([ export const Runes = /** @type {const} */ ([
'$state', '$state',
'$state.raw',
'$props', '$props',
'$derived', '$derived',
'$effect', '$effect',

@ -258,6 +258,7 @@ export interface Binding {
| 'prop' | 'prop'
| 'rest_prop' | 'rest_prop'
| 'state' | 'state'
| 'raw_state'
| 'derived' | 'derived'
| 'each' | 'each'
| 'store_sub' | 'store_sub'

@ -17,6 +17,23 @@ declare module '*.svelte' {
declare function $state<T>(initial: T): T; declare function $state<T>(initial: T): T;
declare function $state<T>(): T | undefined; declare function $state<T>(): T | undefined;
declare namespace $state {
/**
* Declares reactive state without applying reactivity to nested properties.
*
* Example:
* ```ts
* let count = $state.raw(0);
* ```
*
* https://svelte-5-preview.vercel.app/docs/runes#$state-raw
*
* @param initial The initial value
*/
export function $raw<T>(initial: T): T;
export function $raw<T>(): T | undefined;
}
/** /**
* Declares derived state, i.e. one that depends on other state variables. * Declares derived state, i.e. one that depends on other state variables.
* The expression inside `$derived(...)` should be free of side-effects. * The expression inside `$derived(...)` should be free of side-effects.

@ -0,0 +1,15 @@
import { test } from '../../test';
export default test({
html: `<button>0</button>`,
async test({ assert, target }) {
const btn = target.querySelector('button');
await btn?.click();
assert.htmlEqual(target.innerHTML, `<button>1</button>`);
await btn?.click();
assert.htmlEqual(target.innerHTML, `<button>2</button>`);
}
});

@ -0,0 +1,19 @@
<script>
class Counter {
#count = $state.raw(0);
constructor(initial_count) {
this.#count = initial_count;
}
get count() {
return this.#count;
}
set count(val) {
this.#count = val;
}
}
const counter = new Counter(0);
</script>
<button on:click={() => counter.count++}>{counter.count}</button>

@ -0,0 +1,15 @@
import { test } from '../../test';
export default test({
html: `<button>0</button>`,
async test({ assert, target }) {
const btn = target.querySelector('button');
await btn?.click();
assert.htmlEqual(target.innerHTML, `<button>1</button>`);
await btn?.click();
assert.htmlEqual(target.innerHTML, `<button>2</button>`);
}
});

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

@ -0,0 +1,17 @@
import { test } from '../../test';
import { log } from './log.js';
export default test({
before_test() {
log.length = 0;
},
async test({ assert, target }) {
const [b1, b2] = target.querySelectorAll('button');
b1.click();
b2.click();
await Promise.resolve();
assert.deepEqual(log, [0, 1]);
}
});

@ -0,0 +1,2 @@
/** @type {any[]} */
export const log = [];

@ -0,0 +1,13 @@
<script>
import { log } from './log.js';
let x = $state.raw(0);
let y = $state.raw(0);
$effect(() => {
log.push(x);
});
</script>
<button on:click={() => x++}>{x}</button>
<button on:click={() => y++}>{y}</button>

@ -64,6 +64,26 @@ Objects and arrays [are made reactive](/#H4sIAAAAAAAAE42QwWrDMBBEf2URhUhUNEl7c21
In non-runes mode, a `let` declaration is treated as reactive state if it is updated at some point. Unlike `$state(...)`, which works anywhere in your app, `let` only behaves this way at the top level of a component. In non-runes mode, a `let` declaration is treated as reactive state if it is updated at some point. Unlike `$state(...)`, which works anywhere in your app, `let` only behaves this way at the top level of a component.
## `$state.raw`
Similar to `$state`, `$state.raw` is also declared and can be used in many of the same ways (including on classes). However, reactivity is _not_ applied deeply to properties of any objects or arrays used with `$state.raw`. So if you intend to use objects as state and you want to mutate their properties and have reactivity work by default, it's recommended you use `$state` instead.
For the cases where you don't want Svelte's reactivity to apply deeply to state, and for those who might want to have more control over their data structures, you might find `$state.raw` useful. Furthermore, `$state.raw` is ideal for those who want to work with data using immutable patterns rather than mutable patterns.
```svelte
<script>
let items = $state.raw([0]);
const addItem = () => {
items = [...items, items.length];
};
</script>
<button on:click={addItem}>
{items.join(', ')}
</button>
```
## `$derived` ## `$derived`
Derived state is declared with the `$derived` rune: Derived state is declared with the `$derived` rune:

Loading…
Cancel
Save