feat: allow state/derived/props to be explicitly exported from components (#10523)

* Revert "fix: stricter validation for component exports (#10430)"

This reverts commit dab0a43693.

* 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 <rich.harris@vercel.com>
pull/10569/head
Rich Harris 7 months ago committed by GitHub
parent ad2b8b9112
commit 02c6176622
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,5 @@
---
'svelte': patch
---
feat: allow state/derived/props to be explicitly exported from components

@ -171,11 +171,9 @@ const runes = {
/** @param {string} rune */
'invalid-rune-usage': (rune) => `Cannot use ${rune} rune in non-runes mode`,
'invalid-state-export': () =>
`Cannot export state if it is reassigned. Either export a function returning the state value or only mutate the state value's properties`,
`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`,
'invalid-derived-export': () =>
`Cannot export derived state. To expose the current derived value, export a function returning its value`,
'invalid-prop-export': () =>
`Cannot export properties. To expose the current value of a property, export a function returning its value`,
`Cannot export derived state from a module. To expose the current derived value, export a function returning its value`,
'invalid-props-id': () => `$props() can only be used with an object destructuring pattern`,
'invalid-props-pattern': () =>
`$props() assignment must not contain nested properties or computed keys`,

@ -733,10 +733,6 @@ function validate_export(node, scope, name) {
const binding = scope.get(name);
if (!binding) return;
if (binding.kind === 'prop') {
error(node, 'invalid-prop-export');
}
if (binding.kind === 'derived') {
error(node, 'invalid-derived-export');
}
@ -964,25 +960,12 @@ export const validation_runes = merge(validation, a11y_validators, {
if (node.label.name !== '$' || path.at(-1)?.type !== 'Program') return;
error(node, 'invalid-legacy-reactive-statement');
},
ExportNamedDeclaration(node, { state, next }) {
ExportNamedDeclaration(node, { state }) {
if (node.declaration?.type !== 'VariableDeclaration') return;
// visit children, so bindings are correctly initialised
next();
for (const declarator of node.declaration.declarations) {
for (const id of extract_identifiers(declarator.id)) {
validate_export(node, state.scope, id.name);
}
}
if (state.analysis.instance.scope !== state.scope) return;
if (node.declaration.kind !== 'let') return;
if (state.analysis.instance.scope !== state.scope) return;
error(node, 'invalid-legacy-export');
},
ExportSpecifier(node, { state }) {
validate_export(node, state.scope, node.local.name);
},
CallExpression(node, { state, path }) {
validate_call_expression(node, state.scope, path);
},

@ -225,28 +225,24 @@ export function client_component(source, analysis, options) {
// Bind static exports to props so that people can access them with bind:x
const static_bindings = analysis.exports.map(({ name, alias }) => {
const binding = analysis.instance.scope.get(name);
return b.stmt(
b.call(
'$.bind_prop',
b.id('$$props'),
b.literal(alias ?? name),
binding?.kind === 'state' || binding?.kind === 'frozen_state'
? b.call('$.get', b.id(name))
: b.id(name)
serialize_get_binding(b.id(name), instance_state)
)
);
});
const properties = analysis.exports.map(({ name, alias }) => {
const binding = analysis.instance.scope.get(name);
const is_source = binding !== null && is_state_source(binding, state);
const expression = serialize_get_binding(b.id(name), instance_state);
if (is_source || options.dev) {
return b.get(alias ?? name, [b.return(is_source ? b.call('$.get', b.id(name)) : b.id(name))]);
if (expression.type === 'Identifier' && !options.dev) {
return b.init(alias ?? name, expression);
}
return b.init(alias ?? name, b.id(name));
return b.get(alias ?? name, [b.return(expression)]);
});
if (analysis.accessors) {

@ -4,6 +4,7 @@ export default test({
error: {
code: 'invalid-derived-export',
message:
'Cannot export derived state. To expose the current derived value, export a function returning its value'
'Cannot export derived state from a module. To expose the current derived value, export a function returning its value',
position: [24, 66]
}
});

@ -1,4 +0,0 @@
<script>
let count = $state(0);
export const double = $derived(count * 2);
</script>

@ -1,10 +0,0 @@
import { test } from '../../test';
export default test({
error: {
code: 'invalid-state-export',
message:
"Cannot export state if it is reassigned. Either export a function returning the state value or only mutate the state value's properties",
position: [59, 99]
}
});

@ -1,15 +0,0 @@
<script>
export const object = $state({
ok: true
});
export const primitive = $state('nope');
export function update_object() {
object.ok = !object.ok;
}
export function update_primitive() {
primitive = 'yep';
}
</script>

@ -4,7 +4,7 @@ export default test({
error: {
code: 'invalid-state-export',
message:
"Cannot export state if it is reassigned. Either export a function returning the state value or only mutate the state value's properties",
"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",
position: [46, 86]
}
});

@ -4,7 +4,7 @@ export default test({
error: {
code: 'invalid-state-export',
message:
"Cannot export state if it is reassigned. Either export a function returning the state value or only mutate the state value's properties",
"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",
position: [28, 53]
}
});

@ -1,9 +0,0 @@
import { test } from '../../test';
export default test({
error: {
code: 'invalid-prop-export',
message:
'Cannot export properties. To expose the current value of a property, export a function returning its value'
}
});

@ -1,4 +0,0 @@
<script>
let { foo } = $props();
export { foo };
</script>

@ -7,6 +7,6 @@ export default test({
btn?.click();
await Promise.resolve();
assert.htmlEqual(target.innerHTML, '0 1 <button>0 / 1</button>');
assert.htmlEqual(target.innerHTML, '1 2 <button>1 / 2</button>');
}
});

@ -4,4 +4,4 @@
</script>
<Sub bind:this={sub} />
<button on:click={() => sub.increment()}>{sub?.count1.value} / {sub?.count2.value}</button>
<button on:click={() => sub.increment()}>{sub?.count} / {sub?.doubled}</button>

@ -0,0 +1,13 @@
<script>
let count = $state(0);
let doubled = $derived(count * 2);
export { count, doubled };
export function increment() {
count += 1;
}
</script>
{count}
{doubled}

@ -1,15 +0,0 @@
<script>
export const count1 = $state.frozen({value: 0});
export const count2 = $state({value: 0});
export function increment() {
count2.value += 1;
}
</script>
{count1.value}
{count2.value}
<!-- so that count1/2 become sources -->
<svelte:options accessors />
Loading…
Cancel
Save