diff --git a/.changeset/proud-donuts-tickle.md b/.changeset/proud-donuts-tickle.md
new file mode 100644
index 000000000..68a2fbaf0
--- /dev/null
+++ b/.changeset/proud-donuts-tickle.md
@@ -0,0 +1,5 @@
+---
+'eslint-plugin-svelte': minor
+---
+
+New svelte/rune-prefer-let rule
diff --git a/README.md b/README.md
index 9a6c25600..b5bf563a5 100644
--- a/README.md
+++ b/README.md
@@ -430,6 +430,7 @@ These rules relate to better ways of doing things to help you avoid problems:
| [svelte/require-event-dispatcher-types](https://sveltejs.github.io/eslint-plugin-svelte/rules/require-event-dispatcher-types/) | require type parameters for `createEventDispatcher` | |
| [svelte/require-optimized-style-attribute](https://sveltejs.github.io/eslint-plugin-svelte/rules/require-optimized-style-attribute/) | require style attributes that can be optimized | |
| [svelte/require-stores-init](https://sveltejs.github.io/eslint-plugin-svelte/rules/require-stores-init/) | require initial value in store | |
+| [svelte/rune-prefer-let](https://sveltejs.github.io/eslint-plugin-svelte/rules/rune-prefer-let/) | use let instead of const for reactive variables created by runes | :wrench: |
| [svelte/valid-each-key](https://sveltejs.github.io/eslint-plugin-svelte/rules/valid-each-key/) | enforce keys to use variables defined in the `{#each}` block | |
## Stylistic Issues
diff --git a/docs/rules.md b/docs/rules.md
index 7f115da5c..2412386c8 100644
--- a/docs/rules.md
+++ b/docs/rules.md
@@ -67,6 +67,7 @@ These rules relate to better ways of doing things to help you avoid problems:
| [svelte/require-event-dispatcher-types](./rules/require-event-dispatcher-types.md) | require type parameters for `createEventDispatcher` | |
| [svelte/require-optimized-style-attribute](./rules/require-optimized-style-attribute.md) | require style attributes that can be optimized | |
| [svelte/require-stores-init](./rules/require-stores-init.md) | require initial value in store | |
+| [svelte/rune-prefer-let](./rules/rune-prefer-let.md) | use let instead of const for reactive variables created by runes | :wrench: |
| [svelte/valid-each-key](./rules/valid-each-key.md) | enforce keys to use variables defined in the `{#each}` block | |
## Stylistic Issues
diff --git a/docs/rules/rune-prefer-let.md b/docs/rules/rune-prefer-let.md
new file mode 100644
index 000000000..502d74fc1
--- /dev/null
+++ b/docs/rules/rune-prefer-let.md
@@ -0,0 +1,50 @@
+---
+pageClass: 'rule-details'
+sidebarDepth: 0
+title: 'svelte/rune-prefer-let'
+description: 'use let instead of const for reactive variables created by runes'
+---
+
+# svelte/rune-prefer-let
+
+> use let instead of const for reactive variables created by runes
+
+- :exclamation: **_This rule has not been released yet._**
+- :wrench: The `--fix` option on the [command line](https://eslint.org/docs/user-guide/command-line-interface#fixing-problems) can automatically fix some of the problems reported by this rule.
+
+## :book: Rule Details
+
+This rule reports whenever a rune that creates a reactive value is assigned to a const.
+In JavaScript `const` are defined as immutable references which cannot be reassigned.
+Reactive variables can be reassigned by Svelte's reactivity system.
+
+
+
+
+
+```svelte
+
+```
+
+
+
+## :wrench: Options
+
+Nothing
+
+## :mag: Implementation
+
+- [Rule source](https://github.com/sveltejs/eslint-plugin-svelte/blob/main/src/rules/rune-prefer-let.ts)
+- [Test source](https://github.com/sveltejs/eslint-plugin-svelte/blob/main/tests/src/rules/rune-prefer-let.ts)
diff --git a/packages/eslint-plugin-svelte/src/rule-types.ts b/packages/eslint-plugin-svelte/src/rule-types.ts
index 77d3c4ca4..1e65a9e90 100644
--- a/packages/eslint-plugin-svelte/src/rule-types.ts
+++ b/packages/eslint-plugin-svelte/src/rule-types.ts
@@ -289,6 +289,11 @@ export interface RuleOptions {
* @see https://sveltejs.github.io/eslint-plugin-svelte/rules/require-stores-init/
*/
'svelte/require-stores-init'?: Linter.RuleEntry<[]>
+ /**
+ * use let instead of const for reactive variables created by runes
+ * @see https://sveltejs.github.io/eslint-plugin-svelte/rules/rune-prefer-let/
+ */
+ 'svelte/rune-prefer-let'?: Linter.RuleEntry<[]>
/**
* enforce use of shorthand syntax in attribute
* @see https://sveltejs.github.io/eslint-plugin-svelte/rules/shorthand-attribute/
diff --git a/packages/eslint-plugin-svelte/src/rules/rune-prefer-let.ts b/packages/eslint-plugin-svelte/src/rules/rune-prefer-let.ts
new file mode 100644
index 000000000..4e552ebc2
--- /dev/null
+++ b/packages/eslint-plugin-svelte/src/rules/rune-prefer-let.ts
@@ -0,0 +1,65 @@
+import type { TSESTree } from '@typescript-eslint/types';
+import { createRule } from '../utils';
+
+export default createRule('rune-prefer-let', {
+ meta: {
+ docs: {
+ description: 'use let instead of const for reactive variables created by runes',
+ category: 'Best Practices',
+ recommended: false
+ },
+ schema: [],
+ messages: {
+ useLet: "const is used for a reactive variable from {{rune}}. Use 'let' instead."
+ },
+ type: 'suggestion',
+ fixable: 'code'
+ },
+ create(context) {
+ function preferLet(node: TSESTree.VariableDeclaration, rune: string) {
+ if (node.kind !== 'const') {
+ return;
+ }
+ context.report({
+ node,
+ messageId: 'useLet',
+ data: { rune },
+ fix: (fixer) => fixer.replaceTextRange([node.range[0], node.range[0] + 5], 'let')
+ });
+ }
+
+ return {
+ 'VariableDeclaration > VariableDeclarator > CallExpression > Identifier'(
+ node: TSESTree.Identifier
+ ) {
+ if (['$props', '$derived', '$state'].includes(node.name)) {
+ preferLet(node.parent.parent?.parent as TSESTree.VariableDeclaration, `${node.name}()`);
+ }
+ },
+ 'VariableDeclaration > VariableDeclarator > CallExpression > MemberExpression > Identifier'(
+ node: TSESTree.Identifier
+ ) {
+ if (
+ node.name === 'by' &&
+ ((node.parent as TSESTree.MemberExpression).object as TSESTree.Identifier).name ===
+ '$derived'
+ ) {
+ preferLet(
+ node.parent.parent?.parent?.parent as TSESTree.VariableDeclaration,
+ '$derived.by()'
+ );
+ }
+ if (
+ node.name === 'frozen' &&
+ ((node.parent as TSESTree.MemberExpression).object as TSESTree.Identifier).name ===
+ '$state'
+ ) {
+ preferLet(
+ node.parent.parent?.parent?.parent as TSESTree.VariableDeclaration,
+ '$state.frozen()'
+ );
+ }
+ }
+ };
+ }
+});
diff --git a/packages/eslint-plugin-svelte/src/utils/rules.ts b/packages/eslint-plugin-svelte/src/utils/rules.ts
index af0dd20e6..17f168121 100644
--- a/packages/eslint-plugin-svelte/src/utils/rules.ts
+++ b/packages/eslint-plugin-svelte/src/utils/rules.ts
@@ -57,6 +57,7 @@ import requireOptimizedStyleAttribute from '../rules/require-optimized-style-att
import requireStoreCallbacksUseSetParam from '../rules/require-store-callbacks-use-set-param';
import requireStoreReactiveAccess from '../rules/require-store-reactive-access';
import requireStoresInit from '../rules/require-stores-init';
+import runePreferLet from '../rules/rune-prefer-let';
import shorthandAttribute from '../rules/shorthand-attribute';
import shorthandDirective from '../rules/shorthand-directive';
import sortAttributes from '../rules/sort-attributes';
@@ -122,6 +123,7 @@ export const rules = [
requireStoreCallbacksUseSetParam,
requireStoreReactiveAccess,
requireStoresInit,
+ runePreferLet,
shorthandAttribute,
shorthandDirective,
sortAttributes,
diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/rune-prefer-let/invalid/test01-errors.yaml b/packages/eslint-plugin-svelte/tests/fixtures/rules/rune-prefer-let/invalid/test01-errors.yaml
new file mode 100644
index 000000000..43caffafb
--- /dev/null
+++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/rune-prefer-let/invalid/test01-errors.yaml
@@ -0,0 +1,20 @@
+- message: const is used for a reactive variable from $props(). Use 'let' instead.
+ line: 2
+ column: 2
+ suggestions: null
+- message: const is used for a reactive variable from $state(). Use 'let' instead.
+ line: 4
+ column: 2
+ suggestions: null
+- message: const is used for a reactive variable from $state.frozen(). Use 'let' instead.
+ line: 5
+ column: 2
+ suggestions: null
+- message: const is used for a reactive variable from $derived(). Use 'let' instead.
+ line: 7
+ column: 2
+ suggestions: null
+- message: const is used for a reactive variable from $derived.by(). Use 'let' instead.
+ line: 8
+ column: 2
+ suggestions: null
diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/rune-prefer-let/invalid/test01-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/rune-prefer-let/invalid/test01-input.svelte
new file mode 100644
index 000000000..942055dce
--- /dev/null
+++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/rune-prefer-let/invalid/test01-input.svelte
@@ -0,0 +1,9 @@
+
diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/rune-prefer-let/invalid/test01-output.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/rune-prefer-let/invalid/test01-output.svelte
new file mode 100644
index 000000000..ad50f1d6e
--- /dev/null
+++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/rune-prefer-let/invalid/test01-output.svelte
@@ -0,0 +1,9 @@
+
diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/rune-prefer-let/valid/test01-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/rune-prefer-let/valid/test01-input.svelte
new file mode 100644
index 000000000..ad50f1d6e
--- /dev/null
+++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/rune-prefer-let/valid/test01-input.svelte
@@ -0,0 +1,9 @@
+
diff --git a/packages/eslint-plugin-svelte/tests/src/rules/rune-prefer-let.ts b/packages/eslint-plugin-svelte/tests/src/rules/rune-prefer-let.ts
new file mode 100644
index 000000000..a7e923a3d
--- /dev/null
+++ b/packages/eslint-plugin-svelte/tests/src/rules/rune-prefer-let.ts
@@ -0,0 +1,12 @@
+import { RuleTester } from '../../utils/eslint-compat';
+import rule from '../../../src/rules/rune-prefer-let';
+import { loadTestCases } from '../../utils/utils';
+
+const tester = new RuleTester({
+ languageOptions: {
+ ecmaVersion: 2020,
+ sourceType: 'module'
+ }
+});
+
+tester.run('rune-prefer-let', rule as any, loadTestCases('rune-prefer-let'));