Skip to content

Commit b3d185d

Browse files
authored
fix: correctly call exported state (#10114)
fixes #10104 also cleans up related code and adds support for destructuring `$state.frozen`
1 parent 92408e1 commit b3d185d

File tree

10 files changed

+142
-83
lines changed

10 files changed

+142
-83
lines changed

.changeset/slimy-walls-draw.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: correctly call exported state

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

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { global_visitors } from './visitors/global.js';
77
import { javascript_visitors } from './visitors/javascript.js';
88
import { javascript_visitors_runes } from './visitors/javascript-runes.js';
99
import { javascript_visitors_legacy } from './visitors/javascript-legacy.js';
10-
import { serialize_get_binding } from './utils.js';
10+
import { is_state_source, serialize_get_binding } from './utils.js';
1111
import { remove_types } from '../typescript.js';
1212

1313
/**
@@ -242,9 +242,7 @@ export function client_component(source, analysis, options) {
242242

243243
const properties = analysis.exports.map(({ name, alias }) => {
244244
const binding = analysis.instance.scope.get(name);
245-
const is_source =
246-
(binding?.kind === 'state' || binding?.kind === 'frozen_state') &&
247-
(!state.analysis.immutable || binding.reassigned);
245+
const is_source = binding !== null && is_state_source(binding, state);
248246

249247
// TODO This is always a getter because the `renamed-instance-exports` test wants it that way.
250248
// 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/utils.js

Lines changed: 13 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,18 @@ export function get_assignment_value(node, { state, visit }) {
4545
}
4646
}
4747

48+
/**
49+
* @param {import('#compiler').Binding} binding
50+
* @param {import('./types').ClientTransformState} state
51+
* @returns {boolean}
52+
*/
53+
export function is_state_source(binding, state) {
54+
return (
55+
(binding.kind === 'state' || binding.kind === 'frozen_state') &&
56+
(!state.analysis.immutable || binding.reassigned || state.analysis.accessors)
57+
);
58+
}
59+
4860
/**
4961
* @param {import('estree').Identifier} node
5062
* @param {import('./types').ClientTransformState} state
@@ -93,8 +105,7 @@ export function serialize_get_binding(node, state) {
93105
}
94106

95107
if (
96-
((binding.kind === 'state' || binding.kind === 'frozen_state') &&
97-
(!state.analysis.immutable || state.analysis.accessors || binding.reassigned)) ||
108+
is_state_source(binding, state) ||
98109
binding.kind === 'derived' ||
99110
binding.kind === 'legacy_reactive'
100111
) {
@@ -491,33 +502,6 @@ export function get_prop_source(binding, state, name, initial) {
491502
return b.call('$.prop', ...args);
492503
}
493504

494-
/**
495-
* Creates the output for a state declaration.
496-
* @param {import('estree').VariableDeclarator} declarator
497-
* @param {import('../../scope').Scope} scope
498-
* @param {import('estree').Expression} value
499-
*/
500-
export function create_state_declarators(declarator, scope, value) {
501-
// in the simple `let count = $state(0)` case, we rewrite `$state` as `$.source`
502-
if (declarator.id.type === 'Identifier') {
503-
return [b.declarator(declarator.id, b.call('$.mutable_source', value))];
504-
}
505-
506-
const tmp = scope.generate('tmp');
507-
const paths = extract_paths(declarator.id);
508-
return [
509-
b.declarator(b.id(tmp), value), // TODO inject declarator for opts, so we can use it below
510-
...paths.map((path) => {
511-
const value = path.expression?.(b.id(tmp));
512-
const binding = scope.get(/** @type {import('estree').Identifier} */ (path.node).name);
513-
return b.declarator(
514-
path.node,
515-
binding?.kind === 'state' ? b.call('$.mutable_source', value) : value
516-
);
517-
})
518-
];
519-
}
520-
521505
/** @param {import('estree').Expression} node */
522506
export function should_proxy_or_freeze(node) {
523507
if (

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

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,33 @@
11
import { is_hoistable_function } from '../../utils.js';
22
import * as b from '../../../../utils/builders.js';
33
import { extract_paths } from '../../../../utils/ast.js';
4-
import { create_state_declarators, get_prop_source, serialize_get_binding } from '../utils.js';
4+
import { get_prop_source, serialize_get_binding } from '../utils.js';
5+
6+
/**
7+
* Creates the output for a state declaration.
8+
* @param {import('estree').VariableDeclarator} declarator
9+
* @param {import('../../../scope.js').Scope} scope
10+
* @param {import('estree').Expression} value
11+
*/
12+
function create_state_declarators(declarator, scope, value) {
13+
if (declarator.id.type === 'Identifier') {
14+
return [b.declarator(declarator.id, b.call('$.mutable_source', value))];
15+
}
16+
17+
const tmp = scope.generate('tmp');
18+
const paths = extract_paths(declarator.id);
19+
return [
20+
b.declarator(b.id(tmp), value),
21+
...paths.map((path) => {
22+
const value = path.expression?.(b.id(tmp));
23+
const binding = scope.get(/** @type {import('estree').Identifier} */ (path.node).name);
24+
return b.declarator(
25+
path.node,
26+
binding?.kind === 'state' ? b.call('$.mutable_source', value) : value
27+
);
28+
})
29+
];
30+
}
531

632
/** @type {import('../types.js').ComponentVisitors} */
733
export const javascript_visitors_legacy = {

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

Lines changed: 55 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@ import { get_rune } from '../../../scope.js';
22
import { is_hoistable_function, transform_inspect_rune } from '../../utils.js';
33
import * as b from '../../../../utils/builders.js';
44
import * as assert from '../../../../utils/assert.js';
5-
import { create_state_declarators, get_prop_source, should_proxy_or_freeze } from '../utils.js';
6-
import { unwrap_ts_expression } from '../../../../utils/ast.js';
5+
import { get_prop_source, is_state_source, should_proxy_or_freeze } from '../utils.js';
6+
import { extract_paths, unwrap_ts_expression } from '../../../../utils/ast.js';
77

88
/** @type {import('../types.js').ComponentVisitors} */
99
export const javascript_visitors_runes = {
@@ -223,66 +223,79 @@ export const javascript_visitors_runes = {
223223
}
224224

225225
const args = /** @type {import('estree').CallExpression} */ (init).arguments;
226-
let value =
226+
const value =
227227
args.length === 0
228228
? b.id('undefined')
229229
: /** @type {import('estree').Expression} */ (visit(args[0]));
230230

231-
if (declarator.id.type === 'Identifier') {
232-
if (rune === '$state') {
233-
const binding = /** @type {import('#compiler').Binding} */ (
234-
state.scope.get(declarator.id.name)
235-
);
231+
if (rune === '$state' || rune === '$state.frozen') {
232+
/**
233+
* @param {import('estree').Identifier} id
234+
* @param {import('estree').Expression} value
235+
*/
236+
const create_state_declarator = (id, value) => {
237+
const binding = /** @type {import('#compiler').Binding} */ (state.scope.get(id.name));
236238
if (should_proxy_or_freeze(value)) {
237-
value = b.call('$.proxy', value);
239+
value = b.call(rune === '$state' ? '$.proxy' : '$.freeze', value);
238240
}
239-
240-
if (!state.analysis.immutable || state.analysis.accessors || binding.reassigned) {
241+
if (is_state_source(binding, state)) {
241242
value = b.call('$.source', value);
242243
}
243-
} else if (rune === '$state.frozen') {
244-
const binding = /** @type {import('#compiler').Binding} */ (
245-
state.scope.get(declarator.id.name)
246-
);
247-
if (should_proxy_or_freeze(value)) {
248-
value = b.call('$.freeze', value);
249-
}
244+
return value;
245+
};
250246

251-
if (binding.reassigned) {
252-
value = b.call('$.source', value);
253-
}
247+
if (declarator.id.type === 'Identifier') {
248+
declarations.push(
249+
b.declarator(declarator.id, create_state_declarator(declarator.id, value))
250+
);
254251
} else {
255-
value = b.call('$.derived', b.thunk(value));
252+
const tmp = state.scope.generate('tmp');
253+
const paths = extract_paths(declarator.id);
254+
declarations.push(
255+
b.declarator(b.id(tmp), value),
256+
...paths.map((path) => {
257+
const value = path.expression?.(b.id(tmp));
258+
const binding = state.scope.get(
259+
/** @type {import('estree').Identifier} */ (path.node).name
260+
);
261+
return b.declarator(
262+
path.node,
263+
binding?.kind === 'state' || binding?.kind === 'frozen_state'
264+
? create_state_declarator(binding.node, value)
265+
: value
266+
);
267+
})
268+
);
256269
}
257-
258-
declarations.push(b.declarator(declarator.id, value));
259270
continue;
260271
}
261272

262273
if (rune === '$derived') {
263-
const bindings = state.scope.get_bindings(declarator);
264-
const id = state.scope.generate('derived_value');
265-
declarations.push(
266-
b.declarator(
267-
b.id(id),
268-
b.call(
269-
'$.derived',
270-
b.thunk(
271-
b.block([
272-
b.let(declarator.id, value),
273-
b.return(b.array(bindings.map((binding) => binding.node)))
274-
])
274+
if (declarator.id.type === 'Identifier') {
275+
declarations.push(b.declarator(declarator.id, b.call('$.derived', b.thunk(value))));
276+
} else {
277+
const bindings = state.scope.get_bindings(declarator);
278+
const id = state.scope.generate('derived_value');
279+
declarations.push(
280+
b.declarator(
281+
b.id(id),
282+
b.call(
283+
'$.derived',
284+
b.thunk(
285+
b.block([
286+
b.let(declarator.id, value),
287+
b.return(b.array(bindings.map((binding) => binding.node)))
288+
])
289+
)
275290
)
276291
)
277-
)
278-
);
279-
for (let i = 0; i < bindings.length; i++) {
280-
bindings[i].expression = b.member(b.call('$.get', b.id(id)), b.literal(i), true);
292+
);
293+
for (let i = 0; i < bindings.length; i++) {
294+
bindings[i].expression = b.member(b.call('$.get', b.id(id)), b.literal(i), true);
295+
}
281296
}
282297
continue;
283298
}
284-
285-
declarations.push(...create_state_declarators(declarator, state.scope, value));
286299
}
287300

288301
if (declarations.length === 0) {
Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,11 @@
11
import { test } from '../../test';
22

33
export default test({
4-
html: `<button>0</button>`,
4+
html: `<button>0 / 0</button>`,
55

6-
async test({ assert, target, window }) {
6+
async test({ assert, target }) {
77
const btn = target.querySelector('button');
8-
const clickEvent = new window.Event('click', { bubbles: true });
9-
await btn?.dispatchEvent(clickEvent);
10-
11-
assert.htmlEqual(target.innerHTML, `<button>1</button>`);
8+
await btn?.click();
9+
assert.htmlEqual(target.innerHTML, `<button>1 / 1</button>`);
1210
}
1311
});

packages/svelte/tests/runtime-runes/samples/ambiguous-source/main.svelte

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import { setup } from './utils.js';
33
44
let { num } = $state(setup());
5+
let { num: num_frozen } = $state(setup());
56
</script>
67

7-
<button on:click={() => num++}>{num}</button>
8+
<button on:click={() => { num++; num_frozen++; }}>{num} / {num_frozen}</button>
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { test } from '../../test';
2+
3+
export default test({
4+
async test({ assert, target }) {
5+
assert.htmlEqual(target.innerHTML, `0 0 <button>0 / 0</button>`);
6+
const [btn] = target.querySelectorAll('button');
7+
8+
btn?.click();
9+
await Promise.resolve();
10+
assert.htmlEqual(target.innerHTML, '0 1 <button>0 / 1</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.increment()}>{sub?.count1.value} / {sub?.count2.value}</button>
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<script>
2+
export const count1 = $state.frozen({value: 0});
3+
export const count2 = $state({value: 0});
4+
5+
export function increment() {
6+
count2.value += 1;
7+
}
8+
9+
</script>
10+
11+
{count1.value}
12+
{count2.value}
13+
14+
<!-- so that count1/2 become sources -->
15+
<svelte:options accessors />

0 commit comments

Comments
 (0)