diff --git a/.changeset/red-feet-worry.md b/.changeset/red-feet-worry.md
new file mode 100644
index 000000000000..00caaf81582e
--- /dev/null
+++ b/.changeset/red-feet-worry.md
@@ -0,0 +1,5 @@
+---
+'svelte': patch
+---
+
+feat: allow state/derived/props to be explicitly exported from components
diff --git a/packages/svelte/src/compiler/errors.js b/packages/svelte/src/compiler/errors.js
index 050e7ec43283..f6f70ec3e39e 100644
--- a/packages/svelte/src/compiler/errors.js
+++ b/packages/svelte/src/compiler/errors.js
@@ -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`,
diff --git a/packages/svelte/src/compiler/phases/2-analyze/validation.js b/packages/svelte/src/compiler/phases/2-analyze/validation.js
index 959f50874299..fbf6392fba8c 100644
--- a/packages/svelte/src/compiler/phases/2-analyze/validation.js
+++ b/packages/svelte/src/compiler/phases/2-analyze/validation.js
@@ -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);
},
diff --git a/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js b/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js
index 80628cc2f2d4..139df0afea5b 100644
--- a/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js
+++ b/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js
@@ -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) {
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 31ee52b2ba2b..fd7049583611 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
@@ -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]
}
});
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
deleted file mode 100644
index 634022db6aca..000000000000
--- a/packages/svelte/tests/compiler-errors/samples/export-derived-state/main.svelte
+++ /dev/null
@@ -1,4 +0,0 @@
-
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
deleted file mode 100644
index 09fc4498faca..000000000000
--- a/packages/svelte/tests/compiler-errors/samples/export-state-2/_config.js
+++ /dev/null
@@ -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]
- }
-});
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
deleted file mode 100644
index ed9877766457..000000000000
--- a/packages/svelte/tests/compiler-errors/samples/export-state-2/main.svelte
+++ /dev/null
@@ -1,15 +0,0 @@
-
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 a8e8b4b25896..1f7bd0f76735 100644
--- a/packages/svelte/tests/compiler-errors/samples/export-state/_config.js
+++ b/packages/svelte/tests/compiler-errors/samples/export-state/_config.js
@@ -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]
}
});
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 1eac29db14bb..fb1be5730bcf 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
@@ -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]
}
});
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
deleted file mode 100644
index ce8f97e0d14c..000000000000
--- a/packages/svelte/tests/compiler-errors/samples/runes-export-prop/_config.js
+++ /dev/null
@@ -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'
- }
-});
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
deleted file mode 100644
index 3d3fd554fa73..000000000000
--- a/packages/svelte/tests/compiler-errors/samples/runes-export-prop/main.svelte
+++ /dev/null
@@ -1,4 +0,0 @@
-
diff --git a/packages/svelte/tests/runtime-runes/samples/exports1/_config.js b/packages/svelte/tests/runtime-runes/samples/exports-1/_config.js
similarity index 100%
rename from packages/svelte/tests/runtime-runes/samples/exports1/_config.js
rename to packages/svelte/tests/runtime-runes/samples/exports-1/_config.js
diff --git a/packages/svelte/tests/runtime-runes/samples/exports1/main.svelte b/packages/svelte/tests/runtime-runes/samples/exports-1/main.svelte
similarity index 100%
rename from packages/svelte/tests/runtime-runes/samples/exports1/main.svelte
rename to packages/svelte/tests/runtime-runes/samples/exports-1/main.svelte
diff --git a/packages/svelte/tests/runtime-runes/samples/exports1/sub.svelte b/packages/svelte/tests/runtime-runes/samples/exports-1/sub.svelte
similarity index 100%
rename from packages/svelte/tests/runtime-runes/samples/exports1/sub.svelte
rename to packages/svelte/tests/runtime-runes/samples/exports-1/sub.svelte
diff --git a/packages/svelte/tests/runtime-runes/samples/exports2/_config.js b/packages/svelte/tests/runtime-runes/samples/exports-2/_config.js
similarity index 100%
rename from packages/svelte/tests/runtime-runes/samples/exports2/_config.js
rename to packages/svelte/tests/runtime-runes/samples/exports-2/_config.js
diff --git a/packages/svelte/tests/runtime-runes/samples/exports2/main.svelte b/packages/svelte/tests/runtime-runes/samples/exports-2/main.svelte
similarity index 100%
rename from packages/svelte/tests/runtime-runes/samples/exports2/main.svelte
rename to packages/svelte/tests/runtime-runes/samples/exports-2/main.svelte
diff --git a/packages/svelte/tests/runtime-runes/samples/exports2/sub.svelte b/packages/svelte/tests/runtime-runes/samples/exports-2/sub.svelte
similarity index 100%
rename from packages/svelte/tests/runtime-runes/samples/exports2/sub.svelte
rename to packages/svelte/tests/runtime-runes/samples/exports-2/sub.svelte
diff --git a/packages/svelte/tests/runtime-runes/samples/exports3/_config.js b/packages/svelte/tests/runtime-runes/samples/exports-3/_config.js
similarity index 80%
rename from packages/svelte/tests/runtime-runes/samples/exports3/_config.js
rename to packages/svelte/tests/runtime-runes/samples/exports-3/_config.js
index d56d3b306d65..6eb043db3dea 100644
--- a/packages/svelte/tests/runtime-runes/samples/exports3/_config.js
+++ b/packages/svelte/tests/runtime-runes/samples/exports-3/_config.js
@@ -7,6 +7,6 @@ export default test({
btn?.click();
await Promise.resolve();
- assert.htmlEqual(target.innerHTML, '0 1 ');
+ assert.htmlEqual(target.innerHTML, '1 2 ');
}
});
diff --git a/packages/svelte/tests/runtime-runes/samples/exports3/main.svelte b/packages/svelte/tests/runtime-runes/samples/exports-3/main.svelte
similarity index 51%
rename from packages/svelte/tests/runtime-runes/samples/exports3/main.svelte
rename to packages/svelte/tests/runtime-runes/samples/exports-3/main.svelte
index ea5c63234398..b1cabb399fa9 100644
--- a/packages/svelte/tests/runtime-runes/samples/exports3/main.svelte
+++ b/packages/svelte/tests/runtime-runes/samples/exports-3/main.svelte
@@ -4,4 +4,4 @@
-
+
diff --git a/packages/svelte/tests/runtime-runes/samples/exports-3/sub.svelte b/packages/svelte/tests/runtime-runes/samples/exports-3/sub.svelte
new file mode 100644
index 000000000000..53fad513d4f3
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/exports-3/sub.svelte
@@ -0,0 +1,13 @@
+
+
+{count}
+{doubled}
diff --git a/packages/svelte/tests/runtime-runes/samples/exports3/sub.svelte b/packages/svelte/tests/runtime-runes/samples/exports3/sub.svelte
deleted file mode 100644
index 37de78094a22..000000000000
--- a/packages/svelte/tests/runtime-runes/samples/exports3/sub.svelte
+++ /dev/null
@@ -1,15 +0,0 @@
-
-
-{count1.value}
-{count2.value}
-
-
-