Repository: sveltejs/eslint-plugin-svelte
Pull request #933 (2024-11-28 10:11:53)

feat(prefer-const): add rule
Adds a new rule named `prefer-const` which:

- Overrides the ESLint `prefer-const rule to support Svelte syntax, as discussed
  in #818.
- ~Ports~ Proxies the original `prefer-const` rule from ESLint, adapting it for Svelte.

~This PR is an updated version of #816, which did not include types. Also, some
opinionated changes were made in terms of code style, not in terms of behavior.~
Avoided re-implemeting the core rule, by using the `getCoreRule` and proxying the
default implementation.

I found that this rule could be very useful for Svelte, and there could be many
developers who would benefit from it. ~At the same time, I thought that the
initial implementation was a good starting point, and that adding types and
would make the rule more robust to changes.~  
However, the rule should not have many changes, as how variables are handled in
JS is not likely to change.

> Acknowledgement: this rule is based on the `prefer-const` rule from ESLint and
> the initial work done by @bfanger.


changeset-bot[bot]: ###  🦋  Changeset detected

Latest commit: 3cb3d449e3a74400333e2aaa9c1f96f0e714b260

**The changes in this PR will be included in the next version bump.**

<details><summary>This PR includes changesets to release 1 package</summary>

  | Name                 | Type  |
| -------------------- | ----- |
| eslint-plugin-svelte | Minor |

</details>

Not sure what this means? [Click here  to learn what changesets are](https://github.com/changesets/changesets/blob/main/docs/adding-a-changeset.md).

[Click here if you're a maintainer who wants to add another changeset to this PR](https://github.com/mikededo/eslint-plugin-svelte/new/feat/add-prefer-const-rule?filename=.changeset/tender-moons-sing.md&value=---%0A%22eslint-plugin-svelte%22%3A%20patch%0A---%0A%0Afeat(prefer-const)%3A%20add%20rule%0A)


ota-meshi: Thanks for working on implementing this rule!
However, could you please implement it using `getCoreRule()` in the same way as `no-inner-declarations`?
https://github.com/sveltejs/eslint-plugin-svelte/blob/main/packages/eslint-plugin-svelte/src/rules/no-inner-declarations.ts

I think it just avoids calling listeners on `VariableDeclaration` that use runes.
mikededo: Thanks @ota-meshi for pointing this out, my initial idea was to use a proxy for the rule, to avoid having a complete re-write. Should've asked before spending so many hours 😅 🤦🏼 

Will look into it. Maybe I'll end up closing this PR in favour of another one.
mikededo: Hey @ota-meshi, would it be possible to have another review when possible? 🎅🏼 

Patch:
diff --git a/.changeset/blue-panthers-run.md b/.changeset/blue-panthers-run.md
new file mode 100644
index 0000000..afb628d
--- /dev/null
+++ b/.changeset/blue-panthers-run.md
@@ -0,0 +1,5 @@
+---
+'eslint-plugin-svelte': patch
+---
+
+fix: support each blocks without an item
diff --git a/.changeset/green-squids-compete.md b/.changeset/green-squids-compete.md
new file mode 100644
index 0000000..7666a2a
--- /dev/null
+++ b/.changeset/green-squids-compete.md
@@ -0,0 +1,5 @@
+---
+'eslint-plugin-svelte': minor
+---
+
+Add `prefer-const` rule
diff --git a/.changeset/sixty-news-look.md b/.changeset/sixty-news-look.md
new file mode 100644
index 0000000..e70b9cd
--- /dev/null
+++ b/.changeset/sixty-news-look.md
@@ -0,0 +1,5 @@
+---
+'eslint-plugin-svelte': minor
+---
+
+feat: support Svelte5 of `valid-prop-names-in-kit-pages` rule
diff --git a/README.md b/README.md
index 2601875..e492f8a 100644
--- a/README.md
+++ b/README.md
@@ -367,6 +367,7 @@ These rules relate to better ways of doing things to help you avoid problems:
 | [svelte/no-unused-class-name](https://sveltejs.github.io/eslint-plugin-svelte/rules/no-unused-class-name/) | disallow the use of a class in the template without a corresponding style |  |
 | [svelte/no-unused-svelte-ignore](https://sveltejs.github.io/eslint-plugin-svelte/rules/no-unused-svelte-ignore/) | disallow unused svelte-ignore comments | :star: |
 | [svelte/no-useless-mustaches](https://sveltejs.github.io/eslint-plugin-svelte/rules/no-useless-mustaches/) | disallow unnecessary mustache interpolations | :wrench: |
+| [svelte/prefer-const](https://sveltejs.github.io/eslint-plugin-svelte/rules/prefer-const/) | Require `const` declarations for variables that are never reassigned after declared | :wrench: |
 | [svelte/prefer-destructured-store-props](https://sveltejs.github.io/eslint-plugin-svelte/rules/prefer-destructured-store-props/) | destructure values from object stores for better change tracking & fewer redraws | :bulb: |
 | [svelte/require-each-key](https://sveltejs.github.io/eslint-plugin-svelte/rules/require-each-key/) | require keyed `{#each}` block |  |
 | [svelte/require-event-dispatcher-types](https://sveltejs.github.io/eslint-plugin-svelte/rules/require-event-dispatcher-types/) | require type parameters for `createEventDispatcher` |  |
diff --git a/docs/rules.md b/docs/rules.md
index 29701ff..d2dd3d7 100644
--- a/docs/rules.md
+++ b/docs/rules.md
@@ -64,6 +64,7 @@ These rules relate to better ways of doing things to help you avoid problems:
 | [svelte/no-unused-class-name](./rules/no-unused-class-name.md)                           | disallow the use of a class in the template without a corresponding style                                                                 |          |
 | [svelte/no-unused-svelte-ignore](./rules/no-unused-svelte-ignore.md)                     | disallow unused svelte-ignore comments                                                                                                    | :star:   |
 | [svelte/no-useless-mustaches](./rules/no-useless-mustaches.md)                           | disallow unnecessary mustache interpolations                                                                                              | :wrench: |
+| [svelte/prefer-const](./rules/prefer-const.md)                                           | Require `const` declarations for variables that are never reassigned after declared                                                       | :wrench: |
 | [svelte/prefer-destructured-store-props](./rules/prefer-destructured-store-props.md)     | destructure values from object stores for better change tracking & fewer redraws                                                          | :bulb:   |
 | [svelte/require-each-key](./rules/require-each-key.md)                                   | require keyed `{#each}` block                                                                                                             |          |
 | [svelte/require-event-dispatcher-types](./rules/require-event-dispatcher-types.md)       | require type parameters for `createEventDispatcher`                                                                                       |          |
diff --git a/docs/rules/prefer-const.md b/docs/rules/prefer-const.md
new file mode 100644
index 0000000..f24b70f
--- /dev/null
+++ b/docs/rules/prefer-const.md
@@ -0,0 +1,69 @@
+---
+pageClass: 'rule-details'
+sidebarDepth: 0
+title: 'svelte/prefer-const'
+description: 'Require `const` declarations for variables that are never reassigned after declared'
+---
+
+# svelte/prefer-const
+
+> Require `const` declarations for variables that are never reassigned after declared
+
+- :exclamation: <badge text="This rule has not been released yet." vertical="middle" type="error"> **_This rule has not been released yet._** </badge>
+- :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 the same as the base ESLint `prefer-const` rule, except that ignores Svelte reactive values such as `$derived` and `$props`. If this rule is active, make sure to disable the base `prefer-const` rule, as it will conflict with this rule.
+
+<!--eslint-skip-->
+
+```svelte
+<script>
+  /* eslint svelte/prefer-const: "error" */
+
+  // ✓ GOOD
+  const { a, b } = $props();
+  let c = $state('');
+  let d = $derived(a * 2);
+  let e = $derived.by(() => b * 2);
+
+  // ✗ BAD
+  let obj = { a, b };
+  let g = $state(0);
+  let h = $state({ count: 1 });
+</script>
+
+<input bind:value={c} />
+<input bind:value={h.count} />
+```
+
+## :wrench: Options
+
+```json
+{
+  "svelte/prefer-const": [
+    "error",
+    {
+      "destructuring": "any",
+      "ignoreReadonly": true
+    }
+  ]
+}
+```
+
+- `destructuring`: The kind of the way to address variables in destructuring. There are 2 values:
+  - `any` (default): if any variables in destructuring should be const, this rule warns for those variables.
+  - `all`: if all variables in destructuring should be const, this rule warns the variables. Otherwise, ignores them.
+- `ignoreReadonly`: If `true`, this rule will ignore variables that are read between the declaration and the _first_ assignment.
+
+## :books: Further Reading
+
+- See [ESLint `prefer-const` rule](https://eslint.org/docs/latest/rules/prefer-const) for more information about the base rule.
+
+## :mag: Implementation
+
+- [Rule source](https://github.com/sveltejs/eslint-plugin-svelte/blob/main/packages/eslint-plugin-svelte/src/rules/prefer-const.ts)
+- [Test source](https://github.com/sveltejs/eslint-plugin-svelte/blob/main/packages/eslint-plugin-svelte/tests/src/rules/prefer-const.ts)
+
+<sup>Taken with ❤️ [from ESLint core](https://eslint.org/docs/rules/prefer-const)</sup>
diff --git a/package.json b/package.json
index 55c555f..d0cd91b 100644
--- a/package.json
+++ b/package.json
@@ -4,7 +4,7 @@
   "private": true,
   "license": "MIT",
   "funding": "https://github.com/sponsors/ota-meshi",
-  "packageManager": "pnpm@9.15.1",
+  "packageManager": "pnpm@9.15.2",
   "scripts": {
     "prerelease": "cd packages/eslint-plugin-svelte && pnpm clean && pnpm run build && cd ../.. && cp README.md packages/eslint-plugin-svelte",
     "release": "changeset publish",
diff --git a/packages/eslint-plugin-svelte/src/rule-types.ts b/packages/eslint-plugin-svelte/src/rule-types.ts
index 8b7cbe8..71eae84 100644
--- a/packages/eslint-plugin-svelte/src/rule-types.ts
+++ b/packages/eslint-plugin-svelte/src/rule-types.ts
@@ -264,6 +264,11 @@ export interface RuleOptions {
    * @see https://sveltejs.github.io/eslint-plugin-svelte/rules/prefer-class-directive/
    */
   'svelte/prefer-class-directive'?: Linter.RuleEntry<SveltePreferClassDirective>
+  /**
+   * Require `const` declarations for variables that are never reassigned after declared
+   * @see https://sveltejs.github.io/eslint-plugin-svelte/rules/prefer-const/
+   */
+  'svelte/prefer-const'?: Linter.RuleEntry<SveltePreferConst>
   /**
    * destructure values from object stores for better change tracking & fewer redraws
    * @see https://sveltejs.github.io/eslint-plugin-svelte/rules/prefer-destructured-store-props/
@@ -485,6 +490,11 @@ type SvelteNoUselessMustaches = []|[{
 type SveltePreferClassDirective = []|[{
   prefer?: ("always" | "empty")
 }]
+// ----- svelte/prefer-const -----
+type SveltePreferConst = []|[{
+  destructuring?: ("any" | "all")
+  ignoreReadBeforeAssign?: boolean
+}]
 // ----- svelte/shorthand-attribute -----
 type SvelteShorthandAttribute = []|[{
   prefer?: ("always" | "never")
diff --git a/packages/eslint-plugin-svelte/src/rules/prefer-const.ts b/packages/eslint-plugin-svelte/src/rules/prefer-const.ts
new file mode 100644
index 0000000..0d079b1
--- /dev/null
+++ b/packages/eslint-plugin-svelte/src/rules/prefer-const.ts
@@ -0,0 +1,81 @@
+import type { TSESTree } from '@typescript-eslint/types';
+
+import { createRule } from '../utils/index.js';
+import { defineWrapperListener, getCoreRule } from '../utils/eslint-core.js';
+
+const coreRule = getCoreRule('prefer-const');
+
+/**
+ * Finds and returns the callee of a declaration node within variable declarations or object patterns.
+ */
+function findDeclarationCallee(node: TSESTree.Expression) {
+	const { parent } = node;
+	if (parent.type === 'VariableDeclarator' && parent.init?.type === 'CallExpression') {
+		return parent.init.callee;
+	}
+
+	return null;
+}
+
+/**
+ * Determines if a declaration should be skipped in the const preference analysis.
+ * Specifically checks for Svelte's state management utilities ($props, $derived).
+ */
+function shouldSkipDeclaration(declaration: TSESTree.Expression | null) {
+	if (!declaration) {
+		return false;
+	}
+
+	const callee = findDeclarationCallee(declaration);
+	if (!callee) {
+		return false;
+	}
+
+	if (callee.type === 'Identifier' && ['$props', '$derived'].includes(callee.name)) {
+		return true;
+	}
+
+	if (callee.type !== 'MemberExpression' || callee.object.type !== 'Identifier') {
+		return false;
+	}
+
+	if (
+		callee.object.name === '$derived' &&
+		callee.property.type === 'Identifier' &&
+		callee.property.name === 'by'
+	) {
+		return true;
+	}
+
+	return false;
+}
+
+export default createRule('prefer-const', {
+	meta: {
+		...coreRule.meta,
+		docs: {
+			description: coreRule.meta.docs.description,
+			category: 'Best Practices',
+			recommended: false,
+			extensionRule: 'prefer-const'
+		}
+	},
+	create(context) {
+		return defineWrapperListener(coreRule, context, {
+			createListenerProxy(coreListener) {
+				return {
+					...coreListener,
+					VariableDeclaration(node) {
+						for (const decl of node.declarations) {
+							if (shouldSkipDeclaration(decl.init)) {
+								return;
+							}
+						}
+
+						coreListener.VariableDeclaration?.(node);
+					}
+				};
+			}
+		});
+	}
+});
diff --git a/packages/eslint-plugin-svelte/src/rules/require-each-key.ts b/packages/eslint-plugin-svelte/src/rules/require-each-key.ts
index 00a34f5..7303427 100644
--- a/packages/eslint-plugin-svelte/src/rules/require-each-key.ts
+++ b/packages/eslint-plugin-svelte/src/rules/require-each-key.ts
@@ -15,7 +15,9 @@ export default createRule('require-each-key', {
 	create(context) {
 		return {
 			SvelteEachBlock(node: AST.SvelteEachBlock) {
-				if (node.key == null) {
+				// No need a `key` if an each blocks without an item
+				// see: https://svelte.dev/docs/svelte/each#Each-blocks-without-an-item
+				if (node.context != null && node.key == null) {
 					context.report({
 						node,
 						messageId: 'expectedKey'
diff --git a/packages/eslint-plugin-svelte/src/rules/valid-prop-names-in-kit-pages.ts b/packages/eslint-plugin-svelte/src/rules/valid-prop-names-in-kit-pages.ts
index 77b3e98..ff12df7 100644
--- a/packages/eslint-plugin-svelte/src/rules/valid-prop-names-in-kit-pages.ts
+++ b/packages/eslint-plugin-svelte/src/rules/valid-prop-names-in-kit-pages.ts
@@ -2,9 +2,27 @@ import type { AST } from 'svelte-eslint-parser';
 import type { TSESTree } from '@typescript-eslint/types';
 import { createRule } from '../utils/index.js';
 import { isKitPageComponent } from '../utils/svelte-kit.js';
+import type { RuleContext } from '../types.js';
 
 const EXPECTED_PROP_NAMES = ['data', 'errors', 'form', 'snapshot'];
 
+function checkProp(node: TSESTree.VariableDeclarator, context: RuleContext) {
+	if (node.id.type !== 'ObjectPattern') return;
+	for (const p of node.id.properties) {
+		if (
+			p.type === 'Property' &&
+			p.value.type === 'Identifier' &&
+			!EXPECTED_PROP_NAMES.includes(p.value.name)
+		) {
+			context.report({
+				node: p.value,
+				loc: p.value.loc,
+				messageId: 'unexpected'
+			});
+		}
+	}
+}
+
 export default createRule('valid-prop-names-in-kit-pages', {
 	meta: {
 		docs: {
@@ -39,6 +57,7 @@ export default createRule('valid-prop-names-in-kit-pages', {
 				isScript = false;
 			},
 
+			// Svelte3,4
 			'ExportNamedDeclaration > VariableDeclaration > VariableDeclarator': (
 				node: TSESTree.VariableDeclarator
 			) => {
@@ -57,20 +76,22 @@ export default createRule('valid-prop-names-in-kit-pages', {
 				}
 
 				// export let { xxx, yyy } = zzz
-				if (node.id.type !== 'ObjectPattern') return;
-				for (const p of node.id.properties) {
-					if (
-						p.type === 'Property' &&
-						p.value.type === 'Identifier' &&
-						!EXPECTED_PROP_NAMES.includes(p.value.name)
-					) {
-						context.report({
-							node: p.value,
-							loc: p.value.loc,
-							messageId: 'unexpected'
-						});
-					}
+				checkProp(node, context);
+			},
+
+			// Svelte5
+			// let { foo, bar } = $props();
+			'VariableDeclaration > VariableDeclarator': (node: TSESTree.VariableDeclarator) => {
+				if (!isScript) return;
+				if (
+					node.init?.type !== 'CallExpression' ||
+					node.init.callee?.type !== 'Identifier' ||
+					node.init.callee?.name !== '$props'
+				) {
+					return;
 				}
+
+				checkProp(node, context);
 			}
 		};
 	}
diff --git a/packages/eslint-plugin-svelte/src/types.ts b/packages/eslint-plugin-svelte/src/types.ts
index 7b6452b..96a6abd 100644
--- a/packages/eslint-plugin-svelte/src/types.ts
+++ b/packages/eslint-plugin-svelte/src/types.ts
@@ -232,6 +232,8 @@ export interface SourceCode {
 
 	getLines(): string[];
 
+	getDeclaredVariables(node: TSESTree.Node): Variable[];
+
 	getAllComments(): AST.Comment[];
 
 	getComments(node: NodeOrToken): {
diff --git a/packages/eslint-plugin-svelte/src/utils/rules.ts b/packages/eslint-plugin-svelte/src/utils/rules.ts
index 296a770..bba9a99 100644
--- a/packages/eslint-plugin-svelte/src/utils/rules.ts
+++ b/packages/eslint-plugin-svelte/src/utils/rules.ts
@@ -52,6 +52,7 @@ import noUnusedClassName from '../rules/no-unused-class-name.js';
 import noUnusedSvelteIgnore from '../rules/no-unused-svelte-ignore.js';
 import noUselessMustaches from '../rules/no-useless-mustaches.js';
 import preferClassDirective from '../rules/prefer-class-directive.js';
+import preferConst from '../rules/prefer-const.js';
 import preferDestructuredStoreProps from '../rules/prefer-destructured-store-props.js';
 import preferStyleDirective from '../rules/prefer-style-directive.js';
 import requireEachKey from '../rules/require-each-key.js';
@@ -120,6 +121,7 @@ export const rules = [
 	noUnusedSvelteIgnore,
 	noUselessMustaches,
 	preferClassDirective,
+	preferConst,
 	preferDestructuredStoreProps,
 	preferStyleDirective,
 	requireEachKey,


Test patch:
diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-const/invalid/test01-errors.yaml b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-const/invalid/test01-errors.yaml
new file mode 100644
index 0000000..fed423c
--- /dev/null
+++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-const/invalid/test01-errors.yaml
@@ -0,0 +1,20 @@
+- message: "'zero' is never reassigned. Use 'const' instead."
+  line: 3
+  column: 6
+  suggestions: null
+- message: "'state' is never reassigned. Use 'const' instead."
+  line: 4
+  column: 6
+  suggestions: null
+- message: "'raw' is never reassigned. Use 'const' instead."
+  line: 5
+  column: 6
+  suggestions: null
+- message: "'doubled' is never reassigned. Use 'const' instead."
+  line: 6
+  column: 6
+  suggestions: null
+- message: "'calculated' is never reassigned. Use 'const' instead."
+  line: 8
+  column: 6
+  suggestions: null
diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-const/invalid/test01-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-const/invalid/test01-input.svelte
new file mode 100644
index 0000000..2a9ff2b
--- /dev/null
+++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-const/invalid/test01-input.svelte
@@ -0,0 +1,11 @@
+<script>
+	let { prop1, prop2 } = $props();
+	let zero = 0;
+	let state = $state(0);
+	let raw = $state.raw(0);
+	let doubled = state * 2;
+	let derived = $derived(state * 2);
+	let calculated = calc();
+	let derivedBy = $derived.by(calc());
+	let noInit;
+</script>
diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-const/invalid/test01-output.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-const/invalid/test01-output.svelte
new file mode 100644
index 0000000..fb6da26
--- /dev/null
+++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-const/invalid/test01-output.svelte
@@ -0,0 +1,11 @@
+<script>
+	let { prop1, prop2 } = $props();
+	const zero = 0;
+	const state = $state(0);
+	const raw = $state.raw(0);
+	const doubled = state * 2;
+	let derived = $derived(state * 2);
+	const calculated = calc();
+	let derivedBy = $derived.by(calc());
+	let noInit;
+</script>
diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-const/valid/test01-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-const/valid/test01-input.svelte
new file mode 100644
index 0000000..5c768eb
--- /dev/null
+++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/prefer-const/valid/test01-input.svelte
@@ -0,0 +1,3 @@
+<script>
+	const a = {};
+</script>
diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/require-each-key/valid/svelte5/_requirements.json b/packages/eslint-plugin-svelte/tests/fixtures/rules/require-each-key/valid/svelte5/_requirements.json
new file mode 100644
index 0000000..0192b10
--- /dev/null
+++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/require-each-key/valid/svelte5/_requirements.json
@@ -0,0 +1,3 @@
+{
+	"svelte": ">=5.0.0-0"
+}
diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/require-each-key/valid/svelte5/each-blocks-without-an-item-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/require-each-key/valid/svelte5/each-blocks-without-an-item-input.svelte
new file mode 100644
index 0000000..e3310ff
--- /dev/null
+++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/require-each-key/valid/svelte5/each-blocks-without-an-item-input.svelte
@@ -0,0 +1,7 @@
+<div class="chess-board">
+	{#each { length: 8 }, rank}
+		{#each { length: 8 }}
+			{rank}
+		{/each}
+	{/each}
+</div>
diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/valid-prop-names-in-kit-pages/invalid/svelte5/+svelte5-test001-errors.yaml b/packages/eslint-plugin-svelte/tests/fixtures/rules/valid-prop-names-in-kit-pages/invalid/svelte5/+svelte5-test001-errors.yaml
new file mode 100644
index 0000000..a5dcdf6
--- /dev/null
+++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/valid-prop-names-in-kit-pages/invalid/svelte5/+svelte5-test001-errors.yaml
@@ -0,0 +1,8 @@
+- message: disallow props other than data or errors in SvelteKit page components.
+  line: 2
+  column: 8
+  suggestions: null
+- message: disallow props other than data or errors in SvelteKit page components.
+  line: 2
+  column: 13
+  suggestions: null
diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/valid-prop-names-in-kit-pages/invalid/svelte5/+svelte5-test001-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/valid-prop-names-in-kit-pages/invalid/svelte5/+svelte5-test001-input.svelte
new file mode 100644
index 0000000..33f7c90
--- /dev/null
+++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/valid-prop-names-in-kit-pages/invalid/svelte5/+svelte5-test001-input.svelte
@@ -0,0 +1,5 @@
+<script>
+	let { foo, bar } = $props();
+</script>
+
+{foo}, {bar}
diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/valid-prop-names-in-kit-pages/invalid/svelte5/+svelte5-test001-requirements.json b/packages/eslint-plugin-svelte/tests/fixtures/rules/valid-prop-names-in-kit-pages/invalid/svelte5/+svelte5-test001-requirements.json
new file mode 100644
index 0000000..0192b10
--- /dev/null
+++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/valid-prop-names-in-kit-pages/invalid/svelte5/+svelte5-test001-requirements.json
@@ -0,0 +1,3 @@
+{
+	"svelte": ">=5.0.0-0"
+}
diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/valid-prop-names-in-kit-pages/invalid/svelte5/_config.json b/packages/eslint-plugin-svelte/tests/fixtures/rules/valid-prop-names-in-kit-pages/invalid/svelte5/_config.json
new file mode 100644
index 0000000..7a71ebe
--- /dev/null
+++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/valid-prop-names-in-kit-pages/invalid/svelte5/_config.json
@@ -0,0 +1,13 @@
+{
+	"languageOptions": {
+		"parserOptions": {
+			"svelteConfig": {
+				"kit": {
+					"files": {
+						"routes": "tests/fixtures/rules/valid-prop-names-in-kit-pages/invalid/svelte5"
+					}
+				}
+			}
+		}
+	}
+}
diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/valid-prop-names-in-kit-pages/valid/not-page/svelte5/test01-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/valid-prop-names-in-kit-pages/valid/not-page/svelte5/test01-input.svelte
new file mode 100644
index 0000000..b042de2
--- /dev/null
+++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/valid-prop-names-in-kit-pages/valid/not-page/svelte5/test01-input.svelte
@@ -0,0 +1,5 @@
+<script>
+	let { data, errors, foo, bar } = $props();
+</script>
+
+{data}, {errors}, {foo}, {bar}
diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/valid-prop-names-in-kit-pages/valid/not-page/svelte5/test01-requirements.json b/packages/eslint-plugin-svelte/tests/fixtures/rules/valid-prop-names-in-kit-pages/valid/not-page/svelte5/test01-requirements.json
new file mode 100644
index 0000000..0192b10
--- /dev/null
+++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/valid-prop-names-in-kit-pages/valid/not-page/svelte5/test01-requirements.json
@@ -0,0 +1,3 @@
+{
+	"svelte": ">=5.0.0-0"
+}
diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/valid-prop-names-in-kit-pages/valid/svelte5/+test001-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/valid-prop-names-in-kit-pages/valid/svelte5/+test001-input.svelte
new file mode 100644
index 0000000..6ed2875
--- /dev/null
+++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/valid-prop-names-in-kit-pages/valid/svelte5/+test001-input.svelte
@@ -0,0 +1,21 @@
+<script>
+	let { data, errors, form } = $props();
+
+	let comment = '';
+
+	export const snapshot = {
+		capture: () => comment,
+		restore: (value) => (comment = value)
+	};
+</script>
+
+{data}, {errors}
+
+{#if form?.success}
+	<p>Successfully logged in! Welcome back, {data.user.name}</p>
+{/if}
+
+<form method="POST">
+	<textarea bind:value={comment} />
+	<button>Post comment</button>
+</form>
diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/valid-prop-names-in-kit-pages/valid/svelte5/+test001-requirements.json b/packages/eslint-plugin-svelte/tests/fixtures/rules/valid-prop-names-in-kit-pages/valid/svelte5/+test001-requirements.json
new file mode 100644
index 0000000..0192b10
--- /dev/null
+++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/valid-prop-names-in-kit-pages/valid/svelte5/+test001-requirements.json
@@ -0,0 +1,3 @@
+{
+	"svelte": ">=5.0.0-0"
+}
diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/valid-prop-names-in-kit-pages/valid/svelte5/_config.json b/packages/eslint-plugin-svelte/tests/fixtures/rules/valid-prop-names-in-kit-pages/valid/svelte5/_config.json
new file mode 100644
index 0000000..cd38d8e
--- /dev/null
+++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/valid-prop-names-in-kit-pages/valid/svelte5/_config.json
@@ -0,0 +1,13 @@
+{
+	"languageOptions": {
+		"parserOptions": {
+			"svelteConfig": {
+				"kit": {
+					"files": {
+						"routes": "tests/fixtures/rules/valid-prop-names-in-kit-pages/valid/svelte5"
+					}
+				}
+			}
+		}
+	}
+}
diff --git a/packages/eslint-plugin-svelte/tests/src/rules/prefer-const.ts b/packages/eslint-plugin-svelte/tests/src/rules/prefer-const.ts
new file mode 100644
index 0000000..ffe176d
--- /dev/null
+++ b/packages/eslint-plugin-svelte/tests/src/rules/prefer-const.ts
@@ -0,0 +1,12 @@
+import { RuleTester } from '../../utils/eslint-compat';
+import rule from '../../../src/rules/prefer-const';
+import { loadTestCases } from '../../utils/utils';
+
+const tester = new RuleTester({
+	languageOptions: {
+		ecmaVersion: 2020,
+		sourceType: 'module'
+	},
+});
+
+tester.run('prefer-const', rule as any, loadTestCases('prefer-const'));