Skip to content

Commit dfb6755

Browse files
authored
feat: add compiler error when encountering a $-prefixed store value outside a .svelte file (#12799)
* feat: add compiler error when encountering a $-prefixed store value outside a .svelte file * add fromState/toState APIs * another test, update types * rename fromState to toStore, and toState to fromStore * docs * add docs * separate client/server entry points for svelte/store
1 parent 31a4449 commit dfb6755

File tree

16 files changed

+472
-10
lines changed

16 files changed

+472
-10
lines changed

.changeset/curvy-papayas-pretend.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 compiler error when encountering a $-prefixed store value outside a .svelte file

packages/svelte/messages/compile-errors/script.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,3 +149,9 @@
149149
## store_invalid_subscription
150150

151151
> Cannot reference store value inside `<script context="module">`
152+
153+
## store_invalid_subscription_module
154+
155+
> Cannot reference store value outside a `.svelte` file
156+
157+
Using a `$` prefix to refer to the value of a store is only possible inside `.svelte` files, where Svelte can automatically create subscriptions when a component is mounted and unsubscribe when the component is unmounted. Consider migrating to runes instead.

packages/svelte/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,8 @@
7676
},
7777
"./store": {
7878
"types": "./types/index.d.ts",
79-
"default": "./src/store/index.js"
79+
"browser": "./src/store/index-client.js",
80+
"default": "./src/store/index-server.js"
8081
},
8182
"./transition": {
8283
"types": "./types/index.d.ts",

packages/svelte/src/compiler/errors.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -415,6 +415,15 @@ export function store_invalid_subscription(node) {
415415
e(node, "store_invalid_subscription", "Cannot reference store value inside `<script context=\"module\">`");
416416
}
417417

418+
/**
419+
* Cannot reference store value outside a `.svelte` file
420+
* @param {null | number | NodeLike} node
421+
* @returns {never}
422+
*/
423+
export function store_invalid_subscription_module(node) {
424+
e(node, "store_invalid_subscription_module", "Cannot reference store value outside a `.svelte` file");
425+
}
426+
418427
/**
419428
* Declaration cannot be empty
420429
* @param {null | number | NodeLike} node

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,12 @@ export function analyze_module(ast, options) {
219219
if (name === '$' || name[1] === '$') {
220220
e.global_reference_invalid(references[0].node, name);
221221
}
222+
223+
const binding = scope.get(name.slice(1));
224+
225+
if (binding !== null) {
226+
e.store_invalid_subscription_module(references[0].node);
227+
}
222228
}
223229

224230
walk(

packages/svelte/src/motion/spring.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/** @import { Task } from '#client' */
22
/** @import { SpringOpts, SpringUpdateOpts, TickContext } from './private.js' */
33
/** @import { Spring } from './public.js' */
4-
import { writable } from '../store/index.js';
4+
import { writable } from '../store/shared/index.js';
55
import { loop } from '../internal/client/loop.js';
66
import { raf } from '../internal/client/timing.js';
77
import { is_date } from './utils.js';

packages/svelte/src/motion/tweened.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/** @import { Task } from '../internal/client/types' */
22
/** @import { Tweened } from './public' */
33
/** @import { TweenedOptions } from './private' */
4-
import { writable } from '../store/index.js';
4+
import { writable } from '../store/shared/index.js';
55
import { raf } from '../internal/client/timing.js';
66
import { loop } from '../internal/client/loop.js';
77
import { linear } from '../easing/index.js';
Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
/** @import { Readable, Writable } from './public.js' */
2+
import { noop } from '../internal/shared/utils.js';
3+
import {
4+
effect_root,
5+
effect_tracking,
6+
render_effect
7+
} from '../internal/client/reactivity/effects.js';
8+
import { source } from '../internal/client/reactivity/sources.js';
9+
import { get as get_source, tick } from '../internal/client/runtime.js';
10+
import { increment } from '../reactivity/utils.js';
11+
import { get, writable } from './shared/index.js';
12+
13+
export { derived, get, readable, readonly, writable } from './shared/index.js';
14+
15+
/**
16+
* @template V
17+
* @overload
18+
* @param {() => V} get
19+
* @param {(v: V) => void} set
20+
* @returns {Writable<V>}
21+
*/
22+
/**
23+
* @template V
24+
* @overload
25+
* @param {() => V} get
26+
* @returns {Readable<V>}
27+
*/
28+
/**
29+
* Create a store from a function that returns state, and (to make a writable store), an
30+
* optional second function that sets state.
31+
*
32+
* ```ts
33+
* import { toStore } from 'svelte/store';
34+
*
35+
* let count = $state(0);
36+
*
37+
* const store = toStore(() => count, (v) => (count = v));
38+
* ```
39+
* @template V
40+
* @param {() => V} get
41+
* @param {(v: V) => void} [set]
42+
* @returns {Writable<V> | Readable<V>}
43+
*/
44+
export function toStore(get, set) {
45+
const store = writable(get(), (set) => {
46+
let ran = false;
47+
48+
// TODO do we need a different implementation on the server?
49+
const teardown = effect_root(() => {
50+
render_effect(() => {
51+
const value = get();
52+
if (ran) set(value);
53+
});
54+
});
55+
56+
ran = true;
57+
58+
return teardown;
59+
});
60+
61+
if (set) {
62+
return {
63+
set,
64+
update: (fn) => set(fn(get())),
65+
subscribe: store.subscribe
66+
};
67+
}
68+
69+
return {
70+
subscribe: store.subscribe
71+
};
72+
}
73+
74+
/**
75+
* @template V
76+
* @overload
77+
* @param {Writable<V>} store
78+
* @returns {{ current: V }}
79+
*/
80+
/**
81+
* @template V
82+
* @overload
83+
* @param {Readable<V>} store
84+
* @returns {{ readonly current: V }}
85+
*/
86+
/**
87+
* Convert a store to an object with a reactive `current` property. If `store`
88+
* is a readable store, `current` will be a readonly property.
89+
*
90+
* ```ts
91+
* import { fromStore, get, writable } from 'svelte/store';
92+
*
93+
* const store = writable(0);
94+
*
95+
* const count = fromStore(store);
96+
*
97+
* count.current; // 0;
98+
* store.set(1);
99+
* count.current; // 1
100+
*
101+
* count.current += 1;
102+
* get(store); // 2
103+
* ```
104+
* @template V
105+
* @param {Writable<V> | Readable<V>} store
106+
*/
107+
export function fromStore(store) {
108+
let value = /** @type {V} */ (undefined);
109+
let version = source(0);
110+
let subscribers = 0;
111+
112+
let unsubscribe = noop;
113+
114+
function current() {
115+
if (effect_tracking()) {
116+
get_source(version);
117+
118+
render_effect(() => {
119+
if (subscribers === 0) {
120+
let ran = false;
121+
122+
unsubscribe = store.subscribe((v) => {
123+
value = v;
124+
if (ran) increment(version);
125+
});
126+
127+
ran = true;
128+
}
129+
130+
subscribers += 1;
131+
132+
return () => {
133+
subscribers -= 1;
134+
135+
tick().then(() => {
136+
if (subscribers === 0) {
137+
unsubscribe();
138+
}
139+
});
140+
};
141+
});
142+
143+
return value;
144+
}
145+
146+
return get(store);
147+
}
148+
149+
if ('set' in store) {
150+
return {
151+
get current() {
152+
return current();
153+
},
154+
set current(v) {
155+
store.set(v);
156+
}
157+
};
158+
}
159+
160+
return {
161+
get current() {
162+
return current();
163+
}
164+
};
165+
}
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
/** @import { Readable, Writable } from './public.js' */
2+
import { get, writable } from './shared/index.js';
3+
4+
export { derived, get, readable, readonly, writable } from './shared/index.js';
5+
6+
/**
7+
* @template V
8+
* @overload
9+
* @param {() => V} get
10+
* @param {(v: V) => void} set
11+
* @returns {Writable<V>}
12+
*/
13+
/**
14+
* @template V
15+
* @overload
16+
* @param {() => V} get
17+
* @returns {Readable<V>}
18+
*/
19+
/**
20+
* Create a store from a function that returns state, and (to make a writable store), an
21+
* optional second function that sets state.
22+
*
23+
* ```ts
24+
* import { toStore } from 'svelte/store';
25+
*
26+
* let count = $state(0);
27+
*
28+
* const store = toStore(() => count, (v) => (count = v));
29+
* ```
30+
* @template V
31+
* @param {() => V} get
32+
* @param {(v: V) => void} [set]
33+
* @returns {Writable<V> | Readable<V>}
34+
*/
35+
export function toStore(get, set) {
36+
const store = writable(get());
37+
38+
if (set) {
39+
return {
40+
set,
41+
update: (fn) => set(fn(get())),
42+
subscribe: store.subscribe
43+
};
44+
}
45+
46+
return {
47+
subscribe: store.subscribe
48+
};
49+
}
50+
51+
/**
52+
* @template V
53+
* @overload
54+
* @param {Writable<V>} store
55+
* @returns {{ current: V }}
56+
*/
57+
/**
58+
* @template V
59+
* @overload
60+
* @param {Readable<V>} store
61+
* @returns {{ readonly current: V }}
62+
*/
63+
/**
64+
* Convert a store to an object with a reactive `current` property. If `store`
65+
* is a readable store, `current` will be a readonly property.
66+
*
67+
* ```ts
68+
* import { fromStore, get, writable } from 'svelte/store';
69+
*
70+
* const store = writable(0);
71+
*
72+
* const count = fromStore(store);
73+
*
74+
* count.current; // 0;
75+
* store.set(1);
76+
* count.current; // 1
77+
*
78+
* count.current += 1;
79+
* get(store); // 2
80+
* ```
81+
* @template V
82+
* @param {Writable<V> | Readable<V>} store
83+
*/
84+
export function fromStore(store) {
85+
if ('set' in store) {
86+
return {
87+
get current() {
88+
return get(store);
89+
},
90+
set current(v) {
91+
store.set(v);
92+
}
93+
};
94+
}
95+
96+
return {
97+
get current() {
98+
return get(store);
99+
}
100+
};
101+
}

packages/svelte/src/store/public.d.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,4 +48,4 @@ interface Writable<T> extends Readable<T> {
4848

4949
export { Readable, StartStopNotifier, Subscriber, Unsubscriber, Updater, Writable };
5050

51-
export * from './index.js';
51+
export * from './index-client.js';

0 commit comments

Comments
 (0)