feat: adds $state.link rune

pull/12545/head
Dominic Gannaway 2 years ago
parent 346cf96599
commit 5fced05b3f

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

@ -125,6 +125,27 @@ declare namespace $state {
*/
export function frozen<T>(initial: T): Readonly<T>;
export function frozen<T>(): Readonly<T> | undefined;
/**
* Declares reactive state that is linked to another value. Local mutations will override the linked value
* until the linked value changes.
*
* Example:
* ```ts
* <script>
* let { text } = $props();
* let local_text = $state.link(text);
* </script>
*
* <input type="text" bind:value={local_text} />
* ```
*
* https://svelte-5-preview.vercel.app/docs/runes#$state-link
*
* @param value The linked value
*/
export function link<T>(value: T): T;
/**
* To take a static snapshot of a deeply reactive `$state` proxy, use `$state.snapshot`:
*

@ -911,6 +911,7 @@ const runes_scope_js_tweaker = {
if (
rune !== '$state' &&
rune !== '$state.link' &&
rune !== '$state.frozen' &&
rune !== '$derived' &&
rune !== '$derived.by'
@ -921,7 +922,13 @@ const runes_scope_js_tweaker = {
// @ts-ignore this fails in CI for some insane reason
const binding = /** @type {import('#compiler').Binding} */ (state.scope.get(path.node.name));
binding.kind =
rune === '$state' ? 'state' : rune === '$state.frozen' ? 'frozen_state' : 'derived';
rune === '$state'
? 'state'
: rune === '$state.link'
? 'link_state'
: rune === '$state.frozen'
? 'frozen_state'
: 'derived';
}
}
};
@ -948,6 +955,7 @@ const runes_scope_tweaker = {
if (
rune !== '$state' &&
rune !== '$state.frozen' &&
rune !== '$state.link' &&
rune !== '$derived' &&
rune !== '$derived.by' &&
rune !== '$props'
@ -962,6 +970,8 @@ const runes_scope_tweaker = {
? 'state'
: rune === '$state.frozen'
? 'frozen_state'
: rune === '$state.link'
? 'link_state'
: rune === '$derived' || rune === '$derived.by'
? 'derived'
: path.is_rest
@ -1279,7 +1289,7 @@ const common_visitors = {
context.state.function_depth === binding.scope.function_depth &&
// If we have $state that can be proxied or frozen and isn't re-assigned, then that means
// it's likely not using a primitive value and thus this warning isn't that helpful.
((binding.kind === 'state' &&
(((binding.kind === 'state' || binding.kind === 'link_state') &&
(binding.reassigned ||
(binding.initial?.type === 'CallExpression' &&
binding.initial.arguments.length === 1 &&
@ -1297,6 +1307,7 @@ const common_visitors = {
}
},
CallExpression(node, context) {
debugger;
const { expression, render_tag } = context.state;
if (
(expression?.type === 'ExpressionTag' || expression?.type === 'SpreadAttribute') &&
@ -1339,6 +1350,16 @@ const common_visitors = {
}
}
if (rune === '$state.link') {
// Similar to $derived above, $state is treated as `$state(() => foo)`
context.next({
...context.state,
function_depth: context.state.function_depth + 1
});
return;
}
if (rune === '$effect' || rune === '$effect.pre') {
// `$effect` needs context because Svelte needs to know whether it should re-run
// effects that invalidate themselves, and that's determined by whether we're in runes mode

@ -391,6 +391,7 @@ const validation = {
!binding ||
(binding.kind !== 'state' &&
binding.kind !== 'frozen_state' &&
binding.kind !== 'link_state' &&
binding.kind !== 'prop' &&
binding.kind !== 'bindable_prop' &&
binding.kind !== 'each' &&
@ -868,7 +869,12 @@ 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 === 'frozen_state' ||
binding.kind === 'link_state') &&
binding.reassigned
) {
e.state_invalid_export(node);
}
}

@ -67,7 +67,7 @@ export interface ComponentClientTransformState extends ClientTransformState {
}
export interface StateField {
kind: 'state' | 'frozen_state' | 'derived' | 'derived_call';
kind: 'state' | 'frozen_state' | 'link_state' | 'derived' | 'derived_call';
id: PrivateIdentifier;
}

@ -100,6 +100,10 @@ export function serialize_get_binding(node, state) {
return b.member(b.id('$$props'), node);
}
if (binding.kind === 'link_state') {
return b.call(node);
}
if (binding.kind === 'legacy_reactive_import') {
return b.call('$$_import_' + node.name);
}
@ -282,6 +286,7 @@ export function serialize_set_binding(node, context, fallback, prefix, options)
if (
binding.kind !== 'state' &&
binding.kind !== 'frozen_state' &&
binding.kind !== 'link_state' &&
binding.kind !== 'prop' &&
binding.kind !== 'bindable_prop' &&
binding.kind !== 'each' &&
@ -302,7 +307,12 @@ export function serialize_set_binding(node, context, fallback, prefix, options)
/**@type {import("estree").Expression}*/ (binding.initial),
context.state.scope
);
if ((binding.kind === 'prop' || binding.kind === 'bindable_prop') && !is_initial_proxy) {
if (
(binding.kind === 'prop' ||
binding.kind === 'bindable_prop' ||
binding.kind === 'link_state') &&
!is_initial_proxy
) {
return b.call(left, value);
} else if (is_store) {
return b.call('$.store_set', serialize_get_binding(b.id(left_name), state), value);
@ -329,7 +339,9 @@ export function serialize_set_binding(node, context, fallback, prefix, options)
: value
);
} else if (
(binding.kind === 'prop' || binding.kind === 'bindable_prop') &&
(binding.kind === 'prop' ||
binding.kind === 'bindable_prop' ||
binding.kind === 'link_state') &&
is_initial_proxy
) {
call = b.call(
@ -337,7 +349,7 @@ export function serialize_set_binding(node, context, fallback, prefix, options)
context.state.analysis.runes &&
!options?.skip_proxy_and_freeze &&
should_proxy_or_freeze(value, context.state.scope) &&
binding.kind === 'bindable_prop'
(binding.kind === 'bindable_prop' || binding.kind === 'link_state')
? serialize_proxy_reassignment(value, left_name, state)
: value
);

@ -68,6 +68,7 @@ export const global_visitors = {
if (
binding?.kind === 'state' ||
binding?.kind === 'frozen_state' ||
binding?.kind === 'link_state' ||
binding?.kind === 'each' ||
binding?.kind === 'legacy_reactive' ||
binding?.kind === 'prop' ||
@ -85,6 +86,7 @@ export const global_visitors = {
args.push(serialize_get_binding(b.id(name), state), b.call('$' + name));
} else {
if (binding.kind === 'prop' || binding.kind === 'bindable_prop') fn += '_prop';
if (binding.kind === 'link_state') fn += '_link';
args.push(b.id(name));
}

@ -43,6 +43,7 @@ export const javascript_visitors_runes = {
if (
rune === '$state' ||
rune === '$state.frozen' ||
rune === '$state.link' ||
rune === '$derived' ||
rune === '$derived.by'
) {
@ -53,6 +54,8 @@ export const javascript_visitors_runes = {
? 'state'
: rune === '$state.frozen'
? 'frozen_state'
: rune === '$state.link'
? 'link_state'
: rune === '$derived.by'
? 'derived_call'
: 'derived',
@ -114,6 +117,13 @@ export const javascript_visitors_runes = {
'$.source',
should_proxy_or_freeze(init, state.scope) ? b.call('$.proxy', init) : init
)
: field.kind === 'link_state'
? b.call(
'$.source_link',
should_proxy_or_freeze(init, state.scope)
? b.thunk(b.call('$.proxy', init))
: b.thunk(init)
)
: field.kind === 'frozen_state'
? b.call(
'$.source',
@ -134,8 +144,24 @@ export const javascript_visitors_runes = {
const member = b.member(b.this, field.id);
body.push(b.prop_def(field.id, value));
if (field.kind === 'link_state') {
// get foo() { return this.#foo; }
body.push(b.method('get', definition.key, [], [b.return(b.call(member))]));
// set foo(value) { this.#foo = value; }
const value = b.id('value');
body.push(
b.method(
'set',
definition.key,
[value],
[b.stmt(b.call(member, serialize_proxy_reassignment(value, field.id, state)))]
)
);
} else {
// get foo() { return this.#foo; }
body.push(b.method('get', definition.key, [], [b.return(b.call('$.get', member))]));
}
if (field.kind === 'state') {
// set foo(value) { this.#foo = value; }
@ -314,7 +340,7 @@ export const javascript_visitors_runes = {
? b.id('undefined')
: /** @type {import('estree').Expression} */ (visit(args[0]));
if (rune === '$state' || rune === '$state.frozen') {
if (rune === '$state' || rune === '$state.frozen' || rune === '$state.link') {
/**
* @param {import('estree').Identifier} id
* @param {import('estree').Expression} value
@ -322,7 +348,13 @@ export const javascript_visitors_runes = {
const create_state_declarator = (id, value) => {
const binding = /** @type {import('#compiler').Binding} */ (state.scope.get(id.name));
if (should_proxy_or_freeze(value, state.scope)) {
value = b.call(rune === '$state' ? '$.proxy' : '$.freeze', value);
value = b.call(
rune === '$state' || rune === '$state.link' ? '$.proxy' : '$.freeze',
value
);
}
if (binding.kind === 'link_state') {
return b.call('$.source_link', b.thunk(value));
}
if (is_state_source(binding, state)) {
value = b.call('$.source', value);

@ -559,7 +559,12 @@ const javascript_visitors_runes = {
if (node.value != null && node.value.type === 'CallExpression') {
const rune = get_rune(node.value, state.scope);
if (rune === '$state' || rune === '$state.frozen' || rune === '$derived') {
if (
rune === '$state' ||
rune === '$state.frozen' ||
rune === '$state.link' ||
rune === '$derived'
) {
return {
...node,
value:

@ -31,6 +31,7 @@ export const PassiveEvents = ['wheel', 'touchstart', 'touchmove', 'touchend', 't
export const Runes = /** @type {const} */ ([
'$state',
'$state.link',
'$state.frozen',
'$state.snapshot',
'$state.is',

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

@ -104,7 +104,14 @@ export {
user_effect,
user_pre_effect
} from './reactivity/effects.js';
export { mutable_source, mutate, source, set } from './reactivity/sources.js';
export {
mutable_source,
mutate,
source,
source_link,
set,
update_link
} from './reactivity/sources.js';
export {
prop,
rest_props,

@ -25,6 +25,7 @@ import {
MAYBE_DIRTY
} from '../constants.js';
import * as e from '../errors.js';
import { derived } from './deriveds.js';
let inspect_effects = new Set();
@ -44,6 +45,47 @@ export function source(v) {
};
}
/**
* @param {() => any} get_value
*/
export function source_link(get_value) {
var was_local = false;
var local_source = source(undefined);
var linked_derived = derived(() => {
var local_value = get(local_source);
var linked_value = get_value();
if (was_local) {
was_local = false;
return local_value;
}
return linked_value;
});
return function (/** @type {any} */ value) {
var linked_value = get(linked_derived);
if (arguments.length > 0) {
was_local = true;
set(local_source, value);
get(linked_derived);
return value;
}
return linked_value;
};
}
/**
* @param {((value?: number) => number)} fn
* @param {1 | -1} [d]
* @returns {number}
*/
export function update_link(fn, d = 1) {
const value = fn();
fn(value + d);
return value;
}
/**
* @template V
* @param {V} initial_value

@ -918,6 +918,7 @@ declare module 'svelte/compiler' {
| 'rest_prop'
| 'state'
| 'frozen_state'
| 'link_state'
| 'derived'
| 'each'
| 'snippet'
@ -2855,6 +2856,27 @@ declare namespace $state {
*/
export function frozen<T>(initial: T): Readonly<T>;
export function frozen<T>(): Readonly<T> | undefined;
/**
* Declares reactive state that is linked to another value. Local mutations will override the linked value
* until the linked value changes.
*
* Example:
* ```ts
* <script>
* let { text } = $props();
* let local_text = $state.link(text);
* </script>
*
* <input type="text" bind:value={local_text} />
* ```
*
* https://svelte-5-preview.vercel.app/docs/runes#$state-link
*
* @param value The linked value
*/
export function link<T>(value: T): T;
/**
* To take a static snapshot of a deeply reactive `$state` proxy, use `$state.snapshot`:
*

@ -118,6 +118,7 @@ const runes = [
{ snippet: '$bindable()', test: is_bindable },
{ snippet: '$effect.root(() => {\n\t${}\n})' },
{ snippet: '$state.snapshot(${})' },
{ snippet: '$state.link(${})' },
{ snippet: '$state.is(${})' },
{ snippet: '$effect.tracking()' },
{ snippet: '$inspect(${});', test: is_statement }

@ -64,6 +64,10 @@ Only plain objects and arrays [are made deeply reactive](/#H4sIAAAAAAAAE42QwWrDM
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.link`
TODO
## `$state.frozen`
State declared with `$state.frozen` cannot be mutated; it can only be _reassigned_. In other words, rather than assigning to a property of an object, or using an array method like `push`, replace the object or array altogether if you'd like to update it:

Loading…
Cancel
Save