From f094ac5346701154f7458d2932d72321073b520e Mon Sep 17 00:00:00 2001 From: Simon Holthausen Date: Sun, 24 May 2026 22:46:11 +0200 Subject: [PATCH] feat: allow declarations in the template Allows `{let/var/const/function ...}` declarations in all places where we already allow `{@const ...}` (which will eventually get deprecated in favor of this new feature). --- .changeset/four-loops-agree.md | 5 + .../docs/03-template-syntax/11-declaration.md | 32 ++++ .../98-reference/.generated/compile-errors.md | 12 ++ .../messages/compile-errors/template.md | 8 + packages/svelte/src/compiler/errors.js | 18 +++ packages/svelte/src/compiler/legacy.js | 4 + .../src/compiler/phases/1-parse/acorn.js | 41 ++++- .../src/compiler/phases/1-parse/state/tag.js | 120 +++++++++++++- .../src/compiler/phases/2-analyze/index.js | 2 + .../src/compiler/phases/2-analyze/types.d.ts | 2 +- .../phases/2-analyze/visitors/ConstTag.js | 49 +----- .../2-analyze/visitors/DeclarationTag.js | 65 ++++++++ .../phases/2-analyze/visitors/Identifier.js | 2 +- .../phases/2-analyze/visitors/shared/utils.js | 27 ++++ .../3-transform/client/transform-client.js | 2 + .../3-transform/client/visitors/ConstTag.js | 37 ++--- .../client/visitors/DeclarationTag.js | 86 +++++++++++ .../3-transform/server/transform-server.js | 2 + .../phases/3-transform/server/types.d.ts | 2 +- .../3-transform/server/visitors/ConstTag.js | 33 ++-- .../server/visitors/DeclarationTag.js | 84 ++++++++++ .../src/compiler/phases/3-transform/utils.js | 1 + packages/svelte/src/compiler/phases/scope.js | 20 ++- packages/svelte/src/compiler/print/index.js | 6 + .../svelte/src/compiler/types/template.d.ts | 14 ++ .../loose-declaration-tag/input.svelte | 5 + .../samples/loose-declaration-tag/output.json | 146 ++++++++++++++++++ .../samples/declaration-tag/input.svelte | 9 ++ .../samples/declaration-tag/output.svelte | 11 ++ .../samples/async-declaration-tag/_config.js | 27 ++++ .../samples/async-declaration-tag/main.svelte | 11 ++ .../markup-declaration-tags/main.svelte | 18 +++ .../markup-declaration-tags/_config.js | 7 + .../markup-declaration-tags/_expected.html | 1 + .../markup-declaration-tags/main.svelte | 11 ++ .../declaration-tag-invalid-type/errors.json | 14 ++ .../declaration-tag-invalid-type/input.svelte | 3 + .../declaration-tag-placement/errors.json | 14 ++ .../declaration-tag-placement/input.svelte | 5 + packages/svelte/types/index.d.ts | 9 +- 40 files changed, 861 insertions(+), 104 deletions(-) create mode 100644 .changeset/four-loops-agree.md create mode 100644 documentation/docs/03-template-syntax/11-declaration.md create mode 100644 packages/svelte/src/compiler/phases/2-analyze/visitors/DeclarationTag.js create mode 100644 packages/svelte/src/compiler/phases/3-transform/client/visitors/DeclarationTag.js create mode 100644 packages/svelte/src/compiler/phases/3-transform/server/visitors/DeclarationTag.js create mode 100644 packages/svelte/tests/parser-modern/samples/loose-declaration-tag/input.svelte create mode 100644 packages/svelte/tests/parser-modern/samples/loose-declaration-tag/output.json create mode 100644 packages/svelte/tests/print/samples/declaration-tag/input.svelte create mode 100644 packages/svelte/tests/print/samples/declaration-tag/output.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/async-declaration-tag/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-declaration-tag/main.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/markup-declaration-tags/main.svelte create mode 100644 packages/svelte/tests/server-side-rendering/samples/markup-declaration-tags/_config.js create mode 100644 packages/svelte/tests/server-side-rendering/samples/markup-declaration-tags/_expected.html create mode 100644 packages/svelte/tests/server-side-rendering/samples/markup-declaration-tags/main.svelte create mode 100644 packages/svelte/tests/validator/samples/declaration-tag-invalid-type/errors.json create mode 100644 packages/svelte/tests/validator/samples/declaration-tag-invalid-type/input.svelte create mode 100644 packages/svelte/tests/validator/samples/declaration-tag-placement/errors.json create mode 100644 packages/svelte/tests/validator/samples/declaration-tag-placement/input.svelte diff --git a/.changeset/four-loops-agree.md b/.changeset/four-loops-agree.md new file mode 100644 index 0000000000..46d30e8464 --- /dev/null +++ b/.changeset/four-loops-agree.md @@ -0,0 +1,5 @@ +--- +'svelte': minor +--- + +feat: allow declarations in the template diff --git a/documentation/docs/03-template-syntax/11-declaration.md b/documentation/docs/03-template-syntax/11-declaration.md new file mode 100644 index 0000000000..ebc31fa631 --- /dev/null +++ b/documentation/docs/03-template-syntax/11-declaration.md @@ -0,0 +1,32 @@ +--- +title: {} +--- + +Declaration tags define local variables and functions inside markup. + +You can use `let`, `const`, `var` and `function` declarations: + +```svelte +{#each boxes as box} + {const area = box.width * box.height} + {function label(value) { + return `${value} square pixels`; + }} + +

{label(area)}

+{/each} +``` + +Unlike [`{@const ...}`](@const), declaration tags are plain JavaScript declarations. This means `{const ...}` is not reactive by itself; use runes such as `$state` or `$derived` when you need reactive values: + +```svelte +{#if user} + {let name = $state(user.name)} + {let greeting = $derived(`Hello ${name}`)} + + +

{greeting}

+{/if} +``` + +Declaration tags are only allowed as an immediate child of a block — `{#if ...}`, `{#each ...}`, `{#snippet ...}` and so on — a `` or a ``. diff --git a/documentation/docs/98-reference/.generated/compile-errors.md b/documentation/docs/98-reference/.generated/compile-errors.md index 01b2dd1494..0186985de2 100644 --- a/documentation/docs/98-reference/.generated/compile-errors.md +++ b/documentation/docs/98-reference/.generated/compile-errors.md @@ -399,6 +399,18 @@ Invalid selector Cannot declare a variable with the same name as an import from ` + +{#if id} + {let name = $state(await id)} + {let greeting = $derived(await `Hello ${name}`)} + + +

{greeting}

+{/if} diff --git a/packages/svelte/tests/runtime-runes/samples/markup-declaration-tags/main.svelte b/packages/svelte/tests/runtime-runes/samples/markup-declaration-tags/main.svelte new file mode 100644 index 0000000000..28adec813b --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/markup-declaration-tags/main.svelte @@ -0,0 +1,18 @@ + + + + +{#if visible} + {let counter = $state({ value: initial })} + {let doubled = $derived(counter.value * 2)} + {var suffix = ' total'} + {function format(value) { + return `${value}${suffix}`; + }} + + +

{format(doubled)}

+{/if} diff --git a/packages/svelte/tests/server-side-rendering/samples/markup-declaration-tags/_config.js b/packages/svelte/tests/server-side-rendering/samples/markup-declaration-tags/_config.js new file mode 100644 index 0000000000..232ae9dcd7 --- /dev/null +++ b/packages/svelte/tests/server-side-rendering/samples/markup-declaration-tags/_config.js @@ -0,0 +1,7 @@ +import { test } from '../../test'; + +export default test({ + props: { + items: [1, 3] + } +}); diff --git a/packages/svelte/tests/server-side-rendering/samples/markup-declaration-tags/_expected.html b/packages/svelte/tests/server-side-rendering/samples/markup-declaration-tags/_expected.html new file mode 100644 index 0000000000..d299c763b5 --- /dev/null +++ b/packages/svelte/tests/server-side-rendering/samples/markup-declaration-tags/_expected.html @@ -0,0 +1 @@ +

value: 2

value: 6

diff --git a/packages/svelte/tests/server-side-rendering/samples/markup-declaration-tags/main.svelte b/packages/svelte/tests/server-side-rendering/samples/markup-declaration-tags/main.svelte new file mode 100644 index 0000000000..aee0a30f3c --- /dev/null +++ b/packages/svelte/tests/server-side-rendering/samples/markup-declaration-tags/main.svelte @@ -0,0 +1,11 @@ + + +{#each items as item} + {const doubled = item * 2} + {function label(value) { + return `value: ${value}`; + }} +

{label(doubled)}

+{/each} diff --git a/packages/svelte/tests/validator/samples/declaration-tag-invalid-type/errors.json b/packages/svelte/tests/validator/samples/declaration-tag-invalid-type/errors.json new file mode 100644 index 0000000000..39e0b5c07d --- /dev/null +++ b/packages/svelte/tests/validator/samples/declaration-tag-invalid-type/errors.json @@ -0,0 +1,14 @@ +[ + { + "code": "declaration_tag_invalid_type", + "message": "Declaration tags must be `let`, `const`, `var` or `function` declarations", + "start": { + "line": 2, + "column": 2 + }, + "end": { + "line": 2, + "column": 7 + } + } +] diff --git a/packages/svelte/tests/validator/samples/declaration-tag-invalid-type/input.svelte b/packages/svelte/tests/validator/samples/declaration-tag-invalid-type/input.svelte new file mode 100644 index 0000000000..3dd25b21db --- /dev/null +++ b/packages/svelte/tests/validator/samples/declaration-tag-invalid-type/input.svelte @@ -0,0 +1,3 @@ +{#if true} + {class Foo {}} +{/if} diff --git a/packages/svelte/tests/validator/samples/declaration-tag-placement/errors.json b/packages/svelte/tests/validator/samples/declaration-tag-placement/errors.json new file mode 100644 index 0000000000..bf8427ee89 --- /dev/null +++ b/packages/svelte/tests/validator/samples/declaration-tag-placement/errors.json @@ -0,0 +1,14 @@ +[ + { + "code": "declaration_tag_invalid_placement", + "message": "Declaration tags must be the immediate child of `{#snippet}`, `{#if}`, `{:else if}`, `{:else}`, `{#each}`, `{:then}`, `{:catch}`, ``, `` or ``", + "start": { + "line": 5, + "column": 0 + }, + "end": { + "line": 5, + "column": 15 + } + } +] diff --git a/packages/svelte/tests/validator/samples/declaration-tag-placement/input.svelte b/packages/svelte/tests/validator/samples/declaration-tag-placement/input.svelte new file mode 100644 index 0000000000..2d9c799deb --- /dev/null +++ b/packages/svelte/tests/validator/samples/declaration-tag-placement/input.svelte @@ -0,0 +1,5 @@ + + +{let b = a + 1} diff --git a/packages/svelte/types/index.d.ts b/packages/svelte/types/index.d.ts index 3f71d44177..dda06bfc64 100644 --- a/packages/svelte/types/index.d.ts +++ b/packages/svelte/types/index.d.ts @@ -852,7 +852,7 @@ declare module 'svelte/attachments' { declare module 'svelte/compiler' { import type { SourceMap } from 'magic-string'; - import type { ArrayExpression, ArrowFunctionExpression, VariableDeclaration, VariableDeclarator, Expression, Identifier, MemberExpression, Node, ObjectExpression, Pattern, Program, ChainExpression, SimpleCallExpression, SequenceExpression, SourceLocation } from 'estree'; + import type { ArrayExpression, ArrowFunctionExpression, FunctionDeclaration, VariableDeclaration, VariableDeclarator, Expression, Identifier, MemberExpression, Node, ObjectExpression, Pattern, Program, ChainExpression, SimpleCallExpression, SequenceExpression, SourceLocation } from 'estree'; import type { Location } from 'locate-character'; import type { default as ts } from 'esrap/languages/ts'; /** @@ -1303,6 +1303,12 @@ declare module 'svelte/compiler' { }; } + /** A `{let ...}`, `{const ...}`, `{var ...}` or `{function ...}` tag */ + export interface DeclarationTag extends BaseNode { + type: 'DeclarationTag'; + declaration: VariableDeclaration | FunctionDeclaration; + } + /** A `{@debug ...}` tag */ export interface DebugTag extends BaseNode { type: 'DebugTag'; @@ -1613,6 +1619,7 @@ declare module 'svelte/compiler' { export type Tag = | AST.AttachTag | AST.ConstTag + | AST.DeclarationTag | AST.DebugTag | AST.ExpressionTag | AST.HtmlTag