diff --git a/.changeset/odd-taxis-retire.md b/.changeset/odd-taxis-retire.md
new file mode 100644
index 000000000000..b60a450c3a87
--- /dev/null
+++ b/.changeset/odd-taxis-retire.md
@@ -0,0 +1,5 @@
+---
+"svelte": patch
+---
+
+fix: disallow exporting props, derived and reassigned state from within components
diff --git a/packages/svelte/src/compiler/errors.js b/packages/svelte/src/compiler/errors.js
index 4d6b91c007ed..414f5e3bd2fe 100644
--- a/packages/svelte/src/compiler/errors.js
+++ b/packages/svelte/src/compiler/errors.js
@@ -169,8 +169,12 @@ const runes = {
'invalid-legacy-export': () => `Cannot use \`export let\` in runes mode — use $props instead`,
/** @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`,
- 'invalid-derived-export': () => `Cannot export derived state`,
+ '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`,
+ '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`,
'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`,
diff --git a/packages/svelte/src/compiler/phases/2-analyze/validation.js b/packages/svelte/src/compiler/phases/2-analyze/validation.js
index 6a1aaf0fecce..1855079ef049 100644
--- a/packages/svelte/src/compiler/phases/2-analyze/validation.js
+++ b/packages/svelte/src/compiler/phases/2-analyze/validation.js
@@ -710,6 +710,10 @@ 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');
}
@@ -941,10 +945,20 @@ 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 }) {
+ ExportNamedDeclaration(node, { state, next }) {
if (node.declaration?.type !== 'VariableDeclaration') return;
- if (node.declaration.kind !== 'let') 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;
error(node, 'invalid-legacy-export');
},
ExportSpecifier(node, { state }) {
diff --git a/packages/svelte/tests/compiler-errors/samples/export-derived-state/_config.js b/packages/svelte/tests/compiler-errors/samples/export-derived-state/_config.js
index 1c171d19c08f..31ee52b2ba2b 100644
--- a/packages/svelte/tests/compiler-errors/samples/export-derived-state/_config.js
+++ b/packages/svelte/tests/compiler-errors/samples/export-derived-state/_config.js
@@ -3,7 +3,7 @@ import { test } from '../../test';
export default test({
error: {
code: 'invalid-derived-export',
- message: 'Cannot export derived state',
- position: [24, 66]
+ message:
+ 'Cannot export derived state. To expose the current derived value, export a function returning its value'
}
});
diff --git a/packages/svelte/tests/compiler-errors/samples/export-derived-state/main.svelte b/packages/svelte/tests/compiler-errors/samples/export-derived-state/main.svelte
new file mode 100644
index 000000000000..634022db6aca
--- /dev/null
+++ b/packages/svelte/tests/compiler-errors/samples/export-derived-state/main.svelte
@@ -0,0 +1,4 @@
+
diff --git a/packages/svelte/tests/compiler-errors/samples/export-state-2/_config.js b/packages/svelte/tests/compiler-errors/samples/export-state-2/_config.js
new file mode 100644
index 000000000000..09fc4498faca
--- /dev/null
+++ b/packages/svelte/tests/compiler-errors/samples/export-state-2/_config.js
@@ -0,0 +1,10 @@
+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]
+ }
+});
diff --git a/packages/svelte/tests/compiler-errors/samples/export-state-2/main.svelte b/packages/svelte/tests/compiler-errors/samples/export-state-2/main.svelte
new file mode 100644
index 000000000000..ed9877766457
--- /dev/null
+++ b/packages/svelte/tests/compiler-errors/samples/export-state-2/main.svelte
@@ -0,0 +1,15 @@
+
diff --git a/packages/svelte/tests/compiler-errors/samples/export-state/_config.js b/packages/svelte/tests/compiler-errors/samples/export-state/_config.js
index 5ddb2a859a9c..a8e8b4b25896 100644
--- a/packages/svelte/tests/compiler-errors/samples/export-state/_config.js
+++ b/packages/svelte/tests/compiler-errors/samples/export-state/_config.js
@@ -3,7 +3,8 @@ import { test } from '../../test';
export default test({
error: {
code: 'invalid-state-export',
- message: 'Cannot export state if it is reassigned',
+ 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: [46, 86]
}
});
diff --git a/packages/svelte/tests/compiler-errors/samples/runes-export-named-state/_config.js b/packages/svelte/tests/compiler-errors/samples/runes-export-named-state/_config.js
index a42a651141a9..1eac29db14bb 100644
--- a/packages/svelte/tests/compiler-errors/samples/runes-export-named-state/_config.js
+++ b/packages/svelte/tests/compiler-errors/samples/runes-export-named-state/_config.js
@@ -3,7 +3,8 @@ import { test } from '../../test';
export default test({
error: {
code: 'invalid-state-export',
- message: 'Cannot export state if it is reassigned',
+ 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: [28, 53]
}
});
diff --git a/packages/svelte/tests/compiler-errors/samples/runes-export-prop/_config.js b/packages/svelte/tests/compiler-errors/samples/runes-export-prop/_config.js
new file mode 100644
index 000000000000..ce8f97e0d14c
--- /dev/null
+++ b/packages/svelte/tests/compiler-errors/samples/runes-export-prop/_config.js
@@ -0,0 +1,9 @@
+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'
+ }
+});
diff --git a/packages/svelte/tests/compiler-errors/samples/runes-export-prop/main.svelte b/packages/svelte/tests/compiler-errors/samples/runes-export-prop/main.svelte
new file mode 100644
index 000000000000..3d3fd554fa73
--- /dev/null
+++ b/packages/svelte/tests/compiler-errors/samples/runes-export-prop/main.svelte
@@ -0,0 +1,4 @@
+