Skip to content

Commit

Permalink
feat: add $state.raw rune
Browse files Browse the repository at this point in the history
fix typo

fix typo
  • Loading branch information
trueadm committed Dec 7, 2023
1 parent 3a9b143 commit bb90b71
Show file tree
Hide file tree
Showing 21 changed files with 189 additions and 23 deletions.
5 changes: 5 additions & 0 deletions .changeset/dry-clocks-grow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'svelte': patch
---

feat: add $state.raw rune
30 changes: 17 additions & 13 deletions packages/svelte/src/compiler/phases/2-analyze/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -585,6 +585,7 @@ const legacy_scope_tweaker = {
);
if (
binding.kind === 'state' ||
binding.kind === 'raw_state' ||
(binding.kind === 'normal' && binding.declaration_kind === 'let')
) {
binding.kind = 'prop';
Expand Down Expand Up @@ -636,18 +637,18 @@ const legacy_scope_tweaker = {
const runes_scope_js_tweaker = {
VariableDeclarator(node, { state }) {
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;
if (callee.type !== 'Identifier') return;
if (callee.type !== 'Identifier' && callee.type !== 'MemberExpression') return;

const name = callee.name;
if (name !== '$state' && name !== '$derived') return;
if (rune !== '$state' && rune !== '$state.raw' && rune !== '$derived') return;

for (const path of extract_paths(node.id)) {
// @ts-ignore this fails in CI for some insane reason
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';
}
}
};
Expand All @@ -665,28 +666,31 @@ const runes_scope_tweaker = {
VariableDeclarator(node, { state }) {
const init = unwrap_ts_expression(node.init);
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;
if (callee.type !== 'Identifier') return;
if (callee.type !== 'Identifier' && callee.type !== 'MemberExpression') return;

const name = callee.name;
if (name !== '$state' && name !== '$derived' && name !== '$props') return;
if (rune !== '$state' && rune !== '$state.raw' && rune !== '$derived' && rune !== '$props')
return;

for (const path of extract_paths(node.id)) {
// @ts-ignore this fails in CI for some insane reason
const binding = /** @type {import('#compiler').Binding} */ (state.scope.get(path.node.name));
binding.kind =
name === '$state'
rune === '$state'
? 'state'
: name === '$derived'
: rune === '$state.raw'
? 'raw_state'
: rune === '$derived'
? 'derived'
: path.is_rest
? 'rest_prop'
: 'prop';
}

if (name === '$props') {
if (rune === '$props') {
for (const property of /** @type {import('estree').ObjectPattern} */ (node.id).properties) {
if (property.type !== 'Property') continue;

Expand Down Expand Up @@ -898,7 +902,7 @@ const common_visitors = {

if (
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
) {
warn(context.state.analysis.warnings, node, context.path, 'static-state-reference');
Expand Down
7 changes: 5 additions & 2 deletions packages/svelte/src/compiler/phases/2-analyze/validation.js
Original file line number Diff line number Diff line change
Expand Up @@ -349,6 +349,7 @@ export const validation = {
if (
!binding ||
(binding.kind !== 'state' &&
binding.kind !== 'raw_state' &&
binding.kind !== 'prop' &&
binding.kind !== 'each' &&
binding.kind !== 'store_sub' &&
Expand Down Expand Up @@ -660,7 +661,7 @@ function validate_export(node, scope, name) {
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');
}
}
Expand Down Expand Up @@ -834,7 +835,9 @@ function validate_no_const_assignment(node, argument, scope, is_binding) {
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 !== 'normal' || !binding.initial)
binding.kind !== 'state' &&
binding.kind !== 'raw_state' &&
(binding.kind !== 'normal' || !binding.initial)
);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -233,15 +233,18 @@ export function client_component(source, analysis, options) {
'$.bind_prop',
b.id('$$props'),
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)
)
);
});

const properties = analysis.exports.map(({ name, alias }) => {
const binding = analysis.instance.scope.get(name);
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.
// Should we for code size reasons make it an init in runes mode and/or non-dev mode?
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ export interface ComponentClientTransformState extends ClientTransformState {
}

export interface StateField {
kind: 'state' | 'derived';
kind: 'state' | 'raw_state' | 'derived';
id: PrivateIdentifier;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ export function serialize_get_binding(node, state) {
}

if (
(binding.kind === 'state' &&
((binding.kind === 'state' || binding.kind === 'raw_state') &&
(!state.analysis.immutable || state.analysis.accessors || binding.reassigned)) ||
binding.kind === 'derived' ||
binding.kind === 'legacy_reactive'
Expand Down Expand Up @@ -200,6 +200,7 @@ export function serialize_set_binding(node, context, fallback) {

if (
binding.kind !== 'state' &&
binding.kind !== 'raw_state' &&
binding.kind !== 'prop' &&
binding.kind !== 'each' &&
binding.kind !== 'legacy_reactive' &&
Expand All @@ -217,12 +218,14 @@ export function serialize_set_binding(node, context, fallback) {
return b.call(left, value);
} else if (is_store) {
return b.call('$.store_set', serialize_get_binding(b.id(left_name), state), value);
} else {
} else if (binding.kind === 'state') {
return b.call(
'$.set',
b.id(left_name),
context.state.analysis.runes && should_proxy(value) ? b.call('$.proxy', value) : value
);
} else {
return b.call('$.set', b.id(left_name), value);
}
} else {
if (is_store) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ export const global_visitors = {
// use runtime functions for smaller output
if (
binding?.kind === 'state' ||
binding?.kind === 'raw_state' ||
binding?.kind === 'each' ||
binding?.kind === 'legacy_reactive' ||
binding?.kind === 'prop' ||
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,10 @@ export const javascript_visitors_runes = {

if (definition.value?.type === 'CallExpression') {
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} */
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
id: is_private ? definition.key : null
};
Expand Down Expand Up @@ -85,6 +85,8 @@ export const javascript_visitors_runes = {
value =
field.kind === 'state'
? 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));
} else {
// if no arguments, we know it's state as `$derived()` is a compile error
Expand Down Expand Up @@ -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) {
body.push(
b.method(
Expand Down Expand Up @@ -224,6 +234,13 @@ export const javascript_visitors_runes = {
if (!state.analysis.immutable || state.analysis.accessors || binding.reassigned) {
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 {
value = b.call('$.derived', b.thunk(value));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1227,6 +1227,7 @@ function serialize_event_handler(node, { state, visit }) {
if (
binding !== null &&
(binding.kind === 'state' ||
binding.kind === 'raw_state' ||
binding.kind === 'legacy_reactive' ||
binding.kind === 'derived' ||
binding.kind === 'prop' ||
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -446,6 +446,7 @@ function serialize_set_binding(node, context, fallback) {

if (
binding.kind !== 'state' &&
binding.kind !== 'raw_state' &&
binding.kind !== 'prop' &&
binding.kind !== 'each' &&
binding.kind !== 'legacy_reactive' &&
Expand Down Expand Up @@ -558,7 +559,7 @@ const javascript_visitors_runes = {
if (node.value != null && node.value.type === 'CallExpression') {
const rune = get_rune(node.value, state.scope);

if (rune === '$state' || rune === '$derived') {
if (rune === '$state' || rune === '$state.raw' || rune === '$derived') {
return {
...node,
value:
Expand Down
1 change: 1 addition & 0 deletions packages/svelte/src/compiler/phases/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ export const ElementBindings = [

export const Runes = /** @type {const} */ ([
'$state',
'$state.raw',
'$props',
'$derived',
'$effect',
Expand Down
1 change: 1 addition & 0 deletions packages/svelte/src/compiler/types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,7 @@ export interface Binding {
| 'prop'
| 'rest_prop'
| 'state'
| 'raw_state'
| 'derived'
| 'each'
| 'store_sub'
Expand Down
17 changes: 17 additions & 0 deletions packages/svelte/src/main/ambient.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,23 @@ declare module '*.svelte' {
declare function $state<T>(initial: T): T;
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.
* The expression inside `$derived(...)` should be free of side-effects.
Expand Down
Original file line number Diff line number Diff line change
@@ -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>`);
}
});
Original file line number Diff line number Diff line change
@@ -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>
Original file line number Diff line number Diff line change
@@ -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>`);
}
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<script>
class Counter {
count = $state.raw(0);
}
const counter = new Counter();
</script>

<button on:click={() => counter.count++}>{counter.count}</button>
17 changes: 17 additions & 0 deletions packages/svelte/tests/runtime-runes/samples/raw-state/_config.js
Original file line number Diff line number Diff line change
@@ -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]);
}
});
2 changes: 2 additions & 0 deletions packages/svelte/tests/runtime-runes/samples/raw-state/log.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
/** @type {any[]} */
export const log = [];
13 changes: 13 additions & 0 deletions packages/svelte/tests/runtime-runes/samples/raw-state/main.svelte
Original file line number Diff line number Diff line change
@@ -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>
Loading

0 comments on commit bb90b71

Please sign in to comment.