diff --git a/.changeset/two-ties-guess.md b/.changeset/two-ties-guess.md
new file mode 100644
index 000000000000..71fb25fd016b
--- /dev/null
+++ b/.changeset/two-ties-guess.md
@@ -0,0 +1,5 @@
+---
+'svelte': patch
+---
+
+breaking: make non-bindable props read-only
diff --git a/packages/svelte/messages/compile-errors/script.md b/packages/svelte/messages/compile-errors/script.md
index e84174e42a7d..aeda7f286497 100644
--- a/packages/svelte/messages/compile-errors/script.md
+++ b/packages/svelte/messages/compile-errors/script.md
@@ -6,6 +6,8 @@
> Cannot assign to %thing%
+> Cannot assign to %thing%. %suggestion%
+
## constant_binding
> Cannot bind to %thing%
diff --git a/packages/svelte/src/compiler/errors.js b/packages/svelte/src/compiler/errors.js
index f0fca61e9ac7..e2a69369d1ab 100644
--- a/packages/svelte/src/compiler/errors.js
+++ b/packages/svelte/src/compiler/errors.js
@@ -69,13 +69,14 @@ export function bindable_invalid_location(node) {
}
/**
- * Cannot assign to %thing%
+ * Cannot assign to %thing%. %suggestion%
* @param {null | number | NodeLike} node
* @param {string} thing
+ * @param {string | undefined | null} [suggestion]
* @returns {never}
*/
-export function constant_assignment(node, thing) {
- e(node, "constant_assignment", `Cannot assign to ${thing}`);
+export function constant_assignment(node, thing, suggestion) {
+ e(node, "constant_assignment", suggestion ? `Cannot assign to ${thing}. ${suggestion}` : `Cannot assign to ${thing}`);
}
/**
diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/utils.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/utils.js
index 0d3165e35592..cd2d270f03af 100644
--- a/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/utils.js
+++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/utils.js
@@ -23,6 +23,14 @@ export function validate_assignment(node, argument, state) {
e.constant_assignment(node, 'derived state');
}
+ if (binding?.kind === 'prop') {
+ e.constant_assignment(
+ node,
+ 'a non-bindable prop',
+ 'Use `$state.link(...)` to create a local copy of the value'
+ );
+ }
+
if (binding?.kind === 'each') {
e.each_item_invalid_assignment(node);
}
diff --git a/packages/svelte/tests/runtime-runes/samples/props-default-reactivity/Counter.svelte b/packages/svelte/tests/runtime-runes/samples/props-default-reactivity/Counter.svelte
index 7b0f28b2139b..e900f1ca7f72 100644
--- a/packages/svelte/tests/runtime-runes/samples/props-default-reactivity/Counter.svelte
+++ b/packages/svelte/tests/runtime-runes/samples/props-default-reactivity/Counter.svelte
@@ -14,7 +14,3 @@
-
-
diff --git a/packages/svelte/tests/runtime-runes/samples/props-default-reactivity/_config.js b/packages/svelte/tests/runtime-runes/samples/props-default-reactivity/_config.js
index 5eff145cb7f5..d32de5a70d4e 100644
--- a/packages/svelte/tests/runtime-runes/samples/props-default-reactivity/_config.js
+++ b/packages/svelte/tests/runtime-runes/samples/props-default-reactivity/_config.js
@@ -6,11 +6,10 @@ export default test({
-
`,
async test({ assert, target }) {
- const [btn1, btn2, btn3, btn4] = target.querySelectorAll('button');
+ const [btn1, btn2, btn3] = target.querySelectorAll('button');
flushSync(() => {
btn1?.click();
@@ -22,7 +21,6 @@ export default test({
-
`
);
@@ -36,7 +34,6 @@ export default test({
-
`
);
@@ -50,7 +47,6 @@ export default test({
-
`
);
@@ -64,35 +60,6 @@ export default test({
-
- `
- );
-
- flushSync(() => {
- btn4?.click();
- });
-
- assert.htmlEqual(
- target.innerHTML,
- `
-
-
-
-
- `
- );
-
- flushSync(() => {
- btn3?.click();
- });
-
- assert.htmlEqual(
- target.innerHTML,
- `
-
-
-
-
`
);
}