diff --git a/.changeset/chubby-wasps-marry.md b/.changeset/chubby-wasps-marry.md
index e92f4c44c..0362f25b5 100644
--- a/.changeset/chubby-wasps-marry.md
+++ b/.changeset/chubby-wasps-marry.md
@@ -1,6 +1,6 @@
---
-"svelte-check": patch
-"svelte2tsx": patch
+'svelte-check': patch
+'svelte2tsx': patch
---
fix: properly handle props with the name `slot` inside Svelte 5 snippets
diff --git a/.changeset/metal-hotels-film.md b/.changeset/metal-hotels-film.md
new file mode 100644
index 000000000..918957b4d
--- /dev/null
+++ b/.changeset/metal-hotels-film.md
@@ -0,0 +1,6 @@
+---
+'svelte-check': minor
+'svelte2tsx': patch
+---
+
+feat: support Svelte 5 declaration tags
diff --git a/packages/language-server/test/plugins/typescript/features/diagnostics/fixtures/declaration-tag.v5/expectedv2.json b/packages/language-server/test/plugins/typescript/features/diagnostics/fixtures/declaration-tag.v5/expectedv2.json
new file mode 100644
index 000000000..3aab95978
--- /dev/null
+++ b/packages/language-server/test/plugins/typescript/features/diagnostics/fixtures/declaration-tag.v5/expectedv2.json
@@ -0,0 +1,46 @@
+[
+ {
+ "range": {
+ "start": { "line": 11, "character": 9 },
+ "end": { "line": 11, "character": 14 }
+ },
+ "severity": 1,
+ "source": "ts",
+ "message": "Type 'string' is not assignable to type 'number'.",
+ "code": 2322,
+ "tags": []
+ },
+ {
+ "range": {
+ "start": { "line": 12, "character": 11 },
+ "end": { "line": 12, "character": 18 }
+ },
+ "severity": 1,
+ "source": "ts",
+ "message": "Type 'number' is not assignable to type 'string'.",
+ "code": 2322,
+ "tags": []
+ },
+ {
+ "range": {
+ "start": { "line": 14, "character": 8 },
+ "end": { "line": 14, "character": 21 }
+ },
+ "severity": 1,
+ "source": "ts",
+ "message": "This comparison appears to be unintentional because the types 'string' and 'number' have no overlap.",
+ "code": 2367,
+ "tags": []
+ },
+ {
+ "range": {
+ "start": { "line": 14, "character": 24 },
+ "end": { "line": 14, "character": 41 }
+ },
+ "severity": 1,
+ "source": "ts",
+ "message": "This comparison appears to be unintentional because the types 'number' and 'string' have no overlap.",
+ "code": 2367,
+ "tags": []
+ }
+]
diff --git a/packages/language-server/test/plugins/typescript/features/diagnostics/fixtures/declaration-tag.v5/input.svelte b/packages/language-server/test/plugins/typescript/features/diagnostics/fixtures/declaration-tag.v5/input.svelte
new file mode 100644
index 000000000..01db9ebea
--- /dev/null
+++ b/packages/language-server/test/plugins/typescript/features/diagnostics/fixtures/declaration-tag.v5/input.svelte
@@ -0,0 +1,20 @@
+
+
+{#each boxes as box}
+ {const area = box.width * box.height}
+ {let label: number = $state(`${area} square pixels`)}
+ {const doubled: string = area * 2}
+
+
{doubled === 1} {label === 'large'}
+
+ {const area = 'nested'}
+ {area}
+
+{/each}
diff --git a/packages/svelte2tsx/src/htmlxtojsx_v2/index.ts b/packages/svelte2tsx/src/htmlxtojsx_v2/index.ts
index 8fbda3710..e93b98217 100644
--- a/packages/svelte2tsx/src/htmlxtojsx_v2/index.ts
+++ b/packages/svelte2tsx/src/htmlxtojsx_v2/index.ts
@@ -2,7 +2,14 @@ import MagicString from 'magic-string';
import { walk } from 'estree-walker';
// @ts-ignore
import { TemplateNode, Text } from 'svelte/types/compiler/interfaces';
-import { Attribute, BaseNode, BaseDirective, StyleDirective, ConstTag } from '../interfaces';
+import {
+ Attribute,
+ BaseNode,
+ BaseDirective,
+ StyleDirective,
+ ConstTag,
+ DeclarationTag
+} from '../interfaces';
import { parseHtmlx } from '../utils/htmlxparser';
import { handleActionDirective } from './nodes/Action';
import { handleAnimateDirective } from './nodes/Animation';
@@ -16,6 +23,7 @@ import {
handleTrailingEndComment
} from './nodes/Comment';
import { handleConstTag } from './nodes/ConstTag';
+import { handleDeclarationTag } from './nodes/DeclarationTag';
import { handleDebug } from './nodes/DebugTag';
import { handleEach } from './nodes/EachBlock';
import { Element } from './nodes/Element';
@@ -371,6 +379,9 @@ export function convertHtmlxToJsx(
case 'ConstTag':
handleConstTag(str, node as ConstTag);
break;
+ case 'DeclarationTag':
+ handleDeclarationTag(str, node as DeclarationTag);
+ break;
case 'RenderTag':
handleRenderTag(str, node);
break;
diff --git a/packages/svelte2tsx/src/htmlxtojsx_v2/nodes/DeclarationTag.ts b/packages/svelte2tsx/src/htmlxtojsx_v2/nodes/DeclarationTag.ts
new file mode 100644
index 000000000..9536b0531
--- /dev/null
+++ b/packages/svelte2tsx/src/htmlxtojsx_v2/nodes/DeclarationTag.ts
@@ -0,0 +1,10 @@
+import MagicString from 'magic-string';
+import { DeclarationTag } from '../../interfaces';
+
+/**
+ * `{let x = y}` --> `let x = y;`
+ */
+export function handleDeclarationTag(str: MagicString, declarationTag: DeclarationTag): void {
+ str.remove(declarationTag.start, declarationTag.declaration.start);
+ str.overwrite(declarationTag.declaration.end, declarationTag.end, ';');
+}
diff --git a/packages/svelte2tsx/src/interfaces.ts b/packages/svelte2tsx/src/interfaces.ts
index ebdfe4d93..d64a77e8d 100644
--- a/packages/svelte2tsx/src/interfaces.ts
+++ b/packages/svelte2tsx/src/interfaces.ts
@@ -1,4 +1,4 @@
-import { ArrayPattern, Identifier, ObjectPattern, Node } from 'estree';
+import { ArrayPattern, Identifier, ObjectPattern, Node, VariableDeclaration } from 'estree';
// @ts-ignore
import { DirectiveType, TemplateNode } from 'svelte/types/compiler/interfaces';
@@ -23,6 +23,11 @@ export interface ConstTag extends NodeRange {
expression: any;
}
+export interface DeclarationTag extends NodeRange {
+ type: 'DeclarationTag';
+ declaration: VariableDeclaration & NodeRange & { kind: 'let' | 'const' };
+}
+
// Copied from the Svelte type definitions
export interface BaseNode {
start: number;
diff --git a/packages/svelte2tsx/test/svelte2tsx/samples/declaration-tag-async.v5/expectedv2.ts b/packages/svelte2tsx/test/svelte2tsx/samples/declaration-tag-async.v5/expectedv2.ts
new file mode 100644
index 000000000..36a98316d
--- /dev/null
+++ b/packages/svelte2tsx/test/svelte2tsx/samples/declaration-tag-async.v5/expectedv2.ts
@@ -0,0 +1,31 @@
+///
+;function $$render() {
+
+ type Box = { width: number; height: number };
+
+ let boxes: Box[] = [
+ { width: 3, height: 4 },
+ { width: 5, height: 7 }
+ ];
+;
+async () => {
+
+ for(let box of __sveltets_2_ensureArray(boxes)){
+ const foo/*Ωignore_positionΩ*/ = ()/*Ωignore_startΩ*/: ReturnType/*Ωignore_endΩ*/ => { async ()/*Ωignore_positionΩ*/ => {
+ const area = await 'snippet';
+ area;
+ };return __sveltets_2_any(0)};const area = box.width * box.height;
+ let label = $state(await `${area} square pixels`);
+
+ { svelteHTML.createElement("p", {});label; }
+ { svelteHTML.createElement("div", {});
+ const area = await 'nested';
+ area;
+ }
+
+
+}};
+return { props: /** @type {Record} */ ({}), exports: {}, bindings: __sveltets_$$bindings(''), slots: {}, events: {} }}
+const Input__SvelteComponent_ = __sveltets_2_fn_component($$render());
+/*Ωignore_startΩ*/type Input__SvelteComponent_ = ReturnType;
+/*Ωignore_endΩ*/export default Input__SvelteComponent_;
\ No newline at end of file
diff --git a/packages/svelte2tsx/test/svelte2tsx/samples/declaration-tag-async.v5/input.svelte b/packages/svelte2tsx/test/svelte2tsx/samples/declaration-tag-async.v5/input.svelte
new file mode 100644
index 000000000..a29e6b447
--- /dev/null
+++ b/packages/svelte2tsx/test/svelte2tsx/samples/declaration-tag-async.v5/input.svelte
@@ -0,0 +1,24 @@
+
+
+{#each boxes as box}
+ {const area = box.width * box.height}
+ {let label = $state(await `${area} square pixels`)}
+
+ {label}
+
+ {const area = await 'nested'}
+ {area}
+
+
+ {#snippet foo()}
+ {const area = await 'snippet'}
+ {area}
+ {/snippet}
+{/each}
diff --git a/packages/svelte2tsx/test/svelte2tsx/samples/ts-declaration-tag.v5/expectedv2.ts b/packages/svelte2tsx/test/svelte2tsx/samples/ts-declaration-tag.v5/expectedv2.ts
new file mode 100644
index 000000000..7545db73e
--- /dev/null
+++ b/packages/svelte2tsx/test/svelte2tsx/samples/ts-declaration-tag.v5/expectedv2.ts
@@ -0,0 +1,27 @@
+///
+;function $$render() {
+
+ type Box = { width: number; height: number };
+
+ let boxes: Box[] = [
+ { width: 3, height: 4 },
+ { width: 5, height: 7 }
+ ];
+;
+async () => {
+
+ for(let box of __sveltets_2_ensureArray(boxes)){
+ const area = box.width * box.height;
+ let label = $state(`${area} square pixels`);
+ const doubled = area * 2;
+
+ { svelteHTML.createElement("p", {});doubled; label; }
+ { svelteHTML.createElement("div", {});
+ const area = 'nested';
+ area;
+ }
+}};
+return { props: {} as Record, exports: {}, bindings: __sveltets_$$bindings(''), slots: {}, events: {} }}
+const Input__SvelteComponent_ = __sveltets_2_fn_component($$render());
+/*Ωignore_startΩ*/type Input__SvelteComponent_ = ReturnType;
+/*Ωignore_endΩ*/export default Input__SvelteComponent_;
\ No newline at end of file
diff --git a/packages/svelte2tsx/test/svelte2tsx/samples/ts-declaration-tag.v5/input.svelte b/packages/svelte2tsx/test/svelte2tsx/samples/ts-declaration-tag.v5/input.svelte
new file mode 100644
index 000000000..fa5a1729b
--- /dev/null
+++ b/packages/svelte2tsx/test/svelte2tsx/samples/ts-declaration-tag.v5/input.svelte
@@ -0,0 +1,20 @@
+
+
+{#each boxes as box}
+ {const area = box.width * box.height}
+ {let label = $state(`${area} square pixels`)}
+ {const doubled = area * 2}
+
+ {doubled} {label}
+
+ {const area = 'nested'}
+ {area}
+
+{/each}