Skip to content

Commit e8c3729

Browse files
fix: make $state component exports settable (#12345)
* fix: make `$state` component exports settable fixes #11983 * failing test * fix --------- Co-authored-by: Rich Harris <[email protected]>
1 parent 14cbb65 commit e8c3729

File tree

8 files changed

+67
-8
lines changed

8 files changed

+67
-8
lines changed

.changeset/tidy-lizards-happen.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+
fix: make `$state` component exports settable

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1010,6 +1010,9 @@ const runes_scope_tweaker = {
10101010
name: node.local.name,
10111011
alias: node.exported.name
10121012
});
1013+
1014+
const binding = state.scope.get(node.local.name);
1015+
if (binding) binding.reassigned = true;
10131016
},
10141017
ExportNamedDeclaration(node, { next, state }) {
10151018
if (!node.declaration || state.ast_type !== 'instance') {

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

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -198,14 +198,38 @@ export function client_component(source, analysis, options) {
198198
}
199199

200200
/** @type {Array<ESTree.Property | ESTree.SpreadElement>} */
201-
const component_returned_object = analysis.exports.map(({ name, alias }) => {
201+
const component_returned_object = analysis.exports.flatMap(({ name, alias }) => {
202+
const binding = instance_state.scope.get(name);
202203
const expression = serialize_get_binding(b.id(name), instance_state);
204+
const getter = b.get(alias ?? name, [b.return(expression)]);
205+
206+
if (expression.type === 'Identifier') {
207+
if (binding?.declaration_kind === 'let' || binding?.declaration_kind === 'var') {
208+
return [
209+
getter,
210+
b.set(alias ?? name, [b.stmt(b.assignment('=', expression, b.id('$$value')))])
211+
];
212+
} else if (!options.dev) {
213+
return b.init(alias ?? name, expression);
214+
}
215+
}
203216

204-
if (expression.type === 'Identifier' && !options.dev) {
205-
return b.init(alias ?? name, expression);
217+
if (binding?.kind === 'state' || binding?.kind === 'frozen_state') {
218+
return [
219+
getter,
220+
b.set(alias ?? name, [
221+
b.stmt(
222+
b.call(
223+
'$.set',
224+
b.id(name),
225+
b.call(binding.kind === 'state' ? '$.proxy' : '$.freeze', b.id('$$value'))
226+
)
227+
)
228+
])
229+
];
206230
}
207231

208-
return b.get(alias ?? name, [b.return(expression)]);
232+
return getter;
209233
});
210234

211235
const properties = [...analysis.instance.scope.declarations].filter(

packages/svelte/tests/runtime-runes/samples/exports-3/_config.js

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,9 @@ import { test } from '../../test';
44
export default test({
55
test({ assert, target }) {
66
assert.htmlEqual(target.innerHTML, `0 0 <button>0 / 0</button>`);
7-
const [btn] = target.querySelectorAll('button');
7+
const btn = target.querySelector('button');
88

9-
btn?.click();
10-
flushSync();
9+
flushSync(() => btn?.click());
1110
assert.htmlEqual(target.innerHTML, '1 2 <button>1 / 2</button>');
1211
}
1312
});

packages/svelte/tests/runtime-runes/samples/exports-3/main.svelte

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<script>
2-
import Sub from './sub.svelte'
2+
import Sub from './sub.svelte';
33
let sub = $state();
44
</script>
55

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { flushSync } from 'svelte';
2+
import { test } from '../../test';
3+
4+
export default test({
5+
test({ assert, target }) {
6+
assert.htmlEqual(target.innerHTML, `0 0 <button>0 / 0</button>`);
7+
const btn = target.querySelector('button');
8+
9+
flushSync(() => btn?.click());
10+
assert.htmlEqual(target.innerHTML, '1 2 <button>1 / 2</button>');
11+
}
12+
});
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<script>
2+
import Sub from './sub.svelte';
3+
let sub = $state();
4+
</script>
5+
6+
<Sub bind:this={sub} />
7+
<button on:click={() => sub.count++}>{sub?.count} / {sub?.doubled}</button>
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<script>
2+
let count = $state(0);
3+
let doubled = $derived(count * 2);
4+
5+
export { count, doubled };
6+
</script>
7+
8+
{count}
9+
{doubled}

0 commit comments

Comments
 (0)