Skip to content

Commit 02c6176

Browse files
authored
feat: allow state/derived/props to be explicitly exported from components (#10523)
* Revert "fix: stricter validation for component exports (#10430)" This reverts commit dab0a43. * dont remove old changeset * changeset * tweak error messages * make component-exported state work * consistency * fix * fix * update messages * update tests --------- Co-authored-by: Rich Harris <[email protected]>
1 parent ad2b8b9 commit 02c6176

File tree

22 files changed

+33
-94
lines changed

22 files changed

+33
-94
lines changed

.changeset/red-feet-worry.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: allow state/derived/props to be explicitly exported from components

packages/svelte/src/compiler/errors.js

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -171,11 +171,9 @@ const runes = {
171171
/** @param {string} rune */
172172
'invalid-rune-usage': (rune) => `Cannot use ${rune} rune in non-runes mode`,
173173
'invalid-state-export': () =>
174-
`Cannot export state if it is reassigned. Either export a function returning the state value or only mutate the state value's properties`,
174+
`Cannot export state from a module if it is reassigned. Either export a function returning the state value or only mutate the state value's properties`,
175175
'invalid-derived-export': () =>
176-
`Cannot export derived state. To expose the current derived value, export a function returning its value`,
177-
'invalid-prop-export': () =>
178-
`Cannot export properties. To expose the current value of a property, export a function returning its value`,
176+
`Cannot export derived state from a module. To expose the current derived value, export a function returning its value`,
179177
'invalid-props-id': () => `$props() can only be used with an object destructuring pattern`,
180178
'invalid-props-pattern': () =>
181179
`$props() assignment must not contain nested properties or computed keys`,

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

Lines changed: 2 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -733,10 +733,6 @@ function validate_export(node, scope, name) {
733733
const binding = scope.get(name);
734734
if (!binding) return;
735735

736-
if (binding.kind === 'prop') {
737-
error(node, 'invalid-prop-export');
738-
}
739-
740736
if (binding.kind === 'derived') {
741737
error(node, 'invalid-derived-export');
742738
}
@@ -964,25 +960,12 @@ export const validation_runes = merge(validation, a11y_validators, {
964960
if (node.label.name !== '$' || path.at(-1)?.type !== 'Program') return;
965961
error(node, 'invalid-legacy-reactive-statement');
966962
},
967-
ExportNamedDeclaration(node, { state, next }) {
963+
ExportNamedDeclaration(node, { state }) {
968964
if (node.declaration?.type !== 'VariableDeclaration') return;
969-
970-
// visit children, so bindings are correctly initialised
971-
next();
972-
973-
for (const declarator of node.declaration.declarations) {
974-
for (const id of extract_identifiers(declarator.id)) {
975-
validate_export(node, state.scope, id.name);
976-
}
977-
}
978-
979-
if (state.analysis.instance.scope !== state.scope) return;
980965
if (node.declaration.kind !== 'let') return;
966+
if (state.analysis.instance.scope !== state.scope) return;
981967
error(node, 'invalid-legacy-export');
982968
},
983-
ExportSpecifier(node, { state }) {
984-
validate_export(node, state.scope, node.local.name);
985-
},
986969
CallExpression(node, { state, path }) {
987970
validate_call_expression(node, state.scope, path);
988971
},

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

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -225,28 +225,24 @@ export function client_component(source, analysis, options) {
225225

226226
// Bind static exports to props so that people can access them with bind:x
227227
const static_bindings = analysis.exports.map(({ name, alias }) => {
228-
const binding = analysis.instance.scope.get(name);
229228
return b.stmt(
230229
b.call(
231230
'$.bind_prop',
232231
b.id('$$props'),
233232
b.literal(alias ?? name),
234-
binding?.kind === 'state' || binding?.kind === 'frozen_state'
235-
? b.call('$.get', b.id(name))
236-
: b.id(name)
233+
serialize_get_binding(b.id(name), instance_state)
237234
)
238235
);
239236
});
240237

241238
const properties = analysis.exports.map(({ name, alias }) => {
242-
const binding = analysis.instance.scope.get(name);
243-
const is_source = binding !== null && is_state_source(binding, state);
239+
const expression = serialize_get_binding(b.id(name), instance_state);
244240

245-
if (is_source || options.dev) {
246-
return b.get(alias ?? name, [b.return(is_source ? b.call('$.get', b.id(name)) : b.id(name))]);
241+
if (expression.type === 'Identifier' && !options.dev) {
242+
return b.init(alias ?? name, expression);
247243
}
248244

249-
return b.init(alias ?? name, b.id(name));
245+
return b.get(alias ?? name, [b.return(expression)]);
250246
});
251247

252248
if (analysis.accessors) {

packages/svelte/tests/compiler-errors/samples/export-derived-state/_config.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ export default test({
44
error: {
55
code: 'invalid-derived-export',
66
message:
7-
'Cannot export derived state. To expose the current derived value, export a function returning its value'
7+
'Cannot export derived state from a module. To expose the current derived value, export a function returning its value',
8+
position: [24, 66]
89
}
910
});

packages/svelte/tests/compiler-errors/samples/export-derived-state/main.svelte

Lines changed: 0 additions & 4 deletions
This file was deleted.

packages/svelte/tests/compiler-errors/samples/export-state-2/_config.js

Lines changed: 0 additions & 10 deletions
This file was deleted.

packages/svelte/tests/compiler-errors/samples/export-state-2/main.svelte

Lines changed: 0 additions & 15 deletions
This file was deleted.

packages/svelte/tests/compiler-errors/samples/export-state/_config.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ export default test({
44
error: {
55
code: 'invalid-state-export',
66
message:
7-
"Cannot export state if it is reassigned. Either export a function returning the state value or only mutate the state value's properties",
7+
"Cannot export state from a module if it is reassigned. Either export a function returning the state value or only mutate the state value's properties",
88
position: [46, 86]
99
}
1010
});

packages/svelte/tests/compiler-errors/samples/runes-export-named-state/_config.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ export default test({
44
error: {
55
code: 'invalid-state-export',
66
message:
7-
"Cannot export state if it is reassigned. Either export a function returning the state value or only mutate the state value's properties",
7+
"Cannot export state from a module if it is reassigned. Either export a function returning the state value or only mutate the state value's properties",
88
position: [28, 53]
99
}
1010
});

packages/svelte/tests/compiler-errors/samples/runes-export-prop/_config.js

Lines changed: 0 additions & 9 deletions
This file was deleted.

packages/svelte/tests/compiler-errors/samples/runes-export-prop/main.svelte

Lines changed: 0 additions & 4 deletions
This file was deleted.

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,6 @@ export default test({
77

88
btn?.click();
99
await Promise.resolve();
10-
assert.htmlEqual(target.innerHTML, '0 1 <button>0 / 1</button>');
10+
assert.htmlEqual(target.innerHTML, '1 2 <button>1 / 2</button>');
1111
}
1212
});

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,4 @@
44
</script>
55

66
<Sub bind:this={sub} />
7-
<button on:click={() => sub.increment()}>{sub?.count1.value} / {sub?.count2.value}</button>
7+
<button on:click={() => sub.increment()}>{sub?.count} / {sub?.doubled}</button>
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<script>
2+
let count = $state(0);
3+
let doubled = $derived(count * 2);
4+
5+
export { count, doubled };
6+
7+
export function increment() {
8+
count += 1;
9+
}
10+
</script>
11+
12+
{count}
13+
{doubled}

packages/svelte/tests/runtime-runes/samples/exports3/sub.svelte

Lines changed: 0 additions & 15 deletions
This file was deleted.

0 commit comments

Comments
 (0)