Skip to content

Commit bb90b71

Browse files
committed
feat: add $state.raw rune
fix typo fix typo
1 parent 3a9b143 commit bb90b71

File tree

21 files changed

+189
-23
lines changed

21 files changed

+189
-23
lines changed

.changeset/dry-clocks-grow.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'svelte': patch
3+
---
4+
5+
feat: add $state.raw rune

packages/svelte/src/compiler/phases/2-analyze/index.js

Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -585,6 +585,7 @@ const legacy_scope_tweaker = {
585585
);
586586
if (
587587
binding.kind === 'state' ||
588+
binding.kind === 'raw_state' ||
588589
(binding.kind === 'normal' && binding.declaration_kind === 'let')
589590
) {
590591
binding.kind = 'prop';
@@ -636,18 +637,18 @@ const legacy_scope_tweaker = {
636637
const runes_scope_js_tweaker = {
637638
VariableDeclarator(node, { state }) {
638639
if (node.init?.type !== 'CallExpression') return;
639-
if (get_rune(node.init, state.scope) === null) return;
640+
const rune = get_rune(node.init, state.scope);
641+
if (rune === null) return;
640642

641643
const callee = node.init.callee;
642-
if (callee.type !== 'Identifier') return;
644+
if (callee.type !== 'Identifier' && callee.type !== 'MemberExpression') return;
643645

644-
const name = callee.name;
645-
if (name !== '$state' && name !== '$derived') return;
646+
if (rune !== '$state' && rune !== '$state.raw' && rune !== '$derived') return;
646647

647648
for (const path of extract_paths(node.id)) {
648649
// @ts-ignore this fails in CI for some insane reason
649650
const binding = /** @type {import('#compiler').Binding} */ (state.scope.get(path.node.name));
650-
binding.kind = name === '$state' ? 'state' : 'derived';
651+
binding.kind = rune === '$state' ? 'state' : rune === '$state.raw' ? 'raw_state' : 'derived';
651652
}
652653
}
653654
};
@@ -665,28 +666,31 @@ const runes_scope_tweaker = {
665666
VariableDeclarator(node, { state }) {
666667
const init = unwrap_ts_expression(node.init);
667668
if (!init || init.type !== 'CallExpression') return;
668-
if (get_rune(init, state.scope) === null) return;
669+
const rune = get_rune(init, state.scope);
670+
if (rune === null) return;
669671

670672
const callee = init.callee;
671-
if (callee.type !== 'Identifier') return;
673+
if (callee.type !== 'Identifier' && callee.type !== 'MemberExpression') return;
672674

673-
const name = callee.name;
674-
if (name !== '$state' && name !== '$derived' && name !== '$props') return;
675+
if (rune !== '$state' && rune !== '$state.raw' && rune !== '$derived' && rune !== '$props')
676+
return;
675677

676678
for (const path of extract_paths(node.id)) {
677679
// @ts-ignore this fails in CI for some insane reason
678680
const binding = /** @type {import('#compiler').Binding} */ (state.scope.get(path.node.name));
679681
binding.kind =
680-
name === '$state'
682+
rune === '$state'
681683
? 'state'
682-
: name === '$derived'
684+
: rune === '$state.raw'
685+
? 'raw_state'
686+
: rune === '$derived'
683687
? 'derived'
684688
: path.is_rest
685689
? 'rest_prop'
686690
: 'prop';
687691
}
688692

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

@@ -898,7 +902,7 @@ const common_visitors = {
898902

899903
if (
900904
node !== binding.node &&
901-
(binding.kind === 'state' || binding.kind === 'derived') &&
905+
(binding.kind === 'state' || binding.kind === 'raw_state' || binding.kind === 'derived') &&
902906
context.state.function_depth === binding.scope.function_depth
903907
) {
904908
warn(context.state.analysis.warnings, node, context.path, 'static-state-reference');

packages/svelte/src/compiler/phases/2-analyze/validation.js

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -349,6 +349,7 @@ export const validation = {
349349
if (
350350
!binding ||
351351
(binding.kind !== 'state' &&
352+
binding.kind !== 'raw_state' &&
352353
binding.kind !== 'prop' &&
353354
binding.kind !== 'each' &&
354355
binding.kind !== 'store_sub' &&
@@ -660,7 +661,7 @@ function validate_export(node, scope, name) {
660661
error(node, 'invalid-derived-export');
661662
}
662663

663-
if (binding.kind === 'state' && binding.reassigned) {
664+
if ((binding.kind === 'state' || binding.kind === 'raw_state') && binding.reassigned) {
664665
error(node, 'invalid-state-export');
665666
}
666667
}
@@ -834,7 +835,9 @@ function validate_no_const_assignment(node, argument, scope, is_binding) {
834835
is_binding,
835836
// This takes advantage of the fact that we don't assign initial for let directives and then/catch variables.
836837
// If we start doing that, we need another property on the binding to differentiate, or give up on the more precise error message.
837-
binding.kind !== 'state' && (binding.kind !== 'normal' || !binding.initial)
838+
binding.kind !== 'state' &&
839+
binding.kind !== 'raw_state' &&
840+
(binding.kind !== 'normal' || !binding.initial)
838841
);
839842
}
840843
}

packages/svelte/src/compiler/phases/3-transform/client/transform-client.js

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -233,15 +233,18 @@ export function client_component(source, analysis, options) {
233233
'$.bind_prop',
234234
b.id('$$props'),
235235
b.literal(alias ?? name),
236-
binding?.kind === 'state' ? b.call('$.get', b.id(name)) : b.id(name)
236+
binding?.kind === 'state' || binding?.kind === 'raw_state'
237+
? b.call('$.get', b.id(name))
238+
: b.id(name)
237239
)
238240
);
239241
});
240242

241243
const properties = analysis.exports.map(({ name, alias }) => {
242244
const binding = analysis.instance.scope.get(name);
243245
const is_source =
244-
binding?.kind === 'state' && (!state.analysis.immutable || binding.reassigned);
246+
(binding?.kind === 'state' || binding?.kind === 'raw_state') &&
247+
(!state.analysis.immutable || binding.reassigned);
245248

246249
// TODO This is always a getter because the `renamed-instance-exports` test wants it that way.
247250
// Should we for code size reasons make it an init in runes mode and/or non-dev mode?

packages/svelte/src/compiler/phases/3-transform/client/types.d.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ export interface ComponentClientTransformState extends ClientTransformState {
5959
}
6060

6161
export interface StateField {
62-
kind: 'state' | 'derived';
62+
kind: 'state' | 'raw_state' | 'derived';
6363
id: PrivateIdentifier;
6464
}
6565

packages/svelte/src/compiler/phases/3-transform/client/utils.js

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ export function serialize_get_binding(node, state) {
8989
}
9090

9191
if (
92-
(binding.kind === 'state' &&
92+
((binding.kind === 'state' || binding.kind === 'raw_state') &&
9393
(!state.analysis.immutable || state.analysis.accessors || binding.reassigned)) ||
9494
binding.kind === 'derived' ||
9595
binding.kind === 'legacy_reactive'
@@ -200,6 +200,7 @@ export function serialize_set_binding(node, context, fallback) {
200200

201201
if (
202202
binding.kind !== 'state' &&
203+
binding.kind !== 'raw_state' &&
203204
binding.kind !== 'prop' &&
204205
binding.kind !== 'each' &&
205206
binding.kind !== 'legacy_reactive' &&
@@ -217,12 +218,14 @@ export function serialize_set_binding(node, context, fallback) {
217218
return b.call(left, value);
218219
} else if (is_store) {
219220
return b.call('$.store_set', serialize_get_binding(b.id(left_name), state), value);
220-
} else {
221+
} else if (binding.kind === 'state') {
221222
return b.call(
222223
'$.set',
223224
b.id(left_name),
224225
context.state.analysis.runes && should_proxy(value) ? b.call('$.proxy', value) : value
225226
);
227+
} else {
228+
return b.call('$.set', b.id(left_name), value);
226229
}
227230
} else {
228231
if (is_store) {

packages/svelte/src/compiler/phases/3-transform/client/visitors/global.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ export const global_visitors = {
4949
// use runtime functions for smaller output
5050
if (
5151
binding?.kind === 'state' ||
52+
binding?.kind === 'raw_state' ||
5253
binding?.kind === 'each' ||
5354
binding?.kind === 'legacy_reactive' ||
5455
binding?.kind === 'prop' ||

packages/svelte/src/compiler/phases/3-transform/client/visitors/javascript-runes.js

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,10 @@ export const javascript_visitors_runes = {
2929

3030
if (definition.value?.type === 'CallExpression') {
3131
const rune = get_rune(definition.value, state.scope);
32-
if (rune === '$state' || rune === '$derived') {
32+
if (rune === '$state' || rune === '$state.raw' || rune === '$derived') {
3333
/** @type {import('../types.js').StateField} */
3434
const field = {
35-
kind: rune === '$state' ? 'state' : 'derived',
35+
kind: rune === '$state' ? 'state' : rune === '$state.raw' ? 'raw_state' : 'derived',
3636
// @ts-expect-error this is set in the next pass
3737
id: is_private ? definition.key : null
3838
};
@@ -85,6 +85,8 @@ export const javascript_visitors_runes = {
8585
value =
8686
field.kind === 'state'
8787
? b.call('$.source', should_proxy(init) ? b.call('$.proxy', init) : init)
88+
: field.kind === 'raw_state'
89+
? b.call('$.source', init)
8890
: b.call('$.derived', b.thunk(init));
8991
} else {
9092
// if no arguments, we know it's state as `$derived()` is a compile error
@@ -114,6 +116,14 @@ export const javascript_visitors_runes = {
114116
);
115117
}
116118

119+
if (field.kind === 'raw_state') {
120+
// set foo(value) { this.#foo = value; }
121+
const value = b.id('value');
122+
body.push(
123+
b.method('set', definition.key, [value], [b.stmt(b.call('$.set', member, value))])
124+
);
125+
}
126+
117127
if (field.kind === 'derived' && state.options.dev) {
118128
body.push(
119129
b.method(
@@ -224,6 +234,13 @@ export const javascript_visitors_runes = {
224234
if (!state.analysis.immutable || state.analysis.accessors || binding.reassigned) {
225235
value = b.call('$.source', value);
226236
}
237+
} else if (rune === '$state.raw') {
238+
const binding = /** @type {import('#compiler').Binding} */ (
239+
state.scope.get(declarator.id.name)
240+
);
241+
if (binding.reassigned) {
242+
value = b.call('$.source', value);
243+
}
227244
} else {
228245
value = b.call('$.derived', b.thunk(value));
229246
}

packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1227,6 +1227,7 @@ function serialize_event_handler(node, { state, visit }) {
12271227
if (
12281228
binding !== null &&
12291229
(binding.kind === 'state' ||
1230+
binding.kind === 'raw_state' ||
12301231
binding.kind === 'legacy_reactive' ||
12311232
binding.kind === 'derived' ||
12321233
binding.kind === 'prop' ||

packages/svelte/src/compiler/phases/3-transform/server/transform-server.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -446,6 +446,7 @@ function serialize_set_binding(node, context, fallback) {
446446

447447
if (
448448
binding.kind !== 'state' &&
449+
binding.kind !== 'raw_state' &&
449450
binding.kind !== 'prop' &&
450451
binding.kind !== 'each' &&
451452
binding.kind !== 'legacy_reactive' &&
@@ -558,7 +559,7 @@ const javascript_visitors_runes = {
558559
if (node.value != null && node.value.type === 'CallExpression') {
559560
const rune = get_rune(node.value, state.scope);
560561

561-
if (rune === '$state' || rune === '$derived') {
562+
if (rune === '$state' || rune === '$state.raw' || rune === '$derived') {
562563
return {
563564
...node,
564565
value:

packages/svelte/src/compiler/phases/constants.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ export const ElementBindings = [
7272

7373
export const Runes = /** @type {const} */ ([
7474
'$state',
75+
'$state.raw',
7576
'$props',
7677
'$derived',
7778
'$effect',

packages/svelte/src/compiler/types/index.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,7 @@ export interface Binding {
258258
| 'prop'
259259
| 'rest_prop'
260260
| 'state'
261+
| 'raw_state'
261262
| 'derived'
262263
| 'each'
263264
| 'store_sub'

packages/svelte/src/main/ambient.d.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,23 @@ declare module '*.svelte' {
1717
declare function $state<T>(initial: T): T;
1818
declare function $state<T>(): T | undefined;
1919

20+
declare namespace $state {
21+
/**
22+
* Declares reactive state without applying reactivity to nested properties.
23+
*
24+
* Example:
25+
* ```ts
26+
* let count = $state.raw(0);
27+
* ```
28+
*
29+
* https://svelte-5-preview.vercel.app/docs/runes#$state-raw
30+
*
31+
* @param initial The initial value
32+
*/
33+
export function $raw<T>(initial: T): T;
34+
export function $raw<T>(): T | undefined;
35+
}
36+
2037
/**
2138
* Declares derived state, i.e. one that depends on other state variables.
2239
* The expression inside `$derived(...)` should be free of side-effects.
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { test } from '../../test';
2+
3+
export default test({
4+
html: `<button>0</button>`,
5+
6+
async test({ assert, target }) {
7+
const btn = target.querySelector('button');
8+
9+
await btn?.click();
10+
assert.htmlEqual(target.innerHTML, `<button>1</button>`);
11+
12+
await btn?.click();
13+
assert.htmlEqual(target.innerHTML, `<button>2</button>`);
14+
}
15+
});
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<script>
2+
class Counter {
3+
#count = $state.raw(0);
4+
5+
constructor(initial_count) {
6+
this.#count = initial_count;
7+
}
8+
9+
get count() {
10+
return this.#count;
11+
}
12+
set count(val) {
13+
this.#count = val;
14+
}
15+
}
16+
const counter = new Counter(0);
17+
</script>
18+
19+
<button on:click={() => counter.count++}>{counter.count}</button>
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { test } from '../../test';
2+
3+
export default test({
4+
html: `<button>0</button>`,
5+
6+
async test({ assert, target }) {
7+
const btn = target.querySelector('button');
8+
9+
await btn?.click();
10+
assert.htmlEqual(target.innerHTML, `<button>1</button>`);
11+
12+
await btn?.click();
13+
assert.htmlEqual(target.innerHTML, `<button>2</button>`);
14+
}
15+
});
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<script>
2+
class Counter {
3+
count = $state.raw(0);
4+
}
5+
const counter = new Counter();
6+
</script>
7+
8+
<button on:click={() => counter.count++}>{counter.count}</button>
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { test } from '../../test';
2+
import { log } from './log.js';
3+
4+
export default test({
5+
before_test() {
6+
log.length = 0;
7+
},
8+
9+
async test({ assert, target }) {
10+
const [b1, b2] = target.querySelectorAll('button');
11+
b1.click();
12+
b2.click();
13+
await Promise.resolve();
14+
15+
assert.deepEqual(log, [0, 1]);
16+
}
17+
});
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
/** @type {any[]} */
2+
export const log = [];
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<script>
2+
import { log } from './log.js';
3+
4+
let x = $state.raw(0);
5+
let y = $state.raw(0);
6+
7+
$effect(() => {
8+
log.push(x);
9+
});
10+
</script>
11+
12+
<button on:click={() => x++}>{x}</button>
13+
<button on:click={() => y++}>{y}</button>

0 commit comments

Comments
 (0)