From a9e82dd388815bdd3c86bec88afd997ec0dfe342 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 29 May 2026 07:51:17 -0400 Subject: [PATCH 01/19] chore: remove `on_fork_commit` (#18318) Simon thinks it was because he thought that committing the fork != committing the batch, and that this could result in an error boundary showing up too late. But that's not the case --- .../internal/client/dom/blocks/boundary.js | 2 +- .../src/internal/client/reactivity/batch.js | 21 ------------------- 2 files changed, 1 insertion(+), 22 deletions(-) diff --git a/packages/svelte/src/internal/client/dom/blocks/boundary.js b/packages/svelte/src/internal/client/dom/blocks/boundary.js index beaa7d6869..0c0903ff52 100644 --- a/packages/svelte/src/internal/client/dom/blocks/boundary.js +++ b/packages/svelte/src/internal/client/dom/blocks/boundary.js @@ -396,7 +396,7 @@ export class Boundary { if (this.#pending_effect) current_batch.skip_effect(this.#pending_effect); if (this.#failed_effect) current_batch.skip_effect(this.#failed_effect); - current_batch.on_fork_commit(() => { + current_batch.oncommit(() => { this.#handle_error(error); }); } else { diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index 8dbcacf429..3b82059788 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -140,12 +140,6 @@ export class Batch { */ #discard_callbacks = new Set(); - /** - * Callbacks that should run only when a fork is committed. - * @type {Set<(batch: Batch) => void>} - */ - #fork_commit_callbacks = new Set(); - /** * The number of async effects that are currently in flight */ @@ -634,7 +628,6 @@ export class Batch { discard() { for (const fn of this.#discard_callbacks) fn(this); this.#discard_callbacks.clear(); - this.#fork_commit_callbacks.clear(); this.#unlink(); this.#deferred?.resolve(); @@ -840,16 +833,6 @@ export class Batch { this.#discard_callbacks.add(fn); } - /** @param {(batch: Batch) => void} fn */ - on_fork_commit(fn) { - this.#fork_commit_callbacks.add(fn); - } - - run_fork_commit_callbacks() { - for (const fn of this.#fork_commit_callbacks) fn(this); - this.#fork_commit_callbacks.clear(); - } - settled() { return (this.#deferred ??= deferred()).promise; } @@ -1410,10 +1393,6 @@ export function fork(fn) { source.wv = increment_write_version(); } - batch.activate(); - batch.run_fork_commit_callbacks(); - batch.deactivate(); - // trigger any `$state.eager(...)` expressions with the new state. // eager effects don't get scheduled like other effects, so we // can't just encounter them during traversal, we need to From 3fad9cf8380889dfda288bcd0e10bcdbb8136f35 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 29 May 2026 07:54:33 -0400 Subject: [PATCH 02/19] chore: fix browser-support regression check (#18316) vercel bot caught this in the release PR https://github.com/sveltejs/svelte/pull/18315#pullrequestreview-4383622174 --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4f72b6c23e..c25e191702 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -99,7 +99,7 @@ jobs: if: (${{ success() }} || ${{ failure() }}) # ensures this step runs even if previous steps fail run: pnpm build && { [ "`git status --porcelain=v1`" == "" ] || (echo "Generated types have changed — please regenerate types locally with `cd packages/svelte && pnpm generate:types` and commit the changes after you have reviewed them"; git diff; exit 1); } - name: check browser-support docs page is up to date - run: '{ [ "`git status --porcelain=v1 documentation/docs/07-misc/05-browser-support.md`" == "" ] || (echo "The browser-support docs page is out of date — please regenerate it locally with \`cd packages/svelte && pnpm generate:browser-support\` and commit the changes"; git diff documentation/docs/07-misc/05-browser-support.md; exit 1); }' + run: '{ [ "`git status --porcelain=v1 documentation/docs/07-misc/.generated/`" == "" ] || (echo "The browser-support docs page is out of date — please regenerate it locally with \`cd packages/svelte && pnpm generate:browser-support\` and commit the changes"; git diff documentation/docs/07-misc/.generated/; exit 1); }' Benchmarks: permissions: {} runs-on: ubuntu-latest From 656dae67808651fa6a7ca5f8446e28e3a4e76a62 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 29 May 2026 07:55:42 -0400 Subject: [PATCH 03/19] chore: try and discourage slop, even a tiny bit (#18293) i don't know how to stem the flood of slop PRs but maybe this will help a tiny bit --------- Co-authored-by: vercel[bot] <35613825+vercel[bot]@users.noreply.github.com> --- AGENTS.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/AGENTS.md b/AGENTS.md index c6cd3ea310..7f143248aa 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -4,6 +4,8 @@ This guide is for AI coding agents working in the Svelte monorepo. **Important:** Read and follow [`CONTRIBUTING.md`](./CONTRIBUTING.md) as well - it contains essential information about testing, code structure, and contribution guidelines that applies here. +When submitting a PR, you **MUST** read [`PULL_REQUEST_TEMPLATE.md`](./.github/PULL_REQUEST_TEMPLATE.md) and fill it out correctly. **DO NOT** submit a PR without running the full test suite. + ## Quick Reference If asked to do a performance investigation, use the `performance-investigation` skill. From 59d3a36f825d9f2ca29dbdbec0ad27e4f5bf1105 Mon Sep 17 00:00:00 2001 From: Simon H <5968653+dummdidumm@users.noreply.github.com> Date: Fri, 29 May 2026 14:05:13 +0200 Subject: [PATCH 04/19] feat: allow declarations in the template (#18282) Allows `{let/const ...}` declarations in all places (and more) where we already allow `{@const ...}` (which will eventually get deprecated in favor of this new feature). Closes: #16490 Companion PRs: - https://github.com/sveltejs/language-tools/pull/3033 - https://github.com/sveltejs/prettier-plugin-svelte/pull/533 - https://github.com/sveltejs/eslint-plugin-svelte/pull/1533 - https://github.com/sveltejs/svelte-eslint-parser/pull/891 --------- Co-authored-by: Rich Harris --- .changeset/four-loops-agree.md | 5 + .../docs/03-template-syntax/10-@const.md | 2 + .../03-template-syntax/11-declaration-tags.md | 70 +++++++++++ .../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 | 45 ++++++- .../src/compiler/phases/1-parse/index.js | 5 +- .../src/compiler/phases/1-parse/state/tag.js | 92 +++++++++++++- .../src/compiler/phases/2-analyze/index.js | 5 + .../src/compiler/phases/2-analyze/types.d.ts | 4 +- .../2-analyze/visitors/CallExpression.js | 3 + .../phases/2-analyze/visitors/ConstTag.js | 27 +---- .../2-analyze/visitors/DeclarationTag.js | 58 +++++++++ .../phases/2-analyze/visitors/Identifier.js | 2 +- .../3-transform/client/transform-client.js | 19 +-- .../phases/3-transform/client/utils.js | 21 ++++ .../3-transform/client/visitors/ConstTag.js | 37 ++---- .../client/visitors/DeclarationTag.js | 87 ++++++++++++++ .../client/visitors/RegularElement.js | 32 ++++- .../client/visitors/SvelteBoundary.js | 9 +- .../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 | 85 +++++++++++++ .../server/visitors/RegularElement.js | 31 +++-- .../src/compiler/phases/3-transform/utils.js | 1 + packages/svelte/src/compiler/phases/nodes.js | 1 + packages/svelte/src/compiler/print/index.js | 42 +++++++ .../svelte/src/compiler/types/template.d.ts | 13 ++ .../loose-declaration-tag/input.svelte | 4 + .../samples/loose-declaration-tag/output.json | 113 ++++++++++++++++++ .../samples/declaration-tag/input.svelte | 7 ++ .../samples/declaration-tag/output.svelte | 7 ++ .../async-declaration-tag-2/_config.js | 13 ++ .../async-declaration-tag-2/main.svelte | 31 +++++ .../samples/async-declaration-tag/_config.js | 43 +++++++ .../samples/async-declaration-tag/main.svelte | 20 ++++ .../declaration-tags-no-script/_config.js | 13 ++ .../declaration-tags-no-script/main.svelte | 3 + .../samples/declaration-tags/_config.js | 37 ++++++ .../samples/declaration-tags/main.svelte | 29 +++++ .../errors.json | 14 +++ .../input.svelte | 3 + .../declaration-tag-invalid-type/errors.json | 14 +++ .../declaration-tag-invalid-type/input.svelte | 3 + .../declaration-tag-legacy-mode/errors.json | 14 +++ .../declaration-tag-legacy-mode/input.svelte | 5 + .../declaration-tag-maybe-runes/errors.json | 1 + .../declaration-tag-maybe-runes/input.svelte | 6 + packages/svelte/types/index.d.ts | 7 ++ 52 files changed, 1053 insertions(+), 109 deletions(-) create mode 100644 .changeset/four-loops-agree.md create mode 100644 documentation/docs/03-template-syntax/11-declaration-tags.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-2/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-declaration-tag-2/main.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/declaration-tags-no-script/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/declaration-tags-no-script/main.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/declaration-tags/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/declaration-tags/main.svelte create mode 100644 packages/svelte/tests/validator/samples/declaration-tag-invalid-function/errors.json create mode 100644 packages/svelte/tests/validator/samples/declaration-tag-invalid-function/input.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-legacy-mode/errors.json create mode 100644 packages/svelte/tests/validator/samples/declaration-tag-legacy-mode/input.svelte create mode 100644 packages/svelte/tests/validator/samples/declaration-tag-maybe-runes/errors.json create mode 100644 packages/svelte/tests/validator/samples/declaration-tag-maybe-runes/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/10-@const.md b/documentation/docs/03-template-syntax/10-@const.md index 2a587b7a3d..6f2edc1a37 100644 --- a/documentation/docs/03-template-syntax/10-@const.md +++ b/documentation/docs/03-template-syntax/10-@const.md @@ -2,6 +2,8 @@ title: {@const ...} --- +> [!NOTE] `{@const x = y}` is legacy syntax — use [`{const x = $derived(y)}`](declaration-tags) instead + The `{@const ...}` tag defines a local constant. ```svelte diff --git a/documentation/docs/03-template-syntax/11-declaration-tags.md b/documentation/docs/03-template-syntax/11-declaration-tags.md new file mode 100644 index 0000000000..34b7164775 --- /dev/null +++ b/documentation/docs/03-template-syntax/11-declaration-tags.md @@ -0,0 +1,70 @@ +--- +title: {let/const ...} +--- + +Declaration tags define local variables inside markup with `const` or `let`: + + +```svelte + + + +{#each boxes as box} + {const area = box.width * box.height} + {const label = `${box.width} ⨉ ${box.height} = ${area}`} + +

{label}

+{/each} +``` + + +> [!NOTE] The [`{@const ...}`](@const) syntax is considered legacy — use declaration tags instead. + +When values should be reactive, you can use `$state` and `$derived`: + + +```svelte + + + +

Hello {user.name}

+ + +{#if editing} + {let name = $state(user.name)} + {const greeting = $derived(`Hello ${name}`)} + +
+ +

{greeting}

+ + +{/if} +``` + + +Declaration tags can be used anywhere inside the component. They can reference values declared outside themselves (for example in the ` + + + {const sync = 'sync'} + {const number = await Promise.resolve(5)} + {const after_async =number + 1} + {const { length, 0: first } = await '01234'} + + {#snippet greet()} + {const greeting = $derived(await `Hello, ${name}!`)} +

{greeting}

+ {number} + {#if number > 4 && after_async && greeting} + {const length = $derived(await number)} + {#each { length }, index} + {const i = $derived(await index)} + {i} + {/each} + {/if} + {/snippet} + + {@render greet()} + {number} {sync} {after_async} {length} {first} + + {#if sync} + {const double = $derived(number * 2)} + {double} + {/if} +
diff --git a/packages/svelte/tests/runtime-runes/samples/async-declaration-tag/_config.js b/packages/svelte/tests/runtime-runes/samples/async-declaration-tag/_config.js new file mode 100644 index 0000000000..8fd2fc0976 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-declaration-tag/_config.js @@ -0,0 +1,43 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target }) { + await tick(); + const [top, change] = target.querySelectorAll('button'); + + assert.htmlEqual( + target.innerHTML, + ` + + +

Hello name

+
nested Hi name
+ ` + ); + + top.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + + +

Hello name

+
nested Hi name
+ ` + ); + + change.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + + +

Hello other

+
nested Hi other
+ ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-declaration-tag/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-declaration-tag/main.svelte new file mode 100644 index 0000000000..4521ea2e41 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-declaration-tag/main.svelte @@ -0,0 +1,20 @@ + + +{let name = $state(top_id)} + + +{#if id} + {let name = $state(await id)} + {let greeting = $derived(await `Hello ${name}`)} + + +

{greeting}

+
+ {const nested = 'nested'} + {const greeting2 = $derived(await `Hi ${name}`)} + {nested} {greeting2} +
+{/if} diff --git a/packages/svelte/tests/runtime-runes/samples/declaration-tags-no-script/_config.js b/packages/svelte/tests/runtime-runes/samples/declaration-tags-no-script/_config.js new file mode 100644 index 0000000000..901df42df7 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/declaration-tags-no-script/_config.js @@ -0,0 +1,13 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + html: '', + async test({ assert, target }) { + const [increment] = target.querySelectorAll('button'); + + increment.click(); + await tick(); + assert.htmlEqual(target.innerHTML, ''); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/declaration-tags-no-script/main.svelte b/packages/svelte/tests/runtime-runes/samples/declaration-tags-no-script/main.svelte new file mode 100644 index 0000000000..da72bc69c6 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/declaration-tags-no-script/main.svelte @@ -0,0 +1,3 @@ +{let count = $state(0)} +{let doubled = $derived(count * 2)} + diff --git a/packages/svelte/tests/runtime-runes/samples/declaration-tags/_config.js b/packages/svelte/tests/runtime-runes/samples/declaration-tags/_config.js new file mode 100644 index 0000000000..54e390ec83 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/declaration-tags/_config.js @@ -0,0 +1,37 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + html: `

4 total

nested
nested
`, + async test({ assert, target }) { + const [top, toggle, increment] = target.querySelectorAll('button'); + + top.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + `

4 total

nested
nested
` + ); + + increment.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + `

6 total

nested
nested
` + ); + + toggle.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + `
nested
` + ); + + toggle.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + `

4 total

nested
nested
` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/declaration-tags/main.svelte b/packages/svelte/tests/runtime-runes/samples/declaration-tags/main.svelte new file mode 100644 index 0000000000..5953104092 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/declaration-tags/main.svelte @@ -0,0 +1,29 @@ + + +{let top = $state(1)} +{let top_doubled = $derived(top * 2)} + + + + +{#if visible} + {let counter = $state({ value: initial })} + {let doubled = $derived(counter.value * 2)} + {const suffix = ' total'} + {const format = (value) => `${value}${suffix}`} + + +

{format(doubled)}

+
+ {const doubled = 'nested'} + {doubled} +
+{/if} + +
+ {const nested = 'nested'} + {nested} +
diff --git a/packages/svelte/tests/validator/samples/declaration-tag-invalid-function/errors.json b/packages/svelte/tests/validator/samples/declaration-tag-invalid-function/errors.json new file mode 100644 index 0000000000..bf0266225f --- /dev/null +++ b/packages/svelte/tests/validator/samples/declaration-tag-invalid-function/errors.json @@ -0,0 +1,14 @@ +[ + { + "code": "declaration_tag_invalid_type", + "message": "Declaration tags must be `let` or `const` declarations", + "start": { + "line": 2, + "column": 2 + }, + "end": { + "line": 2, + "column": 10 + } + } +] diff --git a/packages/svelte/tests/validator/samples/declaration-tag-invalid-function/input.svelte b/packages/svelte/tests/validator/samples/declaration-tag-invalid-function/input.svelte new file mode 100644 index 0000000000..8cfdf59c0f --- /dev/null +++ b/packages/svelte/tests/validator/samples/declaration-tag-invalid-function/input.svelte @@ -0,0 +1,3 @@ +{#if true} + {function foo() {}} +{/if} 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..2a9b3c0140 --- /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` or `const` declarations", + "start": { + "line": 2, + "column": 2 + }, + "end": { + "line": 2, + "column": 5 + } + } +] 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..eb9fcd5e75 --- /dev/null +++ b/packages/svelte/tests/validator/samples/declaration-tag-invalid-type/input.svelte @@ -0,0 +1,3 @@ +{#if true} + {var foo = 1} +{/if} diff --git a/packages/svelte/tests/validator/samples/declaration-tag-legacy-mode/errors.json b/packages/svelte/tests/validator/samples/declaration-tag-legacy-mode/errors.json new file mode 100644 index 0000000000..6b89f2eab8 --- /dev/null +++ b/packages/svelte/tests/validator/samples/declaration-tag-legacy-mode/errors.json @@ -0,0 +1,14 @@ +[ + { + "code": "declaration_tag_no_legacy_mode", + "message": "Declaration tags cannot be used in legacy mode", + "start": { + "line": 5, + "column": 0 + }, + "end": { + "line": 5, + "column": 19 + } + } +] diff --git a/packages/svelte/tests/validator/samples/declaration-tag-legacy-mode/input.svelte b/packages/svelte/tests/validator/samples/declaration-tag-legacy-mode/input.svelte new file mode 100644 index 0000000000..026d4eff1e --- /dev/null +++ b/packages/svelte/tests/validator/samples/declaration-tag-legacy-mode/input.svelte @@ -0,0 +1,5 @@ + + +{const foo = 'foo'} diff --git a/packages/svelte/tests/validator/samples/declaration-tag-maybe-runes/errors.json b/packages/svelte/tests/validator/samples/declaration-tag-maybe-runes/errors.json new file mode 100644 index 0000000000..fe51488c70 --- /dev/null +++ b/packages/svelte/tests/validator/samples/declaration-tag-maybe-runes/errors.json @@ -0,0 +1 @@ +[] diff --git a/packages/svelte/tests/validator/samples/declaration-tag-maybe-runes/input.svelte b/packages/svelte/tests/validator/samples/declaration-tag-maybe-runes/input.svelte new file mode 100644 index 0000000000..081f242a81 --- /dev/null +++ b/packages/svelte/tests/validator/samples/declaration-tag-maybe-runes/input.svelte @@ -0,0 +1,6 @@ + + +Usage when no explicit runes/legacy mode should be ok +{const foo = world} diff --git a/packages/svelte/types/index.d.ts b/packages/svelte/types/index.d.ts index 3f71d44177..1c3795be80 100644 --- a/packages/svelte/types/index.d.ts +++ b/packages/svelte/types/index.d.ts @@ -1303,6 +1303,12 @@ declare module 'svelte/compiler' { }; } + /** A `{let ...}` or `{const ...}` tag */ + export interface DeclarationTag extends BaseNode { + type: 'DeclarationTag'; + declaration: VariableDeclaration; + } + /** 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 From 5300843e8683948e15eebe8b3342cdef6614a41d Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 29 May 2026 09:28:27 -0400 Subject: [PATCH 05/19] chore: bump playwright (#18319) hopefully this will fix the CI timeouts --- .github/workflows/ci.yml | 2 +- package.json | 2 +- packages/svelte/package.json | 6 +-- pnpm-lock.yaml | 72 +++++++++++++++++++++--------------- 4 files changed, 47 insertions(+), 35 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c25e191702..7eff1b9fe7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -80,7 +80,7 @@ jobs: Lint: permissions: {} runs-on: ubuntu-latest - timeout-minutes: 5 + timeout-minutes: 10 steps: - uses: actions/checkout@v6 - uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v4 diff --git a/package.json b/package.json index 34079f43cf..f741ec3e1f 100644 --- a/package.json +++ b/package.json @@ -37,7 +37,7 @@ "eslint-plugin-lube": "^0.5.1", "eslint-plugin-svelte": "^3.15.0", "jsdom": "25.0.1", - "playwright": "^1.58.0", + "playwright": "^1.60.0", "prettier": "^3.2.4", "prettier-plugin-svelte": "^3.4.0", "svelte": "workspace:^", diff --git a/packages/svelte/package.json b/packages/svelte/package.json index 5e3bb5e031..713c7c04c0 100644 --- a/packages/svelte/package.json +++ b/packages/svelte/package.json @@ -152,7 +152,7 @@ }, "devDependencies": { "@jridgewell/trace-mapping": "^0.3.25", - "@playwright/test": "^1.58.0", + "@playwright/test": "^1.60.0", "@rollup/plugin-commonjs": "^28.0.1", "@rollup/plugin-node-resolve": "^15.3.0", "@rollup/plugin-terser": "^0.4.4", @@ -162,12 +162,12 @@ "baseline-browser-mapping": "^2.10.32", "dts-buddy": "^0.5.5", "esbuild": "^0.25.10", - "web-features": "^3.29.0", "rollup": "^4.59.0", "source-map": "^0.7.4", "tinyglobby": "^0.2.12", "typescript": "^5.5.4", - "vitest": "^2.1.9" + "vitest": "^2.1.9", + "web-features": "^3.29.0" }, "dependencies": { "@jridgewell/remapping": "^2.3.4", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4c077f0f6b..767af0515a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -42,8 +42,8 @@ importers: specifier: 25.0.1 version: 25.0.1 playwright: - specifier: ^1.58.0 - version: 1.58.0 + specifier: ^1.60.0 + version: 1.60.0 prettier: specifier: ^3.2.4 version: 3.2.4 @@ -121,8 +121,8 @@ importers: specifier: ^0.3.25 version: 0.3.31 '@playwright/test': - specifier: ^1.58.0 - version: 1.58.0 + specifier: ^1.60.0 + version: 1.60.0 '@rollup/plugin-commonjs': specifier: ^28.0.1 version: 28.0.1(rollup@4.60.1) @@ -864,8 +864,8 @@ packages: resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} - '@playwright/test@1.58.0': - resolution: {integrity: sha512-fWza+Lpbj6SkQKCrU6si4iu+fD2dD3gxNHFhUPxsfXBPhnv3rRSQVd0NtBUT9Z/RhF/boCBcuUaMUSTRTopjZg==} + '@playwright/test@1.60.0': + resolution: {integrity: sha512-O71yZIbAh/PxDMNGns37GHBIfrVkEVyn+AXyIa5dOTfb4/xNvRWV+Vv/NMbNCtODB/pO7vLlF2OTmMVLhmr7Ag==} engines: {node: '>=18'} hasBin: true @@ -1096,6 +1096,9 @@ packages: '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + '@types/estree@1.0.9': + resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} + '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} @@ -1459,8 +1462,8 @@ packages: emoji-regex@9.2.2: resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} - enhanced-resolve@5.20.1: - resolution: {integrity: sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA==} + enhanced-resolve@5.22.1: + resolution: {integrity: sha512-6QEuw3zoX1SJQc7b87aBXke/no+mG2bTBgw29gWMQonLmpEkWoCAVkl+M49e48AZlWzxiDzDZzYdp6kobcyLww==} engines: {node: '>=10.13.0'} enquirer@2.4.1: @@ -1699,8 +1702,8 @@ packages: function-bind@1.1.2: resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} - get-tsconfig@4.13.7: - resolution: {integrity: sha512-7tN6rFgBlMgpBML5j8typ92BKFi2sFQvIdpAqLA2beia5avZDrMs0FLZiM5etShWq5irVyGcGMEA1jcDaK7A/Q==} + get-tsconfig@4.14.0: + resolution: {integrity: sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==} glob-parent@5.1.2: resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} @@ -2162,13 +2165,13 @@ packages: resolution: {integrity: sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==} engines: {node: '>=6'} - playwright-core@1.58.0: - resolution: {integrity: sha512-aaoB1RWrdNi3//rOeKuMiS65UCcgOVljU46At6eFcOFPFHWtd2weHRRow6z/n+Lec0Lvu0k9ZPKJSjPugikirw==} + playwright-core@1.60.0: + resolution: {integrity: sha512-9bW6zvX/m0lEbgTKJ6YppOKx8H3VOPBMOCFh2irXFOT4BbHgrx5hPjwJYLT40Lu+4qtD36qKc/Hn56StUW57IA==} engines: {node: '>=18'} hasBin: true - playwright@1.58.0: - resolution: {integrity: sha512-2SVA0sbPktiIY/MCOPX8e86ehA/e+tDNq+e5Y8qjKYti2Z/JG7xnronT/TXTIkKbYGWlCbuucZ6dziEgkoEjQQ==} + playwright@1.60.0: + resolution: {integrity: sha512-hheHdokM8cdqCb0lcE3s+zT4t4W+vvjpGxsZlDnikarzx8tSzMebh3UiFtgqwFwnTnjYQcsyMF8ei2mCO/tpeA==} engines: {node: '>=18'} hasBin: true @@ -2307,6 +2310,11 @@ packages: engines: {node: '>=10'} hasBin: true + semver@7.8.1: + resolution: {integrity: sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg==} + engines: {node: '>=10'} + hasBin: true + serialize-javascript@6.0.2: resolution: {integrity: sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==} @@ -2403,8 +2411,8 @@ packages: symbol-tree@3.2.4: resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} - tapable@2.3.2: - resolution: {integrity: sha512-1MOpMXuhGzGL5TTCZFItxCc0AARf1EZFQkGqMm7ERKj8+Hgr5oLvJOVFcC+lRmR8hCe2S3jC4T5D7Vg/d7/fhA==} + tapable@2.3.3: + resolution: {integrity: sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==} engines: {node: '>=6'} term-size@2.2.1: @@ -3283,9 +3291,9 @@ snapshots: '@pkgjs/parseargs@0.11.0': optional: true - '@playwright/test@1.58.0': + '@playwright/test@1.60.0': dependencies: - playwright: 1.58.0 + playwright: 1.60.0 '@polka/url@1.0.0-next.25': {} @@ -3463,13 +3471,15 @@ snapshots: '@types/eslint@8.56.12': dependencies: - '@types/estree': 1.0.8 + '@types/estree': 1.0.9 '@types/json-schema': 7.0.15 '@types/esrecurse@4.3.1': {} '@types/estree@1.0.8': {} + '@types/estree@1.0.9': {} + '@types/json-schema@7.0.15': {} '@types/node@12.20.55': {} @@ -3840,10 +3850,10 @@ snapshots: emoji-regex@9.2.2: {} - enhanced-resolve@5.20.1: + enhanced-resolve@5.22.1: dependencies: graceful-fs: 4.2.11 - tapable: 2.3.2 + tapable: 2.3.3 enquirer@2.4.1: dependencies: @@ -3945,7 +3955,7 @@ snapshots: eslint-compat-utils@0.5.1(eslint@10.0.0): dependencies: eslint: 10.0.0 - semver: 7.7.4 + semver: 7.8.1 eslint-config-prettier@9.1.0(eslint@10.0.0): dependencies: @@ -3965,14 +3975,14 @@ snapshots: eslint-plugin-n@17.24.0(eslint@10.0.0)(typescript@5.5.4): dependencies: '@eslint-community/eslint-utils': 4.9.1(eslint@10.0.0) - enhanced-resolve: 5.20.1 + enhanced-resolve: 5.22.1 eslint: 10.0.0 eslint-plugin-es-x: 7.8.0(eslint@10.0.0) - get-tsconfig: 4.13.7 + get-tsconfig: 4.14.0 globals: 15.15.0 globrex: 0.1.2 ignore: 5.3.2 - semver: 7.7.4 + semver: 7.8.1 ts-declaration-location: 1.0.7(typescript@5.5.4) transitivePeerDependencies: - typescript @@ -4176,7 +4186,7 @@ snapshots: function-bind@1.1.2: {} - get-tsconfig@4.13.7: + get-tsconfig@4.14.0: dependencies: resolve-pkg-maps: 1.0.0 @@ -4596,11 +4606,11 @@ snapshots: pify@4.0.1: {} - playwright-core@1.58.0: {} + playwright-core@1.60.0: {} - playwright@1.58.0: + playwright@1.60.0: dependencies: - playwright-core: 1.58.0 + playwright-core: 1.60.0 optionalDependencies: fsevents: 2.3.2 @@ -4742,6 +4752,8 @@ snapshots: semver@7.7.4: {} + semver@7.8.1: {} + serialize-javascript@6.0.2: dependencies: randombytes: 2.1.0 @@ -4829,7 +4841,7 @@ snapshots: symbol-tree@3.2.4: {} - tapable@2.3.2: {} + tapable@2.3.3: {} term-size@2.2.1: {} From 980c7e2321b5e9041e868c7d32de7029631a3650 Mon Sep 17 00:00:00 2001 From: Simon H <5968653+dummdidumm@users.noreply.github.com> Date: Fri, 29 May 2026 21:51:33 +0200 Subject: [PATCH 06/19] fix: don't error on `{type}` in declaration tags (#18321) The `\b` in the regex matched on `}`, too, so you would get a very confusing "invalid declaration tag" error on `{type}`. Match on whitespace instead (because that's what has to come afterwards) --------- Co-authored-by: vercel[bot] <35613825+vercel[bot]@users.noreply.github.com> Co-authored-by: Vercel Co-authored-by: Rich-Harris --- .../src/compiler/phases/1-parse/state/tag.js | 5 ++++- .../declaration-tag-invalid-type-2/errors.json | 14 ++++++++++++++ .../declaration-tag-invalid-type-2/input.svelte | 15 +++++++++++++++ 3 files changed, 33 insertions(+), 1 deletion(-) create mode 100644 packages/svelte/tests/validator/samples/declaration-tag-invalid-type-2/errors.json create mode 100644 packages/svelte/tests/validator/samples/declaration-tag-invalid-type-2/input.svelte diff --git a/packages/svelte/src/compiler/phases/1-parse/state/tag.js b/packages/svelte/src/compiler/phases/1-parse/state/tag.js index 7428c8660c..45d0081707 100644 --- a/packages/svelte/src/compiler/phases/1-parse/state/tag.js +++ b/packages/svelte/src/compiler/phases/1-parse/state/tag.js @@ -12,7 +12,10 @@ import { find_matching_bracket, match_bracket } from '../utils/bracket.js'; const regex_whitespace_with_closing_curly_brace = /\s*}/y; const regex_supported_declaration = /(?:let|const)\b/y; -const regex_unsupported_declaration = /(?:var|function|class|type|interface|enum)\b/y; +// All except `type` are reserved keywords and cannot be used as variable names. +// For type we check if it's not something like `type .x` / `type ()` / `type % 2` / ... +const regex_unsupported_declaration = + /(?:(?:var|function|class|interface|enum)\b)|(?:type\s+[^?.(`<[&|%^}])/y; const pointy_bois = { '<': '>' }; diff --git a/packages/svelte/tests/validator/samples/declaration-tag-invalid-type-2/errors.json b/packages/svelte/tests/validator/samples/declaration-tag-invalid-type-2/errors.json new file mode 100644 index 0000000000..052636cddb --- /dev/null +++ b/packages/svelte/tests/validator/samples/declaration-tag-invalid-type-2/errors.json @@ -0,0 +1,14 @@ +[ + { + "code": "declaration_tag_invalid_type", + "message": "Declaration tags must be `let` or `const` declarations", + "start": { + "line": 14, + "column": 2 + }, + "end": { + "line": 14, + "column": 8 + } + } +] diff --git a/packages/svelte/tests/validator/samples/declaration-tag-invalid-type-2/input.svelte b/packages/svelte/tests/validator/samples/declaration-tag-invalid-type-2/input.svelte new file mode 100644 index 0000000000..129a8ac7ba --- /dev/null +++ b/packages/svelte/tests/validator/samples/declaration-tag-invalid-type-2/input.svelte @@ -0,0 +1,15 @@ +{#if true} + + {type} + {type } + {type && foo} + {type || bar} + {type % 2} + {type .x} + {type ?.x} + {type ()} + {type [1]} + {type `tag`} + + {type foo = boolean} +{/if} From a40c745fd95e855a7c667b24ee6bb149783d1813 Mon Sep 17 00:00:00 2001 From: Mathias Picker <48158184+MathiasWP@users.noreply.github.com> Date: Fri, 29 May 2026 21:55:07 +0200 Subject: [PATCH 07/19] perf: hoist rest_props exclude list as a module-scope Set (#18252) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The compiler emitted an inline string array as the second argument to `$.rest_props(...)`, and the runtime did a linear `Array.prototype.includes` on it on every property access via the rest-props Proxy. The exclude list only depends on the component definition, not on the instance, so it can be hoisted to module scope and shared by every instance. Switching it to a `Set` at the same time makes each lookup O(1). For a component like ``); +var root = $.from_html(``); export default function Delegated_locally_declared_shadowed($$anchor) { var fragment = $.comment(); var node = $.first_child(fragment); $.each(node, 0, () => ({ length: 1 }), $.index, ($$anchor, $$item, index) => { - var button = root_1(); + var button = root(); $.set_attribute(button, 'data-index', index); diff --git a/packages/svelte/tests/snapshot/samples/each-index-non-null/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/each-index-non-null/_expected/client/index.svelte.js index 804a7c26f1..049d47a96f 100644 --- a/packages/svelte/tests/snapshot/samples/each-index-non-null/_expected/client/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/each-index-non-null/_expected/client/index.svelte.js @@ -2,14 +2,14 @@ import 'svelte/internal/disclose-version'; import 'svelte/internal/flags/legacy'; import * as $ from 'svelte/internal/client'; -var root_1 = $.from_html(`

`); +var root = $.from_html(`

`); export default function Each_index_non_null($$anchor) { var fragment = $.comment(); var node = $.first_child(fragment); $.each(node, 0, () => Array(10), $.index, ($$anchor, $$item, i) => { - var p = root_1(); + var p = root(); p.textContent = `index: ${i}`; $.append($$anchor, p); diff --git a/packages/svelte/tests/snapshot/samples/select-with-rich-content/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/select-with-rich-content/_expected/client/index.svelte.js index 8f8b115d70..c8354fe67e 100644 --- a/packages/svelte/tests/snapshot/samples/select-with-rich-content/_expected/client/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/select-with-rich-content/_expected/client/index.svelte.js @@ -4,63 +4,51 @@ import * as $ from 'svelte/internal/client'; import Option from './Option.svelte'; const opt = ($$anchor) => { - var option = root_1(); + var option = root(); $.append($$anchor, option); }; const option_snippet = ($$anchor) => { - var option_1 = root_2(); + var option_1 = root_1(); $.append($$anchor, option_1); }; const option_snippet2 = ($$anchor) => { - var option_2 = root_3(); + var option_2 = root_2(); $.append($$anchor, option_2); }; const conditional_option = ($$anchor) => { - var option_3 = root_4(); + var option_3 = root_3(); $.append($$anchor, option_3); }; -var root_1 = $.from_html(``); -var root_2 = $.from_html(``); -var root_3 = $.from_html(``); -var root_4 = $.from_html(``); +var root = $.from_html(``); +var root_1 = $.from_html(``); +var root_2 = $.from_html(``); +var root_3 = $.from_html(``); var option_content = $.from_html(`Rich`, 1); -var root_5 = $.from_html(``); -var root_6 = $.from_html(``); -var root_7 = $.from_html(``); +var root_4 = $.from_html(``); +var root_5 = $.from_html(``); +var root_6 = $.from_html(``); var select_content = $.from_html(``, 1); -var root_8 = $.from_html(``); var option_content_1 = $.from_html(`Bold`, 1); -var root_9 = $.from_html(``); var option_content_2 = $.from_html(`Italic text`, 1); var option_content_3 = $.from_html(` `, 1); -var root_10 = $.from_html(``); -var root_12 = $.from_html(``); -var root_13 = $.from_html(``); +var root_7 = $.from_html(``); +var root_8 = $.from_html(``); var option_content_4 = $.from_html(`Rich in boundary`, 1); -var root_14 = $.from_html(``); -var select_content_1 = $.from_html(``, 1); -var select_content_2 = $.from_html(``, 1); -var select_content_3 = $.from_html(``, 1); -var optgroup_content = $.from_html(``, 1); -var optgroup_content_1 = $.from_html(``, 1); -var option_content_5 = $.from_html(``, 1); -var select_content_4 = $.from_html(``, 1); -var select_content_5 = $.from_html(``, 1); -var root = $.from_html(` `, 1); +var root_9 = $.from_html(` `, 1); export default function Select_with_rich_content($$anchor) { let items = [1, 2, 3]; let show = true; let html = ''; - var fragment = root(); + var fragment = root_9(); var select = $.first_child(fragment); var option_4 = $.child(select); @@ -76,7 +64,7 @@ export default function Select_with_rich_content($$anchor) { var select_1 = $.sibling(select, 2); $.each(select_1, 5, () => items, $.index, ($$anchor, item) => { - var option_5 = root_5(); + var option_5 = root_4(); var text = $.child(option_5, true); $.reset(option_5); @@ -101,7 +89,7 @@ export default function Select_with_rich_content($$anchor) { { var consequent = ($$anchor) => { - var option_6 = root_6(); + var option_6 = root_5(); $.append($$anchor, option_6); }; @@ -117,7 +105,7 @@ export default function Select_with_rich_content($$anchor) { var node_1 = $.child(select_3); $.key(node_1, () => items, ($$anchor) => { - var option_7 = root_7(); + var option_7 = root_6(); $.append($$anchor, option_7); }); @@ -139,7 +127,7 @@ export default function Select_with_rich_content($$anchor) { $.each(select_5, 5, () => items, $.index, ($$anchor, item) => { const x = $.derived_safe_equal(() => $.get(item) * 2); - var option_8 = root_8(); + var option_8 = root_4(); var text_1 = $.child(option_8, true); $.reset(option_8); @@ -177,7 +165,7 @@ export default function Select_with_rich_content($$anchor) { var optgroup_1 = $.child(select_7); $.each(optgroup_1, 5, () => items, $.index, ($$anchor, item) => { - var option_10 = root_9(); + var option_10 = root_4(); var text_2 = $.child(option_10, true); $.reset(option_10); @@ -215,7 +203,7 @@ export default function Select_with_rich_content($$anchor) { var select_9 = $.sibling(select_8, 2); $.each(select_9, 5, () => items, $.index, ($$anchor, item) => { - var option_12 = root_10(); + var option_12 = root_7(); $.customizable_select(option_12, () => { var anchor_4 = $.child(option_12); @@ -242,7 +230,7 @@ export default function Select_with_rich_content($$anchor) { var node_4 = $.first_child(fragment_6); $.each(node_4, 1, () => items, $.index, ($$anchor, item) => { - var option_13 = root_12(); + var option_13 = root_4(); var text_4 = $.child(option_13, true); $.reset(option_13); @@ -274,7 +262,7 @@ export default function Select_with_rich_content($$anchor) { var node_5 = $.child(select_11); $.boundary(node_5, {}, ($$anchor) => { - var option_14 = root_13(); + var option_14 = root_8(); $.append($$anchor, option_14); }); @@ -285,7 +273,7 @@ export default function Select_with_rich_content($$anchor) { var node_6 = $.child(select_12); $.boundary(node_6, {}, ($$anchor) => { - var option_15 = root_14(); + var option_15 = root_7(); $.customizable_select(option_15, () => { var anchor_5 = $.child(option_15); @@ -303,7 +291,7 @@ export default function Select_with_rich_content($$anchor) { $.customizable_select(select_13, () => { var anchor_6 = $.child(select_13); - var fragment_8 = select_content_1(); + var fragment_8 = select_content(); var node_7 = $.first_child(fragment_8); Option(node_7, {}); @@ -314,7 +302,7 @@ export default function Select_with_rich_content($$anchor) { $.customizable_select(select_14, () => { var anchor_7 = $.child(select_14); - var fragment_9 = select_content_2(); + var fragment_9 = select_content(); var node_8 = $.first_child(fragment_9); option_snippet(node_8); @@ -325,7 +313,7 @@ export default function Select_with_rich_content($$anchor) { $.customizable_select(select_15, () => { var anchor_8 = $.child(select_15); - var fragment_10 = select_content_3(); + var fragment_10 = select_content(); var node_9 = $.first_child(fragment_10); $.html(node_9, () => html); @@ -337,7 +325,7 @@ export default function Select_with_rich_content($$anchor) { $.customizable_select(optgroup_2, () => { var anchor_9 = $.child(optgroup_2); - var fragment_11 = optgroup_content(); + var fragment_11 = select_content(); var node_10 = $.first_child(fragment_11); Option(node_10, {}); @@ -351,7 +339,7 @@ export default function Select_with_rich_content($$anchor) { $.customizable_select(optgroup_3, () => { var anchor_10 = $.child(optgroup_3); - var fragment_12 = optgroup_content_1(); + var fragment_12 = select_content(); var node_11 = $.first_child(fragment_12); option_snippet2(node_11); @@ -365,7 +353,7 @@ export default function Select_with_rich_content($$anchor) { $.customizable_select(option_16, () => { var anchor_11 = $.child(option_16); - var fragment_13 = option_content_5(); + var fragment_13 = select_content(); var node_12 = $.first_child(fragment_13); $.html(node_12, () => 'Bold HTML'); @@ -378,7 +366,7 @@ export default function Select_with_rich_content($$anchor) { $.customizable_select(select_19, () => { var anchor_12 = $.child(select_19); - var fragment_14 = select_content_4(); + var fragment_14 = select_content(); var node_13 = $.first_child(fragment_14); $.each(node_13, 1, () => items, $.index, ($$anchor, item) => { @@ -392,7 +380,7 @@ export default function Select_with_rich_content($$anchor) { $.customizable_select(select_20, () => { var anchor_13 = $.child(select_20); - var fragment_16 = select_content_5(); + var fragment_16 = select_content(); var node_14 = $.first_child(fragment_16); { From 6a955758a67a3c9c80f58c45961c0db2a52d463f Mon Sep 17 00:00:00 2001 From: 7nik Date: Fri, 29 May 2026 23:22:55 +0300 Subject: [PATCH 09/19] docs: mention since version of delcaration tags (#18322) --- documentation/docs/03-template-syntax/11-declaration-tags.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/documentation/docs/03-template-syntax/11-declaration-tags.md b/documentation/docs/03-template-syntax/11-declaration-tags.md index 34b7164775..e0edaf6a38 100644 --- a/documentation/docs/03-template-syntax/11-declaration-tags.md +++ b/documentation/docs/03-template-syntax/11-declaration-tags.md @@ -20,6 +20,8 @@ Declaration tags define local variables inside markup with `const` or `let`: ``` +> [!NOTE] Declaration tags are available since Svelte 5.56. + > [!NOTE] The [`{@const ...}`](@const) syntax is considered legacy — use declaration tags instead. When values should be reactive, you can use `$state` and `$derived`: From 6d26dce265de128d65a99e1735e576651d9c1a75 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 29 May 2026 17:05:12 -0400 Subject: [PATCH 10/19] allow class/function expressions in tags (#18324) We shouldn't error on encountering `class` and `function`, because they are treated as expressions rather than declarations --- .../src/compiler/phases/1-parse/state/tag.js | 3 +-- .../declaration-tag-invalid-function/errors.json | 15 +-------------- 2 files changed, 2 insertions(+), 16 deletions(-) diff --git a/packages/svelte/src/compiler/phases/1-parse/state/tag.js b/packages/svelte/src/compiler/phases/1-parse/state/tag.js index 45d0081707..3c8eba4b26 100644 --- a/packages/svelte/src/compiler/phases/1-parse/state/tag.js +++ b/packages/svelte/src/compiler/phases/1-parse/state/tag.js @@ -14,8 +14,7 @@ const regex_whitespace_with_closing_curly_brace = /\s*}/y; const regex_supported_declaration = /(?:let|const)\b/y; // All except `type` are reserved keywords and cannot be used as variable names. // For type we check if it's not something like `type .x` / `type ()` / `type % 2` / ... -const regex_unsupported_declaration = - /(?:(?:var|function|class|interface|enum)\b)|(?:type\s+[^?.(`<[&|%^}])/y; +const regex_unsupported_declaration = /(?:(?:var|interface|enum)\b)|(?:type\s+[^?.(`<[&|%^}])/y; const pointy_bois = { '<': '>' }; diff --git a/packages/svelte/tests/validator/samples/declaration-tag-invalid-function/errors.json b/packages/svelte/tests/validator/samples/declaration-tag-invalid-function/errors.json index bf0266225f..fe51488c70 100644 --- a/packages/svelte/tests/validator/samples/declaration-tag-invalid-function/errors.json +++ b/packages/svelte/tests/validator/samples/declaration-tag-invalid-function/errors.json @@ -1,14 +1 @@ -[ - { - "code": "declaration_tag_invalid_type", - "message": "Declaration tags must be `let` or `const` declarations", - "start": { - "line": 2, - "column": 2 - }, - "end": { - "line": 2, - "column": 10 - } - } -] +[] From 70afafe18e48a9973c5f711d62d87d088234655e Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 29 May 2026 17:55:34 -0400 Subject: [PATCH 11/19] Version Packages (#18315) This PR was opened by the [Changesets release](https://github.com/changesets/action) GitHub action. When you're ready to do a release, you can merge this and the packages will be published to npm automatically. If you're not ready to do a release yet, that's fine, whenever you add more changesets to main, this PR will be updated. # Releases ## svelte@5.56.0 ### Minor Changes - feat: allow declarations in the template ([#18282](https://github.com/sveltejs/svelte/pull/18282)) ### Patch Changes - perf: use `createElement` instead of `createElementNS` for HTML elements ([#18262](https://github.com/sveltejs/svelte/pull/18262)) - perf: store `current_sources` as a `Set` for O(1) membership checks ([#18278](https://github.com/sveltejs/svelte/pull/18278)) - perf: deduplicate identical hoisted templates within a component ([#18320](https://github.com/sveltejs/svelte/pull/18320)) - perf: hoist `rest_props` exclude list as a module-scope `Set` ([#18252](https://github.com/sveltejs/svelte/pull/18252)) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .changeset/create-element-fast-path.md | 5 ----- .changeset/current-sources-set.md | 5 ----- .changeset/dedupe-hoisted-templates.md | 5 ----- .changeset/four-loops-agree.md | 5 ----- .changeset/hoist-rest-excludes.md | 5 ----- packages/svelte/CHANGELOG.md | 16 ++++++++++++++++ packages/svelte/package.json | 2 +- packages/svelte/src/version.js | 2 +- 8 files changed, 18 insertions(+), 27 deletions(-) delete mode 100644 .changeset/create-element-fast-path.md delete mode 100644 .changeset/current-sources-set.md delete mode 100644 .changeset/dedupe-hoisted-templates.md delete mode 100644 .changeset/four-loops-agree.md delete mode 100644 .changeset/hoist-rest-excludes.md diff --git a/.changeset/create-element-fast-path.md b/.changeset/create-element-fast-path.md deleted file mode 100644 index 8b88a14163..0000000000 --- a/.changeset/create-element-fast-path.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -perf: use `createElement` instead of `createElementNS` for HTML elements diff --git a/.changeset/current-sources-set.md b/.changeset/current-sources-set.md deleted file mode 100644 index e1a4dc43b2..0000000000 --- a/.changeset/current-sources-set.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -perf: store `current_sources` as a `Set` for O(1) membership checks diff --git a/.changeset/dedupe-hoisted-templates.md b/.changeset/dedupe-hoisted-templates.md deleted file mode 100644 index a91882f525..0000000000 --- a/.changeset/dedupe-hoisted-templates.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -perf: deduplicate identical hoisted templates within a component diff --git a/.changeset/four-loops-agree.md b/.changeset/four-loops-agree.md deleted file mode 100644 index 46d30e8464..0000000000 --- a/.changeset/four-loops-agree.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': minor ---- - -feat: allow declarations in the template diff --git a/.changeset/hoist-rest-excludes.md b/.changeset/hoist-rest-excludes.md deleted file mode 100644 index 52efb06092..0000000000 --- a/.changeset/hoist-rest-excludes.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -perf: hoist `rest_props` exclude list as a module-scope `Set` diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index ef40c5d159..57725580e7 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,21 @@ # svelte +## 5.56.0 + +### Minor Changes + +- feat: allow declarations in the template ([#18282](https://github.com/sveltejs/svelte/pull/18282)) + +### Patch Changes + +- perf: use `createElement` instead of `createElementNS` for HTML elements ([#18262](https://github.com/sveltejs/svelte/pull/18262)) + +- perf: store `current_sources` as a `Set` for O(1) membership checks ([#18278](https://github.com/sveltejs/svelte/pull/18278)) + +- perf: deduplicate identical hoisted templates within a component ([#18320](https://github.com/sveltejs/svelte/pull/18320)) + +- perf: hoist `rest_props` exclude list as a module-scope `Set` ([#18252](https://github.com/sveltejs/svelte/pull/18252)) + ## 5.55.10 ### Patch Changes diff --git a/packages/svelte/package.json b/packages/svelte/package.json index 713c7c04c0..6e51dd9a93 100644 --- a/packages/svelte/package.json +++ b/packages/svelte/package.json @@ -2,7 +2,7 @@ "name": "svelte", "description": "Cybernetically enhanced web apps", "license": "MIT", - "version": "5.55.10", + "version": "5.56.0", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index 638534d369..1b080d134a 100644 --- a/packages/svelte/src/version.js +++ b/packages/svelte/src/version.js @@ -4,5 +4,5 @@ * The current version, as set in package.json. * @type {string} */ -export const VERSION = '5.55.10'; +export const VERSION = '5.56.0'; export const PUBLIC_VERSION = '5'; From e027d8bf224256714778b4fff41a926177a457f9 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 29 May 2026 17:55:55 -0400 Subject: [PATCH 12/19] chore: annotate `each_array` return value (#18325) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit tiny self-mergeable change extracted from #18035 — just turns `each_array` from a `Derived` to a `Derived` --- packages/svelte/src/internal/client/dom/blocks/each.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/svelte/src/internal/client/dom/blocks/each.js b/packages/svelte/src/internal/client/dom/blocks/each.js index 5f8adee1b5..2df1d6ffa1 100644 --- a/packages/svelte/src/internal/client/dom/blocks/each.js +++ b/packages/svelte/src/internal/client/dom/blocks/each.js @@ -203,7 +203,9 @@ export function each(node, flags, get_collection, get_key, render_fn, fallback_f var each_array = derived_safe_equal(() => { var collection = get_collection(); - return is_array(collection) ? collection : collection == null ? [] : array_from(collection); + return /** @type {V[]} */ ( + is_array(collection) ? collection : collection == null ? [] : array_from(collection) + ); }); if (DEV) { From 05a3bce6bbfb10f19376ae2fcd5a80e92c7384c5 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sat, 30 May 2026 08:03:58 -0400 Subject: [PATCH 13/19] chore: minor tidy-ups (#18326) some more stuff extracted from #18035 --- .../client/dom/elements/bindings/input.js | 1 - .../src/internal/client/reactivity/async.js | 33 +++++++++++++------ .../src/internal/client/reactivity/effects.js | 6 ++-- 3 files changed, 27 insertions(+), 13 deletions(-) diff --git a/packages/svelte/src/internal/client/dom/elements/bindings/input.js b/packages/svelte/src/internal/client/dom/elements/bindings/input.js index 55e61c3774..dadcac369c 100644 --- a/packages/svelte/src/internal/client/dom/elements/bindings/input.js +++ b/packages/svelte/src/internal/client/dom/elements/bindings/input.js @@ -7,7 +7,6 @@ import { is } from '../../../proxy.js'; import { queue_micro_task } from '../../task.js'; import { hydrating } from '../../hydration.js'; import { tick, untrack } from '../../../runtime.js'; -import { is_runes } from '../../../context.js'; import { current_batch, previous_batch } from '../../../reactivity/batch.js'; import { async_mode_flag } from '../../../../flags/index.js'; diff --git a/packages/svelte/src/internal/client/reactivity/async.js b/packages/svelte/src/internal/client/reactivity/async.js index 89115108d5..32d33d6f7a 100644 --- a/packages/svelte/src/internal/client/reactivity/async.js +++ b/packages/svelte/src/internal/client/reactivity/async.js @@ -1,4 +1,4 @@ -/** @import { Blocker, Effect, Value } from '#client' */ +/** @import { Blocker, Effect, Source, Value } from '#client' */ import { DESTROYED, STALE_REACTION } from '#client/constants'; import { DEV } from 'esm-env'; import { @@ -38,8 +38,22 @@ export function flatten(blockers, sync, async, fn) { // Filter out already-settled blockers - no need to wait for them var pending = blockers.filter((b) => !b.settled); + var deriveds = sync.map(d); + + if (DEV) { + deriveds.forEach((d, i) => { + // TODO this is kinda useful for debugging but a lousy implementation — + // maybe the compiler could pass through the template string + d.label = sync[i] + .toString() + .replace('() => ', '') + .replaceAll('$.eager(() => ', '$state.eager(') + .replace(/\$\.get\((.+?)\)/g, (_, id) => id); + }); + } + if (async.length === 0 && pending.length === 0) { - fn(sync.map(d)); + fn(deriveds); return; } @@ -53,8 +67,10 @@ export function flatten(blockers, sync, async, fn) { ? Promise.all(pending.map((b) => b.promise)) : null; - /** @param {Value[]} values */ - function finish(values) { + /** + * @param {Source[]} async + */ + function finish(async) { if ((parent.f & DESTROYED) !== 0) { return; } @@ -62,7 +78,7 @@ export function flatten(blockers, sync, async, fn) { restore(); try { - fn(values); + fn([...deriveds, ...async]); } catch (error) { invoke_error_boundary(error, parent); } @@ -74,17 +90,14 @@ export function flatten(blockers, sync, async, fn) { // Fast path: blockers but no async expressions if (async.length === 0) { - /** @type {Promise} */ (blocker_promise) - .then(() => finish(sync.map(d))) - .finally(decrement_pending); - + /** @type {Promise} */ (blocker_promise).then(() => finish([])).finally(decrement_pending); return; } // Full path: has async expressions function run() { Promise.all(async.map((expression) => async_derived(expression))) - .then((result) => finish([...sync.map(d), ...result])) + .then(finish) .catch((error) => invoke_error_boundary(error, parent)) .finally(decrement_pending); } diff --git a/packages/svelte/src/internal/client/reactivity/effects.js b/packages/svelte/src/internal/client/reactivity/effects.js index 1bbda86fa7..c5d195dfae 100644 --- a/packages/svelte/src/internal/client/reactivity/effects.js +++ b/packages/svelte/src/internal/client/reactivity/effects.js @@ -387,7 +387,9 @@ export function render_effect(fn, flags = 0) { */ export function template_effect(fn, sync = [], async = [], blockers = []) { flatten(blockers, sync, async, (values) => { - create_effect(RENDER_EFFECT, () => fn(...values.map(get))); + create_effect(RENDER_EFFECT, () => { + fn(...values.map(get)); + }); }); } @@ -518,7 +520,7 @@ export function destroy_effect(effect, remove_dom = true) { removed = true; } - set_signal_status(effect, DESTROYING); + effect.f |= DESTROYING; destroy_effect_children(effect, remove_dom && !removed); remove_reactions(effect, 0); From 11985c020fe1f7f7755494929c5d59f44a45e990 Mon Sep 17 00:00:00 2001 From: ottomated <31470743+ottomated@users.noreply.github.com> Date: Sun, 31 May 2026 08:41:49 -0700 Subject: [PATCH 14/19] docs: desloppify browser support page (#18333) The text in https://svelte.dev/docs/svelte/browser-support is quite redundant and has a lot of AI smell. I did my best to make it more concise and remove some slop comments in the generation script. Requires a companion PR in svelte.dev: https://github.com/sveltejs/svelte.dev/pull/2013 image ### Before submitting the PR, please make sure you do the following - [ ] It's really useful if your PR references an issue where it is discussed ahead of time. In many cases, features are absent for a reason. For large changes, please create an RFC: https://github.com/sveltejs/rfcs - [x] Prefix your PR title with `feat:`, `fix:`, `chore:`, or `docs:`. - [x] This message body should clearly illustrate what problems it solves. - [ ] Ideally, include a test that fails without this PR but passes with it. - [x] If this PR changes code within `packages/svelte/src`, add a changeset (`npx changeset`). ### Tests and linting - [x] Run the tests with `pnpm test` and lint the project with `pnpm lint` --------- Co-authored-by: Rich Harris --- .prettierignore | 1 + .../.generated/browser-support-features.md | 8 +- .../07-misc/.generated/browser-support.md | 20 +- .../docs/07-misc/05-browser-support.md | 23 +-- .../scripts/generate-browser-support.ts | 175 +++++++++--------- 5 files changed, 109 insertions(+), 118 deletions(-) diff --git a/.prettierignore b/.prettierignore index 92d9bc797b..28f447f359 100644 --- a/.prettierignore +++ b/.prettierignore @@ -8,6 +8,7 @@ packages/**/config/*.js # packages/svelte packages/svelte/messages/**/*.md packages/svelte/scripts/_bundle.js +packages/svelte/scripts/_baseline/*.ts packages/svelte/src/compiler/errors.js packages/svelte/src/compiler/warnings.js packages/svelte/src/internal/client/errors.js diff --git a/documentation/docs/07-misc/.generated/browser-support-features.md b/documentation/docs/07-misc/.generated/browser-support-features.md index 1e1a00d4d3..1ed6da9e61 100644 --- a/documentation/docs/07-misc/.generated/browser-support-features.md +++ b/documentation/docs/07-misc/.generated/browser-support-features.md @@ -1,7 +1,7 @@ | Feature | Chrome/Edge | Firefox | Safari | -| --- | ---: | ---: | ---: | -| `$state.snapshot` | 98 | 94 | 15.4 | -| `bind:devicePixelContentBoxSize` | | 93 | not supported | -| `flip` from `svelte/animate` | | 126 | | \ No newline at end of file +| - | - | - | - | +| [`$state.snapshot`](/docs/svelte/$state#$state.snapshot) | 98 | 94 | 15.4 | +| [`bind:devicePixelContentBoxSize`](/docs/svelte/bind#Dimensions) | | 93 | not supported | +| [`flip` from `svelte/animate`](/docs/svelte/svelte-animate#flip) | | 126 | | diff --git a/documentation/docs/07-misc/.generated/browser-support.md b/documentation/docs/07-misc/.generated/browser-support.md index 8c58588960..1e8b962856 100644 --- a/documentation/docs/07-misc/.generated/browser-support.md +++ b/documentation/docs/07-misc/.generated/browser-support.md @@ -1,13 +1,15 @@ -| Browser | Minimum version | -| ---------------- | --------------- | -| Chrome/Edge | 87 | -| Firefox | 83 | -| Safari | 14 | -| Opera | 73 | -| Opera (Android) | 62 | -| Samsung Internet | 14.0 | -| Android WebView | 87 | +| Browser | Minimum version | +| - | - | +| Chrome/Edge | 87 | +| Firefox | 83 | +| Safari | 14 | +| Opera | 73 | +| Opera (Android) | 62 | +| Samsung Internet | 14.0 | +| Android WebView | 87 | +| Internet Explorer | not supported | + > [!NOTE] This equates to a Baseline target of 2020. \ No newline at end of file diff --git a/documentation/docs/07-misc/05-browser-support.md b/documentation/docs/07-misc/05-browser-support.md index 1a8830bdb9..735f3126de 100644 --- a/documentation/docs/07-misc/05-browser-support.md +++ b/documentation/docs/07-misc/05-browser-support.md @@ -2,29 +2,14 @@ title: Browser support --- -The table below shows the minimum browser versions Svelte's runtime and compiled output are expected to work in. +The table below shows the minimum browser versions Svelte is expected to work in, derived from the browser APIs used by Svelte's internal code. @include .generated/browser-support.md -These numbers describe what Svelte's output _requires_ in order to run — they're derived from the APIs the code uses, not from a list of browsers the team commits to testing. +This table only covers Svelte itself. It does not include [SvelteKit](/docs/kit), other Svelte libraries, or your own code. -## What is covered +## Exceptions -- **Svelte's runtime.** Everything you import from `svelte` or its subpackages, in the form your bundler ships to the browser. -- **Compiler output.** The JavaScript the Svelte compiler emits from your `.svelte` files, including the DOM operations, bindings and transitions used in your components. - -## What is not covered - -- **Your own code** inside ``; } if (tag === 'input') { - // `bind:checked` requires type="checkbox"|"radio"; `bind:group` too. + // `bind:checked` and `bind:group` require type="checkbox" | "radio" const type = name === 'checked' || name === 'indeterminate' ? ' type="checkbox"' @@ -567,7 +563,6 @@ function binding_fixture(name: string, props: BindingProperty): string { * Compile a `.svelte` fixture to JS (no-op for `.js` fixtures), then * bundle the result through the shared `bundle` helper. Fixtures are tiny * so circular-dep warnings from the Svelte runtime are silenced. - * */ async function bundle_fixture(feature: Feature): Promise { const entry_code = @@ -612,6 +607,8 @@ async function find_all_conditional_features( const features = enumerate_features(subpackage_exports); const rows: ConditionalRow[] = []; + const missing_doc_links: string[] = []; + for (let i = 0; i < features.length; i++) { const feature = features[i]; process.stdout.write(`\r ${i + 1}/${features.length} ${feature.name}`.padEnd(80)); @@ -645,25 +642,35 @@ async function find_all_conditional_features( } } + let doc_link = doc_links[feature.name]; + if (doc_link === undefined) { + doc_link = null; + missing_doc_links.push(feature.name); + } + rows.push({ name: feature.name, + doc_link, versions, baseline_year: final_year }); } process.stdout.write('\n'); + if (missing_doc_links.length) { + throw new Error(`Missing documentation url for some features. +Add them to the \`doc_links\` map in \`scripts/generate-browser-support.ts\`, or add an explicit \`null\` if they don't have a documentation url. +${missing_doc_links.map((name) => ` - "${name}"`).join('\n')}`); + } + return rows; } -/** - * Render the per-feature browser-requirements table from the auto-detected - * rows. Sorted by Safari floor descending, then alphabetically. - */ -function render_conditional_table(rows: ConditionalRow[], runtime_floor: RuntimeFloor): string { - if (rows.length === 0) { +function render_conditional_table(features: ConditionalRow[], runtime_floor: RuntimeFloor): string { + if (features.length === 0) { return '_No features currently require browser versions newer than the runtime floor._'; } + features.sort((a, b) => a.name.localeCompare(b.name)); const browsers = [ ['chrome', 'Chrome/Edge'], @@ -671,32 +678,25 @@ function render_conditional_table(rows: ConditionalRow[], runtime_floor: Runtime ['safari', 'Safari'] ] as const; - const runtime_versions = browser_versions_for(runtime_floor); - - const sorted = [...rows].sort((a, b) => { - const sa = Number(a.versions.safari ?? '0'); - const sb = Number(b.versions.safari ?? '0'); - if (sb !== sa) return sb - sa; - return a.name.localeCompare(b.name); - }); - - const header = '| Feature | ' + browsers.map(([, label]) => `${label}`).join(' | ') + ' |'; - const sep = '| --- |' + browsers.map(() => ' ---: |').join(''); + const floor_versions = browser_versions_for(runtime_floor); - const body = sorted.map((entry) => { - const cells = browsers.map(([key]) => { - const v = entry.versions[key]; + const rows: string[][] = []; + for (const row of features) { + const name_cell = row.doc_link ? `[${row.name}](${row.doc_link})` : row.name; + const versions = browsers.map(([key]) => { + const v = row.versions[key]; if (v === null) return 'not supported'; if (v === undefined) return ''; - const floor_v = runtime_versions[key]; - return floor_v && Number(v) <= Number(floor_v) - ? '' - : v; + const floor_v = floor_versions[key]; + if (floor_v && Number(v) <= Number(floor_v)) + return ''; + + return v; }); - return `| ${entry.name} | ${cells.join(' | ')} |`; - }); + rows.push([name_cell, ...versions]); + } - return [header, sep, ...body].join('\n'); + return render_markdown_table(['Feature', ...browsers.map(([, label]) => `${label}`)], rows); } function browser_versions_for(target: RuntimeFloor): Record { @@ -750,53 +750,59 @@ function browser_versions_for(target: RuntimeFloor): Record { return lookup; } -function render_table(versions: Record, target: RuntimeFloor): string { - // Chrome and Edge ship from the same engine and historically resolve to the - // same Baseline version. Collapse them into one row when they match, but - // fall back to listing them separately if they ever drift. - const chrome_edge: [string, string] | null = - versions.chrome && versions.chrome === versions.edge ? ['Chrome/Edge', versions.chrome] : null; - - const base_rows: Array<[string, string]> = chrome_edge - ? [chrome_edge, ['Chrome (Android)', versions.chrome_android]] - : [ - ['Chrome', versions.chrome], - ['Chrome (Android)', versions.chrome_android], - ['Edge', versions.edge] - ]; - - const rows = [ - ...base_rows, - ['Firefox', versions.firefox], - ['Firefox (Android)', versions.firefox_android], - ['Safari', versions.safari], - ['Safari (iOS)', versions.safari_ios], - ['Opera', versions.opera], - ['Opera (Android)', versions.opera_android], - ['Samsung Internet', versions.samsunginternet_android], - ['Android WebView', versions.webview_android] - ].filter(([label, version]) => version !== undefined) as Array<[string, string]>; - - const headings = ['Browser', 'Minimum version']; - - const widths = headings.map((heading, i) => - Math.max(heading.length, ...rows.map((r) => String(r[i]).length)) - ); +const BROWSER = { + chrome: 'Chrome', + edge: 'Edge', + firefox: 'Firefox', + safari: 'Safari', + opera: 'Opera', + samsung_internet: 'Samsung Internet', + webview_android: 'Android WebView', + internet_explorer: 'Internet Explorer' +}; - const pad = (s: string, n: number) => s + ' '.repeat(Math.max(0, n - s.length)); +function render_browser_table(versions: Record, target: RuntimeFloor): string { + const rows: Array<[string, string]> = [ + [BROWSER.chrome, versions.chrome], + [`${BROWSER.chrome} (Android)`, versions.chrome_android] + ]; - const header = `| ${headings.map((heading, i) => pad(heading, widths[i])).join(' | ')} |`; - const sep = `| ${widths.map((width) => '-'.repeat(width)).join(' | ')} |`; - const body = rows - .map(([a, b]) => `| ${pad(a, widths[0])} | ${pad(String(b), widths[1])} |`) - .join('\n'); + if (versions.chrome === versions.edge) { + rows[0][0] += `/${BROWSER.edge}`; + } else { + rows.push([BROWSER.edge, versions.edge]); + } + + rows.push( + [BROWSER.firefox, versions.firefox], + [`${BROWSER.firefox} (Android)`, versions.firefox_android], + [BROWSER.safari, versions.safari], + [`${BROWSER.safari} (iOS)`, versions.safari_ios], + [BROWSER.opera, versions.opera], + [`${BROWSER.opera} (Android)`, versions.opera_android], + [BROWSER.samsung_internet, versions.samsunginternet_android], + [BROWSER.webview_android, versions.webview_android], + [BROWSER.internet_explorer, 'not supported'] + ); const target_label = target === 'newly' ? '"newly available"' : target; - return `${header}\n${sep}\n${body}\n\n> [!NOTE] This equates to a Baseline target of ${target_label}.`; + return ( + render_markdown_table( + ['Browser', 'Minimum version'], + rows.filter(([, version]) => version !== undefined) + ) + + `\n\n> [!NOTE] This equates to a Baseline target of ${target_label}.` + ); +} + +function render_markdown_table(headers: string[], rows: string[][]): string { + return `| ${headers.join(' | ')} | +| ${headers.map(() => '-').join(' | ')} | +${rows.map((row) => `| ${row.join(' | ')} |`).join('\n')} +`; } -/* eslint-disable no-console */ async function main() { console.log('Preparing scratch directory…'); // Wipe and recreate so stale bundles can't leak into the next scan. @@ -844,14 +850,11 @@ async function main() { const versions = browser_versions_for(target); console.log('Rewriting docs page…'); - generate('browser-support.md', render_table(versions, target)); + generate('browser-support.md', render_browser_table(versions, target)); generate('browser-support-features.md', render_conditional_table(conditional_rows, target)); console.log('Done.'); } finally { - // Ensure cleanup happens even on failure — otherwise leftover bundle - // files in `scripts/_baseline/` get picked up by `pnpm lint` on the - // next CI step and produce spurious naming/no-console errors. fs.rmSync(tmp_dir, { recursive: true, force: true }); } } From 2f6307af65fdecce9e7f37ce78464d9431b266ce Mon Sep 17 00:00:00 2001 From: jiyujie2006 <49909156+jiyujie2006@users.noreply.github.com> Date: Mon, 1 Jun 2026 00:07:28 +0800 Subject: [PATCH 15/19] Fix searchParams.set duplicate updates (#18336) ## Summary - detect `SvelteURLSearchParams#set` changes by comparing duplicate value lists instead of joined strings - update reactive subscribers when duplicate params collapse to a single value with the same concatenated text - cover both `SvelteURLSearchParams` and `SvelteURL.searchParams` synchronization ## Tests - pnpm exec vitest run packages/svelte/src/reactivity/url-search-params.test.ts -t "URLSearchParams.set updates when duplicate values collapse to the same joined string" - pnpm exec vitest run packages/svelte/src/reactivity/url-search-params.test.ts packages/svelte/src/reactivity/url.test.ts --------- Co-authored-by: Rich Harris --- .changeset/search-params-duplicate-set.md | 5 +++ .../src/reactivity/url-search-params.js | 5 ++- .../src/reactivity/url-search-params.test.ts | 38 +++++++++++++++++++ packages/svelte/src/reactivity/url.test.ts | 19 ++++++++++ 4 files changed, 65 insertions(+), 2 deletions(-) create mode 100644 .changeset/search-params-duplicate-set.md diff --git a/.changeset/search-params-duplicate-set.md b/.changeset/search-params-duplicate-set.md new file mode 100644 index 0000000000..9fcaa8b289 --- /dev/null +++ b/.changeset/search-params-duplicate-set.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: update `SvelteURLSearchParams` when setting duplicate keys to the same joined value diff --git a/packages/svelte/src/reactivity/url-search-params.js b/packages/svelte/src/reactivity/url-search-params.js index 2e70bf518d..38cf3ebe4f 100644 --- a/packages/svelte/src/reactivity/url-search-params.js +++ b/packages/svelte/src/reactivity/url-search-params.js @@ -132,11 +132,12 @@ export class SvelteURLSearchParams extends URLSearchParams { * @returns {void} */ set(name, value) { - var previous = super.getAll(name).join(''); + var previous = super.getAll(name); super.set(name, value); // can't use has(name, value), because for something like https://svelte.dev?foo=1&bar=2&foo=3 // if you set `foo` to 1, then foo=3 gets deleted whilst `has("foo", "1")` returns true - if (previous !== super.getAll(name).join('')) { + var current = super.getAll(name); + if (previous.length !== current.length || previous.some((value, i) => value !== current[i])) { this.#update_url(); increment(this.#version); } diff --git a/packages/svelte/src/reactivity/url-search-params.test.ts b/packages/svelte/src/reactivity/url-search-params.test.ts index 27a413a553..b0c84872b0 100644 --- a/packages/svelte/src/reactivity/url-search-params.test.ts +++ b/packages/svelte/src/reactivity/url-search-params.test.ts @@ -55,6 +55,44 @@ test('URLSearchParams.set', () => { cleanup(); }); +test('URLSearchParams.set updates when duplicate values collapse to the same joined string', () => { + const params = new SvelteURLSearchParams('a=ab&a=c'); + const log: any = []; + + const cleanup = effect_root(() => { + render_effect(() => { + log.push(params.toString()); + }); + }); + + flushSync(() => { + params.set('a', 'abc'); + }); + + assert.deepEqual(log, ['a=ab&a=c', 'a=abc']); + + cleanup(); +}); + +test('URLSearchParams.set updates when duplicate values collapse to the same comma-joined string', () => { + const params = new SvelteURLSearchParams('a=a&a=b'); + const log: any = []; + + const cleanup = effect_root(() => { + render_effect(() => { + log.push(params.toString()); + }); + }); + + flushSync(() => { + params.set('a', 'a,b'); + }); + + assert.deepEqual(log, ['a=a&a=b', 'a=a%2Cb']); + + cleanup(); +}); + test('URLSearchParams.append', () => { const params = new SvelteURLSearchParams(); const log: any = []; diff --git a/packages/svelte/src/reactivity/url.test.ts b/packages/svelte/src/reactivity/url.test.ts index a76efeb2b2..a79aa32315 100644 --- a/packages/svelte/src/reactivity/url.test.ts +++ b/packages/svelte/src/reactivity/url.test.ts @@ -115,6 +115,25 @@ test('url.searchParams', () => { cleanup(); }); +test('url.searchParams.set updates url when duplicate values collapse to the same joined string', () => { + const url = new SvelteURL('https://svelte.dev?a=ab&a=c'); + const log: any = []; + + const cleanup = effect_root(() => { + render_effect(() => { + log.push(url.href); + }); + }); + + flushSync(() => { + url.searchParams.set('a', 'abc'); + }); + + assert.deepEqual(log, ['https://svelte.dev/?a=ab&a=c', 'https://svelte.dev/?a=abc']); + + cleanup(); +}); + test('url.search normalizes value', () => { const url = new SvelteURL('https://svelte.dev'); const log: any = []; From 378bb25097088c2277aa063408c62818cc1f6c4e Mon Sep 17 00:00:00 2001 From: jdoughty04 Date: Sun, 31 May 2026 12:16:37 -0400 Subject: [PATCH 16/19] fix: set input type before spread value (#18345) ### Before submitting the PR, please make sure you do the following - [x] It's really useful if your PR references an issue where it is discussed ahead of time. In many cases, features are absent for a reason. For large changes, please create an RFC: https://github.com/sveltejs/rfcs - [x] Prefix your PR title with `feat:`, `fix:`, `chore:`, or `docs:`. - [x] This message body should clearly illustrate what problems it solves. - [x] Ideally, include a test that fails without this PR but passes with it. - [x] If this PR changes code within `packages/svelte/src`, add a changeset (`npx changeset`). ### Tests and linting - [x] Run the tests with `pnpm test` and lint the project with `pnpm lint` Fixes #18332. When spread props put `value` before a later `type="hidden"` on an ``, `set_attributes` currently writes the value while the element is still a text input. Browsers sanitize text input values by removing newlines, so the hidden input permanently loses them before `type` is applied. This sets an input's `type` first whenever the same spread update also includes `value` or `__value`, so the existing value handling runs with the final input type. The new runtime-browser sample covers both the problematic spread-first order and the already-working type-first order. Validation: - `corepack pnpm lint` - `CI=1 corepack pnpm test` - `corepack pnpm vitest run packages/svelte/tests/runtime-browser/test.ts -t input-type-before-value-spread` --------- Co-authored-by: Rich Harris Co-authored-by: Rich Harris --- .changeset/input-type-before-value.md | 5 +++++ .../internal/client/dom/elements/attributes.js | 9 +++++++++ .../input-type-before-value-spread/_config.js | 15 +++++++++++++++ .../input-type-before-value-spread/main.svelte | 7 +++++++ 4 files changed, 36 insertions(+) create mode 100644 .changeset/input-type-before-value.md create mode 100644 packages/svelte/tests/runtime-runes/samples/input-type-before-value-spread/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/input-type-before-value-spread/main.svelte diff --git a/.changeset/input-type-before-value.md b/.changeset/input-type-before-value.md new file mode 100644 index 0000000000..68d02f920f --- /dev/null +++ b/.changeset/input-type-before-value.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: preserve newlines in spread input values when the `type` attribute is applied after `value` diff --git a/packages/svelte/src/internal/client/dom/elements/attributes.js b/packages/svelte/src/internal/client/dom/elements/attributes.js index a193a70dd5..589685b5f2 100644 --- a/packages/svelte/src/internal/client/dom/elements/attributes.js +++ b/packages/svelte/src/internal/client/dom/elements/attributes.js @@ -332,6 +332,15 @@ function set_attributes( var setters = get_setters(element); + if (element.nodeName === INPUT_TAG && 'type' in next && ('value' in next || '__value' in next)) { + var type = next.type; + + if (type !== current.type || (type === undefined && element.hasAttribute('type'))) { + current.type = type; + set_attribute(element, 'type', type, skip_warning); + } + } + // since key is captured we use const for (const key in next) { // let instead of var because referenced in a closure diff --git a/packages/svelte/tests/runtime-runes/samples/input-type-before-value-spread/_config.js b/packages/svelte/tests/runtime-runes/samples/input-type-before-value-spread/_config.js new file mode 100644 index 0000000000..7aeaf38eb0 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/input-type-before-value-spread/_config.js @@ -0,0 +1,15 @@ +import { test } from '../../test'; + +const value = 'line1\nline2\nline3'; + +export default test({ + mode: ['client'], + test({ assert, target }) { + const [spread_first, type_first] = target.querySelectorAll('input'); + + assert.equal(spread_first?.type, 'hidden'); + assert.equal(spread_first?.value, value); + assert.equal(type_first?.type, 'hidden'); + assert.equal(type_first?.value, value); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/input-type-before-value-spread/main.svelte b/packages/svelte/tests/runtime-runes/samples/input-type-before-value-spread/main.svelte new file mode 100644 index 0000000000..16e72ab704 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/input-type-before-value-spread/main.svelte @@ -0,0 +1,7 @@ + + + + From b76b937e0053b7368b9a94bf2b351b181bd2eda6 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 1 Jun 2026 03:59:37 -0400 Subject: [PATCH 17/19] fix: various declaration tag bugs (#18348) closes #18334 --- .../declaration-tag-multiple-declarators.md | 5 +++++ .../declaration-tag-state-referenced-locally.md | 5 +++++ .changeset/declaration-tag-whitespace.md | 5 +++++ .../phases/2-analyze/visitors/DeclarationTag.js | 6 ++++-- .../client/visitors/DeclarationTag.js | 4 +++- .../_config.js | 17 +++++++++++++++++ .../main.svelte | 10 ++++++++++ .../input.svelte | 9 +++++++++ .../warnings.json | 14 ++++++++++++++ 9 files changed, 72 insertions(+), 3 deletions(-) create mode 100644 .changeset/declaration-tag-multiple-declarators.md create mode 100644 .changeset/declaration-tag-state-referenced-locally.md create mode 100644 .changeset/declaration-tag-whitespace.md create mode 100644 packages/svelte/tests/runtime-runes/samples/declaration-tag-multiple-declarators/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/declaration-tag-multiple-declarators/main.svelte create mode 100644 packages/svelte/tests/validator/samples/declaration-tag-state-referenced-locally/input.svelte create mode 100644 packages/svelte/tests/validator/samples/declaration-tag-state-referenced-locally/warnings.json diff --git a/.changeset/declaration-tag-multiple-declarators.md b/.changeset/declaration-tag-multiple-declarators.md new file mode 100644 index 0000000000..3f17162b54 --- /dev/null +++ b/.changeset/declaration-tag-multiple-declarators.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: correctly transform references to earlier declarators in a declaration tag (e.g. `{let a = $state(0), b = $derived(a * 2)}`) diff --git a/.changeset/declaration-tag-state-referenced-locally.md b/.changeset/declaration-tag-state-referenced-locally.md new file mode 100644 index 0000000000..00803c3e1c --- /dev/null +++ b/.changeset/declaration-tag-state-referenced-locally.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: avoid spurious `state_referenced_locally` warnings for `$derived` declarations in declaration tags diff --git a/.changeset/declaration-tag-whitespace.md b/.changeset/declaration-tag-whitespace.md new file mode 100644 index 0000000000..b64d1dd5a1 --- /dev/null +++ b/.changeset/declaration-tag-whitespace.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: tolerate whitespace before `let`/`const` in declaration tags diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/DeclarationTag.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/DeclarationTag.js index e75c05f355..b5e8c931f1 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/DeclarationTag.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/DeclarationTag.js @@ -2,14 +2,12 @@ /** @import { Context } from '../types' */ import * as b from '#compiler/builders'; import * as e from '../../../errors.js'; -import { validate_opening_tag } from './shared/utils.js'; /** * @param {AST.DeclarationTag} node * @param {Context} context */ export function DeclarationTag(node, context) { - validate_opening_tag(node, context.state, node.declaration.kind[0]); if (!context.state.analysis.runes && !context.state.analysis.maybe_runes) { e.declaration_tag_no_legacy_mode(node); } @@ -17,6 +15,10 @@ export function DeclarationTag(node, context) { context.visit(node.declaration, { ...context.state, in_declaration_tag: true, + // the declaration lives in the fragment scope, which is one level deeper than the + // `function_depth` we're tracking here (`set_scope` doesn't update `function_depth`). + // align them so that `state_referenced_locally` warnings are calculated correctly + function_depth: context.state.scope.function_depth, expression: node.metadata.expression }); diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/DeclarationTag.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/DeclarationTag.js index bff51b20dd..abec5e828f 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/DeclarationTag.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/DeclarationTag.js @@ -10,8 +10,10 @@ import { add_state_transformers } from './shared/declarations.js'; * @param {ComponentContext} context */ export function DeclarationTag(node, context) { - const declaration = /** @type {Statement | undefined} */ (context.visit(node.declaration)); + // register the transformers _before_ visiting the declaration, so that + // later declarators can reference earlier ones (e.g. `{let a = $state(0), b = $derived(a * 2)}`) add_state_transformers(context); + const declaration = /** @type {Statement | undefined} */ (context.visit(node.declaration)); if ( node.metadata.promises_id && diff --git a/packages/svelte/tests/runtime-runes/samples/declaration-tag-multiple-declarators/_config.js b/packages/svelte/tests/runtime-runes/samples/declaration-tag-multiple-declarators/_config.js new file mode 100644 index 0000000000..c5846ba0bc --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/declaration-tag-multiple-declarators/_config.js @@ -0,0 +1,17 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + html: `

count: 0

doubled: 0

quadrupled: 0

`, + async test({ assert, target }) { + const [button] = target.querySelectorAll('button'); + + button.click(); + await tick(); + + assert.htmlEqual( + target.innerHTML, + `

count: 1

doubled: 2

quadrupled: 4

` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/declaration-tag-multiple-declarators/main.svelte b/packages/svelte/tests/runtime-runes/samples/declaration-tag-multiple-declarators/main.svelte new file mode 100644 index 0000000000..d6fb542396 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/declaration-tag-multiple-declarators/main.svelte @@ -0,0 +1,10 @@ + +{let count = $state(0), doubled = $derived(count * 2)} + + +{ let quadrupled = $derived(doubled * 2) } + + +

count: {count}

+

doubled: {doubled}

+

quadrupled: {quadrupled}

diff --git a/packages/svelte/tests/validator/samples/declaration-tag-state-referenced-locally/input.svelte b/packages/svelte/tests/validator/samples/declaration-tag-state-referenced-locally/input.svelte new file mode 100644 index 0000000000..90cc208e72 --- /dev/null +++ b/packages/svelte/tests/validator/samples/declaration-tag-state-referenced-locally/input.svelte @@ -0,0 +1,9 @@ + +{let a = $state(0), b = $derived(a * 2)} +{let c = $state(0)} +{let d = $derived(c * 2)} + + +{let e = $state(0), f = e} + +{a}{b}{c}{d}{e}{f} diff --git a/packages/svelte/tests/validator/samples/declaration-tag-state-referenced-locally/warnings.json b/packages/svelte/tests/validator/samples/declaration-tag-state-referenced-locally/warnings.json new file mode 100644 index 0000000000..9b87a3afb6 --- /dev/null +++ b/packages/svelte/tests/validator/samples/declaration-tag-state-referenced-locally/warnings.json @@ -0,0 +1,14 @@ +[ + { + "code": "state_referenced_locally", + "message": "This reference only captures the initial value of `e`. Did you mean to reference it inside a closure instead?", + "start": { + "line": 7, + "column": 24 + }, + "end": { + "line": 7, + "column": 25 + } + } +] From c74f44fff99de06270c26098600f891835a15288 Mon Sep 17 00:00:00 2001 From: Yuichiro Yamashita Date: Mon, 1 Jun 2026 17:03:41 +0900 Subject: [PATCH 18/19] fix: don't mistake `type` identifier expressions for TS `type` declarations in tags (#18330) Detect TypeScript `type` declarations by actually parsing instead of by character-class blacklists, so that expressions like `{type === 'all' ? a : b}` or `{type instanceof Foo}` aren't misclassified as malformed declarations. Closes #18328 --------- Co-authored-by: Simon Holthausen Co-authored-by: Simon H <5968653+dummdidumm@users.noreply.github.com> --- .changeset/fix-type-identifier-in-tag.md | 5 + .../src/compiler/phases/1-parse/state/tag.js | 29 ++++-- .../compiler/phases/1-parse/utils/bracket.js | 11 ++- .../loose-declaration-tag/input.svelte | 1 + .../samples/loose-declaration-tag/output.json | 38 +++++++- .../input.svelte | 1 + .../output.json | 92 +++++++++++++++++++ .../errors.json | 6 +- .../input.svelte | 14 +++ 9 files changed, 181 insertions(+), 16 deletions(-) create mode 100644 .changeset/fix-type-identifier-in-tag.md create mode 100644 packages/svelte/tests/parser-modern/samples/tag-type-identifier-probe-comment/input.svelte create mode 100644 packages/svelte/tests/parser-modern/samples/tag-type-identifier-probe-comment/output.json diff --git a/.changeset/fix-type-identifier-in-tag.md b/.changeset/fix-type-identifier-in-tag.md new file mode 100644 index 0000000000..90e03da80f --- /dev/null +++ b/.changeset/fix-type-identifier-in-tag.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: more robust parsing of declaration tags with regards to `type` diff --git a/packages/svelte/src/compiler/phases/1-parse/state/tag.js b/packages/svelte/src/compiler/phases/1-parse/state/tag.js index 3c8eba4b26..15c79e0353 100644 --- a/packages/svelte/src/compiler/phases/1-parse/state/tag.js +++ b/packages/svelte/src/compiler/phases/1-parse/state/tag.js @@ -12,9 +12,9 @@ import { find_matching_bracket, match_bracket } from '../utils/bracket.js'; const regex_whitespace_with_closing_curly_brace = /\s*}/y; const regex_supported_declaration = /(?:let|const)\b/y; -// All except `type` are reserved keywords and cannot be used as variable names. -// For type we check if it's not something like `type .x` / `type ()` / `type % 2` / ... -const regex_unsupported_declaration = /(?:(?:var|interface|enum)\b)|(?:type\s+[^?.(`<[&|%^}])/y; +const regex_unsupported_declaration = /(?:var|interface|enum)\b/y; +// `type` is a contextual keyword; this is just a shape hint, confirmed by parsing. +const regex_maybe_type_declaration = /type\b/y; const pointy_bois = { '<': '>' }; @@ -77,10 +77,17 @@ function read_declaration(parser) { e.declaration_tag_invalid_type({ start, end: start + unsupported.length }); } - if (!parser.match_regex(regex_supported_declaration)) { + if ( + !parser.match_regex(regex_supported_declaration) && + // `type` is special, since it is not a reserved keyword and can be used + // as part of a valid expression. We gotta parse first and then see what it is. + !parser.match_regex(regex_maybe_type_declaration) + ) { return null; } + const initial_comment_count = parser.root.comments.length; + /** @type {import('estree').Statement | import('estree').VariableDeclaration} */ let declaration; try { @@ -117,10 +124,16 @@ function read_declaration(parser) { } if (declaration.type !== 'VariableDeclaration') { - e.declaration_tag_invalid_type({ - start: declaration.start ?? start, - end: declaration.end ?? parser.index - }); + if (declaration.type === 'ExpressionStatement') { + parser.root.comments.length = initial_comment_count; // Else they show up duplicated + return null; + } else { + // This is a TSTypeAliasDeclaration + e.declaration_tag_invalid_type({ + start: declaration.start ?? start, + end: declaration.end ?? parser.index + }); + } } // TODO support using diff --git a/packages/svelte/src/compiler/phases/1-parse/utils/bracket.js b/packages/svelte/src/compiler/phases/1-parse/utils/bracket.js index 63fbc68fb3..567d999a59 100644 --- a/packages/svelte/src/compiler/phases/1-parse/utils/bracket.js +++ b/packages/svelte/src/compiler/phases/1-parse/utils/bracket.js @@ -39,7 +39,9 @@ function find_string_end(string, search_start_index, string_start_char) { * @returns {number} The index of the end of this regex expression, or `Infinity` if not found. */ function find_regex_end(string, search_start_index) { - return find_unescaped_char(string, search_start_index, '/'); + const slash = find_unescaped_char(string, search_start_index, '/'); + const eol = find_unescaped_char(string, search_start_index, '\n'); + return slash < eol ? slash : Infinity; } /** @@ -114,7 +116,12 @@ export function find_matching_bracket(template, index, open) { i = infinity_if_negative(template.indexOf('*/', i + 1)) + '*/'.length; continue; } - i = find_regex_end(template, i + 1) + '/'.length; + const end = find_regex_end(template, i + 1) + '/'.length; + if (end === Infinity) { + i++; + } else { + i = end; + } continue; } default: { diff --git a/packages/svelte/tests/parser-modern/samples/loose-declaration-tag/input.svelte b/packages/svelte/tests/parser-modern/samples/loose-declaration-tag/input.svelte index f222531638..036d5283f5 100644 --- a/packages/svelte/tests/parser-modern/samples/loose-declaration-tag/input.svelte +++ b/packages/svelte/tests/parser-modern/samples/loose-declaration-tag/input.svelte @@ -1,4 +1,5 @@ {#if true} {let } {const x = } + {let x = a / } {/if} diff --git a/packages/svelte/tests/parser-modern/samples/loose-declaration-tag/output.json b/packages/svelte/tests/parser-modern/samples/loose-declaration-tag/output.json index 882e5f34f1..270d16af94 100644 --- a/packages/svelte/tests/parser-modern/samples/loose-declaration-tag/output.json +++ b/packages/svelte/tests/parser-modern/samples/loose-declaration-tag/output.json @@ -2,7 +2,7 @@ "css": null, "js": [], "start": 0, - "end": 38, + "end": 54, "type": "Root", "fragment": { "type": "Fragment", @@ -11,7 +11,7 @@ "type": "IfBlock", "elseif": false, "start": 0, - "end": 38, + "end": 54, "test": { "type": "Literal", "start": 5, @@ -99,7 +99,39 @@ { "type": "Text", "start": 32, - "end": 33, + "end": 34, + "raw": "\n\t", + "data": "\n\t" + }, + { + "type": "DeclarationTag", + "start": 34, + "end": 48, + "declaration": { + "type": "VariableDeclaration", + "kind": "let", + "declarations": [ + { + "type": "VariableDeclarator", + "id": { + "type": "Identifier", + "name": "", + "start": 47, + "end": 47 + }, + "init": null, + "start": 47, + "end": 47 + } + ], + "start": 35, + "end": 47 + } + }, + { + "type": "Text", + "start": 48, + "end": 49, "raw": "\n", "data": "\n" } diff --git a/packages/svelte/tests/parser-modern/samples/tag-type-identifier-probe-comment/input.svelte b/packages/svelte/tests/parser-modern/samples/tag-type-identifier-probe-comment/input.svelte new file mode 100644 index 0000000000..fa4195996e --- /dev/null +++ b/packages/svelte/tests/parser-modern/samples/tag-type-identifier-probe-comment/input.svelte @@ -0,0 +1 @@ +{type instanceof /* probe */ Object} diff --git a/packages/svelte/tests/parser-modern/samples/tag-type-identifier-probe-comment/output.json b/packages/svelte/tests/parser-modern/samples/tag-type-identifier-probe-comment/output.json new file mode 100644 index 0000000000..4fe6468742 --- /dev/null +++ b/packages/svelte/tests/parser-modern/samples/tag-type-identifier-probe-comment/output.json @@ -0,0 +1,92 @@ +{ + "css": null, + "js": [], + "start": 0, + "end": 36, + "type": "Root", + "fragment": { + "type": "Fragment", + "nodes": [ + { + "type": "ExpressionTag", + "start": 0, + "end": 36, + "expression": { + "type": "BinaryExpression", + "start": 1, + "end": 35, + "loc": { + "start": { + "line": 1, + "column": 1 + }, + "end": { + "line": 1, + "column": 35 + } + }, + "left": { + "type": "Identifier", + "start": 1, + "end": 5, + "loc": { + "start": { + "line": 1, + "column": 1 + }, + "end": { + "line": 1, + "column": 5 + } + }, + "name": "type" + }, + "operator": "instanceof", + "right": { + "type": "Identifier", + "start": 29, + "end": 35, + "loc": { + "start": { + "line": 1, + "column": 29 + }, + "end": { + "line": 1, + "column": 35 + } + }, + "name": "Object", + "leadingComments": [ + { + "type": "Block", + "value": " probe ", + "start": 17, + "end": 28 + } + ] + } + } + } + ] + }, + "options": null, + "comments": [ + { + "type": "Block", + "value": " probe ", + "start": 17, + "end": 28, + "loc": { + "start": { + "line": 1, + "column": 17 + }, + "end": { + "line": 1, + "column": 28 + } + } + } + ] +} diff --git a/packages/svelte/tests/validator/samples/declaration-tag-invalid-type-2/errors.json b/packages/svelte/tests/validator/samples/declaration-tag-invalid-type-2/errors.json index 052636cddb..a375ac5776 100644 --- a/packages/svelte/tests/validator/samples/declaration-tag-invalid-type-2/errors.json +++ b/packages/svelte/tests/validator/samples/declaration-tag-invalid-type-2/errors.json @@ -3,12 +3,12 @@ "code": "declaration_tag_invalid_type", "message": "Declaration tags must be `let` or `const` declarations", "start": { - "line": 14, + "line": 28, "column": 2 }, "end": { - "line": 14, - "column": 8 + "line": 28, + "column": 20 } } ] diff --git a/packages/svelte/tests/validator/samples/declaration-tag-invalid-type-2/input.svelte b/packages/svelte/tests/validator/samples/declaration-tag-invalid-type-2/input.svelte index 129a8ac7ba..88295cad58 100644 --- a/packages/svelte/tests/validator/samples/declaration-tag-invalid-type-2/input.svelte +++ b/packages/svelte/tests/validator/samples/declaration-tag-invalid-type-2/input.svelte @@ -1,3 +1,5 @@ + + {#if true} {type} @@ -10,6 +12,18 @@ {type ()} {type [1]} {type `tag`} + {type === 'all' ? 'All types' : type} + {type !== 'all'} + {type == 'all'} + {type != 'all'} + {type + 1} + {type - 1} + {type * 2} + {type / 2} + {type > 1} + {type instanceof Foo} + {type instanceof /* comment */ Object} + {type in foo} {type foo = boolean} {/if} From b471c15e61c90f820f0e059cfe90d56c135a8e3f Mon Sep 17 00:00:00 2001 From: Yuichiro Yamashita Date: Mon, 1 Jun 2026 17:03:59 +0900 Subject: [PATCH 19/19] fix: don't hang on a tag whose expression ends with a trailing slash (#18350) close https://github.com/sveltejs/svelte/issues/18349 --- .changeset/fix-find-matching-bracket-infinite-loop.md | 5 +++++ .../src/compiler/phases/1-parse/utils/bracket.js | 6 +++++- .../samples/declaration-tag-trailing-slash/_config.js | 11 +++++++++++ .../declaration-tag-trailing-slash/main.svelte | 1 + 4 files changed, 22 insertions(+), 1 deletion(-) create mode 100644 .changeset/fix-find-matching-bracket-infinite-loop.md create mode 100644 packages/svelte/tests/compiler-errors/samples/declaration-tag-trailing-slash/_config.js create mode 100644 packages/svelte/tests/compiler-errors/samples/declaration-tag-trailing-slash/main.svelte diff --git a/.changeset/fix-find-matching-bracket-infinite-loop.md b/.changeset/fix-find-matching-bracket-infinite-loop.md new file mode 100644 index 0000000000..c0656ea794 --- /dev/null +++ b/.changeset/fix-find-matching-bracket-infinite-loop.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: prevent infinite loop when a tag's expression ends with a trailing `/` at the end of the input diff --git a/packages/svelte/src/compiler/phases/1-parse/utils/bracket.js b/packages/svelte/src/compiler/phases/1-parse/utils/bracket.js index 567d999a59..47299c9de5 100644 --- a/packages/svelte/src/compiler/phases/1-parse/utils/bracket.js +++ b/packages/svelte/src/compiler/phases/1-parse/utils/bracket.js @@ -107,7 +107,11 @@ export function find_matching_bracket(template, index, open) { continue; case '/': { const next_char = template[i + 1]; - if (!next_char) continue; + if (!next_char) { + // `/` is the last character; advance past it so we don't loop forever + i++; + continue; + } if (next_char === '/') { i = infinity_if_negative(template.indexOf('\n', i + 1)) + '\n'.length; continue; diff --git a/packages/svelte/tests/compiler-errors/samples/declaration-tag-trailing-slash/_config.js b/packages/svelte/tests/compiler-errors/samples/declaration-tag-trailing-slash/_config.js new file mode 100644 index 0000000000..2e605631c5 --- /dev/null +++ b/packages/svelte/tests/compiler-errors/samples/declaration-tag-trailing-slash/_config.js @@ -0,0 +1,11 @@ +import { test } from '../../test'; + +// A tag whose expression ends with a bare `/` at the end of the input used to +// make `find_matching_bracket` loop forever; it should error instead of hanging. +export default test({ + error: { + code: 'unexpected_eof', + message: 'Unexpected end of input', + position: [12, 12] + } +}); diff --git a/packages/svelte/tests/compiler-errors/samples/declaration-tag-trailing-slash/main.svelte b/packages/svelte/tests/compiler-errors/samples/declaration-tag-trailing-slash/main.svelte new file mode 100644 index 0000000000..8ffdb88a18 --- /dev/null +++ b/packages/svelte/tests/compiler-errors/samples/declaration-tag-trailing-slash/main.svelte @@ -0,0 +1 @@ +{let x = a / \ No newline at end of file