From a10e8e47a5946623a60a1e36b9023c23926eae87 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 14 May 2026 13:31:31 -0400 Subject: [PATCH 001/222] fix: keep dependencies of `$state.eager`/`pending` (alternative approach) (#18218) #18108, with two differences: - we use a global map - we use the parent reaction as the key, rather than traversing upwards for a branch I think this has the same outcome? ### 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` --------- Co-authored-by: Simon Holthausen Co-authored-by: Simon H <5968653+dummdidumm@users.noreply.github.com> --- .changeset/breezy-laws-train.md | 5 +++ .../src/internal/client/reactivity/batch.js | 22 +++++++++++-- .../samples/async-eager-block/_config.js | 23 +++++++++++++ .../samples/async-eager-block/main.svelte | 20 ++++++++++++ .../samples/async-eager-each-block/_config.js | 32 +++++++++++++++++++ .../async-eager-each-block/main.svelte | 24 ++++++++++++++ 6 files changed, 123 insertions(+), 3 deletions(-) create mode 100644 .changeset/breezy-laws-train.md create mode 100644 packages/svelte/tests/runtime-runes/samples/async-eager-block/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-eager-block/main.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/async-eager-each-block/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-eager-each-block/main.svelte diff --git a/.changeset/breezy-laws-train.md b/.changeset/breezy-laws-train.md new file mode 100644 index 0000000000..5e88998a0a --- /dev/null +++ b/.changeset/breezy-laws-train.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: keep dependencies of `$state.eager/pending` diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index d822834324..a5c9a51eec 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -16,7 +16,8 @@ import { EAGER_EFFECT, ERROR_VALUE, MANAGED_EFFECT, - REACTION_RAN + REACTION_RAN, + DESTROYING } from '#client/constants'; import { async_mode_flag } from '../../flags/index.js'; import { deferred, define_property, includes } from '../../shared/utils.js'; @@ -33,7 +34,7 @@ import { flush_tasks, queue_micro_task } from '../dom/task.js'; import { DEV } from 'esm-env'; import { invoke_error_boundary } from '../error-handling.js'; import { flush_eager_effects, old_values, set_eager_effects, source, update } from './sources.js'; -import { eager_effect, unlink_effect } from './effects.js'; +import { eager_effect, teardown, unlink_effect } from './effects.js'; import { defer_effect } from './utils.js'; import { UNINITIALIZED } from '../../../constants.js'; import { set_signal_status } from './status.js'; @@ -1234,6 +1235,9 @@ function eager_flush() { }); } +/** @type {Map>} */ +var version_map = new Map(); + /** * Implementation of `$state.eager(fn())` * @template T @@ -1241,10 +1245,22 @@ function eager_flush() { * @returns {T} */ export function eager(fn) { - var version = source(0); var initial = true; var value = /** @type {T} */ (undefined); + if (active_reaction === null) { + return fn(); + } + + let parent = active_reaction; + + let version = version_map.get(parent) ?? source(0); + version_map.set(parent, version); + + teardown(() => { + if (parent.f & DESTROYING) version_map.delete(parent); + }); + get(version); eager_effect(() => { diff --git a/packages/svelte/tests/runtime-runes/samples/async-eager-block/_config.js b/packages/svelte/tests/runtime-runes/samples/async-eager-block/_config.js new file mode 100644 index 0000000000..b6d283c8a5 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-eager-block/_config.js @@ -0,0 +1,23 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target }) { + await tick(); + const [increment, resolve] = target.querySelectorAll('button'); + + increment.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ' 0

loading...

' + ); + + resolve.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ' 1

1

' + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-eager-block/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-eager-block/main.svelte new file mode 100644 index 0000000000..61f4705d06 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-eager-block/main.svelte @@ -0,0 +1,20 @@ + + + + + +{await delay(count)} +{#if $state.eager(count) !== count} +

loading...

+{:else} +

{count}

+{/if} diff --git a/packages/svelte/tests/runtime-runes/samples/async-eager-each-block/_config.js b/packages/svelte/tests/runtime-runes/samples/async-eager-each-block/_config.js new file mode 100644 index 0000000000..a4921a5115 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-eager-each-block/_config.js @@ -0,0 +1,32 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target, logs }) { + await tick(); + const [increment, resolve] = target.querySelectorAll('button'); + logs.length = 0; + + increment.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` +
  • 0 / 0
  • 0 / loading...
  • 0 / 0
` + ); + + resolve.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` +
  • 0 / 0
  • 1 / 1
  • 0 / 0
` + ); + + assert.equal( + logs.some((l) => l.toString().includes('0 ') || l.toString().includes('2')), + false, + 'only the second $state.eager should have been evaluated' + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-eager-each-block/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-eager-each-block/main.svelte new file mode 100644 index 0000000000..f27e256e9f --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-eager-each-block/main.svelte @@ -0,0 +1,24 @@ + + + + + +
    + {#each counts as count, i} +
  • + {await delay(count)} / + {#if console.log(i) || $state.eager(count) !== count} + loading... + {:else} + {count} + {/if} +
  • + {/each} +
From 55f9c85c09d625c3dd80c71ce7542f57386fafb4 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 14 May 2026 14:03:20 -0400 Subject: [PATCH 002/222] Version Packages (#18158) 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.55.6 ### Patch Changes - fix: leave stale promises to wait for a later resolution, instead of rejecting ([#18180](https://github.com/sveltejs/svelte/pull/18180)) - fix: keep dependencies of `$state.eager/pending` ([#18218](https://github.com/sveltejs/svelte/pull/18218)) - fix: reapply context after transforming error during SSR ([#18099](https://github.com/sveltejs/svelte/pull/18099)) - fix: don't rebase just-created batches ([#18117](https://github.com/sveltejs/svelte/pull/18117)) - chore: allow `null` for `pending` in typings ([#18201](https://github.com/sveltejs/svelte/pull/18201)) - fix: flush eager effects in production ([#18107](https://github.com/sveltejs/svelte/pull/18107)) - fix: rethrow error of failed iterable after calling `return()` ([#18169](https://github.com/sveltejs/svelte/pull/18169)) - fix: account for proxified instance when updating `bind:this` ([#18147](https://github.com/sveltejs/svelte/pull/18147)) - fix: ensure scheduled batch is flushed if not obsolete ([#18131](https://github.com/sveltejs/svelte/pull/18131)) - fix: resolve stale deriveds with latest value ([#18167](https://github.com/sveltejs/svelte/pull/18167)) - chore: remove unnecessary `increment_pending` calls ([#18183](https://github.com/sveltejs/svelte/pull/18183)) - fix: correctly compile component member expressions for SSR ([#18192](https://github.com/sveltejs/svelte/pull/18192)) - fix: reset `source.updated` stack traces after `flush` ([#18196](https://github.com/sveltejs/svelte/pull/18196)) - fix: replacing async 'blocking' strategy with 'merging' ([#18205](https://github.com/sveltejs/svelte/pull/18205)) - fix: allow `@debug` tags to reference awaited variables ([#18138](https://github.com/sveltejs/svelte/pull/18138)) - fix: re-run fallback props if dependencies update ([#18146](https://github.com/sveltejs/svelte/pull/18146)) - fix: abort running obsolete async branches ([#18118](https://github.com/sveltejs/svelte/pull/18118)) - fix: ignore comments when reading CSS values ([#18153](https://github.com/sveltejs/svelte/pull/18153)) - fix: wrap `Promise.all` in `save` during SSR ([#18178](https://github.com/sveltejs/svelte/pull/18178)) - fix: ignore false-positive errors of `$inspect` dependencies ([#18106](https://github.com/sveltejs/svelte/pull/18106)) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .changeset/big-webs-sing.md | 5 ---- .changeset/breezy-laws-train.md | 5 ---- .changeset/cruel-boxes-serve.md | 5 ---- .changeset/easy-singers-retire.md | 5 ---- .changeset/fine-bushes-marry.md | 5 ---- .changeset/flat-shrimps-worry.md | 5 ---- .changeset/fresh-stars-grin.md | 5 ---- .changeset/full-waves-tease.md | 5 ---- .changeset/many-pandas-add.md | 5 ---- .changeset/modern-tables-fetch.md | 5 ---- .changeset/public-mammals-float.md | 5 ---- .changeset/quiet-teams-pick.md | 5 ---- .changeset/red-crabs-ring.md | 5 ---- .changeset/shiny-squids-whisper.md | 5 ---- .changeset/small-tools-walk.md | 5 ---- .changeset/smooth-poems-tap.md | 5 ---- .changeset/stupid-baboons-fall.md | 5 ---- .changeset/three-pears-build.md | 5 ---- .changeset/tough-knives-smell.md | 5 ---- .changeset/twelve-cooks-speak.md | 5 ---- packages/svelte/CHANGELOG.md | 44 ++++++++++++++++++++++++++++++ packages/svelte/package.json | 2 +- packages/svelte/src/version.js | 2 +- 23 files changed, 46 insertions(+), 102 deletions(-) delete mode 100644 .changeset/big-webs-sing.md delete mode 100644 .changeset/breezy-laws-train.md delete mode 100644 .changeset/cruel-boxes-serve.md delete mode 100644 .changeset/easy-singers-retire.md delete mode 100644 .changeset/fine-bushes-marry.md delete mode 100644 .changeset/flat-shrimps-worry.md delete mode 100644 .changeset/fresh-stars-grin.md delete mode 100644 .changeset/full-waves-tease.md delete mode 100644 .changeset/many-pandas-add.md delete mode 100644 .changeset/modern-tables-fetch.md delete mode 100644 .changeset/public-mammals-float.md delete mode 100644 .changeset/quiet-teams-pick.md delete mode 100644 .changeset/red-crabs-ring.md delete mode 100644 .changeset/shiny-squids-whisper.md delete mode 100644 .changeset/small-tools-walk.md delete mode 100644 .changeset/smooth-poems-tap.md delete mode 100644 .changeset/stupid-baboons-fall.md delete mode 100644 .changeset/three-pears-build.md delete mode 100644 .changeset/tough-knives-smell.md delete mode 100644 .changeset/twelve-cooks-speak.md diff --git a/.changeset/big-webs-sing.md b/.changeset/big-webs-sing.md deleted file mode 100644 index 946a41d881..0000000000 --- a/.changeset/big-webs-sing.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: leave stale promises to wait for a later resolution, instead of rejecting diff --git a/.changeset/breezy-laws-train.md b/.changeset/breezy-laws-train.md deleted file mode 100644 index 5e88998a0a..0000000000 --- a/.changeset/breezy-laws-train.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: keep dependencies of `$state.eager/pending` diff --git a/.changeset/cruel-boxes-serve.md b/.changeset/cruel-boxes-serve.md deleted file mode 100644 index 592cec4d01..0000000000 --- a/.changeset/cruel-boxes-serve.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: reapply context after transforming error during SSR diff --git a/.changeset/easy-singers-retire.md b/.changeset/easy-singers-retire.md deleted file mode 100644 index 4420286e13..0000000000 --- a/.changeset/easy-singers-retire.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: don't rebase just-created batches diff --git a/.changeset/fine-bushes-marry.md b/.changeset/fine-bushes-marry.md deleted file mode 100644 index ccba53babe..0000000000 --- a/.changeset/fine-bushes-marry.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -chore: allow `null` for `pending` in typings diff --git a/.changeset/flat-shrimps-worry.md b/.changeset/flat-shrimps-worry.md deleted file mode 100644 index a5f76a0f9d..0000000000 --- a/.changeset/flat-shrimps-worry.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: flush eager effects in production diff --git a/.changeset/fresh-stars-grin.md b/.changeset/fresh-stars-grin.md deleted file mode 100644 index 3d56792d1e..0000000000 --- a/.changeset/fresh-stars-grin.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: rethrow error of failed iterable after calling `return()` diff --git a/.changeset/full-waves-tease.md b/.changeset/full-waves-tease.md deleted file mode 100644 index 3915334bf7..0000000000 --- a/.changeset/full-waves-tease.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: account for proxified instance when updating `bind:this` diff --git a/.changeset/many-pandas-add.md b/.changeset/many-pandas-add.md deleted file mode 100644 index 85de7acb35..0000000000 --- a/.changeset/many-pandas-add.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: ensure scheduled batch is flushed if not obsolete diff --git a/.changeset/modern-tables-fetch.md b/.changeset/modern-tables-fetch.md deleted file mode 100644 index 89543910fa..0000000000 --- a/.changeset/modern-tables-fetch.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: resolve stale deriveds with latest value diff --git a/.changeset/public-mammals-float.md b/.changeset/public-mammals-float.md deleted file mode 100644 index d890c9e070..0000000000 --- a/.changeset/public-mammals-float.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -chore: remove unnecessary `increment_pending` calls diff --git a/.changeset/quiet-teams-pick.md b/.changeset/quiet-teams-pick.md deleted file mode 100644 index ed046168be..0000000000 --- a/.changeset/quiet-teams-pick.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: correctly compile component member expressions for SSR diff --git a/.changeset/red-crabs-ring.md b/.changeset/red-crabs-ring.md deleted file mode 100644 index 82b53c5464..0000000000 --- a/.changeset/red-crabs-ring.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: reset `source.updated` stack traces after `flush` diff --git a/.changeset/shiny-squids-whisper.md b/.changeset/shiny-squids-whisper.md deleted file mode 100644 index a8d2d7378c..0000000000 --- a/.changeset/shiny-squids-whisper.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"svelte": patch ---- - -fix: replacing async 'blocking' strategy with 'merging' diff --git a/.changeset/small-tools-walk.md b/.changeset/small-tools-walk.md deleted file mode 100644 index 2b275368b3..0000000000 --- a/.changeset/small-tools-walk.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: allow `@debug` tags to reference awaited variables diff --git a/.changeset/smooth-poems-tap.md b/.changeset/smooth-poems-tap.md deleted file mode 100644 index ac160656cf..0000000000 --- a/.changeset/smooth-poems-tap.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: re-run fallback props if dependencies update diff --git a/.changeset/stupid-baboons-fall.md b/.changeset/stupid-baboons-fall.md deleted file mode 100644 index 66895ad015..0000000000 --- a/.changeset/stupid-baboons-fall.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: abort running obsolete async branches diff --git a/.changeset/three-pears-build.md b/.changeset/three-pears-build.md deleted file mode 100644 index 8a638149a3..0000000000 --- a/.changeset/three-pears-build.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: ignore comments when reading CSS values diff --git a/.changeset/tough-knives-smell.md b/.changeset/tough-knives-smell.md deleted file mode 100644 index 7687188c1a..0000000000 --- a/.changeset/tough-knives-smell.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: wrap `Promise.all` in `save` during SSR diff --git a/.changeset/twelve-cooks-speak.md b/.changeset/twelve-cooks-speak.md deleted file mode 100644 index d4fcd5c339..0000000000 --- a/.changeset/twelve-cooks-speak.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: ignore false-positive errors of `$inspect` dependencies diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index 8e0f8a0916..e5ba5737c4 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,49 @@ # svelte +## 5.55.6 + +### Patch Changes + +- fix: leave stale promises to wait for a later resolution, instead of rejecting ([#18180](https://github.com/sveltejs/svelte/pull/18180)) + +- fix: keep dependencies of `$state.eager/pending` ([#18218](https://github.com/sveltejs/svelte/pull/18218)) + +- fix: reapply context after transforming error during SSR ([#18099](https://github.com/sveltejs/svelte/pull/18099)) + +- fix: don't rebase just-created batches ([#18117](https://github.com/sveltejs/svelte/pull/18117)) + +- chore: allow `null` for `pending` in typings ([#18201](https://github.com/sveltejs/svelte/pull/18201)) + +- fix: flush eager effects in production ([#18107](https://github.com/sveltejs/svelte/pull/18107)) + +- fix: rethrow error of failed iterable after calling `return()` ([#18169](https://github.com/sveltejs/svelte/pull/18169)) + +- fix: account for proxified instance when updating `bind:this` ([#18147](https://github.com/sveltejs/svelte/pull/18147)) + +- fix: ensure scheduled batch is flushed if not obsolete ([#18131](https://github.com/sveltejs/svelte/pull/18131)) + +- fix: resolve stale deriveds with latest value ([#18167](https://github.com/sveltejs/svelte/pull/18167)) + +- chore: remove unnecessary `increment_pending` calls ([#18183](https://github.com/sveltejs/svelte/pull/18183)) + +- fix: correctly compile component member expressions for SSR ([#18192](https://github.com/sveltejs/svelte/pull/18192)) + +- fix: reset `source.updated` stack traces after `flush` ([#18196](https://github.com/sveltejs/svelte/pull/18196)) + +- fix: replacing async 'blocking' strategy with 'merging' ([#18205](https://github.com/sveltejs/svelte/pull/18205)) + +- fix: allow `@debug` tags to reference awaited variables ([#18138](https://github.com/sveltejs/svelte/pull/18138)) + +- fix: re-run fallback props if dependencies update ([#18146](https://github.com/sveltejs/svelte/pull/18146)) + +- fix: abort running obsolete async branches ([#18118](https://github.com/sveltejs/svelte/pull/18118)) + +- fix: ignore comments when reading CSS values ([#18153](https://github.com/sveltejs/svelte/pull/18153)) + +- fix: wrap `Promise.all` in `save` during SSR ([#18178](https://github.com/sveltejs/svelte/pull/18178)) + +- fix: ignore false-positive errors of `$inspect` dependencies ([#18106](https://github.com/sveltejs/svelte/pull/18106)) + ## 5.55.5 ### Patch Changes diff --git a/packages/svelte/package.json b/packages/svelte/package.json index a4ae208913..51aed56a3a 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.5", + "version": "5.55.6", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index 04b0b0398a..b11e7635c7 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.5'; +export const VERSION = '5.55.6'; export const PUBLIC_VERSION = '5'; From 547853e2406a2147ad7fb5ffeba95b01bd9642da Mon Sep 17 00:00:00 2001 From: Simon H <5968653+dummdidumm@users.noreply.github.com> Date: Thu, 14 May 2026 20:51:28 +0200 Subject: [PATCH 003/222] Merge commit from fork * fix: disallow empty attribute names during SSR * Update packages/svelte/tests/server-side-rendering/samples/spread-attributes-event-handler-xss/main.svelte Co-authored-by: Conduitry --------- Co-authored-by: Elliott Johnson Co-authored-by: Conduitry --- .changeset/famous-webs-flash.md | 5 +++++ packages/svelte/src/internal/server/index.js | 2 +- .../spread-attributes-event-handler-xss/main.svelte | 7 ++++++- 3 files changed, 12 insertions(+), 2 deletions(-) create mode 100644 .changeset/famous-webs-flash.md diff --git a/.changeset/famous-webs-flash.md b/.changeset/famous-webs-flash.md new file mode 100644 index 0000000000..75a01f689a --- /dev/null +++ b/.changeset/famous-webs-flash.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: disallow empty attribute names during SSR diff --git a/packages/svelte/src/internal/server/index.js b/packages/svelte/src/internal/server/index.js index 34d0133a31..12f76f188e 100644 --- a/packages/svelte/src/internal/server/index.js +++ b/packages/svelte/src/internal/server/index.js @@ -151,7 +151,7 @@ export function attributes(attrs, css_hash, classes, styles, flags = 0) { // omit functions, internal svelte properties and invalid attribute names if (typeof attrs[name] === 'function') continue; if (name[0] === '$' && name[1] === '$') continue; // faster than name.startsWith('$$') - if (INVALID_ATTR_NAME_CHAR_REGEX.test(name)) continue; + if (name === '' || INVALID_ATTR_NAME_CHAR_REGEX.test(name)) continue; var value = attrs[name]; var lower = name.toLowerCase(); diff --git a/packages/svelte/tests/server-side-rendering/samples/spread-attributes-event-handler-xss/main.svelte b/packages/svelte/tests/server-side-rendering/samples/spread-attributes-event-handler-xss/main.svelte index 2a8c17f048..e47a19408e 100644 --- a/packages/svelte/tests/server-side-rendering/samples/spread-attributes-event-handler-xss/main.svelte +++ b/packages/svelte/tests/server-side-rendering/samples/spread-attributes-event-handler-xss/main.svelte @@ -5,7 +5,12 @@ onclick: 'alert(1)', onerror: 'alert(1)', onfocus: 'alert(1)', - onmouseover: 'alert(1)' + onmouseover: 'alert(1)', + ' onload': 'alert(1)', + '\tonload': 'alert(1)', + '\u00a0onload': 'alert(1)', + "\t": "/onmouseover=alert(1)//", + "": "/onmouseover=alert(1)//" }; From d2375e2ebcab5c88feb5652f1a9d621b8f06b259 Mon Sep 17 00:00:00 2001 From: Simon H <5968653+dummdidumm@users.noreply.github.com> Date: Thu, 14 May 2026 20:51:56 +0200 Subject: [PATCH 004/222] Merge commit from fork --- .changeset/proud-dogs-go.md | 5 ++++ packages/svelte/src/utils.js | 2 +- packages/svelte/src/utils.test.ts | 50 +++++++++++++++++++++++++++++++ 3 files changed, 56 insertions(+), 1 deletion(-) create mode 100644 .changeset/proud-dogs-go.md create mode 100644 packages/svelte/src/utils.test.ts diff --git a/.changeset/proud-dogs-go.md b/.changeset/proud-dogs-go.md new file mode 100644 index 0000000000..c728466138 --- /dev/null +++ b/.changeset/proud-dogs-go.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: harden regex diff --git a/packages/svelte/src/utils.js b/packages/svelte/src/utils.js index 57561e6dc7..54757a6f13 100644 --- a/packages/svelte/src/utils.js +++ b/packages/svelte/src/utils.js @@ -491,7 +491,7 @@ export function is_raw_text_element(name) { // Rejects strings containing whitespace, quotes, angle brackets, slashes, equals, // or other characters that could break out of a tag-name token and enable markup injection. export const REGEX_VALID_TAG_NAME = - /^[a-zA-Z][a-zA-Z0-9]*(-[a-zA-Z0-9.\-_\u00B7\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u037D\u037F-\u1FFF\u200C-\u200D\u203F-\u2040\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD\u{10000}-\u{EFFFF}]+)*$/u; + /^[a-zA-Z][a-zA-Z0-9]*(-[a-zA-Z0-9.\-_\u00B7\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u037D\u037F-\u1FFF\u200C-\u200D\u203F-\u2040\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD\u{10000}-\u{EFFFF}]*)?$/u; /** * Prevent devtools trying to make `location` a clickable link by inserting a zero-width space diff --git a/packages/svelte/src/utils.test.ts b/packages/svelte/src/utils.test.ts new file mode 100644 index 0000000000..02175ae93f --- /dev/null +++ b/packages/svelte/src/utils.test.ts @@ -0,0 +1,50 @@ +import { expect, test } from 'vitest'; +import { REGEX_VALID_TAG_NAME } from './utils'; + +test('REGEX_VALID_TAG_NAME accepts common HTML tag names', () => { + const common_html_tag_names = ['div', 'span', 'button', 'input', 'svg', 'math', 'a']; + + for (const tag_name of common_html_tag_names) { + expect(REGEX_VALID_TAG_NAME.test(tag_name)).toBe(true); + } +}); + +test('REGEX_VALID_TAG_NAME accepts basic custom element names', () => { + const valid_custom_tag_names = ['my-element', 'x-foo', 'todo-item', 'my-element2']; + + for (const tag_name of valid_custom_tag_names) { + expect(REGEX_VALID_TAG_NAME.test(tag_name)).toBe(true); + } +}); + +test('REGEX_VALID_TAG_NAME accepts spec-allowed custom element characters', () => { + const valid_custom_tag_names = [ + 'x-foo.bar', + 'x-foo_bar', + 'x-foo\u00B7bar', + 'x-foo\u00FCbar', + 'x-foo\u{1F600}bar', + 'x-' + ]; + + for (const tag_name of valid_custom_tag_names) { + expect(REGEX_VALID_TAG_NAME.test(tag_name)).toBe(true); + } +}); + +test('REGEX_VALID_TAG_NAME rejects invalid tag names', () => { + const invalid_tag_names = ['', '1', 'x\u00FC', '-x-foo', '1foo', 'x-foo bar', 'x-foo/', 'x-foo>']; + + for (const tag_name of invalid_tag_names) { + expect(REGEX_VALID_TAG_NAME.test(tag_name)).toBe(false); + } +}); + +test('REGEX_VALID_TAG_NAME no ReDoS', () => { + const before = performance.now(); + REGEX_VALID_TAG_NAME.test('a-----------------------------------!'); + const after = performance.now(); + if (after - before > 10) { + throw new Error(`REGEX_VALID_TAG_NAME is vulnerable to ReDoS`); + } +}); From a16ebc67bbcf8f708360195687e1b2719463e1a4 Mon Sep 17 00:00:00 2001 From: Elliott Johnson Date: Thu, 14 May 2026 12:52:15 -0600 Subject: [PATCH 005/222] Merge commit from fork https://github.com/sveltejs/svelte/security/advisories/GHSA-f3cj-j4f6-wq85 Co-authored-by: Simon Holthausen --- .changeset/blue-lions-call.md | 5 +++ .../svelte/src/internal/server/hydratable.js | 9 +++-- .../src/internal/server/hydratable.test.ts | 33 +++++++++++++++++++ playgrounds/sandbox/ssr-dev.js | 5 +-- playgrounds/sandbox/ssr-prod.js | 5 +-- 5 files changed, 51 insertions(+), 6 deletions(-) create mode 100644 .changeset/blue-lions-call.md create mode 100644 packages/svelte/src/internal/server/hydratable.test.ts diff --git a/.changeset/blue-lions-call.md b/.changeset/blue-lions-call.md new file mode 100644 index 0000000000..f9ddb88505 --- /dev/null +++ b/.changeset/blue-lions-call.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: prevent XSS on `hydratable` from user contents diff --git a/packages/svelte/src/internal/server/hydratable.js b/packages/svelte/src/internal/server/hydratable.js index 81f59ab2fd..be75288604 100644 --- a/packages/svelte/src/internal/server/hydratable.js +++ b/packages/svelte/src/internal/server/hydratable.js @@ -3,7 +3,6 @@ import { async_mode_flag } from '../flags/index.js'; import { get_render_context } from './render-context.js'; import * as e from './errors.js'; import * as devalue from 'devalue'; -import { get_stack } from '../shared/dev.js'; import { DEV } from 'esm-env'; import { get_user_code_location } from './dev.js'; @@ -65,7 +64,13 @@ function encode(key, value, unresolved) { const placeholder = `"${uid++}"`; const p = value .then((v) => { - entry.serialized = entry.serialized.replace(placeholder, `r(${uneval(v)})`); + entry.serialized = entry.serialized.replace( + placeholder, + // use the function form here to prevent any string replacement characters from being interpreted + // in `v`, as it's potentially user-controlled and therefore potentially malicious. + // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/replace#specifying_a_string_as_the_replacement + () => `r(${uneval(v)})` + ); }) .catch((devalue_error) => e.hydratable_serialization_failed( diff --git a/packages/svelte/src/internal/server/hydratable.test.ts b/packages/svelte/src/internal/server/hydratable.test.ts new file mode 100644 index 0000000000..8dd54c8f1d --- /dev/null +++ b/packages/svelte/src/internal/server/hydratable.test.ts @@ -0,0 +1,33 @@ +import { afterAll, beforeAll, expect, test } from 'vitest'; +import { Renderer } from './renderer.js'; +import type { Component } from 'svelte'; +import { disable_async_mode_flag, enable_async_mode_flag } from '../flags/index.js'; +import { hydratable } from './hydratable.js'; + +beforeAll(() => { + enable_async_mode_flag(); +}); + +afterAll(() => { + disable_async_mode_flag(); +}); + +test('treats replacement tokens in hydratable promise values as literals', async () => { + const component = (renderer: Renderer) => { + hydratable('key', () => Promise.resolve(`$'`)); + renderer.child(async () => { + await Promise.resolve(); + }); + renderer.push('ok'); + }; + + const { head } = await Renderer.render(component as unknown as Component); + const script_match = head.match(/]*)?>([\s\S]*)<\/script>/); + + expect(script_match, 'expected hydratable script in head output').toBeTruthy(); + + const script_content = script_match![1]; + expect(script_content).toContain('const h = (window.__svelte ??= {}).h ??= new Map();'); + expect(script_content).toContain('r("$\'")'); + expect(script_content).toMatch(/\[\s*"key"\s*,\s*r\("\$'"\)\s*\]/); +}); diff --git a/playgrounds/sandbox/ssr-dev.js b/playgrounds/sandbox/ssr-dev.js index 8a0c063d47..b770848ad1 100644 --- a/playgrounds/sandbox/ssr-dev.js +++ b/playgrounds/sandbox/ssr-dev.js @@ -29,8 +29,9 @@ polka() const { head, body } = await render(App); const html = transformed_template - .replace(``, head) - .replace(``, body) + // use function form to prevent any string replacement characters from being interpreted + .replace(``, () => head) + .replace(``, () => body) // check that Safari doesn't break hydration .replaceAll('+636-555-3226', '+636-555-3226'); diff --git a/playgrounds/sandbox/ssr-prod.js b/playgrounds/sandbox/ssr-prod.js index 0c760188d4..e3ece4d5be 100644 --- a/playgrounds/sandbox/ssr-prod.js +++ b/playgrounds/sandbox/ssr-prod.js @@ -9,8 +9,9 @@ const { head, body } = await render(App); const rendered = fs .readFileSync(path.resolve('./dist/client/index.html'), 'utf-8') - .replace(``, body) - .replace(``, head); + // use function form to prevent any string replacement characters from being interpreted + .replace(``, () => body) + .replace(``, () => head); const types = { '.js': 'application/javascript', From e1cbbd96441e82c9eb8a23a2903c0d06d3cda991 Mon Sep 17 00:00:00 2001 From: Elliott Johnson Date: Thu, 14 May 2026 12:59:01 -0600 Subject: [PATCH 006/222] Merge commit from fork * fix: move Svelte runtime properties to symbols Prevents DOM clobbering * shorten names --------- Co-authored-by: Simon Holthausen --- .changeset/six-snails-sell.md | 5 ++++ .../svelte/src/internal/client/constants.js | 5 ++++ .../client/dom/elements/attributes.js | 23 +++++++++++++------ .../client/dom/elements/bindings/shared.js | 10 ++++---- .../src/internal/client/dom/elements/class.js | 7 +++--- .../src/internal/client/dom/elements/misc.js | 4 ++-- .../src/internal/client/dom/elements/style.js | 7 +++--- .../src/internal/client/dom/operations.js | 23 ++++++++++--------- packages/svelte/src/internal/client/render.js | 9 ++++---- .../_config.js | 23 +++++++++++++++++++ .../main.svelte | 20 ++++++++++++++++ 11 files changed, 97 insertions(+), 39 deletions(-) create mode 100644 .changeset/six-snails-sell.md create mode 100644 packages/svelte/tests/runtime-browser/samples/dom-clobbering-cache-symbols-spread/_config.js create mode 100644 packages/svelte/tests/runtime-browser/samples/dom-clobbering-cache-symbols-spread/main.svelte diff --git a/.changeset/six-snails-sell.md b/.changeset/six-snails-sell.md new file mode 100644 index 0000000000..6c61546e86 --- /dev/null +++ b/.changeset/six-snails-sell.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: move Svelte runtime properties to symbols diff --git a/packages/svelte/src/internal/client/constants.js b/packages/svelte/src/internal/client/constants.js index f92fba73be..043b50b4b2 100644 --- a/packages/svelte/src/internal/client/constants.js +++ b/packages/svelte/src/internal/client/constants.js @@ -63,6 +63,11 @@ export const STATE_SYMBOL = Symbol('$state'); export const LEGACY_PROPS = Symbol('legacy props'); export const LOADING_ATTR_SYMBOL = Symbol(''); export const PROXY_PATH_SYMBOL = Symbol('proxy path'); +export const ATTRIBUTES_CACHE = Symbol('attributes'); +export const CLASS_CACHE = Symbol('class'); +export const STYLE_CACHE = Symbol('style'); +export const TEXT_CACHE = Symbol('text'); +export const FORM_RESET_HANDLER = Symbol('form reset'); /** An anchor might change, via this symbol on the original anchor we can tell HMR about the updated anchor */ export const HMR_ANCHOR = Symbol('hmr anchor'); diff --git a/packages/svelte/src/internal/client/dom/elements/attributes.js b/packages/svelte/src/internal/client/dom/elements/attributes.js index 0cec01191a..a193a70dd5 100644 --- a/packages/svelte/src/internal/client/dom/elements/attributes.js +++ b/packages/svelte/src/internal/client/dom/elements/attributes.js @@ -5,7 +5,12 @@ import { get_descriptors, get_prototype_of } from '../../../shared/utils.js'; import { create_event, delegate, delegated, event, event_symbol } from './events.js'; import { add_form_reset_listener, autofocus } from './misc.js'; import * as w from '../../warnings.js'; -import { IS_XHTML, LOADING_ATTR_SYMBOL } from '#client/constants'; +import { + ATTRIBUTES_CACHE, + FORM_RESET_HANDLER, + IS_XHTML, + LOADING_ATTR_SYMBOL +} from '#client/constants'; import { queue_micro_task } from '../task.js'; import { is_capture_event, can_delegate_event, normalize_attribute } from '../../../../utils.js'; import { @@ -69,8 +74,7 @@ export function remove_input_defaults(input) { } }; - // @ts-expect-error - input.__on_r = remove_defaults; + /** @type {any} */ (input)[FORM_RESET_HANDLER] = remove_defaults; queue_micro_task(remove_defaults); add_form_reset_listener(); } @@ -561,8 +565,7 @@ export function attribute_effect( */ function get_attributes(element) { return /** @type {Record} **/ ( - // @ts-expect-error - element.__attributes ??= { + /** @type {any} */ (element)[ATTRIBUTES_CACHE] ??= { [IS_CUSTOM_ELEMENT]: element.nodeName.includes('-'), [IS_HTML]: element.namespaceURI === NAMESPACE_HTML } @@ -583,13 +586,19 @@ function get_setters(element) { var proto = element; // In the case of custom elements there might be setters on the instance var element_proto = Element.prototype; - // Stop at Element, from there on there's only unnecessary setters we're not interested in + // Stop at Element, from there on there's only unnecessary (and dangerous, like innerHTML) setters we're not interested in // Do not use constructor.name here as that's unreliable in some browser environments while (element_proto !== proto) { descriptors = get_descriptors(proto); for (var key in descriptors) { - if (descriptors[key].set) { + if ( + descriptors[key].set && + // better safe than sorry, we don't want spread attributes to mess with HTML content + key !== 'innerHTML' && + key !== 'textContent' && + key !== 'innerText' + ) { setters.push(key); } } diff --git a/packages/svelte/src/internal/client/dom/elements/bindings/shared.js b/packages/svelte/src/internal/client/dom/elements/bindings/shared.js index aa083776a5..303890adec 100644 --- a/packages/svelte/src/internal/client/dom/elements/bindings/shared.js +++ b/packages/svelte/src/internal/client/dom/elements/bindings/shared.js @@ -5,6 +5,7 @@ import { set_active_effect, set_active_reaction } from '../../../runtime.js'; +import { FORM_RESET_HANDLER } from '../../../constants.js'; import { add_form_reset_listener } from '../misc.js'; /** @@ -58,18 +59,15 @@ export function without_reactive_context(fn) { */ export function listen_to_event_and_reset_event(element, event, handler, on_reset = handler) { element.addEventListener(event, () => without_reactive_context(handler)); - // @ts-expect-error - const prev = element.__on_r; + const prev = /** @type {any} */ (element)[FORM_RESET_HANDLER]; if (prev) { // special case for checkbox that can have multiple binds (group & checked) - // @ts-expect-error - element.__on_r = () => { + /** @type {any} */ (element)[FORM_RESET_HANDLER] = () => { prev(); on_reset(true); }; } else { - // @ts-expect-error - element.__on_r = () => on_reset(true); + /** @type {any} */ (element)[FORM_RESET_HANDLER] = () => on_reset(true); } add_form_reset_listener(); diff --git a/packages/svelte/src/internal/client/dom/elements/class.js b/packages/svelte/src/internal/client/dom/elements/class.js index 038ce33f3e..6478707095 100644 --- a/packages/svelte/src/internal/client/dom/elements/class.js +++ b/packages/svelte/src/internal/client/dom/elements/class.js @@ -1,4 +1,5 @@ import { to_class } from '../../../shared/attributes.js'; +import { CLASS_CACHE } from '../../constants.js'; import { hydrating } from '../hydration.js'; /** @@ -11,8 +12,7 @@ import { hydrating } from '../hydration.js'; * @returns {Record | undefined} */ export function set_class(dom, is_html, value, hash, prev_classes, next_classes) { - // @ts-expect-error need to add __className to patched prototype - var prev = dom.__className; + var prev = /** @type {any} */ (dom)[CLASS_CACHE]; if ( hydrating || @@ -35,8 +35,7 @@ export function set_class(dom, is_html, value, hash, prev_classes, next_classes) } } - // @ts-expect-error need to add __className to patched prototype - dom.__className = value; + /** @type {any} */ (dom)[CLASS_CACHE] = value; } else if (next_classes && prev_classes !== next_classes) { for (var key in next_classes) { var is_present = !!next_classes[key]; diff --git a/packages/svelte/src/internal/client/dom/elements/misc.js b/packages/svelte/src/internal/client/dom/elements/misc.js index f350d1df8c..ad34ab9c1d 100644 --- a/packages/svelte/src/internal/client/dom/elements/misc.js +++ b/packages/svelte/src/internal/client/dom/elements/misc.js @@ -1,6 +1,7 @@ import { hydrating } from '../hydration.js'; import { clear_text_content, get_first_child } from '../operations.js'; import { queue_micro_task } from '../task.js'; +import { FORM_RESET_HANDLER } from '../../constants.js'; /** * @param {HTMLElement} dom @@ -45,8 +46,7 @@ export function add_form_reset_listener() { Promise.resolve().then(() => { if (!evt.defaultPrevented) { for (const e of /**@type {HTMLFormElement} */ (evt.target).elements) { - // @ts-expect-error - e.__on_r?.(); + /** @type {any} */ (e)[FORM_RESET_HANDLER]?.(); } } }); diff --git a/packages/svelte/src/internal/client/dom/elements/style.js b/packages/svelte/src/internal/client/dom/elements/style.js index 3e05eec30e..740c225f29 100644 --- a/packages/svelte/src/internal/client/dom/elements/style.js +++ b/packages/svelte/src/internal/client/dom/elements/style.js @@ -1,4 +1,5 @@ import { to_style } from '../../../shared/attributes.js'; +import { STYLE_CACHE } from '../../constants.js'; import { hydrating } from '../hydration.js'; /** @@ -28,8 +29,7 @@ function update_styles(dom, prev = {}, next, priority) { * @param {Record | [Record, Record]} [next_styles] */ export function set_style(dom, value, prev_styles, next_styles) { - // @ts-expect-error - var prev = dom.__style; + var prev = /** @type {any} */ (dom)[STYLE_CACHE]; if (hydrating || prev !== value) { var next_style_attr = to_style(value, next_styles); @@ -42,8 +42,7 @@ export function set_style(dom, value, prev_styles, next_styles) { } } - // @ts-expect-error - dom.__style = value; + /** @type {any} */ (dom)[STYLE_CACHE] = value; } else if (next_styles) { if (Array.isArray(next_styles)) { update_styles(dom, prev_styles?.[0], next_styles[0]); diff --git a/packages/svelte/src/internal/client/dom/operations.js b/packages/svelte/src/internal/client/dom/operations.js index 4036aa2d61..f6d05162ca 100644 --- a/packages/svelte/src/internal/client/dom/operations.js +++ b/packages/svelte/src/internal/client/dom/operations.js @@ -5,7 +5,14 @@ import { init_array_prototype_warnings } from '../dev/equality.js'; import { get_descriptor, is_extensible } from '../../shared/utils.js'; import { active_effect } from '../runtime.js'; import { async_mode_flag } from '../../flags/index.js'; -import { TEXT_NODE, REACTION_RAN } from '#client/constants'; +import { + ATTRIBUTES_CACHE, + CLASS_CACHE, + REACTION_RAN, + STYLE_CACHE, + TEXT_CACHE, + TEXT_NODE +} from '#client/constants'; import { eager_block_effects } from '../reactivity/batch.js'; import { NAMESPACE_HTML } from '../../../constants.js'; @@ -48,21 +55,15 @@ export function init_operations() { if (is_extensible(element_prototype)) { // the following assignments improve perf of lookups on DOM nodes - // @ts-expect-error - element_prototype.__click = undefined; - // @ts-expect-error - element_prototype.__className = undefined; - // @ts-expect-error - element_prototype.__attributes = null; - // @ts-expect-error - element_prototype.__style = undefined; + /** @type {any} */ (element_prototype)[CLASS_CACHE] = undefined; + /** @type {any} */ (element_prototype)[ATTRIBUTES_CACHE] = null; + /** @type {any} */ (element_prototype)[STYLE_CACHE] = undefined; // @ts-expect-error element_prototype.__e = undefined; } if (is_extensible(text_prototype)) { - // @ts-expect-error - text_prototype.__t = undefined; + /** @type {any} */ (text_prototype)[TEXT_CACHE] = undefined; } if (DEV) { diff --git a/packages/svelte/src/internal/client/render.js b/packages/svelte/src/internal/client/render.js index cb152ed9c1..50832fb3ff 100644 --- a/packages/svelte/src/internal/client/render.js +++ b/packages/svelte/src/internal/client/render.js @@ -23,7 +23,7 @@ import * as w from './warnings.js'; import * as e from './errors.js'; import { assign_nodes } from './dom/template.js'; import { is_passive_event } from '../../utils.js'; -import { COMMENT_NODE, STATE_SYMBOL } from './constants.js'; +import { COMMENT_NODE, STATE_SYMBOL, TEXT_CACHE } from './constants.js'; import { boundary } from './dom/blocks/boundary.js'; /** @@ -46,10 +46,9 @@ export function set_should_intro(value) { export function set_text(text, value) { // For objects, we apply string coercion (which might make things like $state array references in the template reactive) before diffing var str = value == null ? '' : typeof value === 'object' ? `${value}` : value; - // @ts-expect-error - if (str !== (text.__t ??= text.nodeValue)) { - // @ts-expect-error - text.__t = str; + // prettier-ignore + if (str !== (/** @type {any} */ (text)[TEXT_CACHE] ??= text.nodeValue)) { + /** @type {any} */ (text)[TEXT_CACHE] = str; text.nodeValue = `${str}`; } } diff --git a/packages/svelte/tests/runtime-browser/samples/dom-clobbering-cache-symbols-spread/_config.js b/packages/svelte/tests/runtime-browser/samples/dom-clobbering-cache-symbols-spread/_config.js new file mode 100644 index 0000000000..62248851db --- /dev/null +++ b/packages/svelte/tests/runtime-browser/samples/dom-clobbering-cache-symbols-spread/_config.js @@ -0,0 +1,23 @@ +import { assert_ok, test } from '../../assert'; + +export default test({ + async test({ assert, target, waitUntil, window }) { + const form = target.querySelector('form'); + const button = target.querySelector('button'); + const [i1, i2, i3] = target.querySelectorAll('input'); + assert_ok(form); + assert_ok(button); + + assert.equal(form.id, 'initial-form'); + assert.equal(form.className, 'first'); + assert.equal(window.getComputedStyle(form).backgroundColor, 'rgb(255, 0, 0)'); + + button.click(); + await waitUntil(() => form.id === 'updated-form'); + + assert.equal(form.id, 'updated-form'); + assert.equal(form.className, 'second'); + assert.equal(i3.id, '', 'input clobbered form'); + assert.equal(window.getComputedStyle(form).backgroundColor, 'rgb(0, 0, 255)'); + } +}); diff --git a/packages/svelte/tests/runtime-browser/samples/dom-clobbering-cache-symbols-spread/main.svelte b/packages/svelte/tests/runtime-browser/samples/dom-clobbering-cache-symbols-spread/main.svelte new file mode 100644 index 0000000000..60e9b80e81 --- /dev/null +++ b/packages/svelte/tests/runtime-browser/samples/dom-clobbering-cache-symbols-spread/main.svelte @@ -0,0 +1,20 @@ + + + + +
+ + + + +
From 05523088173e10af0753877af6936088de924833 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 14 May 2026 15:00:51 -0400 Subject: [PATCH 007/222] chore: bump devalue (#18219) --- .changeset/eight-things-join.md | 5 +++++ packages/svelte/package.json | 2 +- pnpm-lock.yaml | 10 +++++----- 3 files changed, 11 insertions(+), 6 deletions(-) create mode 100644 .changeset/eight-things-join.md diff --git a/.changeset/eight-things-join.md b/.changeset/eight-things-join.md new file mode 100644 index 0000000000..678d63db33 --- /dev/null +++ b/.changeset/eight-things-join.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +chore: bump devalue diff --git a/packages/svelte/package.json b/packages/svelte/package.json index 51aed56a3a..7e9521bb57 100644 --- a/packages/svelte/package.json +++ b/packages/svelte/package.json @@ -176,7 +176,7 @@ "aria-query": "5.3.1", "axobject-query": "^4.1.0", "clsx": "^2.1.1", - "devalue": "^5.6.4", + "devalue": "^5.8.1", "esm-env": "^1.2.1", "esrap": "^2.2.4", "is-reference": "^3.0.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0e2be208b6..1bfee5c769 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -96,8 +96,8 @@ importers: specifier: ^2.1.1 version: 2.1.1 devalue: - specifier: ^5.6.4 - version: 5.6.4 + specifier: ^5.8.1 + version: 5.8.1 esm-env: specifier: ^1.2.1 version: 1.2.1 @@ -1418,8 +1418,8 @@ packages: engines: {node: '>=0.10'} hasBin: true - devalue@5.6.4: - resolution: {integrity: sha512-Gp6rDldRsFh/7XuouDbxMH3Mx8GMCcgzIb1pDTvNyn8pZGQ22u+Wa+lGV9dQCltFQ7uVw0MhRyb8XDskNFOReA==} + devalue@5.8.1: + resolution: {integrity: sha512-4CXDYRBGqN+57wVJkuXBYmpAVUSg3L6JAQa/DFqm238G73E1wuyc/JhGQJzN7vUf/CMphYau2zXbfWzDR5aTEw==} dir-glob@3.0.1: resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} @@ -3786,7 +3786,7 @@ snapshots: detect-libc@1.0.3: optional: true - devalue@5.6.4: {} + devalue@5.8.1: {} dir-glob@3.0.1: dependencies: From 4d8f99a2709e3c02e48d8bc6c77458f4ba49d0e3 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 14 May 2026 13:06:06 -0600 Subject: [PATCH 008/222] Version Packages (#18220) 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.55.7 ### Patch Changes - fix: prevent XSS on `hydratable` from user contents ([`a16ebc67bbcf8f708360195687e1b2719463e1a4`](https://github.com/sveltejs/svelte/commit/a16ebc67bbcf8f708360195687e1b2719463e1a4)) - chore: bump devalue ([#18219](https://github.com/sveltejs/svelte/pull/18219)) - fix: disallow empty attribute names during SSR ([`547853e2406a2147ad7fb5ffeba95b01bd9642da`](https://github.com/sveltejs/svelte/commit/547853e2406a2147ad7fb5ffeba95b01bd9642da)) - fix: harden regex ([`d2375e2ebcab5c88feb5652f1a9d621b8f06b259`](https://github.com/sveltejs/svelte/commit/d2375e2ebcab5c88feb5652f1a9d621b8f06b259)) - fix: move Svelte runtime properties to symbols ([`e1cbbd96441e82c9eb8a23a2903c0d06d3cda991`](https://github.com/sveltejs/svelte/commit/e1cbbd96441e82c9eb8a23a2903c0d06d3cda991)) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .changeset/blue-lions-call.md | 5 ----- .changeset/eight-things-join.md | 5 ----- .changeset/famous-webs-flash.md | 5 ----- .changeset/proud-dogs-go.md | 5 ----- .changeset/six-snails-sell.md | 5 ----- packages/svelte/CHANGELOG.md | 14 ++++++++++++++ packages/svelte/package.json | 2 +- packages/svelte/src/version.js | 2 +- 8 files changed, 16 insertions(+), 27 deletions(-) delete mode 100644 .changeset/blue-lions-call.md delete mode 100644 .changeset/eight-things-join.md delete mode 100644 .changeset/famous-webs-flash.md delete mode 100644 .changeset/proud-dogs-go.md delete mode 100644 .changeset/six-snails-sell.md diff --git a/.changeset/blue-lions-call.md b/.changeset/blue-lions-call.md deleted file mode 100644 index f9ddb88505..0000000000 --- a/.changeset/blue-lions-call.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: prevent XSS on `hydratable` from user contents diff --git a/.changeset/eight-things-join.md b/.changeset/eight-things-join.md deleted file mode 100644 index 678d63db33..0000000000 --- a/.changeset/eight-things-join.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -chore: bump devalue diff --git a/.changeset/famous-webs-flash.md b/.changeset/famous-webs-flash.md deleted file mode 100644 index 75a01f689a..0000000000 --- a/.changeset/famous-webs-flash.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: disallow empty attribute names during SSR diff --git a/.changeset/proud-dogs-go.md b/.changeset/proud-dogs-go.md deleted file mode 100644 index c728466138..0000000000 --- a/.changeset/proud-dogs-go.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: harden regex diff --git a/.changeset/six-snails-sell.md b/.changeset/six-snails-sell.md deleted file mode 100644 index 6c61546e86..0000000000 --- a/.changeset/six-snails-sell.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: move Svelte runtime properties to symbols diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index e5ba5737c4..1255d8c6a0 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,19 @@ # svelte +## 5.55.7 + +### Patch Changes + +- fix: prevent XSS on `hydratable` from user contents ([`a16ebc67bbcf8f708360195687e1b2719463e1a4`](https://github.com/sveltejs/svelte/commit/a16ebc67bbcf8f708360195687e1b2719463e1a4)) + +- chore: bump devalue ([#18219](https://github.com/sveltejs/svelte/pull/18219)) + +- fix: disallow empty attribute names during SSR ([`547853e2406a2147ad7fb5ffeba95b01bd9642da`](https://github.com/sveltejs/svelte/commit/547853e2406a2147ad7fb5ffeba95b01bd9642da)) + +- fix: harden regex ([`d2375e2ebcab5c88feb5652f1a9d621b8f06b259`](https://github.com/sveltejs/svelte/commit/d2375e2ebcab5c88feb5652f1a9d621b8f06b259)) + +- fix: move Svelte runtime properties to symbols ([`e1cbbd96441e82c9eb8a23a2903c0d06d3cda991`](https://github.com/sveltejs/svelte/commit/e1cbbd96441e82c9eb8a23a2903c0d06d3cda991)) + ## 5.55.6 ### Patch Changes diff --git a/packages/svelte/package.json b/packages/svelte/package.json index 7e9521bb57..2f0743641f 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.6", + "version": "5.55.7", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index b11e7635c7..6b0ed0aea8 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.6'; +export const VERSION = '5.55.7'; export const PUBLIC_VERSION = '5'; From eb10a70cf1f003c3e93f3c4ecc9749870e7581c6 Mon Sep 17 00:00:00 2001 From: Simon H <5968653+dummdidumm@users.noreply.github.com> Date: Mon, 18 May 2026 17:36:17 +0200 Subject: [PATCH 009/222] fix: unset context synchronously in `run` (#18236) Our `run` function which executes top level awaits (and synchronous statements in-between/after) did not unset the context in time in case the function returns an async value. In that case the context was still around until the that promise resolves, which can be too late because unrelated things can be intertwined with the batch. The test shows this: Without the fix, the unrelated count incrementation would not update the view until the top level awaits in the child are done. In the test this just shows as a delayed visual update, but it also can result in stale roots as shown in https://github.com/sveltejs/svelte/issues/18221#issuecomment-4470921077 --- .changeset/tangy-icons-dig.md | 5 ++++ .../src/internal/client/reactivity/async.js | 23 ++++++++++------ .../async-run-isolated-batch/Child.svelte | 7 +++++ .../async-run-isolated-batch/_config.js | 26 +++++++++++++++++++ .../async-run-isolated-batch/main.svelte | 23 ++++++++++++++++ 5 files changed, 76 insertions(+), 8 deletions(-) create mode 100644 .changeset/tangy-icons-dig.md create mode 100644 packages/svelte/tests/runtime-runes/samples/async-run-isolated-batch/Child.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/async-run-isolated-batch/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-run-isolated-batch/main.svelte diff --git a/.changeset/tangy-icons-dig.md b/.changeset/tangy-icons-dig.md new file mode 100644 index 0000000000..6d87912bbb --- /dev/null +++ b/.changeset/tangy-icons-dig.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: unset context synchronously in `run` diff --git a/packages/svelte/src/internal/client/reactivity/async.js b/packages/svelte/src/internal/client/reactivity/async.js index c1d4cbcd67..12c5e9baa5 100644 --- a/packages/svelte/src/internal/client/reactivity/async.js +++ b/packages/svelte/src/internal/client/reactivity/async.js @@ -303,15 +303,21 @@ export function run(thunks) { .then(() => { restore(); - if (errored) { - throw errored.error; + try { + if (errored) { + throw errored.error; + } + + if (aborted(active)) { + throw STALE_REACTION; + } + + return fn(); + } finally { + // We gotta unset context directly in case the function returns a promise, in which case + // unset_context in .finally() would be too late ... + unset_context(); } - - if (aborted(active)) { - throw STALE_REACTION; - } - - return fn(); }) .catch(handle_error); @@ -320,6 +326,7 @@ export function run(thunks) { promise.finally(() => { blocker.settled = true; + // ... but we also need it after such a promise has resolved in case it restores our context unset_context(); }); } diff --git a/packages/svelte/tests/runtime-runes/samples/async-run-isolated-batch/Child.svelte b/packages/svelte/tests/runtime-runes/samples/async-run-isolated-batch/Child.svelte new file mode 100644 index 0000000000..6f84dc319e --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-run-isolated-batch/Child.svelte @@ -0,0 +1,7 @@ + + +{x} {y} diff --git a/packages/svelte/tests/runtime-runes/samples/async-run-isolated-batch/_config.js b/packages/svelte/tests/runtime-runes/samples/async-run-isolated-batch/_config.js new file mode 100644 index 0000000000..a0dc38c660 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-run-isolated-batch/_config.js @@ -0,0 +1,26 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target }) { + const [show, resolve, count] = target.querySelectorAll('button'); + + show.click(); + await tick(); + resolve.click(); + await tick(); + count.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ' ' + ); + + resolve.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + '1 2 ' + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-run-isolated-batch/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-run-isolated-batch/main.svelte new file mode 100644 index 0000000000..0a5346cc47 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-run-isolated-batch/main.svelte @@ -0,0 +1,23 @@ + + + + +{#if show} + +{/if} + + + + From 1e899cba354d2b25593afc1bbf04bf3555b5d26a Mon Sep 17 00:00:00 2001 From: Simon H <5968653+dummdidumm@users.noreply.github.com> Date: Mon, 18 May 2026 17:36:41 +0200 Subject: [PATCH 010/222] fix: execute uninitialized derived even if it's destroyed (#18228) Turns out there are a few unavoidable cases where we have to execute the derived even if we otherwise wouldn't, because of its lazy nature. Fixes #18139 --- .changeset/hungry-ads-beam.md | 5 +++ .../internal/client/reactivity/deriveds.js | 7 +++- .../Child.svelte | 10 ++++++ .../_config.js | 32 +++++++++++++++++++ .../main.svelte | 11 +++++++ 5 files changed, 64 insertions(+), 1 deletion(-) create mode 100644 .changeset/hungry-ads-beam.md create mode 100644 packages/svelte/tests/runtime-runes/samples/transition-derived-uninitialized/Child.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/transition-derived-uninitialized/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/transition-derived-uninitialized/main.svelte diff --git a/.changeset/hungry-ads-beam.md b/.changeset/hungry-ads-beam.md new file mode 100644 index 0000000000..260d177119 --- /dev/null +++ b/.changeset/hungry-ads-beam.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: execute uninitialized derived even if it's destroyed diff --git a/packages/svelte/src/internal/client/reactivity/deriveds.js b/packages/svelte/src/internal/client/reactivity/deriveds.js index 83e23dfab6..75be018552 100644 --- a/packages/svelte/src/internal/client/reactivity/deriveds.js +++ b/packages/svelte/src/internal/client/reactivity/deriveds.js @@ -338,7 +338,12 @@ export function execute_derived(derived) { var prev_active_effect = active_effect; var parent = derived.parent; - if (!is_destroying_effect && parent !== null && (parent.f & (DESTROYED | INERT)) !== 0) { + if ( + !is_destroying_effect && + parent !== null && + derived.v !== UNINITIALIZED && // if it was never evaluated before, it's guaranteed to fail downstream, so we try to execute instead + (parent.f & (DESTROYED | INERT)) !== 0 + ) { w.derived_inert(); return derived.v; diff --git a/packages/svelte/tests/runtime-runes/samples/transition-derived-uninitialized/Child.svelte b/packages/svelte/tests/runtime-runes/samples/transition-derived-uninitialized/Child.svelte new file mode 100644 index 0000000000..20bd606d9b --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/transition-derived-uninitialized/Child.svelte @@ -0,0 +1,10 @@ + + +
+ hello +
diff --git a/packages/svelte/tests/runtime-runes/samples/transition-derived-uninitialized/_config.js b/packages/svelte/tests/runtime-runes/samples/transition-derived-uninitialized/_config.js new file mode 100644 index 0000000000..734c84a0c4 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/transition-derived-uninitialized/_config.js @@ -0,0 +1,32 @@ +import { flushSync } from 'svelte'; +import { raf } from '../../../animation-helpers'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target }) { + const [fly_in, fly_out] = target.querySelectorAll('button'); + + fly_in.click(); + flushSync(); + raf.tick(25); + assert.htmlEqual( + target.innerHTML, + ` + + +
hello
+ ` + ); + + fly_out.click(); + flushSync(); + raf.tick(50); + assert.htmlEqual( + target.innerHTML, + ` + + + ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/transition-derived-uninitialized/main.svelte b/packages/svelte/tests/runtime-runes/samples/transition-derived-uninitialized/main.svelte new file mode 100644 index 0000000000..4fc3395af8 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/transition-derived-uninitialized/main.svelte @@ -0,0 +1,11 @@ + + + + + +{#if show} + +{/if} From dc1f037fa67bf2fbdeb1c5f18b2b1f7d023077e3 Mon Sep 17 00:00:00 2001 From: Simon H <5968653+dummdidumm@users.noreply.github.com> Date: Mon, 18 May 2026 17:37:01 +0200 Subject: [PATCH 011/222] fix: don't run teardown effects when deriveds are unfreezed (#18227) The logic was flawed - a teardown effect only has a teardown function but not `fn` property, but unfreeze thought that everything with a `teardown` needs to be unfreezed Helps with #18221 (though likely doesn't fix it completely, at least not the more general `batch.#roots`problems) --- .changeset/slimy-olives-cross.md | 5 +++++ .../internal/client/reactivity/deriveds.js | 6 +++--- .../async-state-eager-const/_config.js | 21 +++++++++++++++++++ .../async-state-eager-const/main.svelte | 21 +++++++++++++++++++ 4 files changed, 50 insertions(+), 3 deletions(-) create mode 100644 .changeset/slimy-olives-cross.md create mode 100644 packages/svelte/tests/runtime-runes/samples/async-state-eager-const/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-state-eager-const/main.svelte diff --git a/.changeset/slimy-olives-cross.md b/.changeset/slimy-olives-cross.md new file mode 100644 index 0000000000..3309e2deab --- /dev/null +++ b/.changeset/slimy-olives-cross.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: don't run teardown effects when deriveds are unfreezed diff --git a/packages/svelte/src/internal/client/reactivity/deriveds.js b/packages/svelte/src/internal/client/reactivity/deriveds.js index 75be018552..8d240bca31 100644 --- a/packages/svelte/src/internal/client/reactivity/deriveds.js +++ b/packages/svelte/src/internal/client/reactivity/deriveds.js @@ -452,8 +452,8 @@ export function freeze_derived_effects(derived) { // make it a noop so it doesn't get called again if the derived // is unfrozen. we don't set it to `null`, because the existence // of a teardown function is what determines whether the - // effect runs again during unfreezing - e.teardown = noop; + // effect runs again during unfreezing (but not for teardown-only effects) + if (e.fn !== null) e.teardown = noop; e.ac = null; remove_reactions(e, 0); @@ -471,7 +471,7 @@ export function unfreeze_derived_effects(derived) { for (const e of derived.effects) { // if the effect was previously frozen — indicated by the presence // of a teardown function — unfreeze it - if (e.teardown) { + if (e.teardown && e.fn !== null) { update_effect(e); } } diff --git a/packages/svelte/tests/runtime-runes/samples/async-state-eager-const/_config.js b/packages/svelte/tests/runtime-runes/samples/async-state-eager-const/_config.js new file mode 100644 index 0000000000..1511d443e5 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-state-eager-const/_config.js @@ -0,0 +1,21 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + compileOptions: { dev: true }, // for testing that teardown effect in eager $.get(loaded) doesn't lead to a crash (because it means REACTION_RAN is set, which means unfreeze_derived runs) + async test({ assert, target }) { + const [count, shift] = target.querySelectorAll('button'); + + shift.click(); + await tick(); + assert.htmlEqual(target.innerHTML, `

0

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

0 (...)

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

1

`); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-state-eager-const/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-state-eager-const/main.svelte new file mode 100644 index 0000000000..f0e2f1cdac --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-state-eager-const/main.svelte @@ -0,0 +1,21 @@ + + + + + + + {@const loaded = $state.eager(count) === count} +

{await push(count)} {loaded ? '' : '(...)'}

+ + {#snippet pending()}{/snippet} +
From 2bc3592eb1ba1b37bde6c893e84b28e2f630db59 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 18 May 2026 11:53:31 -0400 Subject: [PATCH 012/222] fix: use named symbols everywhere (#18238) This isn't _really_ a fix since these should never surface to the user, but it's useful for debugging when they do, as in https://github.com/sveltejs/kit/pull/15779. Instead of seeing `Symbol()` we see e.g. `Symbol(uninitialized)` which makes it easier to understand where a bug is coming from. --- .changeset/red-moles-talk.md | 5 +++++ packages/svelte/src/constants.js | 2 +- packages/svelte/src/internal/client/reactivity/store.js | 2 +- packages/svelte/src/reactivity/url-search-params.js | 2 +- 4 files changed, 8 insertions(+), 3 deletions(-) create mode 100644 .changeset/red-moles-talk.md diff --git a/.changeset/red-moles-talk.md b/.changeset/red-moles-talk.md new file mode 100644 index 0000000000..7e09dc9b9a --- /dev/null +++ b/.changeset/red-moles-talk.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: use named symbols everywhere diff --git a/packages/svelte/src/constants.js b/packages/svelte/src/constants.js index a3a109b943..1e721b7d30 100644 --- a/packages/svelte/src/constants.js +++ b/packages/svelte/src/constants.js @@ -32,7 +32,7 @@ export const ELEMENT_IS_NAMESPACED = 1; export const ELEMENT_PRESERVE_ATTRIBUTE_CASE = 1 << 1; export const ELEMENT_IS_INPUT = 1 << 2; -export const UNINITIALIZED = Symbol(); +export const UNINITIALIZED = Symbol('uninitialized'); // Dev-time component properties export const FILENAME = Symbol('filename'); diff --git a/packages/svelte/src/internal/client/reactivity/store.js b/packages/svelte/src/internal/client/reactivity/store.js index 7124e23db8..726765b619 100644 --- a/packages/svelte/src/internal/client/reactivity/store.js +++ b/packages/svelte/src/internal/client/reactivity/store.js @@ -21,7 +21,7 @@ export let legacy_is_updating_store = false; */ let is_store_binding = false; -let IS_UNMOUNTED = Symbol(); +let IS_UNMOUNTED = Symbol('unmounted'); /** * Gets the current value of a store. If the store isn't subscribed to yet, it will create a proxy diff --git a/packages/svelte/src/reactivity/url-search-params.js b/packages/svelte/src/reactivity/url-search-params.js index 2381e11875..2e70bf518d 100644 --- a/packages/svelte/src/reactivity/url-search-params.js +++ b/packages/svelte/src/reactivity/url-search-params.js @@ -4,7 +4,7 @@ import { tag } from '../internal/client/dev/tracing.js'; import { get } from '../internal/client/runtime.js'; import { get_current_url } from './url.js'; -export const REPLACE = Symbol(); +export const REPLACE = Symbol('replace'); /** * A reactive version of the built-in [`URLSearchParams`](https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams) object. From ca3f35bf7d3eeea6d0b5f477d208c3fd9c19988c Mon Sep 17 00:00:00 2001 From: OfirHaf Date: Mon, 18 May 2026 18:54:31 +0300 Subject: [PATCH 013/222] fix(print): handle svelte:body and fix keyframe percentage double-printing (#18234) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes #18206 and fixes #18207 — both are printer bugs in `packages/svelte/src/compiler/print/index.js`. ## Changes ### Fix 1: `svelte:body` crashes the printer (#18206) `SvelteBody` was missing from the visitor map, causing a crash with `Error: Not implemented: SvelteBody`. Added the handler (same one-liner pattern as `SvelteDocument`, `SvelteHead`, etc.) and added `SvelteBody` to the `is_block_element` check so whitespace is handled consistently. ### Fix 2: Keyframe percent stops print as `0%%` (#18207) `Percentage.value` already includes the `%` sign (captured by `/\d+(\.\d+)?%/y`), but the printer was writing `` `${node.value}%` `` — appending a second `%`. Changed to `context.write(node.value)` to match the `Nth` printer pattern directly above it. Also updated the existing `style` snapshot which had `50%%` (the bug was silently baked in), and added dedicated test samples for both fixes. --- .../fix-printer-svelte-body-keyframe-percent.md | 5 +++++ packages/svelte/src/compiler/print/index.js | 7 ++++++- .../samples/css-keyframes-percent/input.svelte | 7 +++++++ .../samples/css-keyframes-percent/output.svelte | 13 +++++++++++++ .../svelte/tests/print/samples/style/output.svelte | 2 +- .../tests/print/samples/svelte-body/input.svelte | 1 + .../tests/print/samples/svelte-body/output.svelte | 1 + 7 files changed, 34 insertions(+), 2 deletions(-) create mode 100644 .changeset/fix-printer-svelte-body-keyframe-percent.md create mode 100644 packages/svelte/tests/print/samples/css-keyframes-percent/input.svelte create mode 100644 packages/svelte/tests/print/samples/css-keyframes-percent/output.svelte create mode 100644 packages/svelte/tests/print/samples/svelte-body/input.svelte create mode 100644 packages/svelte/tests/print/samples/svelte-body/output.svelte diff --git a/.changeset/fix-printer-svelte-body-keyframe-percent.md b/.changeset/fix-printer-svelte-body-keyframe-percent.md new file mode 100644 index 0000000000..6854468835 --- /dev/null +++ b/.changeset/fix-printer-svelte-body-keyframe-percent.md @@ -0,0 +1,5 @@ +--- +"svelte": patch +--- + +fix(print): handle `svelte:body` and fix keyframe percentage double-printing diff --git a/packages/svelte/src/compiler/print/index.js b/packages/svelte/src/compiler/print/index.js index 26dc1b88e8..c511ee6da0 100644 --- a/packages/svelte/src/compiler/print/index.js +++ b/packages/svelte/src/compiler/print/index.js @@ -247,7 +247,7 @@ const css_visitors = { }, Percentage(node, context) { - context.write(`${node.value}%`); + context.write(node.value); }, PseudoClassSelector(node, context) { @@ -417,6 +417,7 @@ const svelte_visitors = (comments) => ({ const is_block_element = child_node.type === 'RegularElement' || child_node.type === 'Component' || + child_node.type === 'SvelteBody' || child_node.type === 'SvelteHead' || child_node.type === 'SvelteFragment' || child_node.type === 'SvelteBoundary' || @@ -821,6 +822,10 @@ const svelte_visitors = (comments) => ({ context.write(''); }, + SvelteBody(node, context) { + base_element(node, context, comments); + }, + SvelteBoundary(node, context) { base_element(node, context, comments); }, diff --git a/packages/svelte/tests/print/samples/css-keyframes-percent/input.svelte b/packages/svelte/tests/print/samples/css-keyframes-percent/input.svelte new file mode 100644 index 0000000000..b2a0e3aa7a --- /dev/null +++ b/packages/svelte/tests/print/samples/css-keyframes-percent/input.svelte @@ -0,0 +1,7 @@ + diff --git a/packages/svelte/tests/print/samples/css-keyframes-percent/output.svelte b/packages/svelte/tests/print/samples/css-keyframes-percent/output.svelte new file mode 100644 index 0000000000..e35a5780fa --- /dev/null +++ b/packages/svelte/tests/print/samples/css-keyframes-percent/output.svelte @@ -0,0 +1,13 @@ + diff --git a/packages/svelte/tests/print/samples/style/output.svelte b/packages/svelte/tests/print/samples/style/output.svelte index 03168cccae..9fc5e3fbba 100644 --- a/packages/svelte/tests/print/samples/style/output.svelte +++ b/packages/svelte/tests/print/samples/style/output.svelte @@ -19,7 +19,7 @@ from { opacity: 0; } - 50%% { + 50% { opacity: 0.5; } to { diff --git a/packages/svelte/tests/print/samples/svelte-body/input.svelte b/packages/svelte/tests/print/samples/svelte-body/input.svelte new file mode 100644 index 0000000000..a4c26db5ce --- /dev/null +++ b/packages/svelte/tests/print/samples/svelte-body/input.svelte @@ -0,0 +1 @@ + diff --git a/packages/svelte/tests/print/samples/svelte-body/output.svelte b/packages/svelte/tests/print/samples/svelte-body/output.svelte new file mode 100644 index 0000000000..e13eaae979 --- /dev/null +++ b/packages/svelte/tests/print/samples/svelte-body/output.svelte @@ -0,0 +1 @@ + From f9440dc3ed28a9f1f518b0216d0e10fce00f1618 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 18 May 2026 18:44:05 +0200 Subject: [PATCH 014/222] Version Packages (#18239) 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.55.8 ### Patch Changes - fix(print): handle `svelte:body` and fix keyframe percentage double-printing ([#18234](https://github.com/sveltejs/svelte/pull/18234)) - fix: execute uninitialized derived even if it's destroyed ([#18228](https://github.com/sveltejs/svelte/pull/18228)) - fix: use named symbols everywhere ([#18238](https://github.com/sveltejs/svelte/pull/18238)) - fix: don't run teardown effects when deriveds are unfreezed ([#18227](https://github.com/sveltejs/svelte/pull/18227)) - fix: unset context synchronously in `run` ([#18236](https://github.com/sveltejs/svelte/pull/18236)) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .../fix-printer-svelte-body-keyframe-percent.md | 5 ----- .changeset/hungry-ads-beam.md | 5 ----- .changeset/red-moles-talk.md | 5 ----- .changeset/slimy-olives-cross.md | 5 ----- .changeset/tangy-icons-dig.md | 5 ----- packages/svelte/CHANGELOG.md | 14 ++++++++++++++ packages/svelte/package.json | 2 +- packages/svelte/src/version.js | 2 +- 8 files changed, 16 insertions(+), 27 deletions(-) delete mode 100644 .changeset/fix-printer-svelte-body-keyframe-percent.md delete mode 100644 .changeset/hungry-ads-beam.md delete mode 100644 .changeset/red-moles-talk.md delete mode 100644 .changeset/slimy-olives-cross.md delete mode 100644 .changeset/tangy-icons-dig.md diff --git a/.changeset/fix-printer-svelte-body-keyframe-percent.md b/.changeset/fix-printer-svelte-body-keyframe-percent.md deleted file mode 100644 index 6854468835..0000000000 --- a/.changeset/fix-printer-svelte-body-keyframe-percent.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"svelte": patch ---- - -fix(print): handle `svelte:body` and fix keyframe percentage double-printing diff --git a/.changeset/hungry-ads-beam.md b/.changeset/hungry-ads-beam.md deleted file mode 100644 index 260d177119..0000000000 --- a/.changeset/hungry-ads-beam.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: execute uninitialized derived even if it's destroyed diff --git a/.changeset/red-moles-talk.md b/.changeset/red-moles-talk.md deleted file mode 100644 index 7e09dc9b9a..0000000000 --- a/.changeset/red-moles-talk.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: use named symbols everywhere diff --git a/.changeset/slimy-olives-cross.md b/.changeset/slimy-olives-cross.md deleted file mode 100644 index 3309e2deab..0000000000 --- a/.changeset/slimy-olives-cross.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: don't run teardown effects when deriveds are unfreezed diff --git a/.changeset/tangy-icons-dig.md b/.changeset/tangy-icons-dig.md deleted file mode 100644 index 6d87912bbb..0000000000 --- a/.changeset/tangy-icons-dig.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: unset context synchronously in `run` diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index 1255d8c6a0..36528c741b 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,19 @@ # svelte +## 5.55.8 + +### Patch Changes + +- fix(print): handle `svelte:body` and fix keyframe percentage double-printing ([#18234](https://github.com/sveltejs/svelte/pull/18234)) + +- fix: execute uninitialized derived even if it's destroyed ([#18228](https://github.com/sveltejs/svelte/pull/18228)) + +- fix: use named symbols everywhere ([#18238](https://github.com/sveltejs/svelte/pull/18238)) + +- fix: don't run teardown effects when deriveds are unfreezed ([#18227](https://github.com/sveltejs/svelte/pull/18227)) + +- fix: unset context synchronously in `run` ([#18236](https://github.com/sveltejs/svelte/pull/18236)) + ## 5.55.7 ### Patch Changes diff --git a/packages/svelte/package.json b/packages/svelte/package.json index 2f0743641f..66a555880b 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.7", + "version": "5.55.8", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index 6b0ed0aea8..446904cd14 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.7'; +export const VERSION = '5.55.8'; export const PUBLIC_VERSION = '5'; From a5df6616ea6c8ee0984ff0fc7b04e51472f1d01d Mon Sep 17 00:00:00 2001 From: Sean Kenneth Doherty Date: Mon, 18 May 2026 13:53:10 -0500 Subject: [PATCH 015/222] fix: avoid unnecessary stringify in server attributes (#18232) ### 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. Fixes #10031. - [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`). ### What this changes Server attribute template generation currently wraps each dynamic expression in `$.stringify`, even when the compiler can prove the expression is a string or a known constant. This reuses the existing scope evaluation metadata so server output can avoid `$.stringify` for proven string/constant chunks while keeping it for possibly nullish unknown values. The updated snapshot covers a mixed attribute with a known string, mutable state, `null`, numeric/undefined constants, a known string-producing `typeof`, and an unknown prop value. ### Tests and linting - [x] `pnpm test snapshot -t nullish-coallescence-omittance` - [x] `pnpm test snapshot` - [x] `pnpm --filter svelte check` - [x] `pnpm lint` - [x] `pnpm prettier --check .changeset/slow-bikes-serve.md` --------- Co-authored-by: Rich Harris --- .changeset/slow-bikes-serve.md | 5 +++ .../server/visitors/shared/utils.js | 33 ++++++++++++------- .../_expected/client/index.svelte.js | 13 ++++++-- .../_expected/server/index.svelte.js | 5 +-- .../index.svelte | 4 ++- 5 files changed, 42 insertions(+), 18 deletions(-) create mode 100644 .changeset/slow-bikes-serve.md diff --git a/.changeset/slow-bikes-serve.md b/.changeset/slow-bikes-serve.md new file mode 100644 index 0000000000..5e1d654353 --- /dev/null +++ b/.changeset/slow-bikes-serve.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: inline primitive constants in attribute values during SSR diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/utils.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/utils.js index 9b3ac3ad78..62e1c44094 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/utils.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/utils.js @@ -229,18 +229,25 @@ export function build_attribute_value( ? node.data.replace(regex_whitespaces_strict, ' ') : node.data; } else { - expressions.push( - b.call( - '$.stringify', - transform( - /** @type {Expression} */ (context.visit(node.expression)), - node.metadata.expression - ) - ) - ); + const evaluated = context.state.scope.evaluate(node.expression); - quasi = b.quasi('', i + 1 === value.length); - quasis.push(quasi); + if (evaluated.is_known) { + quasi.value.cooked += (evaluated.value ?? '') + ''; + } else { + const expression = transform( + /** @type {Expression} */ (context.visit(node.expression)), + node.metadata.expression + ); + + expressions.push( + evaluated.is_string && evaluated.is_defined + ? expression + : b.call('$.stringify', expression) + ); + + quasi = b.quasi('', i + 1 === value.length); + quasis.push(quasi); + } } } @@ -248,7 +255,9 @@ export function build_attribute_value( quasi.value.raw = sanitize_template_string(/** @type {string} */ (quasi.value.cooked)); } - return b.template(quasis, expressions); + return expressions.length > 0 + ? b.template(quasis, expressions) + : b.literal(/** @type {string} */ (quasi.value.cooked)); } /** diff --git a/packages/svelte/tests/snapshot/samples/nullish-coallescence-omittance/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/nullish-coallescence-omittance/_expected/client/index.svelte.js index 5281a86582..837a7cd65a 100644 --- a/packages/svelte/tests/snapshot/samples/nullish-coallescence-omittance/_expected/client/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/nullish-coallescence-omittance/_expected/client/index.svelte.js @@ -1,9 +1,9 @@ import 'svelte/internal/disclose-version'; import * as $ from 'svelte/internal/client'; -var root = $.from_html(`

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

`, 1); -export default function Nullish_coallescence_omittance($$anchor) { +export default function Nullish_coallescence_omittance($$anchor, $$props) { let name = 'world'; let count = $.state(0); var fragment = root(); @@ -23,7 +23,14 @@ export default function Nullish_coallescence_omittance($$anchor) { var h1_1 = $.sibling(button, 2); h1_1.textContent = 'Hello, world'; - $.template_effect(() => $.set_text(text, `Count is ${$.get(count) ?? ''}`)); + + var div = $.sibling(h1_1, 2); + + $.template_effect(() => { + $.set_text(text, `Count is ${$.get(count) ?? ''}`); + $.set_attribute(div, 'title', `Hello, world ${$.get(count) ?? ''} 1 ${typeof $$props.value} ${$$props.value ?? ''}`); + }); + $.delegated('click', button, () => $.update(count)); $.append($$anchor, fragment); } diff --git a/packages/svelte/tests/snapshot/samples/nullish-coallescence-omittance/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/nullish-coallescence-omittance/_expected/server/index.svelte.js index a7e580acb8..357a887c9b 100644 --- a/packages/svelte/tests/snapshot/samples/nullish-coallescence-omittance/_expected/server/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/nullish-coallescence-omittance/_expected/server/index.svelte.js @@ -1,8 +1,9 @@ import * as $ from 'svelte/internal/server'; -export default function Nullish_coallescence_omittance($$renderer) { +export default function Nullish_coallescence_omittance($$renderer, $$props) { let name = 'world'; let count = 0; + let { value } = $$props; - $$renderer.push(`

Hello, world!

123

Hello, world

`); + $$renderer.push(`

Hello, world!

123

Hello, world

`); } \ No newline at end of file diff --git a/packages/svelte/tests/snapshot/samples/nullish-coallescence-omittance/index.svelte b/packages/svelte/tests/snapshot/samples/nullish-coallescence-omittance/index.svelte index a67c574fee..ed88f5a269 100644 --- a/packages/svelte/tests/snapshot/samples/nullish-coallescence-omittance/index.svelte +++ b/packages/svelte/tests/snapshot/samples/nullish-coallescence-omittance/index.svelte @@ -1,8 +1,10 @@

Hello, {null}{name}!

{1 ?? 'stuff'}{2 ?? 'more stuff'}{3 ?? 'even more stuff'} -

Hello, {name ?? 'earth' ?? null}

\ No newline at end of file +

Hello, {name ?? 'earth' ?? null}

+
From a63bf862c03d5c63b73e412f2ca952cb83d444d8 Mon Sep 17 00:00:00 2001 From: Mukunda Rao Katta Date: Mon, 18 May 2026 12:42:20 -0700 Subject: [PATCH 016/222] docs: clarify context state getter pattern (#18226) Closes #18022 This updates the context docs to show the getter pattern for state that may be reassigned, such as primitive state. The new example passes a function through context so children read the current value instead of capturing the initial value. This is a docs-only clarification and does not change runtime behavior. Validation: - `git diff --check` - `pnpm exec prettier --check documentation/docs/06-runtime/02-context.md` Full `pnpm test`/`pnpm lint` were not run because this only changes documentation. --------- Co-authored-by: Rich Harris --- documentation/docs/06-runtime/02-context.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/documentation/docs/06-runtime/02-context.md b/documentation/docs/06-runtime/02-context.md index aee51c9a78..439272df8f 100644 --- a/documentation/docs/06-runtime/02-context.md +++ b/documentation/docs/06-runtime/02-context.md @@ -163,6 +163,8 @@ export const [getCounter, setCounter] = createContext(); Svelte will warn you if you get it wrong. +Similarly, to pass primitive values through context, use functions as described in [Passing state into functions]($state#Passing-state-into-functions). + ## Component testing When writing [component tests](testing#Unit-and-component-tests-with-Vitest-Component-testing), it can be useful to create a wrapper component that sets the context in order to check the behaviour of a component that uses it. As of version 5.49, you can do this sort of thing: From be8ffeb2a4b0c36657fb5c1bdcf36971da814227 Mon Sep 17 00:00:00 2001 From: Kiwi Date: Tue, 19 May 2026 10:58:35 -0400 Subject: [PATCH 017/222] docs(contributing): fix grammar in Prioritization section (#18241) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Small typo in `CONTRIBUTING.md` — "the most important and active ones repositories" reads like a leftover from an edit. Removed the dangling "ones". ```diff -PRs to the most important and active ones repositories get reviewed more quickly +PRs to the most important and active repositories get reviewed more quickly ``` --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e940252892..586c6fe6ae 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -43,7 +43,7 @@ The maintainers meet on the final Saturday of each month. While these meetings a ### Prioritization -We do our best to review PRs and RFCs as they are sent, but it is difficult to keep up. We welcome help in reviewing PRs, RFCs, and issues. If an item aligns with the current priority on our [roadmap](https://svelte.dev/roadmap), it is more likely to be reviewed quickly. PRs to the most important and active ones repositories get reviewed more quickly while PRs to smaller inactive repos may sit for a bit before we periodically come by and review the pending PRs in a batch. +We do our best to review PRs and RFCs as they are sent, but it is difficult to keep up. We welcome help in reviewing PRs, RFCs, and issues. If an item aligns with the current priority on our [roadmap](https://svelte.dev/roadmap), it is more likely to be reviewed quickly. PRs to the most important and active repositories get reviewed more quickly while PRs to smaller inactive repos may sit for a bit before we periodically come by and review the pending PRs in a batch. ## Bugs From 000c594e05d27e3e56c08cf30274689615d99092 Mon Sep 17 00:00:00 2001 From: Simon H <5968653+dummdidumm@users.noreply.github.com> Date: Tue, 19 May 2026 22:16:03 +0200 Subject: [PATCH 018/222] fix: `{#await await ...}` and async dependencies fixes (#18243) This started out as an investigation to get rid of the runtime code for `{#await ...}` that deactivated a batch prior to reading the promise function. That can result in a new batch being created if the promise invocation happens to write a source. Through that I discovered two other bugs: 1. The way we handle `{#await await ...}` was flawed around SSR/hydration: On the server it would await the expression (which it should not; `{#await await ...}` is kind of a weird special case here) and during hydration it did not produce matching nodes, leading to hydration fails. 2. When reading a dependency after an await expression which we add to the current reaction, we did not deduplicate those reads. That can lead to duplicate dependencies, which in turn can lead to bugs when `remove_reaction` later runs. Conside this: You have `deps = [count, unrelated, count]`. Now you do `remove_reactions(deps, 1)`, i.e. "remove all reactions after the first one". That means the "disconnect these from each other" logic runs for `count`, too, because it's also in the third position, but that is wrong because it is also in the first position, i.e. the connection should be kept. --------- Co-authored-by: Rich Harris --- .changeset/clean-ties-start.md | 5 +++ .changeset/fair-otters-notice.md | 5 +++ .changeset/loud-phones-return.md | 5 +++ .../3-transform/client/visitors/AwaitBlock.js | 4 +- .../client/visitors/VariableDeclaration.js | 18 +++++--- .../3-transform/server/visitors/AwaitBlock.js | 9 +++- .../src/internal/client/dom/blocks/await.js | 28 ++++++------ .../svelte/src/internal/client/runtime.js | 12 ++++-- .../samples/async-await-block-2/_config.js | 24 +++++++++++ .../samples/async-await-block-2/main.svelte | 22 ++++++++++ .../async-duplicate-dependencies/_config.js | 43 +++++++++++++++++++ .../async-duplicate-dependencies/main.svelte | 20 +++++++++ 12 files changed, 170 insertions(+), 25 deletions(-) create mode 100644 .changeset/clean-ties-start.md create mode 100644 .changeset/fair-otters-notice.md create mode 100644 .changeset/loud-phones-return.md create mode 100644 packages/svelte/tests/runtime-runes/samples/async-await-block-2/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-await-block-2/main.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/async-duplicate-dependencies/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-duplicate-dependencies/main.svelte diff --git a/.changeset/clean-ties-start.md b/.changeset/clean-ties-start.md new file mode 100644 index 0000000000..825ed675a5 --- /dev/null +++ b/.changeset/clean-ties-start.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: don't unset batch when calling `{#await ...}` promise diff --git a/.changeset/fair-otters-notice.md b/.changeset/fair-otters-notice.md new file mode 100644 index 0000000000..eb5a181847 --- /dev/null +++ b/.changeset/fair-otters-notice.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: promise-ify `{#await await ...}` expressions on the server and correctly hydrate them on the client diff --git a/.changeset/loud-phones-return.md b/.changeset/loud-phones-return.md new file mode 100644 index 0000000000..02f2b3a2e7 --- /dev/null +++ b/.changeset/loud-phones-return.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: deduplicate dependencies that are added outside the init/update cycle diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/AwaitBlock.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/AwaitBlock.js index 832c56818b..b8def178f2 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/AwaitBlock.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/AwaitBlock.js @@ -71,14 +71,14 @@ export function AwaitBlock(node, context) { 'await' ); - if (node.metadata.expression.has_blockers()) { + if (node.metadata.expression.has_blockers() || node.metadata.expression.has_await) { context.state.init.push( b.stmt( b.call( '$.async', context.state.node, node.metadata.expression.blockers(), - b.array([]), + b.array([]), // {#await await ...} is special insofar that the await should not be waited on b.arrow([context.state.node], b.block([stmt])) ) ) diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/VariableDeclaration.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/VariableDeclaration.js index 16aa164d84..dffa79cd7a 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/VariableDeclaration.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/VariableDeclaration.js @@ -386,13 +386,17 @@ export function VariableDeclaration(node, context) { * @param {Expression} value */ function create_state_declarators(declarator, context, value) { + /** + * @param {Expression} value + * @param {string} name + */ + const mutable_source = (value, name) => { + const call = b.call('$.mutable_source', value, context.state.analysis.immutable && b.true); + return dev ? b.call('$.tag', call, b.literal(name)) : call; + }; + if (declarator.id.type === 'Identifier') { - return [ - b.declarator( - declarator.id, - b.call('$.mutable_source', value, context.state.analysis.immutable ? b.true : undefined) - ) - ]; + return [b.declarator(declarator.id, mutable_source(value, declarator.id.name))]; } const tmp = b.id(context.state.scope.generate('tmp')); @@ -414,7 +418,7 @@ function create_state_declarators(declarator, context, value) { return b.declarator( path.node, binding?.kind === 'state' - ? b.call('$.mutable_source', value, context.state.analysis.immutable ? b.true : undefined) + ? mutable_source(value, /** @type {Identifier} */ (path.node).name) : value ); }) diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/AwaitBlock.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/AwaitBlock.js index 84c2a81612..ba05ca1357 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/AwaitBlock.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/AwaitBlock.js @@ -9,12 +9,19 @@ import { block_close, create_child_block } from './shared/utils.js'; * @param {ComponentContext} context */ export function AwaitBlock(node, context) { + let expression = /** @type {Expression} */ (context.visit(node.expression)); + if (node.metadata.expression.has_await) { + // If this is an await expression, turn it into a IIFE so that the result is a promise. + // {#await await ...} is special insofar that the await should not be waited on. + expression = b.call(b.arrow([], expression, true)); + } + /** @type {Statement} */ let statement = b.stmt( b.call( '$.await', b.id('$$renderer'), - /** @type {Expression} */ (context.visit(node.expression)), + expression, b.thunk( node.pending ? /** @type {BlockStatement} */ (context.visit(node.pending)) : b.block([]) ), diff --git a/packages/svelte/src/internal/client/dom/blocks/await.js b/packages/svelte/src/internal/client/dom/blocks/await.js index d6430547b5..194a7ccc0e 100644 --- a/packages/svelte/src/internal/client/dom/blocks/await.js +++ b/packages/svelte/src/internal/client/dom/blocks/await.js @@ -7,7 +7,8 @@ import { hydrating, skip_nodes, set_hydrate_node, - set_hydrating + set_hydrating, + hydrate_node } from '../hydration.js'; import { queue_micro_task } from '../task.js'; import { HYDRATION_START_ELSE, UNINITIALIZED } from '../../../../constants.js'; @@ -15,6 +16,7 @@ import { is_runes } from '../../context.js'; import { Batch, current_batch, flushSync, is_flushing_sync } from '../../reactivity/batch.js'; import { BranchManager } from './branches.js'; import { capture, unset_context } from '../../reactivity/async.js'; +import { DEV } from 'esm-env'; const PENDING = 0; const THEN = 1; @@ -42,16 +44,16 @@ export function await_block(node, get_input, pending_fn, then_fn, catch_fn) { var value = runes ? source(v) : mutable_source(v, false, false); var error = runes ? source(v) : mutable_source(v, false, false); + if (DEV) { + value.label = '{#await ...} value'; + error.label = '{#await ...} error'; + } + var branches = new BranchManager(node); block(() => { var batch = /** @type {Batch} */ (current_batch); - - // we null out `current_batch` because otherwise `save(...)` will incorrectly restore it — - // the batch will already have been committed by the time it resolves - batch.deactivate(); var input = get_input(); - batch.activate(); var destroyed = false; @@ -79,15 +81,17 @@ export function await_block(node, get_input, pending_fn, then_fn, catch_fn) { // We don't want to restore the previous batch here; {#await} blocks don't follow the async logic // we have elsewhere, instead pending/resolve/fail states are each their own batch so to speak. restore(false); + // ...but it might still be set here. That means a `save(...)` has restored it — but that batch will + // likely already have been committed by the time it resolves, and this resolve should be processed + // in a separate batch. We're not using batch.deactivate()/activate() above because get_input() + // could write to sources, which would then incorrectly create a new batch or could mess with + // async_derived expecting a current_batch to exist. + if (current_batch === batch) { + batch.deactivate(); + } // Make sure we have a batch, since the branch manager expects one to exist Batch.ensure(); - if (hydrating) { - // `restore()` could set `hydrating` to `true`, which we very much - // don't want — we want to restore everything _except_ this - set_hydrating(false); - } - try { fn(); } finally { diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index 033afe98dc..87abbd71b6 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -560,9 +560,15 @@ export function get(signal) { } } } else { - // we're adding a dependency outside the init/update cycle - // (i.e. after an `await`) - (active_reaction.deps ??= []).push(signal); + // We're adding a dependency outside the init/update cycle (i.e. after an `await`). + // We have to deduplicate deps/reactions in this case or remove_reactions could + // disconnect deps/reactions that are actually still in use (if skip_deps says + // "disconnect all after this index" and some of the signals are also present in + // list prior to the cutoff index, i.e. that should be kept). + active_reaction.deps ??= []; + if (!includes.call(active_reaction.deps, signal)) { + active_reaction.deps.push(signal); + } var reactions = signal.reactions; diff --git a/packages/svelte/tests/runtime-runes/samples/async-await-block-2/_config.js b/packages/svelte/tests/runtime-runes/samples/async-await-block-2/_config.js new file mode 100644 index 0000000000..0a927e64f6 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-await-block-2/_config.js @@ -0,0 +1,24 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target }) { + await tick(); + const [incremet, resolve] = target.querySelectorAll('button'); + + incremet.click(); + await tick(); + incremet.click(); + await tick(); + resolve.click(); + await tick(); + resolve.click(); + await tick(); + resolve.click(); + await tick(); + resolve.click(); + await tick(); + + assert.htmlEqual(target.innerHTML, ' 4 4 1'); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-await-block-2/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-await-block-2/main.svelte new file mode 100644 index 0000000000..b619c6a949 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-await-block-2/main.svelte @@ -0,0 +1,22 @@ + + + + +{#await request(count) then result}{result}{/await} +{#await await push(count) + count then result}{result}{/await} +{#await await 1 then result}{result}{/await} diff --git a/packages/svelte/tests/runtime-runes/samples/async-duplicate-dependencies/_config.js b/packages/svelte/tests/runtime-runes/samples/async-duplicate-dependencies/_config.js new file mode 100644 index 0000000000..b7451282ac --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-duplicate-dependencies/_config.js @@ -0,0 +1,43 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target }) { + await tick(); + const [increment, resolve] = target.querySelectorAll('button'); + + increment.click(); + await tick(); + increment.click(); + await tick(); + resolve.click(); + await tick(); + resolve.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + + + 4 + ` + ); + + increment.click(); + await tick(); + increment.click(); + await tick(); + resolve.click(); + await tick(); + resolve.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + + + 8 + ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-duplicate-dependencies/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-duplicate-dependencies/main.svelte new file mode 100644 index 0000000000..58a5848e3b --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-duplicate-dependencies/main.svelte @@ -0,0 +1,20 @@ + + + + +{await request(count)} From d654db83ef3c299e0b7833e6c418ce02d6127abd Mon Sep 17 00:00:00 2001 From: Simon H <5968653+dummdidumm@users.noreply.github.com> Date: Wed, 20 May 2026 03:04:31 +0200 Subject: [PATCH 019/222] fix: avoid false-positive batch invariant error (#18246) Thanks to https://github.com/sveltejs/svelte/issues/17940#issuecomment-4480016550 I was finally able to isolate and reproduce a false-positive invariant error. I had a hunch this could happen and this shows it. Essentially, you can end up in situations where two batches are scheduled to run in the same microtask queue flush, and if the first rebases the second the invariant will throw, which is wrong. We can avoid this by checking if a decrement is queued. --- .changeset/slimy-frogs-share.md | 5 +++++ .../src/internal/client/reactivity/batch.js | 7 +++++-- .../samples/async-pending-batch/_config.js | 16 ++++++++++++++++ .../samples/async-pending-batch/main.svelte | 8 ++++++++ 4 files changed, 34 insertions(+), 2 deletions(-) create mode 100644 .changeset/slimy-frogs-share.md create mode 100644 packages/svelte/tests/runtime-runes/samples/async-pending-batch/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-pending-batch/main.svelte diff --git a/.changeset/slimy-frogs-share.md b/.changeset/slimy-frogs-share.md new file mode 100644 index 0000000000..27a393d564 --- /dev/null +++ b/.changeset/slimy-frogs-share.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: avoid false-positive batch invariant error diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index a5c9a51eec..ae4f5a6dac 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -679,7 +679,9 @@ export class Batch { batch.discard(); } } else if (sources.length > 0) { - if (DEV) { + // The microtask queue can contain the batch already scheduled to run right + // after this one is finished, so throwing the invariant would be wrong here. + if (DEV && !batch.#decrement_queued) { invariant(batch.#roots.length === 0, 'Batch has scheduled roots'); } @@ -732,7 +734,8 @@ export class Batch { } // Only apply and traverse when we know we triggered async work with marking the effects - if (batch.#roots.length > 0) { + // and know this won't run anyway right afterwards + if (batch.#roots.length > 0 && !batch.#decrement_queued) { batch.apply(); for (var root of batch.#roots) { diff --git a/packages/svelte/tests/runtime-runes/samples/async-pending-batch/_config.js b/packages/svelte/tests/runtime-runes/samples/async-pending-batch/_config.js new file mode 100644 index 0000000000..e7359efa2d --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-pending-batch/_config.js @@ -0,0 +1,16 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + // This test mainly checks that we don't run into the 'Batch has scheduled roots' invariant wrongly. + // It is crafted such that two batches are scheduled to run in the same microtask, and the first + // tries to rebase the second. + async test({ assert, target }) { + await tick(); + const [run] = target.querySelectorAll('button'); + + run.click(); + await tick(); + assert.htmlEqual(target.innerHTML, ' none none 0'); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-pending-batch/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-pending-batch/main.svelte new file mode 100644 index 0000000000..19305cfb76 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-pending-batch/main.svelte @@ -0,0 +1,8 @@ + + + + +{selectedId ?? "none"} {selectedOption ?? "none"} {$effect.pending()} From ef319ed492869e075bfb29685a606bce4818adb7 Mon Sep 17 00:00:00 2001 From: Simon H <5968653+dummdidumm@users.noreply.github.com> Date: Wed, 20 May 2026 11:53:01 +0200 Subject: [PATCH 020/222] chore: bump acorn-typescript/esrap (#18248) --- packages/svelte/package.json | 4 +-- .../svelte/scripts/process-messages/index.js | 1 - pnpm-lock.yaml | 26 ++++++++++++------- 3 files changed, 18 insertions(+), 13 deletions(-) diff --git a/packages/svelte/package.json b/packages/svelte/package.json index 66a555880b..f364b78e88 100644 --- a/packages/svelte/package.json +++ b/packages/svelte/package.json @@ -169,7 +169,7 @@ "dependencies": { "@jridgewell/remapping": "^2.3.4", "@jridgewell/sourcemap-codec": "^1.5.0", - "@sveltejs/acorn-typescript": "^1.0.5", + "@sveltejs/acorn-typescript": "^1.0.10", "@types/estree": "^1.0.5", "@types/trusted-types": "^2.0.7", "acorn": "^8.12.1", @@ -178,7 +178,7 @@ "clsx": "^2.1.1", "devalue": "^5.8.1", "esm-env": "^1.2.1", - "esrap": "^2.2.4", + "esrap": "^2.2.9", "is-reference": "^3.0.3", "locate-character": "^3.0.0", "magic-string": "^0.30.11", diff --git a/packages/svelte/scripts/process-messages/index.js b/packages/svelte/scripts/process-messages/index.js index 1904f1dfb4..c8d7418db4 100644 --- a/packages/svelte/scripts/process-messages/index.js +++ b/packages/svelte/scripts/process-messages/index.js @@ -378,7 +378,6 @@ function run() { }; const block = esrap.print( - // @ts-expect-error some bullshit /** @type {ESTree.Program} */ ({ ...ast, body: [clone] }), ts({ comments: [jsdoc_clone] }) ).code; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1bfee5c769..25899b297d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -75,8 +75,8 @@ importers: specifier: ^1.5.0 version: 1.5.0 '@sveltejs/acorn-typescript': - specifier: ^1.0.5 - version: 1.0.5(acorn@8.16.0) + specifier: ^1.0.10 + version: 1.0.10(acorn@8.16.0) '@types/estree': specifier: ^1.0.5 version: 1.0.8 @@ -102,8 +102,8 @@ importers: specifier: ^1.2.1 version: 1.2.1 esrap: - specifier: ^2.2.4 - version: 2.2.4 + specifier: ^2.2.9 + version: 2.2.9(@typescript-eslint/types@8.56.0) is-reference: specifier: ^3.0.3 version: 3.0.3 @@ -1042,8 +1042,8 @@ packages: peerDependencies: eslint: '>=8.40.0' - '@sveltejs/acorn-typescript@1.0.5': - resolution: {integrity: sha512-IwQk4yfwLdibDlrXVE04jTZYlLnwsTT2PIOQQGNLWfjavGifnk1JD1LcZjZaBTRcxZu2FfPfNLOE04DSu9lqtQ==} + '@sveltejs/acorn-typescript@1.0.10': + resolution: {integrity: sha512-4WfKk68eTih+MiJD4fSbxN7E8kVBmTMPWHUPYjvl2N0rMs53YLTT8/YjKU5Dtnz5LqDjl7LEw4U7lXR2W3J5WA==} peerDependencies: acorn: ^8.9.0 @@ -1574,8 +1574,13 @@ packages: resolution: {integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==} engines: {node: '>=0.10'} - esrap@2.2.4: - resolution: {integrity: sha512-suICpxAmZ9A8bzJjEl/+rLJiDKC0X4gYWUxT6URAWBLvlXmtbZd5ySMu/N2ZGEtMCAmflUDPSehrP9BQcsGcSg==} + esrap@2.2.9: + resolution: {integrity: sha512-4KijP+NxCWthMCUC3qHbE6n4vCjqgJS1uAYKhuT/GWfFTf1Qyive2TgOjep+gzbSzRfnNyaN/UU9YmdOt8Eg0A==} + peerDependencies: + '@typescript-eslint/types': ^8.2.0 + peerDependenciesMeta: + '@typescript-eslint/types': + optional: true esrecurse@4.3.0: resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} @@ -3392,7 +3397,7 @@ snapshots: eslint-visitor-keys: 3.4.3 espree: 9.6.1 - '@sveltejs/acorn-typescript@1.0.5(acorn@8.16.0)': + '@sveltejs/acorn-typescript@1.0.10(acorn@8.16.0)': dependencies: acorn: 8.16.0 @@ -4046,9 +4051,10 @@ snapshots: dependencies: estraverse: 5.3.0 - esrap@2.2.4: + esrap@2.2.9(@typescript-eslint/types@8.56.0): dependencies: '@jridgewell/sourcemap-codec': 1.5.0 + optionalDependencies: '@typescript-eslint/types': 8.56.0 esrecurse@4.3.0: From b65a3f3fc5e1607966e3e27f7e28fc691e622683 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 20 May 2026 12:07:18 +0200 Subject: [PATCH 021/222] Version Packages (#18240) 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.55.9 ### Patch Changes - fix: don't unset batch when calling `{#await ...}` promise ([#18243](https://github.com/sveltejs/svelte/pull/18243)) - fix: promise-ify `{#await await ...}` expressions on the server and correctly hydrate them on the client ([#18243](https://github.com/sveltejs/svelte/pull/18243)) - fix: deduplicate dependencies that are added outside the init/update cycle ([#18243](https://github.com/sveltejs/svelte/pull/18243)) - fix: avoid false-positive batch invariant error ([#18246](https://github.com/sveltejs/svelte/pull/18246)) - fix: inline primitive constants in attribute values during SSR ([#18232](https://github.com/sveltejs/svelte/pull/18232)) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .changeset/clean-ties-start.md | 5 ----- .changeset/fair-otters-notice.md | 5 ----- .changeset/loud-phones-return.md | 5 ----- .changeset/slimy-frogs-share.md | 5 ----- .changeset/slow-bikes-serve.md | 5 ----- packages/svelte/CHANGELOG.md | 14 ++++++++++++++ packages/svelte/package.json | 2 +- packages/svelte/src/version.js | 2 +- 8 files changed, 16 insertions(+), 27 deletions(-) delete mode 100644 .changeset/clean-ties-start.md delete mode 100644 .changeset/fair-otters-notice.md delete mode 100644 .changeset/loud-phones-return.md delete mode 100644 .changeset/slimy-frogs-share.md delete mode 100644 .changeset/slow-bikes-serve.md diff --git a/.changeset/clean-ties-start.md b/.changeset/clean-ties-start.md deleted file mode 100644 index 825ed675a5..0000000000 --- a/.changeset/clean-ties-start.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: don't unset batch when calling `{#await ...}` promise diff --git a/.changeset/fair-otters-notice.md b/.changeset/fair-otters-notice.md deleted file mode 100644 index eb5a181847..0000000000 --- a/.changeset/fair-otters-notice.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: promise-ify `{#await await ...}` expressions on the server and correctly hydrate them on the client diff --git a/.changeset/loud-phones-return.md b/.changeset/loud-phones-return.md deleted file mode 100644 index 02f2b3a2e7..0000000000 --- a/.changeset/loud-phones-return.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: deduplicate dependencies that are added outside the init/update cycle diff --git a/.changeset/slimy-frogs-share.md b/.changeset/slimy-frogs-share.md deleted file mode 100644 index 27a393d564..0000000000 --- a/.changeset/slimy-frogs-share.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: avoid false-positive batch invariant error diff --git a/.changeset/slow-bikes-serve.md b/.changeset/slow-bikes-serve.md deleted file mode 100644 index 5e1d654353..0000000000 --- a/.changeset/slow-bikes-serve.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: inline primitive constants in attribute values during SSR diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index 36528c741b..1f3f9d1149 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,19 @@ # svelte +## 5.55.9 + +### Patch Changes + +- fix: don't unset batch when calling `{#await ...}` promise ([#18243](https://github.com/sveltejs/svelte/pull/18243)) + +- fix: promise-ify `{#await await ...}` expressions on the server and correctly hydrate them on the client ([#18243](https://github.com/sveltejs/svelte/pull/18243)) + +- fix: deduplicate dependencies that are added outside the init/update cycle ([#18243](https://github.com/sveltejs/svelte/pull/18243)) + +- fix: avoid false-positive batch invariant error ([#18246](https://github.com/sveltejs/svelte/pull/18246)) + +- fix: inline primitive constants in attribute values during SSR ([#18232](https://github.com/sveltejs/svelte/pull/18232)) + ## 5.55.8 ### Patch Changes diff --git a/packages/svelte/package.json b/packages/svelte/package.json index f364b78e88..b9bb48aa72 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.8", + "version": "5.55.9", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index 446904cd14..ee05138797 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.8'; +export const VERSION = '5.55.9'; export const PUBLIC_VERSION = '5'; From 65283bc13a54c41ee96940b559c1a027b8770882 Mon Sep 17 00:00:00 2001 From: Simon H <5968653+dummdidumm@users.noreply.github.com> Date: Wed, 20 May 2026 23:49:38 +0200 Subject: [PATCH 022/222] fix: transfer effects when merging batches (#18254) An effect could be gated behind a branch. If we don't defer + transfer them upon merge, the branch would still be marked clean but the effect behind it is dirty but no longer reachable. It's not reachable via mark either because that one only concerns itself with block/async effects, and the branch gating the effect is not guaranteed to be touched by that. Fixes #18249 Little sad side-effect: Since we cannot reliably know _before_ traversal if we have no blocking pending work left (the traversal could mark an if block falsy which contains the last blocker), we gotta undo a performance optimization. --- .changeset/evil-stars-wave.md | 5 +++ .../src/internal/client/reactivity/batch.js | 31 ++++++++++++------- .../async-batch-merge-effect/_config.js | 25 +++++++++++++++ .../async-batch-merge-effect/main.svelte | 25 +++++++++++++++ 4 files changed, 74 insertions(+), 12 deletions(-) create mode 100644 .changeset/evil-stars-wave.md create mode 100644 packages/svelte/tests/runtime-runes/samples/async-batch-merge-effect/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-batch-merge-effect/main.svelte diff --git a/.changeset/evil-stars-wave.md b/.changeset/evil-stars-wave.md new file mode 100644 index 0000000000..b199afe1dd --- /dev/null +++ b/.changeset/evil-stars-wave.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: transfer effects when merging batches diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index ae4f5a6dac..2803ffc84c 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -289,19 +289,19 @@ export class Batch { } } - // we only reschedule previously-deferred effects if we expect - // to be able to run them after processing the batch - if (!this.#is_deferred()) { - for (const e of this.#dirty_effects) { - this.#maybe_dirty_effects.delete(e); - set_signal_status(e, DIRTY); - this.schedule(e); - } + // We always reschedule previously-deferred effects, not just when + // #is_deferred() is true, because traversing the tree could make + // an if block that contains the last blocking pending effect falsy, + // causing the block to no longer be deferred. + for (const e of this.#dirty_effects) { + this.#maybe_dirty_effects.delete(e); + set_signal_status(e, DIRTY); + this.schedule(e); + } - for (const e of this.#maybe_dirty_effects) { - set_signal_status(e, MAYBE_DIRTY); - this.schedule(e); - } + for (const e of this.#maybe_dirty_effects) { + set_signal_status(e, MAYBE_DIRTY); + this.schedule(e); } const roots = this.#roots; @@ -362,6 +362,10 @@ export class Batch { const earlier_batch = this.#find_earlier_batch(); if (earlier_batch) { + // If this batch collected deferred effects during traversal, they still need + // to run after being merged into the earlier batch. + this.#defer_effects(render_effects); + this.#defer_effects(effects); earlier_batch.#merge(this); return; } @@ -503,6 +507,9 @@ export class Batch { if (d) deferred.promise.then(d.resolve); } + // Mark is not guaranteed not touch these, so we transfer them + this.transfer_effects(batch.#dirty_effects, batch.#maybe_dirty_effects); + /** * mark all effects that depend on `batch.current`, except the * async effects that we just resolved (TODO unless they depend diff --git a/packages/svelte/tests/runtime-runes/samples/async-batch-merge-effect/_config.js b/packages/svelte/tests/runtime-runes/samples/async-batch-merge-effect/_config.js new file mode 100644 index 0000000000..c2be623de2 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-batch-merge-effect/_config.js @@ -0,0 +1,25 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target }) { + await tick(); + const [x, x_y, pop] = target.querySelectorAll('button'); + + x.click(); + await tick(); + x_y.click(); + await tick(); + pop.click(); + await tick(); + pop.click(); + await tick(); + pop.click(); + await tick(); + + assert.htmlEqual( + target.innerHTML, + ' 2 1 1' + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-batch-merge-effect/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-batch-merge-effect/main.svelte new file mode 100644 index 0000000000..61efd4fca0 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-batch-merge-effect/main.svelte @@ -0,0 +1,25 @@ + + + + + + +{await push(x)} {await push(y)} + +{#if true} + {y} +{/if} + From 91a42e2ed6bda52205b269d28e70b9d15cd87292 Mon Sep 17 00:00:00 2001 From: Simon H <5968653+dummdidumm@users.noreply.github.com> Date: Thu, 21 May 2026 20:25:51 +0200 Subject: [PATCH 023/222] fix: correctly coordinate component-level effects inside async blocks (#18260) While looking at the reproduction in https://github.com/sveltejs/svelte/issues/18221#issuecomment-4497918414 I immediately got greeted with a runtime error when running it in the playground (weirdly not in the Stackblitz version). The error was that a component expected a binding to be set in onMount, but the timing of onMount was wrong. Turns out it's because our logic to determine whether or not to defer top level effects is flawed. `REACTION_RAN`, which was used previously, is already set if the initialized component is inside an async block. We instead check for `component_context.i` which is set to `true` on `pop()`. --- .changeset/tasty-tires-wait.md | 5 +++++ .../svelte/src/internal/client/reactivity/effects.js | 7 +++++-- .../samples/async-effect-mount-timing/Child.svelte | 6 ++++++ .../samples/async-effect-mount-timing/_config.js | 10 ++++++++++ .../samples/async-effect-mount-timing/main.svelte | 5 +++++ 5 files changed, 31 insertions(+), 2 deletions(-) create mode 100644 .changeset/tasty-tires-wait.md create mode 100644 packages/svelte/tests/runtime-runes/samples/async-effect-mount-timing/Child.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/async-effect-mount-timing/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-effect-mount-timing/main.svelte diff --git a/.changeset/tasty-tires-wait.md b/.changeset/tasty-tires-wait.md new file mode 100644 index 0000000000..0f3fd2d671 --- /dev/null +++ b/.changeset/tasty-tires-wait.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: correctly coordinate component-level effects inside async blocks diff --git a/packages/svelte/src/internal/client/reactivity/effects.js b/packages/svelte/src/internal/client/reactivity/effects.js index 5bdba037b1..1bbda86fa7 100644 --- a/packages/svelte/src/internal/client/reactivity/effects.js +++ b/packages/svelte/src/internal/client/reactivity/effects.js @@ -20,7 +20,6 @@ import { EFFECT, DESTROYED, INERT, - REACTION_RAN, BLOCK_EFFECT, ROOT_EFFECT, EFFECT_TRANSPARENT, @@ -213,7 +212,11 @@ export function user_effect(fn) { // Non-nested `$effect(...)` in a component should be deferred // until the component is mounted var flags = /** @type {Effect} */ (active_effect).f; - var defer = !active_reaction && (flags & BRANCH_EFFECT) !== 0 && (flags & REACTION_RAN) === 0; + var defer = + !active_reaction && + (flags & BRANCH_EFFECT) !== 0 && + component_context !== null && + !component_context.i; if (defer) { // Top-level `$effect(...)` in an unmounted component — defer until mount diff --git a/packages/svelte/tests/runtime-runes/samples/async-effect-mount-timing/Child.svelte b/packages/svelte/tests/runtime-runes/samples/async-effect-mount-timing/Child.svelte new file mode 100644 index 0000000000..18856d71e1 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-effect-mount-timing/Child.svelte @@ -0,0 +1,6 @@ + + +
diff --git a/packages/svelte/tests/runtime-runes/samples/async-effect-mount-timing/_config.js b/packages/svelte/tests/runtime-runes/samples/async-effect-mount-timing/_config.js new file mode 100644 index 0000000000..7a6e436825 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-effect-mount-timing/_config.js @@ -0,0 +1,10 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + // Test that $effect/onMount etc at the top level of components are correctly deferred/coordinated if inside an async block + async test({ assert, logs }) { + await tick(); + assert.deepEqual(logs, [true]); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-effect-mount-timing/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-effect-mount-timing/main.svelte new file mode 100644 index 0000000000..934d9d7f5b --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-effect-mount-timing/main.svelte @@ -0,0 +1,5 @@ + + + From 4656e6895dde764e99c01d9dd86379df2c881c8a Mon Sep 17 00:00:00 2001 From: Simon H <5968653+dummdidumm@users.noreply.github.com> Date: Thu, 21 May 2026 20:26:09 +0200 Subject: [PATCH 024/222] fix: make unnecessary commit work less likely (#18263) While looking at https://github.com/sveltejs/svelte/issues/18221#issuecomment-4497918414 and trying to understand how the invariant can happen I noticed that we are not correctly filtering during commit. - we were not ignoring deriveds - we were not comparing the correct values (checking `source.v` instead of the saved value) and not checking if their "is a derived" state differs I'm not able to come up with a test where something fails without these (possibly because it's more about an optimization to do less reruns and not about correctness) fixes, but they do make sense. --- .changeset/tired-socks-brake.md | 5 +++++ .../src/internal/client/reactivity/batch.js | 19 ++++++++++++------- 2 files changed, 17 insertions(+), 7 deletions(-) create mode 100644 .changeset/tired-socks-brake.md diff --git a/.changeset/tired-socks-brake.md b/.changeset/tired-socks-brake.md new file mode 100644 index 0000000000..1d302ffa60 --- /dev/null +++ b/.changeset/tired-socks-brake.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: make unnecessary commit work less likely diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index 2803ffc84c..4952001dd6 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -677,8 +677,10 @@ export class Batch { if (!batch.#started) continue; - // Re-run async/block effects that depend on distinct values changed in both batches - var others = [...batch.current.keys()].filter((s) => !this.current.has(s)); + // Re-run async/block effects that depend on distinct values changed in both batches (ignoring deriveds) + var others = [...batch.current.keys()].filter( + (s) => !(/** @type {[any, boolean]} */ (batch.current.get(s))[1]) && !this.current.has(s) + ); if (others.length === 0) { if (is_earlier) { @@ -718,11 +720,14 @@ export class Batch { } checked = new Map(); - var current_unequal = [...batch.current.keys()].filter((c) => - this.current.has(c) - ? /** @type {[any, boolean]} */ (this.current.get(c))[0] !== c.v - : true - ); + var current_unequal = [...batch.current] + .filter(([c, v1]) => { + const v2 = this.current.get(c); + if (!v2) return true; + // Either their values are different or one is a derived but not the other + return v2[0] !== v1[0] || v2[1] !== v1[1]; + }) + .map(([c]) => c); if (current_unequal.length > 0) { for (const effect of this.#new_effects) { From a6002b587c9d2f40fe2dc48391c372360abbaf0f Mon Sep 17 00:00:00 2001 From: Simon H <5968653+dummdidumm@users.noreply.github.com> Date: Thu, 21 May 2026 20:26:47 +0200 Subject: [PATCH 025/222] fix: unlink errored and otherwise finished batch (#18264) This fixes the issue in https://github.com/sveltejs/svelte/issues/18221#issuecomment-4497918414 where an error can create follup-up invariant errors. The batch errors, has no chance to run otherwise (no more pending work) and is therefore "dead". That means we need to unlink it otherwise it's becoming a "zombie" and hangs around, causing unnecessary and potentially buggy (as seen in the reproduction) merge/commit work. I was not able to reduce the reproduction down to a test case that fails without the fix, but it does make a related error test from #17888 work more correctly. --- .changeset/beige-bobcats-eat.md | 5 +++++ packages/svelte/src/internal/client/reactivity/batch.js | 6 ++++++ .../tests/runtime-runes/samples/error-recovery/_config.js | 6 +++--- 3 files changed, 14 insertions(+), 3 deletions(-) create mode 100644 .changeset/beige-bobcats-eat.md diff --git a/.changeset/beige-bobcats-eat.md b/.changeset/beige-bobcats-eat.md new file mode 100644 index 0000000000..95390c3c2e --- /dev/null +++ b/.changeset/beige-bobcats-eat.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: unlink errored and otherwise finished batch diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index 4952001dd6..b9721b6243 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -326,6 +326,12 @@ export class Batch { this.#traverse(root, effects, render_effects); } catch (e) { reset_all(root); + // If there's no async work left, this branch is now dead and needs + // to be unlinked to not become a zombie that is never cleaned up. + // See https://github.com/sveltejs/svelte/issues/18221#issuecomment-4497918414 + // for a (non-minimal) reproduction that demonstrates a case where this is necessary + // to not get follow-up false-positives via "batch has scheduled roots" invariant errors. + if (!this.#is_deferred()) this.#unlink(); throw e; } } diff --git a/packages/svelte/tests/runtime-runes/samples/error-recovery/_config.js b/packages/svelte/tests/runtime-runes/samples/error-recovery/_config.js index 52c1bbd1bf..1f80251806 100644 --- a/packages/svelte/tests/runtime-runes/samples/error-recovery/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/error-recovery/_config.js @@ -2,7 +2,7 @@ import { flushSync } from 'svelte'; import { test } from '../../test'; export default test({ - async test({ assert, target, compileOptions }) { + async test({ assert, target }) { const [toggle, increment] = target.querySelectorAll('button'); flushSync(() => increment.click()); @@ -25,8 +25,8 @@ export default test({ ` -

show: ${compileOptions.experimental?.async ? 'false' : 'true'}

- ` +

show: true

+ ` // show: false would also be fine; this is more about ensuring that things continue to work _somehow_ ); } }); From 078f901f611237d4c6fed16ef894f33572f9ccba Mon Sep 17 00:00:00 2001 From: Simon H <5968653+dummdidumm@users.noreply.github.com> Date: Thu, 21 May 2026 20:27:13 +0200 Subject: [PATCH 026/222] fix: catch rejected promises while merging/committing (#18266) A committing/merging batch can have promises that were rejected (e.g. as obsolete). We gotta "forward" this rejection, too, instead of just the successful promise. At best it results in a uncaught rejection (`async-branch-merge-obsolete`), at worst it means error boundaries are not correctly displayed (`async-later-promise-fails-first`). Solves the reproduction in https://github.com/sveltejs/svelte/issues/18221#issuecomment-4507803845 --- .changeset/true-pigs-go.md | 5 ++++ .../src/internal/client/reactivity/batch.js | 4 +-- .../async-branch-merge-obsolete/_config.js | 17 +++++++++++++ .../async-branch-merge-obsolete/main.svelte | 21 ++++++++++++++++ .../_config.js | 25 +++++++++++++++++++ .../main.svelte | 22 ++++++++++++++++ 6 files changed, 92 insertions(+), 2 deletions(-) create mode 100644 .changeset/true-pigs-go.md create mode 100644 packages/svelte/tests/runtime-runes/samples/async-branch-merge-obsolete/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-branch-merge-obsolete/main.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/async-later-promise-fails-first/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-later-promise-fails-first/main.svelte diff --git a/.changeset/true-pigs-go.md b/.changeset/true-pigs-go.md new file mode 100644 index 0000000000..b4900b38fa --- /dev/null +++ b/.changeset/true-pigs-go.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: catch rejected promises while merging/committing diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index b9721b6243..82c97cf95c 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -510,7 +510,7 @@ export class Batch { for (const [effect, deferred] of batch.async_deriveds) { const d = this.async_deriveds.get(effect); - if (d) deferred.promise.then(d.resolve); + if (d) deferred.promise.then(d.resolve).catch(d.reject); } // Mark is not guaranteed not touch these, so we transfer them @@ -677,7 +677,7 @@ export class Batch { // immediately resolving them? Likely not because of how this.apply() works. for (const [effect, deferred] of this.async_deriveds) { const d = batch.async_deriveds.get(effect); - if (d) deferred.promise.then(d.resolve); + if (d) deferred.promise.then(d.resolve).catch(d.reject); } } diff --git a/packages/svelte/tests/runtime-runes/samples/async-branch-merge-obsolete/_config.js b/packages/svelte/tests/runtime-runes/samples/async-branch-merge-obsolete/_config.js new file mode 100644 index 0000000000..faf1ff7f6b --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-branch-merge-obsolete/_config.js @@ -0,0 +1,17 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target }) { + await tick(); + const [increment] = target.querySelectorAll('button'); + + increment.click(); + await tick(); + increment.click(); + await tick(); + increment.click(); + await tick(); + assert.htmlEqual(target.innerHTML, ' done'); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-branch-merge-obsolete/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-branch-merge-obsolete/main.svelte new file mode 100644 index 0000000000..4780442293 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-branch-merge-obsolete/main.svelte @@ -0,0 +1,21 @@ + + + + +{#if count < 3} + {await push(count)} +{:else} + done +{/if} diff --git a/packages/svelte/tests/runtime-runes/samples/async-later-promise-fails-first/_config.js b/packages/svelte/tests/runtime-runes/samples/async-later-promise-fails-first/_config.js new file mode 100644 index 0000000000..5e18c953c7 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-later-promise-fails-first/_config.js @@ -0,0 +1,25 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target }) { + await tick(); + const [increment, pop] = target.querySelectorAll('button'); + + increment.click(); + await tick(); + increment.click(); + await tick(); + increment.click(); + await tick(); + pop.click(); + await tick(); + assert.htmlEqual(target.innerHTML, ' failed'); + + pop.click(); + await tick(); + pop.click(); + await tick(); + assert.htmlEqual(target.innerHTML, ' failed'); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-later-promise-fails-first/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-later-promise-fails-first/main.svelte new file mode 100644 index 0000000000..3293e4ab88 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-later-promise-fails-first/main.svelte @@ -0,0 +1,22 @@ + + + + + + + {await push(count)} + + {#snippet failed()}failed{/snippet} + From 04d408b29d059f131766448265768ce705801aaf Mon Sep 17 00:00:00 2001 From: Mathias Picker <48158184+MathiasWP@users.noreply.github.com> Date: Thu, 21 May 2026 21:53:34 +0200 Subject: [PATCH 027/222] perf: walk composedPath() directly in delegated event propagation (#18268) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary The propagation walk in `handle_event_propagation` already calls `event.composedPath()` at the start to find the entry index, but then re-derives the same chain step-by-step via `current_target.assignedSlot || current_target.parentNode || .host`. Three property reads per iteration is measurable on the hot event path. Walk the captured `path` array by index instead. ## Notes on behavior `composedPath()` is the spec-compliant snapshot of the dispatch chain: - Same shadow-DOM crossings (slots and shadow roots are included for composed events). - Same `host` traversal (composed-path crosses shadow boundaries when appropriate). - Differs from the previous walk in one edge case: if a handler removes a parent mid-dispatch, the snapshot-based walk continues through the captured chain (matches native browser semantics — the previous `parentNode` walk would have stopped at a null parent). ## Performance Measured in real Chromium on a click through a 30-deep tree with five delegated handlers: **~245k hz → ~277k hz** (~+13%, ~−12% per-event time). ## Test plan - [x] All 6006 runtime tests pass (runtime-runes + runtime-legacy + runtime-browser) - [x] Native shadow-DOM event tests (in runtime-browser) pass unchanged 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.7 (1M context) --- .changeset/event-walk-composed-path.md | 5 +++++ .../src/internal/client/dom/elements/events.js | 15 +++++---------- 2 files changed, 10 insertions(+), 10 deletions(-) create mode 100644 .changeset/event-walk-composed-path.md diff --git a/.changeset/event-walk-composed-path.md b/.changeset/event-walk-composed-path.md new file mode 100644 index 0000000000..8b24573930 --- /dev/null +++ b/.changeset/event-walk-composed-path.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +perf: walk composedPath() directly in delegated event propagation diff --git a/packages/svelte/src/internal/client/dom/elements/events.js b/packages/svelte/src/internal/client/dom/elements/events.js index 5aa41e1c4d..45e042a8c6 100644 --- a/packages/svelte/src/internal/client/dom/elements/events.js +++ b/packages/svelte/src/internal/client/dom/elements/events.js @@ -257,12 +257,7 @@ export function handle_event_propagation(event) { var other_errors = []; while (current_target !== null) { - /** @type {null | Element} */ - var parent_element = - current_target.assignedSlot || - current_target.parentNode || - /** @type {any} */ (current_target).host || - null; + if (current_target === handler_element) break; try { // @ts-expect-error @@ -284,10 +279,10 @@ export function handle_event_propagation(event) { throw_error = error; } } - if (event.cancelBubble || parent_element === handler_element || parent_element === null) { - break; - } - current_target = parent_element; + if (event.cancelBubble) break; + + path_idx++; + current_target = path_idx < path.length ? /** @type {Element} */ (path[path_idx]) : null; } if (throw_error) { From 8b961be0b190078815219e614b739b170e1087b1 Mon Sep 17 00:00:00 2001 From: Puneet Dixit Date: Fri, 22 May 2026 01:54:11 +0530 Subject: [PATCH 028/222] fix: remove raw-text hydration markers (#18269) Fixes #14413. This keeps the temporary raw-text hydration sentinel used by dynamic `` from becoming part of the final DOM. Hydration still gets a marker to advance through for raw-text children, but the marker is removed after the child renderer runs, so `` : ''; window.document.body.innerHTML = '
'; - window.addEventListener('error', (e) => { - unhandled_rejection = e.error; - e.preventDefault(); - }); + window.addEventListener('error', window_error_listener, true); globalThis.requestAnimationFrame = globalThis.setTimeout; @@ -423,7 +468,6 @@ async function run_test_variant( await config.test_ssr({ logs, warnings, - // @ts-expect-error assert: { ...assert, htmlEqual: assert_html_equal, @@ -515,7 +559,6 @@ async function run_test_variant( } await config.test({ - // @ts-expect-error TS doesn't get it assert: { ...assert, htmlEqual: assert_html_equal, @@ -525,6 +568,7 @@ async function run_test_variant( component: runes ? props : instance, instance, mod, + ok, target, snapshot, window, @@ -593,6 +637,8 @@ async function run_test_variant( throw new Error('Hydration state was not cleared'); } + window.removeEventListener('error', window_error_listener, true); + config.after_test?.(); // Free up the microtask queue diff --git a/packages/svelte/tests/sourcemaps/test.ts b/packages/svelte/tests/sourcemaps/test.ts index 6749483079..b9d0e5abdc 100644 --- a/packages/svelte/tests/sourcemaps/test.ts +++ b/packages/svelte/tests/sourcemaps/test.ts @@ -31,7 +31,21 @@ interface SourcemapTest extends BaseTest { /** The expected `sources` array in the source map */ css_map_sources?: string[]; test?: (obj: { - assert: typeof assert; + // chai 5's `asserts value` signatures trip TS2775 in `_config.js` files + // where `assert` is a destructured parameter (non-explicit). Override + // the assertion methods we use with non-assertion equivalents. + assert: Omit< + typeof assert, + 'ok' | 'isOk' | 'isTrue' | 'isFalse' | 'exists' | 'notExists' | 'instanceOf' + > & { + ok(value: unknown, message?: string): void; + isOk(value: unknown, message?: string): void; + isTrue(value: unknown, message?: string): void; + isFalse(value: unknown, message?: string): void; + exists(value: unknown, message?: string): void; + notExists(value: unknown, message?: string): void; + instanceOf(value: unknown, type: Function, message?: string): void; + }; input: string; map_preprocessed: any; code_preprocessed: string; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 767af0515a..a970a9cf6a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -27,8 +27,8 @@ importers: specifier: ^4.0.2 version: 4.0.2 '@vitest/coverage-v8': - specifier: ^2.1.9 - version: 2.1.9(vitest@2.1.9(@types/node@20.19.17)(jsdom@25.0.1)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0)) + specifier: ^4.1.7 + version: 4.1.7(vitest@4.1.7) eslint: specifier: ^10.0.0 version: 10.0.0 @@ -63,8 +63,8 @@ importers: specifier: ^1.2.5 version: 1.2.5 vitest: - specifier: ^2.1.9 - version: 2.1.9(@types/node@20.19.17)(jsdom@25.0.1)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0) + specifier: ^4.1.7 + version: 4.1.7(@types/node@20.19.17)(@vitest/coverage-v8@4.1.7)(jsdom@25.0.1)(vite@7.3.2(@types/node@20.19.17)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0)) packages/svelte: dependencies: @@ -163,8 +163,8 @@ importers: specifier: ^5.5.4 version: 5.5.4 vitest: - specifier: ^2.1.9 - version: 2.1.9(@types/node@20.19.17)(jsdom@25.0.1)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0) + specifier: ^4.1.7 + version: 4.1.7(@types/node@20.19.17)(@vitest/coverage-v8@4.1.7)(jsdom@25.0.1)(vite@7.3.2(@types/node@20.19.17)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0)) web-features: specifier: ^3.29.0 version: 3.29.0 @@ -202,20 +202,16 @@ packages: resolution: {integrity: sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==} engines: {node: '>=0.10.0'} - '@ampproject/remapping@2.3.0': - resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} - engines: {node: '>=6.0.0'} - - '@babel/helper-string-parser@7.24.8': - resolution: {integrity: sha512-pO9KhhRcuUyGnJWwyEgnRJTSIZHiT+vMD0kPeD+so0l7mxkMT19g3pjY9GTnHySck/hDzq+dtW/4VgnMkippsQ==} + '@babel/helper-string-parser@7.27.1': + resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} engines: {node: '>=6.9.0'} - '@babel/helper-validator-identifier@7.24.7': - resolution: {integrity: sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==} + '@babel/helper-validator-identifier@7.28.5': + resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} engines: {node: '>=6.9.0'} - '@babel/parser@7.25.4': - resolution: {integrity: sha512-nq+eWrOgdtu3jG5Os4TQP3x3cLA8hR8TvJNjD8vnPa20WGycimcparWnLK4jJhElTK6SDyuJo1weMKO/5LpmLA==} + '@babel/parser@7.29.3': + resolution: {integrity: sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA==} engines: {node: '>=6.0.0'} hasBin: true @@ -223,12 +219,13 @@ packages: resolution: {integrity: sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==} engines: {node: '>=6.9.0'} - '@babel/types@7.25.4': - resolution: {integrity: sha512-zQ1ijeeCXVEh+aNL0RlmkPkG8HUiDcU2pzQQFjtbntgAczRASFzj4H+6+bV+dy1ntKR14I/DypeuRG1uma98iQ==} + '@babel/types@7.29.0': + resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==} engines: {node: '>=6.9.0'} - '@bcoe/v8-coverage@0.2.3': - resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==} + '@bcoe/v8-coverage@1.0.2': + resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==} + engines: {node: '>=18'} '@changesets/apply-release-plan@7.0.14': resolution: {integrity: sha512-ddBvf9PHdy2YY0OUiEl3TV78mH9sckndJR14QAt87KLEbIov81XO0q0QAmvooBxXlqRRP8I9B7XOzZwQG7JkWA==} @@ -288,12 +285,6 @@ packages: '@changesets/write@0.4.0': resolution: {integrity: sha512-CdTLvIOPiCNuH71pyDu3rA+Q0n65cmAbXnwWH84rKGiFumFzkmHNT8KHTMEchcxN+Kl8I54xGUhJ7l3E7X396Q==} - '@esbuild/aix-ppc64@0.21.5': - resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==} - engines: {node: '>=12'} - cpu: [ppc64] - os: [aix] - '@esbuild/aix-ppc64@0.25.11': resolution: {integrity: sha512-Xt1dOL13m8u0WE8iplx9Ibbm+hFAO0GsU2P34UNoDGvZYkY8ifSiy6Zuc1lYxfG7svWE2fzqCUmFp5HCn51gJg==} engines: {node: '>=18'} @@ -306,12 +297,6 @@ packages: cpu: [ppc64] os: [aix] - '@esbuild/android-arm64@0.21.5': - resolution: {integrity: sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==} - engines: {node: '>=12'} - cpu: [arm64] - os: [android] - '@esbuild/android-arm64@0.25.11': resolution: {integrity: sha512-9slpyFBc4FPPz48+f6jyiXOx/Y4v34TUeDDXJpZqAWQn/08lKGeD8aDp9TMn9jDz2CiEuHwfhRmGBvpnd/PWIQ==} engines: {node: '>=18'} @@ -324,12 +309,6 @@ packages: cpu: [arm64] os: [android] - '@esbuild/android-arm@0.21.5': - resolution: {integrity: sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==} - engines: {node: '>=12'} - cpu: [arm] - os: [android] - '@esbuild/android-arm@0.25.11': resolution: {integrity: sha512-uoa7dU+Dt3HYsethkJ1k6Z9YdcHjTrSb5NUy66ZfZaSV8hEYGD5ZHbEMXnqLFlbBflLsl89Zke7CAdDJ4JI+Gg==} engines: {node: '>=18'} @@ -342,12 +321,6 @@ packages: cpu: [arm] os: [android] - '@esbuild/android-x64@0.21.5': - resolution: {integrity: sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==} - engines: {node: '>=12'} - cpu: [x64] - os: [android] - '@esbuild/android-x64@0.25.11': resolution: {integrity: sha512-Sgiab4xBjPU1QoPEIqS3Xx+R2lezu0LKIEcYe6pftr56PqPygbB7+szVnzoShbx64MUupqoE0KyRlN7gezbl8g==} engines: {node: '>=18'} @@ -360,12 +333,6 @@ packages: cpu: [x64] os: [android] - '@esbuild/darwin-arm64@0.21.5': - resolution: {integrity: sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==} - engines: {node: '>=12'} - cpu: [arm64] - os: [darwin] - '@esbuild/darwin-arm64@0.25.11': resolution: {integrity: sha512-VekY0PBCukppoQrycFxUqkCojnTQhdec0vevUL/EDOCnXd9LKWqD/bHwMPzigIJXPhC59Vd1WFIL57SKs2mg4w==} engines: {node: '>=18'} @@ -378,12 +345,6 @@ packages: cpu: [arm64] os: [darwin] - '@esbuild/darwin-x64@0.21.5': - resolution: {integrity: sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==} - engines: {node: '>=12'} - cpu: [x64] - os: [darwin] - '@esbuild/darwin-x64@0.25.11': resolution: {integrity: sha512-+hfp3yfBalNEpTGp9loYgbknjR695HkqtY3d3/JjSRUyPg/xd6q+mQqIb5qdywnDxRZykIHs3axEqU6l1+oWEQ==} engines: {node: '>=18'} @@ -396,12 +357,6 @@ packages: cpu: [x64] os: [darwin] - '@esbuild/freebsd-arm64@0.21.5': - resolution: {integrity: sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==} - engines: {node: '>=12'} - cpu: [arm64] - os: [freebsd] - '@esbuild/freebsd-arm64@0.25.11': resolution: {integrity: sha512-CmKjrnayyTJF2eVuO//uSjl/K3KsMIeYeyN7FyDBjsR3lnSJHaXlVoAK8DZa7lXWChbuOk7NjAc7ygAwrnPBhA==} engines: {node: '>=18'} @@ -414,12 +369,6 @@ packages: cpu: [arm64] os: [freebsd] - '@esbuild/freebsd-x64@0.21.5': - resolution: {integrity: sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==} - engines: {node: '>=12'} - cpu: [x64] - os: [freebsd] - '@esbuild/freebsd-x64@0.25.11': resolution: {integrity: sha512-Dyq+5oscTJvMaYPvW3x3FLpi2+gSZTCE/1ffdwuM6G1ARang/mb3jvjxs0mw6n3Lsw84ocfo9CrNMqc5lTfGOw==} engines: {node: '>=18'} @@ -432,12 +381,6 @@ packages: cpu: [x64] os: [freebsd] - '@esbuild/linux-arm64@0.21.5': - resolution: {integrity: sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==} - engines: {node: '>=12'} - cpu: [arm64] - os: [linux] - '@esbuild/linux-arm64@0.25.11': resolution: {integrity: sha512-Qr8AzcplUhGvdyUF08A1kHU3Vr2O88xxP0Tm8GcdVOUm25XYcMPp2YqSVHbLuXzYQMf9Bh/iKx7YPqECs6ffLA==} engines: {node: '>=18'} @@ -450,12 +393,6 @@ packages: cpu: [arm64] os: [linux] - '@esbuild/linux-arm@0.21.5': - resolution: {integrity: sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==} - engines: {node: '>=12'} - cpu: [arm] - os: [linux] - '@esbuild/linux-arm@0.25.11': resolution: {integrity: sha512-TBMv6B4kCfrGJ8cUPo7vd6NECZH/8hPpBHHlYI3qzoYFvWu2AdTvZNuU/7hsbKWqu/COU7NIK12dHAAqBLLXgw==} engines: {node: '>=18'} @@ -468,12 +405,6 @@ packages: cpu: [arm] os: [linux] - '@esbuild/linux-ia32@0.21.5': - resolution: {integrity: sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==} - engines: {node: '>=12'} - cpu: [ia32] - os: [linux] - '@esbuild/linux-ia32@0.25.11': resolution: {integrity: sha512-TmnJg8BMGPehs5JKrCLqyWTVAvielc615jbkOirATQvWWB1NMXY77oLMzsUjRLa0+ngecEmDGqt5jiDC6bfvOw==} engines: {node: '>=18'} @@ -486,12 +417,6 @@ packages: cpu: [ia32] os: [linux] - '@esbuild/linux-loong64@0.21.5': - resolution: {integrity: sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==} - engines: {node: '>=12'} - cpu: [loong64] - os: [linux] - '@esbuild/linux-loong64@0.25.11': resolution: {integrity: sha512-DIGXL2+gvDaXlaq8xruNXUJdT5tF+SBbJQKbWy/0J7OhU8gOHOzKmGIlfTTl6nHaCOoipxQbuJi7O++ldrxgMw==} engines: {node: '>=18'} @@ -504,12 +429,6 @@ packages: cpu: [loong64] os: [linux] - '@esbuild/linux-mips64el@0.21.5': - resolution: {integrity: sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==} - engines: {node: '>=12'} - cpu: [mips64el] - os: [linux] - '@esbuild/linux-mips64el@0.25.11': resolution: {integrity: sha512-Osx1nALUJu4pU43o9OyjSCXokFkFbyzjXb6VhGIJZQ5JZi8ylCQ9/LFagolPsHtgw6himDSyb5ETSfmp4rpiKQ==} engines: {node: '>=18'} @@ -522,12 +441,6 @@ packages: cpu: [mips64el] os: [linux] - '@esbuild/linux-ppc64@0.21.5': - resolution: {integrity: sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==} - engines: {node: '>=12'} - cpu: [ppc64] - os: [linux] - '@esbuild/linux-ppc64@0.25.11': resolution: {integrity: sha512-nbLFgsQQEsBa8XSgSTSlrnBSrpoWh7ioFDUmwo158gIm5NNP+17IYmNWzaIzWmgCxq56vfr34xGkOcZ7jX6CPw==} engines: {node: '>=18'} @@ -540,12 +453,6 @@ packages: cpu: [ppc64] os: [linux] - '@esbuild/linux-riscv64@0.21.5': - resolution: {integrity: sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==} - engines: {node: '>=12'} - cpu: [riscv64] - os: [linux] - '@esbuild/linux-riscv64@0.25.11': resolution: {integrity: sha512-HfyAmqZi9uBAbgKYP1yGuI7tSREXwIb438q0nqvlpxAOs3XnZ8RsisRfmVsgV486NdjD7Mw2UrFSw51lzUk1ww==} engines: {node: '>=18'} @@ -558,12 +465,6 @@ packages: cpu: [riscv64] os: [linux] - '@esbuild/linux-s390x@0.21.5': - resolution: {integrity: sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==} - engines: {node: '>=12'} - cpu: [s390x] - os: [linux] - '@esbuild/linux-s390x@0.25.11': resolution: {integrity: sha512-HjLqVgSSYnVXRisyfmzsH6mXqyvj0SA7pG5g+9W7ESgwA70AXYNpfKBqh1KbTxmQVaYxpzA/SvlB9oclGPbApw==} engines: {node: '>=18'} @@ -576,12 +477,6 @@ packages: cpu: [s390x] os: [linux] - '@esbuild/linux-x64@0.21.5': - resolution: {integrity: sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==} - engines: {node: '>=12'} - cpu: [x64] - os: [linux] - '@esbuild/linux-x64@0.25.11': resolution: {integrity: sha512-HSFAT4+WYjIhrHxKBwGmOOSpphjYkcswF449j6EjsjbinTZbp8PJtjsVK1XFJStdzXdy/jaddAep2FGY+wyFAQ==} engines: {node: '>=18'} @@ -606,12 +501,6 @@ packages: cpu: [arm64] os: [netbsd] - '@esbuild/netbsd-x64@0.21.5': - resolution: {integrity: sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==} - engines: {node: '>=12'} - cpu: [x64] - os: [netbsd] - '@esbuild/netbsd-x64@0.25.11': resolution: {integrity: sha512-u7tKA+qbzBydyj0vgpu+5h5AeudxOAGncb8N6C9Kh1N4n7wU1Xw1JDApsRjpShRpXRQlJLb9wY28ELpwdPcZ7A==} engines: {node: '>=18'} @@ -636,12 +525,6 @@ packages: cpu: [arm64] os: [openbsd] - '@esbuild/openbsd-x64@0.21.5': - resolution: {integrity: sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==} - engines: {node: '>=12'} - cpu: [x64] - os: [openbsd] - '@esbuild/openbsd-x64@0.25.11': resolution: {integrity: sha512-CN+7c++kkbrckTOz5hrehxWN7uIhFFlmS/hqziSFVWpAzpWrQoAG4chH+nN3Be+Kzv/uuo7zhX716x3Sn2Jduw==} engines: {node: '>=18'} @@ -666,12 +549,6 @@ packages: cpu: [arm64] os: [openharmony] - '@esbuild/sunos-x64@0.21.5': - resolution: {integrity: sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==} - engines: {node: '>=12'} - cpu: [x64] - os: [sunos] - '@esbuild/sunos-x64@0.25.11': resolution: {integrity: sha512-nq2xdYaWxyg9DcIyXkZhcYulC6pQ2FuCgem3LI92IwMgIZ69KHeY8T4Y88pcwoLIjbed8n36CyKoYRDygNSGhA==} engines: {node: '>=18'} @@ -684,12 +561,6 @@ packages: cpu: [x64] os: [sunos] - '@esbuild/win32-arm64@0.21.5': - resolution: {integrity: sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==} - engines: {node: '>=12'} - cpu: [arm64] - os: [win32] - '@esbuild/win32-arm64@0.25.11': resolution: {integrity: sha512-3XxECOWJq1qMZ3MN8srCJ/QfoLpL+VaxD/WfNRm1O3B4+AZ/BnLVgFbUV3eiRYDMXetciH16dwPbbHqwe1uU0Q==} engines: {node: '>=18'} @@ -702,12 +573,6 @@ packages: cpu: [arm64] os: [win32] - '@esbuild/win32-ia32@0.21.5': - resolution: {integrity: sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==} - engines: {node: '>=12'} - cpu: [ia32] - os: [win32] - '@esbuild/win32-ia32@0.25.11': resolution: {integrity: sha512-3ukss6gb9XZ8TlRyJlgLn17ecsK4NSQTmdIXRASVsiS2sQ6zPPZklNJT5GR5tE/MUarymmy8kCEf5xPCNCqVOA==} engines: {node: '>=18'} @@ -720,12 +585,6 @@ packages: cpu: [ia32] os: [win32] - '@esbuild/win32-x64@0.21.5': - resolution: {integrity: sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==} - engines: {node: '>=12'} - cpu: [x64] - os: [win32] - '@esbuild/win32-x64@0.25.11': resolution: {integrity: sha512-D7Hpz6A2L4hzsRpPaCYkQnGOotdUpDzSGRIv9I+1ITdHROSFUWW95ZPZWQmGka1Fg7W3zFJowyn9WGwMJ0+KPA==} engines: {node: '>=18'} @@ -810,14 +669,6 @@ packages: resolution: {integrity: sha512-WMz71T1JS624nWj2n2fnYAuPovhv7EUhk69R6i9dsVyzxt5eM3bjwvgk9L+APE1TRscGysAVMANkB0jh0LQZrQ==} engines: {node: 20 || >=22} - '@isaacs/cliui@8.0.2': - resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} - engines: {node: '>=12'} - - '@istanbuljs/schema@0.1.3': - resolution: {integrity: sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==} - engines: {node: '>=8'} - '@jridgewell/gen-mapping@0.3.5': resolution: {integrity: sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==} engines: {node: '>=6.0.0'} @@ -839,6 +690,9 @@ packages: '@jridgewell/sourcemap-codec@1.5.0': resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==} + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + '@jridgewell/trace-mapping@0.3.31': resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} @@ -860,10 +714,6 @@ packages: resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} engines: {node: '>= 8'} - '@pkgjs/parseargs@0.11.0': - resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} - engines: {node: '>=14'} - '@playwright/test@1.60.0': resolution: {integrity: sha512-O71yZIbAh/PxDMNGns37GHBIfrVkEVyn+AXyIa5dOTfb4/xNvRWV+Vv/NMbNCtODB/pO7vLlF2OTmMVLhmr7Ag==} engines: {node: '>=18'} @@ -1042,6 +892,9 @@ packages: cpu: [x64] os: [win32] + '@standard-schema/spec@1.1.0': + resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + '@stylistic/eslint-plugin-js@1.8.0': resolution: {integrity: sha512-jdvnzt+pZPg8TfclZlTZPiUbbima93ylvQ+wNgHLNmup3obY6heQvgewSu9i2CfS61BnRByv+F9fxQLPoNeHag==} engines: {node: ^16.0.0 || >=18.0.0} @@ -1087,6 +940,12 @@ packages: '@types/aria-query@5.0.4': resolution: {integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==} + '@types/chai@5.2.3': + resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + + '@types/deep-eql@4.0.2': + resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + '@types/eslint@8.56.12': resolution: {integrity: sha512-03ruubjWyOHlmljCVoxSuNDdmfZDzsrrz0P2LeJsOXr+ZwFQ+0yQIwNCwt/GYhV7Z31fgtXJTAEs+FYlEL851g==} @@ -1183,43 +1042,43 @@ packages: resolution: {integrity: sha512-q+SL+b+05Ud6LbEE35qe4A99P+htKTKVbyiNEe45eCbJFyh/HVK9QXwlrbz+Q4L8SOW4roxSVwXYj4DMBT7Ieg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@vitest/coverage-v8@2.1.9': - resolution: {integrity: sha512-Z2cOr0ksM00MpEfyVE8KXIYPEcBFxdbLSs56L8PO0QQMxt/6bDj45uQfxoc96v05KW3clk7vvgP0qfDit9DmfQ==} + '@vitest/coverage-v8@4.1.7': + resolution: {integrity: sha512-qsYPeXc5Q9dFLd1i8Ap+Bx8sQgcp+rFVQo4R0dDsWNBzl26ldVF1qOO+RL24K7FDrR6pA+50XedRLSoSG24bVQ==} peerDependencies: - '@vitest/browser': 2.1.9 - vitest: 2.1.9 + '@vitest/browser': 4.1.7 + vitest: 4.1.7 peerDependenciesMeta: '@vitest/browser': optional: true - '@vitest/expect@2.1.9': - resolution: {integrity: sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==} + '@vitest/expect@4.1.7': + resolution: {integrity: sha512-1R+tw0ortHEbZDGMymm+pN7/AFQ/RkFFdtd7EN+VBpynKmLbP8A3rpEXdshBJ7+8hQ9zBJh/i1s0yKNtxAnU7w==} - '@vitest/mocker@2.1.9': - resolution: {integrity: sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg==} + '@vitest/mocker@4.1.7': + resolution: {integrity: sha512-vY7nuamKgfvpA1Koa3oYIw/k7D6kZnpGyNMZW8loow2bsBYla1TFdqTaXncWdRn4pgwNs+90RhnXhJScDwQeJA==} peerDependencies: msw: ^2.4.9 - vite: ^5.0.0 + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 peerDependenciesMeta: msw: optional: true vite: optional: true - '@vitest/pretty-format@2.1.9': - resolution: {integrity: sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==} + '@vitest/pretty-format@4.1.7': + resolution: {integrity: sha512-umgCarTOYQWIaDMvGDRZij+6b9oVeLIyJzfN+AS88e0ZOU3QTgNNSTtjQOpcvWr3np1N0j4WgZj+sb3oYBDscw==} - '@vitest/runner@2.1.9': - resolution: {integrity: sha512-ZXSSqTFIrzduD63btIfEyOmNcBmQvgOVsPNPe0jYtESiXkhd8u2erDLnMxmGrDCwHCCHE7hxwRDCT3pt0esT4g==} + '@vitest/runner@4.1.7': + resolution: {integrity: sha512-BapjmAQ2aI78WdMEfeUWivnfVzB+VPGwWRQcJE0OUq7qEeEcBsCSf+0T5iREBNE5nBb4wA5Ya0W6IA+sghdEFw==} - '@vitest/snapshot@2.1.9': - resolution: {integrity: sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ==} + '@vitest/snapshot@4.1.7': + resolution: {integrity: sha512-ZacLzja+TmJeZ1h14xW2FB/WpeimUD3haBXQPyJqxvo8jQTmfeA8zv58mtjN2C7EHXZDYVcVYdYmAxjkWVvKCw==} - '@vitest/spy@2.1.9': - resolution: {integrity: sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ==} + '@vitest/spy@4.1.7': + resolution: {integrity: sha512-kbkI5LMWakyuTIvs6fUJ5qdIVb1XVKsYJAT4OJ938cHMROYMSfmoQdZy0aaAnjbbc8F61vkoTqz/Az+/HiIu5Q==} - '@vitest/utils@2.1.9': - resolution: {integrity: sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==} + '@vitest/utils@4.1.7': + resolution: {integrity: sha512-T532WBu791cBxJlCl6SO+J14l81DQx6uQHm1bQbmCDY7nqlEIgkza/UFnSBNaUtSf41unldDFjdOBYEQC4b5Hw==} acorn-jsx@5.3.2: resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} @@ -1246,18 +1105,6 @@ packages: resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} engines: {node: '>=8'} - ansi-regex@6.0.1: - resolution: {integrity: sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==} - engines: {node: '>=12'} - - ansi-styles@4.3.0: - resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} - engines: {node: '>=8'} - - ansi-styles@6.2.1: - resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==} - engines: {node: '>=12'} - ansis@4.1.0: resolution: {integrity: sha512-BGcItUBWSMRgOCe+SVZJ+S7yTRG0eGt9cXAHev72yuGcY23hnLA7Bky5L/xLyPINoSN95geovfBkqoTlNZYa7w==} engines: {node: '>=14'} @@ -1284,6 +1131,9 @@ packages: resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} engines: {node: '>=12'} + ast-v8-to-istanbul@1.0.0: + resolution: {integrity: sha512-1fSfIwuDICFA4LKkCzRPO7F0hzFf0B7+Xqrl27ynQaa+Rh0e1Es0v6kWHPott3lU10AyAr7oKHa65OppjLn3Rg==} + asynckit@0.4.0: resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} @@ -1324,21 +1174,13 @@ packages: resolution: {integrity: sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==} engines: {node: '>=18'} - cac@6.7.14: - resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} - engines: {node: '>=8'} - - chai@5.3.3: - resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==} + chai@6.2.2: + resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} engines: {node: '>=18'} chardet@2.1.0: resolution: {integrity: sha512-bNFETTG/pM5ryzQ9Ad0lJOTa6HWD/YsScAR3EnCPZRPlQh77JocYktSHOUHelyhm8IARL+o4c4F1bP5KVOjiRA==} - check-error@2.1.1: - resolution: {integrity: sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==} - engines: {node: '>= 16'} - chokidar@3.6.0: resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} engines: {node: '>= 8.10.0'} @@ -1351,13 +1193,6 @@ packages: resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} engines: {node: '>=6'} - color-convert@2.0.1: - resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} - engines: {node: '>=7.0.0'} - - color-name@1.1.4: - resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} - combined-stream@1.0.8: resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} engines: {node: '>= 0.8'} @@ -1368,6 +1203,9 @@ packages: commondir@1.0.1: resolution: {integrity: sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==} + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + cross-spawn@7.0.6: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} @@ -1400,10 +1238,6 @@ packages: decimal.js@10.4.3: resolution: {integrity: sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==} - deep-eql@5.0.2: - resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} - engines: {node: '>=6'} - deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} @@ -1453,15 +1287,6 @@ packages: peerDependencies: typescript: '>=5.0.4 <5.8' - eastasianwidth@0.2.0: - resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} - - emoji-regex@8.0.0: - resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} - - emoji-regex@9.2.2: - resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} - enhanced-resolve@5.22.1: resolution: {integrity: sha512-6QEuw3zoX1SJQc7b87aBXke/no+mG2bTBgw29gWMQonLmpEkWoCAVkl+M49e48AZlWzxiDzDZzYdp6kobcyLww==} engines: {node: '>=10.13.0'} @@ -1477,13 +1302,8 @@ packages: error-stack-parser-es@1.0.5: resolution: {integrity: sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA==} - es-module-lexer@1.7.0: - resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} - - esbuild@0.21.5: - resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==} - engines: {node: '>=12'} - hasBin: true + es-module-lexer@2.1.0: + resolution: {integrity: sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==} esbuild@0.25.11: resolution: {integrity: sha512-KohQwyzrKTQmhXDW1PjCv3Tyspn9n5GcY2RTDqeORIdIJY8yKIF7sTSopFmn/wpMPW4rdPXI0UE5LJLuq3bx0Q==} @@ -1618,8 +1438,8 @@ packages: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} engines: {node: '>=0.10.0'} - expect-type@1.2.2: - resolution: {integrity: sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA==} + expect-type@1.3.0: + resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} engines: {node: '>=12.0.0'} extendable-error@0.1.7: @@ -1673,10 +1493,6 @@ packages: flatted@3.2.9: resolution: {integrity: sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==} - foreground-child@3.3.0: - resolution: {integrity: sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==} - engines: {node: '>=14'} - form-data@4.0.0: resolution: {integrity: sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==} engines: {node: '>= 6'} @@ -1713,11 +1529,6 @@ packages: resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} engines: {node: '>=10.13.0'} - glob@10.5.0: - resolution: {integrity: sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==} - deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me - hasBin: true - globals@15.15.0: resolution: {integrity: sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==} engines: {node: '>=18'} @@ -1806,10 +1617,6 @@ packages: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} - is-fullwidth-code-point@3.0.0: - resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} - engines: {node: '>=8'} - is-glob@4.0.3: resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} engines: {node: '>=0.10.0'} @@ -1858,16 +1665,12 @@ packages: resolution: {integrity: sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==} engines: {node: '>=10'} - istanbul-lib-source-maps@5.0.6: - resolution: {integrity: sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==} - engines: {node: '>=10'} - - istanbul-reports@3.1.7: - resolution: {integrity: sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==} + istanbul-reports@3.2.0: + resolution: {integrity: sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==} engines: {node: '>=8'} - jackspeak@3.4.3: - resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} + js-tokens@10.0.0: + resolution: {integrity: sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==} js-yaml@3.14.2: resolution: {integrity: sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==} @@ -1988,17 +1791,14 @@ packages: lodash.startcase@4.4.0: resolution: {integrity: sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg==} - loupe@3.2.1: - resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==} - - lru-cache@10.4.3: - resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} - magic-string@0.30.17: resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==} - magicast@0.3.5: - resolution: {integrity: sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==} + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + magicast@0.5.3: + resolution: {integrity: sha512-pVKE4UdSQ7DvHzivsCIFx2BJn1mHG6KsyrFcaxFx6tONdneEuThrDx0Cj3AMg58KyN4pzYT+LHOotxDQDjNvkw==} make-dir@4.0.0: resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} @@ -2028,10 +1828,6 @@ packages: resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} engines: {node: '>=16 || 14 >=14.17'} - minipass@7.1.2: - resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} - engines: {node: '>=16 || 14 >=14.17'} - mri@1.2.0: resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} engines: {node: '>=4'} @@ -2067,6 +1863,9 @@ packages: nwsapi@2.2.12: resolution: {integrity: sha512-qXDmcVlZV4XRtKFzddidpfVP4oMSGhga+xdMc25mv8kaLUHtgzCDhUxkrN8exkGdTlLNaXj7CV3GtON7zuGZ+w==} + obug@2.1.1: + resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==} + ohash@2.0.11: resolution: {integrity: sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==} @@ -2109,9 +1908,6 @@ packages: resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} engines: {node: '>=6'} - package-json-from-dist@1.0.0: - resolution: {integrity: sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw==} - package-manager-detector@0.2.11: resolution: {integrity: sha512-BEnLolu+yuz22S56CU1SUKq3XC3PkwD5wv4ikR4MfGvnRVcmzXR9DwSlW2fEamyTPyXHomBJRzgapeuBvRNzJQ==} @@ -2129,24 +1925,13 @@ packages: path-parse@1.0.7: resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} - path-scurry@1.11.1: - resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} - engines: {node: '>=16 || 14 >=14.18'} - path-type@4.0.0: resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} engines: {node: '>=8'} - pathe@1.1.2: - resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==} - pathe@2.0.3: resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} - pathval@2.0.0: - resolution: {integrity: sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==} - engines: {node: '>= 14.16'} - perfect-debounce@2.0.0: resolution: {integrity: sha512-fkEH/OBiKrqqI/yIgjR92lMfs2K8105zt/VT6+7eTjNwisrsh47CeIED9z58zI7DfKdH3uHAn25ziRZn3kgAow==} @@ -2368,25 +2153,13 @@ packages: stackback@0.0.2: resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} - std-env@3.9.0: - resolution: {integrity: sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw==} - - string-width@4.2.3: - resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} - engines: {node: '>=8'} - - string-width@5.1.2: - resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} - engines: {node: '>=12'} + std-env@4.1.0: + resolution: {integrity: sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==} strip-ansi@6.0.1: resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} engines: {node: '>=8'} - strip-ansi@7.1.0: - resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==} - engines: {node: '>=12'} - strip-bom@3.0.0: resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} engines: {node: '>=4'} @@ -2424,30 +2197,19 @@ packages: engines: {node: '>=10'} hasBin: true - test-exclude@7.0.1: - resolution: {integrity: sha512-pFYqmTw68LXVjeWJMST4+borgQP2AyMNbg1BpZh9LbyhUeNkeaPF9gzfPGUAnSMV3qPYdWUwDIjjCLiSDOl7vg==} - engines: {node: '>=18'} - tinybench@2.9.0: resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} - tinyexec@0.3.2: - resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} + tinyexec@1.1.2: + resolution: {integrity: sha512-dAqSqE/RabpBKI8+h26GfLq6Vb3JVXs30XYQjdMjaj/c2tS8IYYMbIzP599KtRj7c57/wYApb3QjgRgXmrCukA==} + engines: {node: '>=18'} tinyglobby@0.2.15: resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} engines: {node: '>=12.0.0'} - tinypool@1.1.1: - resolution: {integrity: sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==} - engines: {node: ^18.0.0 || >=20.0.0} - - tinyrainbow@1.2.0: - resolution: {integrity: sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==} - engines: {node: '>=14.0.0'} - - tinyspy@3.0.2: - resolution: {integrity: sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==} + tinyrainbow@3.1.0: + resolution: {integrity: sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==} engines: {node: '>=14.0.0'} tldts-core@6.1.64: @@ -2457,10 +2219,6 @@ packages: resolution: {integrity: sha512-ph4AE5BXWIOsSy9stpoeo7bYe/Cy7VfpciIH4RhVZUPItCJmhqWCN0EVzxd8BOHiyNb42vuJc6NWTjJkg91Tuw==} hasBin: true - to-fast-properties@2.0.0: - resolution: {integrity: sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==} - engines: {node: '>=4'} - to-regex-range@5.0.1: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} @@ -2555,11 +2313,6 @@ packages: peerDependencies: vite: ^2.6.0 || ^3.0.0 || ^4.0.0 || ^5.0.0-0 || ^6.0.0-0 || ^7.0.0-0 - vite-node@2.1.9: - resolution: {integrity: sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==} - engines: {node: ^18.0.0 || >=20.0.0} - hasBin: true - vite-plugin-devtools-json@1.0.0: resolution: {integrity: sha512-MobvwqX76Vqt/O4AbnNMNWoXWGrKUqZbphCUle/J2KXH82yKQiunOeKnz/nqEPosPsoWWPP9FtNuPBSYpiiwkw==} peerDependencies: @@ -2575,37 +2328,6 @@ packages: '@nuxt/kit': optional: true - vite@5.4.20: - resolution: {integrity: sha512-j3lYzGC3P+B5Yfy/pfKNgVEg4+UtcIJcVRt2cDjIOmhLourAqPqf8P7acgxeiSgUB7E3p2P8/3gNIgDLpwzs4g==} - engines: {node: ^18.0.0 || >=20.0.0} - hasBin: true - peerDependencies: - '@types/node': ^18.0.0 || >=20.0.0 - less: '*' - lightningcss: ^1.21.0 - sass: '*' - sass-embedded: '*' - stylus: '*' - sugarss: '*' - terser: ^5.4.0 - peerDependenciesMeta: - '@types/node': - optional: true - less: - optional: true - lightningcss: - optional: true - sass: - optional: true - sass-embedded: - optional: true - stylus: - optional: true - sugarss: - optional: true - terser: - optional: true - vite@7.3.2: resolution: {integrity: sha512-Bby3NOsna2jsjfLVOHKes8sGwgl4TT0E6vvpYgnAYDIF/tie7MRaFthmKuHx1NSXjiTueXH3do80FMQgvEktRg==} engines: {node: ^20.19.0 || >=22.12.0} @@ -2654,23 +2376,39 @@ packages: vite: optional: true - vitest@2.1.9: - resolution: {integrity: sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q==} - engines: {node: ^18.0.0 || >=20.0.0} + vitest@4.1.7: + resolution: {integrity: sha512-flYyaFd2CgoCoU+0UKt3pxksgC+S02iTDN0n3LtqaMeXsI9SBcdNujc2k0DeFLzUn/0k538yNjOSdwgCqcrwJA==} + engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} hasBin: true peerDependencies: '@edge-runtime/vm': '*' - '@types/node': ^18.0.0 || >=20.0.0 - '@vitest/browser': 2.1.9 - '@vitest/ui': 2.1.9 + '@opentelemetry/api': ^1.9.0 + '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 + '@vitest/browser-playwright': 4.1.7 + '@vitest/browser-preview': 4.1.7 + '@vitest/browser-webdriverio': 4.1.7 + '@vitest/coverage-istanbul': 4.1.7 + '@vitest/coverage-v8': 4.1.7 + '@vitest/ui': 4.1.7 happy-dom: '*' jsdom: '*' + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 peerDependenciesMeta: '@edge-runtime/vm': optional: true + '@opentelemetry/api': + optional: true '@types/node': optional: true - '@vitest/browser': + '@vitest/browser-playwright': + optional: true + '@vitest/browser-preview': + optional: true + '@vitest/browser-webdriverio': + optional: true + '@vitest/coverage-istanbul': + optional: true + '@vitest/coverage-v8': optional: true '@vitest/ui': optional: true @@ -2719,14 +2457,6 @@ packages: engines: {node: '>=8'} hasBin: true - wrap-ansi@7.0.0: - resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} - engines: {node: '>=10'} - - wrap-ansi@8.1.0: - resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} - engines: {node: '>=12'} - ws@8.18.0: resolution: {integrity: sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==} engines: {node: '>=10.0.0'} @@ -2765,28 +2495,22 @@ snapshots: '@aashutoshrathi/word-wrap@1.2.6': {} - '@ampproject/remapping@2.3.0': - dependencies: - '@jridgewell/gen-mapping': 0.3.5 - '@jridgewell/trace-mapping': 0.3.31 - - '@babel/helper-string-parser@7.24.8': {} + '@babel/helper-string-parser@7.27.1': {} - '@babel/helper-validator-identifier@7.24.7': {} + '@babel/helper-validator-identifier@7.28.5': {} - '@babel/parser@7.25.4': + '@babel/parser@7.29.3': dependencies: - '@babel/types': 7.25.4 + '@babel/types': 7.29.0 '@babel/runtime@7.28.4': {} - '@babel/types@7.25.4': + '@babel/types@7.29.0': dependencies: - '@babel/helper-string-parser': 7.24.8 - '@babel/helper-validator-identifier': 7.24.7 - to-fast-properties: 2.0.0 + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 - '@bcoe/v8-coverage@0.2.3': {} + '@bcoe/v8-coverage@1.0.2': {} '@changesets/apply-release-plan@7.0.14': dependencies: @@ -2939,153 +2663,102 @@ snapshots: human-id: 4.1.1 prettier: 2.8.8 - '@esbuild/aix-ppc64@0.21.5': - optional: true - '@esbuild/aix-ppc64@0.25.11': optional: true '@esbuild/aix-ppc64@0.27.7': optional: true - '@esbuild/android-arm64@0.21.5': - optional: true - '@esbuild/android-arm64@0.25.11': optional: true '@esbuild/android-arm64@0.27.7': optional: true - '@esbuild/android-arm@0.21.5': - optional: true - '@esbuild/android-arm@0.25.11': optional: true '@esbuild/android-arm@0.27.7': optional: true - '@esbuild/android-x64@0.21.5': - optional: true - '@esbuild/android-x64@0.25.11': optional: true '@esbuild/android-x64@0.27.7': optional: true - '@esbuild/darwin-arm64@0.21.5': - optional: true - '@esbuild/darwin-arm64@0.25.11': optional: true '@esbuild/darwin-arm64@0.27.7': optional: true - '@esbuild/darwin-x64@0.21.5': - optional: true - '@esbuild/darwin-x64@0.25.11': optional: true '@esbuild/darwin-x64@0.27.7': optional: true - '@esbuild/freebsd-arm64@0.21.5': - optional: true - '@esbuild/freebsd-arm64@0.25.11': optional: true '@esbuild/freebsd-arm64@0.27.7': optional: true - '@esbuild/freebsd-x64@0.21.5': - optional: true - '@esbuild/freebsd-x64@0.25.11': optional: true '@esbuild/freebsd-x64@0.27.7': optional: true - '@esbuild/linux-arm64@0.21.5': - optional: true - '@esbuild/linux-arm64@0.25.11': optional: true '@esbuild/linux-arm64@0.27.7': optional: true - '@esbuild/linux-arm@0.21.5': - optional: true - '@esbuild/linux-arm@0.25.11': optional: true '@esbuild/linux-arm@0.27.7': optional: true - '@esbuild/linux-ia32@0.21.5': - optional: true - '@esbuild/linux-ia32@0.25.11': optional: true '@esbuild/linux-ia32@0.27.7': optional: true - '@esbuild/linux-loong64@0.21.5': - optional: true - '@esbuild/linux-loong64@0.25.11': optional: true '@esbuild/linux-loong64@0.27.7': optional: true - '@esbuild/linux-mips64el@0.21.5': - optional: true - '@esbuild/linux-mips64el@0.25.11': optional: true '@esbuild/linux-mips64el@0.27.7': optional: true - '@esbuild/linux-ppc64@0.21.5': - optional: true - '@esbuild/linux-ppc64@0.25.11': optional: true '@esbuild/linux-ppc64@0.27.7': optional: true - '@esbuild/linux-riscv64@0.21.5': - optional: true - '@esbuild/linux-riscv64@0.25.11': optional: true '@esbuild/linux-riscv64@0.27.7': optional: true - '@esbuild/linux-s390x@0.21.5': - optional: true - '@esbuild/linux-s390x@0.25.11': optional: true '@esbuild/linux-s390x@0.27.7': optional: true - '@esbuild/linux-x64@0.21.5': - optional: true - '@esbuild/linux-x64@0.25.11': optional: true @@ -3098,9 +2771,6 @@ snapshots: '@esbuild/netbsd-arm64@0.27.7': optional: true - '@esbuild/netbsd-x64@0.21.5': - optional: true - '@esbuild/netbsd-x64@0.25.11': optional: true @@ -3113,9 +2783,6 @@ snapshots: '@esbuild/openbsd-arm64@0.27.7': optional: true - '@esbuild/openbsd-x64@0.21.5': - optional: true - '@esbuild/openbsd-x64@0.25.11': optional: true @@ -3128,36 +2795,24 @@ snapshots: '@esbuild/openharmony-arm64@0.27.7': optional: true - '@esbuild/sunos-x64@0.21.5': - optional: true - '@esbuild/sunos-x64@0.25.11': optional: true '@esbuild/sunos-x64@0.27.7': optional: true - '@esbuild/win32-arm64@0.21.5': - optional: true - '@esbuild/win32-arm64@0.25.11': optional: true '@esbuild/win32-arm64@0.27.7': optional: true - '@esbuild/win32-ia32@0.21.5': - optional: true - '@esbuild/win32-ia32@0.25.11': optional: true '@esbuild/win32-ia32@0.27.7': optional: true - '@esbuild/win32-x64@0.21.5': - optional: true - '@esbuild/win32-x64@0.25.11': optional: true @@ -3222,17 +2877,6 @@ snapshots: dependencies: '@isaacs/balanced-match': 4.0.1 - '@isaacs/cliui@8.0.2': - dependencies: - string-width: 5.1.2 - string-width-cjs: string-width@4.2.3 - strip-ansi: 7.1.0 - strip-ansi-cjs: strip-ansi@6.0.1 - wrap-ansi: 8.1.0 - wrap-ansi-cjs: wrap-ansi@7.0.0 - - '@istanbuljs/schema@0.1.3': {} - '@jridgewell/gen-mapping@0.3.5': dependencies: '@jridgewell/set-array': 1.2.1 @@ -3255,6 +2899,8 @@ snapshots: '@jridgewell/sourcemap-codec@1.5.0': {} + '@jridgewell/sourcemap-codec@1.5.5': {} + '@jridgewell/trace-mapping@0.3.31': dependencies: '@jridgewell/resolve-uri': 3.1.1 @@ -3288,9 +2934,6 @@ snapshots: '@nodelib/fs.scandir': 2.1.5 fastq: 1.16.0 - '@pkgjs/parseargs@0.11.0': - optional: true - '@playwright/test@1.60.0': dependencies: playwright: 1.60.0 @@ -3414,6 +3057,8 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.60.1': optional: true + '@standard-schema/spec@1.1.0': {} + '@stylistic/eslint-plugin-js@1.8.0(eslint@10.0.0)': dependencies: '@types/eslint': 8.56.12 @@ -3469,6 +3114,13 @@ snapshots: '@types/aria-query@5.0.4': {} + '@types/chai@5.2.3': + dependencies: + '@types/deep-eql': 4.0.2 + assertion-error: 2.0.1 + + '@types/deep-eql@4.0.2': {} + '@types/eslint@8.56.12': dependencies: '@types/estree': 1.0.9 @@ -3592,63 +3244,60 @@ snapshots: '@typescript-eslint/types': 8.56.0 eslint-visitor-keys: 5.0.0 - '@vitest/coverage-v8@2.1.9(vitest@2.1.9(@types/node@20.19.17)(jsdom@25.0.1)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0))': + '@vitest/coverage-v8@4.1.7(vitest@4.1.7)': dependencies: - '@ampproject/remapping': 2.3.0 - '@bcoe/v8-coverage': 0.2.3 - debug: 4.4.3 + '@bcoe/v8-coverage': 1.0.2 + '@vitest/utils': 4.1.7 + ast-v8-to-istanbul: 1.0.0 istanbul-lib-coverage: 3.2.2 istanbul-lib-report: 3.0.1 - istanbul-lib-source-maps: 5.0.6 - istanbul-reports: 3.1.7 - magic-string: 0.30.17 - magicast: 0.3.5 - std-env: 3.9.0 - test-exclude: 7.0.1 - tinyrainbow: 1.2.0 - vitest: 2.1.9(@types/node@20.19.17)(jsdom@25.0.1)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0) - transitivePeerDependencies: - - supports-color + istanbul-reports: 3.2.0 + magicast: 0.5.3 + obug: 2.1.1 + std-env: 4.1.0 + tinyrainbow: 3.1.0 + vitest: 4.1.7(@types/node@20.19.17)(@vitest/coverage-v8@4.1.7)(jsdom@25.0.1)(vite@7.3.2(@types/node@20.19.17)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0)) - '@vitest/expect@2.1.9': + '@vitest/expect@4.1.7': dependencies: - '@vitest/spy': 2.1.9 - '@vitest/utils': 2.1.9 - chai: 5.3.3 - tinyrainbow: 1.2.0 + '@standard-schema/spec': 1.1.0 + '@types/chai': 5.2.3 + '@vitest/spy': 4.1.7 + '@vitest/utils': 4.1.7 + chai: 6.2.2 + tinyrainbow: 3.1.0 - '@vitest/mocker@2.1.9(vite@5.4.20(@types/node@20.19.17)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0))': + '@vitest/mocker@4.1.7(vite@7.3.2(@types/node@20.19.17)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0))': dependencies: - '@vitest/spy': 2.1.9 + '@vitest/spy': 4.1.7 estree-walker: 3.0.3 - magic-string: 0.30.17 + magic-string: 0.30.21 optionalDependencies: - vite: 5.4.20(@types/node@20.19.17)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0) + vite: 7.3.2(@types/node@20.19.17)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0) - '@vitest/pretty-format@2.1.9': + '@vitest/pretty-format@4.1.7': dependencies: - tinyrainbow: 1.2.0 + tinyrainbow: 3.1.0 - '@vitest/runner@2.1.9': + '@vitest/runner@4.1.7': dependencies: - '@vitest/utils': 2.1.9 - pathe: 1.1.2 + '@vitest/utils': 4.1.7 + pathe: 2.0.3 - '@vitest/snapshot@2.1.9': + '@vitest/snapshot@4.1.7': dependencies: - '@vitest/pretty-format': 2.1.9 - magic-string: 0.30.17 - pathe: 1.1.2 + '@vitest/pretty-format': 4.1.7 + '@vitest/utils': 4.1.7 + magic-string: 0.30.21 + pathe: 2.0.3 - '@vitest/spy@2.1.9': - dependencies: - tinyspy: 3.0.2 + '@vitest/spy@4.1.7': {} - '@vitest/utils@2.1.9': + '@vitest/utils@4.1.7': dependencies: - '@vitest/pretty-format': 2.1.9 - loupe: 3.2.1 - tinyrainbow: 1.2.0 + '@vitest/pretty-format': 4.1.7 + convert-source-map: 2.0.0 + tinyrainbow: 3.1.0 acorn-jsx@5.3.2(acorn@8.16.0): dependencies: @@ -3673,14 +3322,6 @@ snapshots: ansi-regex@5.0.1: {} - ansi-regex@6.0.1: {} - - ansi-styles@4.3.0: - dependencies: - color-convert: 2.0.1 - - ansi-styles@6.2.1: {} - ansis@4.1.0: {} anymatch@3.1.3: @@ -3701,6 +3342,12 @@ snapshots: assertion-error@2.0.1: {} + ast-v8-to-istanbul@1.0.0: + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + estree-walker: 3.0.3 + js-tokens: 10.0.0 + asynckit@0.4.0: {} axobject-query@4.1.0: {} @@ -3732,20 +3379,10 @@ snapshots: dependencies: run-applescript: 7.0.0 - cac@6.7.14: {} - - chai@5.3.3: - dependencies: - assertion-error: 2.0.1 - check-error: 2.1.1 - deep-eql: 5.0.2 - loupe: 3.2.1 - pathval: 2.0.0 + chai@6.2.2: {} chardet@2.1.0: {} - check-error@2.1.1: {} - chokidar@3.6.0: dependencies: anymatch: 3.1.3 @@ -3763,12 +3400,6 @@ snapshots: clsx@2.1.1: {} - color-convert@2.0.1: - dependencies: - color-name: 1.1.4 - - color-name@1.1.4: {} - combined-stream@1.0.8: dependencies: delayed-stream: 1.0.0 @@ -3777,6 +3408,8 @@ snapshots: commondir@1.0.1: {} + convert-source-map@2.0.0: {} + cross-spawn@7.0.6: dependencies: path-key: 3.1.1 @@ -3802,8 +3435,6 @@ snapshots: decimal.js@10.4.3: {} - deep-eql@5.0.2: {} - deep-is@0.1.4: {} deepmerge@4.3.1: {} @@ -3844,12 +3475,6 @@ snapshots: ts-api-utils: 1.3.0(typescript@5.5.4) typescript: 5.5.4 - eastasianwidth@0.2.0: {} - - emoji-regex@8.0.0: {} - - emoji-regex@9.2.2: {} - enhanced-resolve@5.22.1: dependencies: graceful-fs: 4.2.11 @@ -3864,33 +3489,7 @@ snapshots: error-stack-parser-es@1.0.5: {} - es-module-lexer@1.7.0: {} - - esbuild@0.21.5: - optionalDependencies: - '@esbuild/aix-ppc64': 0.21.5 - '@esbuild/android-arm': 0.21.5 - '@esbuild/android-arm64': 0.21.5 - '@esbuild/android-x64': 0.21.5 - '@esbuild/darwin-arm64': 0.21.5 - '@esbuild/darwin-x64': 0.21.5 - '@esbuild/freebsd-arm64': 0.21.5 - '@esbuild/freebsd-x64': 0.21.5 - '@esbuild/linux-arm': 0.21.5 - '@esbuild/linux-arm64': 0.21.5 - '@esbuild/linux-ia32': 0.21.5 - '@esbuild/linux-loong64': 0.21.5 - '@esbuild/linux-mips64el': 0.21.5 - '@esbuild/linux-ppc64': 0.21.5 - '@esbuild/linux-riscv64': 0.21.5 - '@esbuild/linux-s390x': 0.21.5 - '@esbuild/linux-x64': 0.21.5 - '@esbuild/netbsd-x64': 0.21.5 - '@esbuild/openbsd-x64': 0.21.5 - '@esbuild/sunos-x64': 0.21.5 - '@esbuild/win32-arm64': 0.21.5 - '@esbuild/win32-ia32': 0.21.5 - '@esbuild/win32-x64': 0.21.5 + es-module-lexer@2.1.0: {} esbuild@0.25.11: optionalDependencies: @@ -4104,7 +3703,7 @@ snapshots: esutils@2.0.3: {} - expect-type@1.2.2: {} + expect-type@1.3.0: {} extendable-error@0.1.7: {} @@ -4155,11 +3754,6 @@ snapshots: flatted@3.2.9: {} - foreground-child@3.3.0: - dependencies: - cross-spawn: 7.0.6 - signal-exit: 4.1.0 - form-data@4.0.0: dependencies: asynckit: 0.4.0 @@ -4198,15 +3792,6 @@ snapshots: dependencies: is-glob: 4.0.3 - glob@10.5.0: - dependencies: - foreground-child: 3.3.0 - jackspeak: 3.4.3 - minimatch: 9.0.5 - minipass: 7.1.2 - package-json-from-dist: 1.0.0 - path-scurry: 1.11.1 - globals@15.15.0: {} globals@16.3.0: {} @@ -4284,8 +3869,6 @@ snapshots: is-extglob@2.1.1: {} - is-fullwidth-code-point@3.0.0: {} - is-glob@4.0.3: dependencies: is-extglob: 2.1.1 @@ -4328,24 +3911,12 @@ snapshots: make-dir: 4.0.0 supports-color: 7.2.0 - istanbul-lib-source-maps@5.0.6: - dependencies: - '@jridgewell/trace-mapping': 0.3.31 - debug: 4.4.3 - istanbul-lib-coverage: 3.2.2 - transitivePeerDependencies: - - supports-color - - istanbul-reports@3.1.7: + istanbul-reports@3.2.0: dependencies: html-escaper: 2.0.2 istanbul-lib-report: 3.0.1 - jackspeak@3.4.3: - dependencies: - '@isaacs/cliui': 8.0.2 - optionalDependencies: - '@pkgjs/parseargs': 0.11.0 + js-tokens@10.0.0: {} js-yaml@3.14.2: dependencies: @@ -4463,18 +4034,18 @@ snapshots: lodash.startcase@4.4.0: {} - loupe@3.2.1: {} - - lru-cache@10.4.3: {} - magic-string@0.30.17: dependencies: '@jridgewell/sourcemap-codec': 1.5.0 - magicast@0.3.5: + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + magicast@0.5.3: dependencies: - '@babel/parser': 7.25.4 - '@babel/types': 7.25.4 + '@babel/parser': 7.29.3 + '@babel/types': 7.29.0 source-map-js: 1.2.1 make-dir@4.0.0: @@ -4502,8 +4073,6 @@ snapshots: dependencies: brace-expansion: 2.0.2 - minipass@7.1.2: {} - mri@1.2.0: {} mrmime@2.0.0: {} @@ -4523,6 +4092,8 @@ snapshots: nwsapi@2.2.12: {} + obug@2.1.1: {} + ohash@2.0.11: {} open@10.2.0: @@ -4567,8 +4138,6 @@ snapshots: p-try@2.2.0: {} - package-json-from-dist@1.0.0: {} - package-manager-detector@0.2.11: dependencies: quansync: 0.2.11 @@ -4583,19 +4152,10 @@ snapshots: path-parse@1.0.7: {} - path-scurry@1.11.1: - dependencies: - lru-cache: 10.4.3 - minipass: 7.1.2 - path-type@4.0.0: {} - pathe@1.1.2: {} - pathe@2.0.3: {} - pathval@2.0.0: {} - perfect-debounce@2.0.0: {} picocolors@1.1.1: {} @@ -4798,28 +4358,12 @@ snapshots: stackback@0.0.2: {} - std-env@3.9.0: {} - - string-width@4.2.3: - dependencies: - emoji-regex: 8.0.0 - is-fullwidth-code-point: 3.0.0 - strip-ansi: 6.0.1 - - string-width@5.1.2: - dependencies: - eastasianwidth: 0.2.0 - emoji-regex: 9.2.2 - strip-ansi: 7.1.0 + std-env@4.1.0: {} strip-ansi@6.0.1: dependencies: ansi-regex: 5.0.1 - strip-ansi@7.1.0: - dependencies: - ansi-regex: 6.0.1 - strip-bom@3.0.0: {} supports-color@7.2.0: @@ -4852,26 +4396,16 @@ snapshots: commander: 2.20.3 source-map-support: 0.5.21 - test-exclude@7.0.1: - dependencies: - '@istanbuljs/schema': 0.1.3 - glob: 10.5.0 - minimatch: 9.0.5 - tinybench@2.9.0: {} - tinyexec@0.3.2: {} + tinyexec@1.1.2: {} tinyglobby@0.2.15: dependencies: fdir: 6.5.0(picomatch@4.0.4) picomatch: 4.0.4 - tinypool@1.1.1: {} - - tinyrainbow@1.2.0: {} - - tinyspy@3.0.2: {} + tinyrainbow@3.1.0: {} tldts-core@6.1.64: {} @@ -4879,8 +4413,6 @@ snapshots: dependencies: tldts-core: 6.1.64 - to-fast-properties@2.0.0: {} - to-regex-range@5.0.1: dependencies: is-number: 7.0.0 @@ -4962,24 +4494,6 @@ snapshots: dependencies: vite: 7.3.2(@types/node@24.5.2)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0) - vite-node@2.1.9(@types/node@20.19.17)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0): - dependencies: - cac: 6.7.14 - debug: 4.4.3 - es-module-lexer: 1.7.0 - pathe: 1.1.2 - vite: 5.4.20(@types/node@20.19.17)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0) - transitivePeerDependencies: - - '@types/node' - - less - - lightningcss - - sass - - sass-embedded - - stylus - - sugarss - - supports-color - - terser - vite-plugin-devtools-json@1.0.0(vite@7.3.2(@types/node@24.5.2)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0)): dependencies: uuid: 11.1.0 @@ -5000,11 +4514,14 @@ snapshots: transitivePeerDependencies: - supports-color - vite@5.4.20(@types/node@20.19.17)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0): + vite@7.3.2(@types/node@20.19.17)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0): dependencies: - esbuild: 0.21.5 + esbuild: 0.27.7 + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 postcss: 8.5.9 rollup: 4.60.1 + tinyglobby: 0.2.15 optionalDependencies: '@types/node': 20.19.17 fsevents: 2.3.3 @@ -5031,41 +4548,34 @@ snapshots: optionalDependencies: vite: 7.3.2(@types/node@24.5.2)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0) - vitest@2.1.9(@types/node@20.19.17)(jsdom@25.0.1)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0): - dependencies: - '@vitest/expect': 2.1.9 - '@vitest/mocker': 2.1.9(vite@5.4.20(@types/node@20.19.17)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0)) - '@vitest/pretty-format': 2.1.9 - '@vitest/runner': 2.1.9 - '@vitest/snapshot': 2.1.9 - '@vitest/spy': 2.1.9 - '@vitest/utils': 2.1.9 - chai: 5.3.3 - debug: 4.4.3 - expect-type: 1.2.2 - magic-string: 0.30.17 - pathe: 1.1.2 - std-env: 3.9.0 + vitest@4.1.7(@types/node@20.19.17)(@vitest/coverage-v8@4.1.7)(jsdom@25.0.1)(vite@7.3.2(@types/node@20.19.17)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0)): + dependencies: + '@vitest/expect': 4.1.7 + '@vitest/mocker': 4.1.7(vite@7.3.2(@types/node@20.19.17)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0)) + '@vitest/pretty-format': 4.1.7 + '@vitest/runner': 4.1.7 + '@vitest/snapshot': 4.1.7 + '@vitest/spy': 4.1.7 + '@vitest/utils': 4.1.7 + es-module-lexer: 2.1.0 + expect-type: 1.3.0 + magic-string: 0.30.21 + obug: 2.1.1 + pathe: 2.0.3 + picomatch: 4.0.4 + std-env: 4.1.0 tinybench: 2.9.0 - tinyexec: 0.3.2 - tinypool: 1.1.1 - tinyrainbow: 1.2.0 - vite: 5.4.20(@types/node@20.19.17)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0) - vite-node: 2.1.9(@types/node@20.19.17)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0) + tinyexec: 1.1.2 + tinyglobby: 0.2.15 + tinyrainbow: 3.1.0 + vite: 7.3.2(@types/node@20.19.17)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0) why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 20.19.17 + '@vitest/coverage-v8': 4.1.7(vitest@4.1.7) jsdom: 25.0.1 transitivePeerDependencies: - - less - - lightningcss - msw - - sass - - sass-embedded - - stylus - - sugarss - - supports-color - - terser w3c-xmlserializer@5.0.0: dependencies: @@ -5102,18 +4612,6 @@ snapshots: siginfo: 2.0.0 stackback: 0.0.2 - wrap-ansi@7.0.0: - dependencies: - ansi-styles: 4.3.0 - string-width: 4.2.3 - strip-ansi: 6.0.1 - - wrap-ansi@8.1.0: - dependencies: - ansi-styles: 6.2.1 - string-width: 5.1.2 - strip-ansi: 7.1.0 - ws@8.18.0: {} wsl-utils@0.1.0: diff --git a/vitest-xhtml-environment.ts b/vitest-xhtml-environment.ts index dc8b271440..4535f5a5e3 100644 --- a/vitest-xhtml-environment.ts +++ b/vitest-xhtml-environment.ts @@ -1,4 +1,4 @@ -import { type Environment, builtinEnvironments } from 'vitest/environments'; +import { type Environment, builtinEnvironments } from 'vitest/runtime'; const xhtml_page = ` @@ -6,7 +6,7 @@ const xhtml_page = ` export default { name: 'jsdom-xhtml', - transformMode: 'web', + viteEnvironment: 'client', setup(global, { jsdom = {} }) { return builtinEnvironments.jsdom.setup(global, { jsdom: { diff --git a/vitest.config.js b/vitest.config.js index ba1edb355b..116dfae9d8 100644 --- a/vitest.config.js +++ b/vitest.config.js @@ -29,6 +29,11 @@ export default defineConfig({ test: { dir: '.', reporters: ['dot'], + // A handful of dev-mode tests trigger Svelte's `effect_update_depth_exceeded` + // guard, which involves ~1000 Error objects per flush for stack tracking — + // slow enough under vitest 4's deeper async stacks (and CI's slower workers) + // to overrun the 5s default. + testTimeout: 30_000, include: [ 'packages/svelte/**/*.test.ts', 'packages/svelte/tests/*/test.ts', From 1b4c43b8e59e22c1b8185421307fbab91e573866 Mon Sep 17 00:00:00 2001 From: Simon H <5968653+dummdidumm@users.noreply.github.com> Date: Wed, 3 Jun 2026 01:12:25 +0200 Subject: [PATCH 076/222] fix: ignore declaration tags for animation directive (#18366) Fixes #18365 --- .changeset/tangy-keys-drop.md | 5 +++++ .../phases/2-analyze/visitors/shared/element.js | 1 + .../animation-each-with-declaration-tag/errors.json | 1 + .../animation-each-with-declaration-tag/input.svelte | 10 ++++++++++ 4 files changed, 17 insertions(+) create mode 100644 .changeset/tangy-keys-drop.md create mode 100644 packages/svelte/tests/validator/samples/animation-each-with-declaration-tag/errors.json create mode 100644 packages/svelte/tests/validator/samples/animation-each-with-declaration-tag/input.svelte diff --git a/.changeset/tangy-keys-drop.md b/.changeset/tangy-keys-drop.md new file mode 100644 index 0000000000..38f0c10543 --- /dev/null +++ b/.changeset/tangy-keys-drop.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: ignore declaration tags for animation directive diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/element.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/element.js index 725a4aded8..ff02dee02a 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/element.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/element.js @@ -100,6 +100,7 @@ export function validate_element(node, context) { (n) => n.type !== 'Comment' && n.type !== 'ConstTag' && + n.type !== 'DeclarationTag' && (n.type !== 'Text' || n.data.trim() !== '') ).length > 1 ) { diff --git a/packages/svelte/tests/validator/samples/animation-each-with-declaration-tag/errors.json b/packages/svelte/tests/validator/samples/animation-each-with-declaration-tag/errors.json new file mode 100644 index 0000000000..fe51488c70 --- /dev/null +++ b/packages/svelte/tests/validator/samples/animation-each-with-declaration-tag/errors.json @@ -0,0 +1 @@ +[] diff --git a/packages/svelte/tests/validator/samples/animation-each-with-declaration-tag/input.svelte b/packages/svelte/tests/validator/samples/animation-each-with-declaration-tag/input.svelte new file mode 100644 index 0000000000..680efa7eed --- /dev/null +++ b/packages/svelte/tests/validator/samples/animation-each-with-declaration-tag/input.svelte @@ -0,0 +1,10 @@ + + +
+ {#each [] as n (n)} + {const a = n} +
+ {/each} +
From 94bd27953e0a5ffef80e9451b27e122d185d093f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Minh=20L=C3=AA?= <115204145+DucMinhNe@users.noreply.github.com> Date: Wed, 3 Jun 2026 09:41:15 +0700 Subject: [PATCH 077/222] docs: fix invalid `rows` CSS property in {#each} chess-board example (#18367) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Problem The `{#each}` docs page has a runnable "chess board" example whose ` + + +
+

Benchmark comparison

+

+ Runtime results across branches. Green cells are fastest for an entry and red cells expose + the largest regressions. Overall runtime normalizes every benchmark to its fastest result + before averaging, so long-running entries do not outweigh short ones. +

+ +
+ +
+
+
+

Branch standings

+

+ Wins count the fastest branch for each comparable entry. Normalized runtime is the + average slowdown against each entry's fastest result; lower is better. Click a heading + to sort. +

+
+
+
+ + + + + + + + + +
+ +
+
+
+ +
+
+
+

Results by benchmark

+

+ Each cell shows runtime, difference from the winner, and GC time. Missing entries are + excluded from both standings. +

+
+
+
+ + +
+
+
+
+
+ +
+
+ + + + + From a91b0687866c8b2f78692010702bb811bf27b9a8 Mon Sep 17 00:00:00 2001 From: Fedor Nezhivoi Date: Tue, 14 Jul 2026 16:40:30 +0700 Subject: [PATCH 103/222] fix: abort deriveds own AbortSignal when it disconnects (#18400) Fixes https://github.com/sveltejs/svelte/issues/18301 --------- Co-authored-by: Fedor Nezhivoi Co-authored-by: Simon H <5968653+dummdidumm@users.noreply.github.com> Co-authored-by: Simon Holthausen --- .changeset/early-crabs-rest.md | 5 +++++ .../internal/client/reactivity/deriveds.js | 9 +++++++-- .../svelte/src/internal/client/runtime.js | 8 ++++++++ .../abort-signal-derived-destroy/Child.svelte | 20 +++++++++++++++++++ .../abort-signal-derived-destroy/_config.js | 14 +++++++++++++ .../abort-signal-derived-destroy/main.svelte | 14 +++++++++++++ .../abort-signal-derived-set-state/_config.js | 2 ++ .../main.svelte | 8 ++++---- 8 files changed, 74 insertions(+), 6 deletions(-) create mode 100644 .changeset/early-crabs-rest.md create mode 100644 packages/svelte/tests/runtime-runes/samples/abort-signal-derived-destroy/Child.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/abort-signal-derived-destroy/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/abort-signal-derived-destroy/main.svelte diff --git a/.changeset/early-crabs-rest.md b/.changeset/early-crabs-rest.md new file mode 100644 index 0000000000..d3f290a5c2 --- /dev/null +++ b/.changeset/early-crabs-rest.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: abort deriveds own AbortSignal when it disconnects diff --git a/packages/svelte/src/internal/client/reactivity/deriveds.js b/packages/svelte/src/internal/client/reactivity/deriveds.js index fd64f3b45d..6b48bc8a23 100644 --- a/packages/svelte/src/internal/client/reactivity/deriveds.js +++ b/packages/svelte/src/internal/client/reactivity/deriveds.js @@ -28,6 +28,7 @@ import { skipped_deps, new_deps } from '../runtime.js'; +import { without_reactive_context } from '../dom/elements/bindings/shared.js'; import { equals, safe_equals } from './equality.js'; import * as e from '../errors.js'; import * as w from '../warnings.js'; @@ -450,14 +451,18 @@ export function freeze_derived_effects(derived) { // if the effect has a teardown function or abort signal, call it if (e.teardown || e.ac) { e.teardown?.(); - e.ac?.abort(STALE_REACTION); + if (e.ac !== null) { + without_reactive_context(() => { + /** @type {AbortController} */ (e.ac).abort(STALE_REACTION); + e.ac = null; + }); + } // make it a noop so it doesn't get called again if the derived // is unfrozen. we don't set it to `null`, because the existence // of a teardown function is what determines whether the // effect runs again during unfreezing (but not for teardown-only effects) if (e.fn !== null) e.teardown = noop; - e.ac = null; remove_reactions(e, 0); destroy_effect_children(e); diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index 188d16a820..2ebce07916 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -408,6 +408,14 @@ function remove_reaction(signal, dependency) { update_derived_status(derived); } + // Call abort controller, noone's listening to this derived anymore + if (derived.ac !== null) { + without_reactive_context(() => { + /** @type {AbortController} */ (derived.ac).abort(STALE_REACTION); + derived.ac = null; + }); + } + // freeze any effects inside this derived freeze_derived_effects(derived); diff --git a/packages/svelte/tests/runtime-runes/samples/abort-signal-derived-destroy/Child.svelte b/packages/svelte/tests/runtime-runes/samples/abort-signal-derived-destroy/Child.svelte new file mode 100644 index 0000000000..5a837e5ea4 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/abort-signal-derived-destroy/Child.svelte @@ -0,0 +1,20 @@ + + +{der} diff --git a/packages/svelte/tests/runtime-runes/samples/abort-signal-derived-destroy/_config.js b/packages/svelte/tests/runtime-runes/samples/abort-signal-derived-destroy/_config.js new file mode 100644 index 0000000000..3c6ec98fe6 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/abort-signal-derived-destroy/_config.js @@ -0,0 +1,14 @@ +import { ok, test } from '../../test'; +import { flushSync } from 'svelte'; + +export default test({ + async test({ assert, target, errors }) { + const btn = target.querySelector('button'); + + flushSync(() => { + btn?.click(); + }); + assert.htmlEqual(target.innerHTML, '1 '); + assert.deepEqual(errors, []); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/abort-signal-derived-destroy/main.svelte b/packages/svelte/tests/runtime-runes/samples/abort-signal-derived-destroy/main.svelte new file mode 100644 index 0000000000..57c467a80c --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/abort-signal-derived-destroy/main.svelte @@ -0,0 +1,14 @@ + + +{aborted} + + + +{#if count % 2 === 0} + +{/if} diff --git a/packages/svelte/tests/runtime-runes/samples/abort-signal-derived-set-state/_config.js b/packages/svelte/tests/runtime-runes/samples/abort-signal-derived-set-state/_config.js index 2dacf188d7..aa38dd8280 100644 --- a/packages/svelte/tests/runtime-runes/samples/abort-signal-derived-set-state/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/abort-signal-derived-set-state/_config.js @@ -4,9 +4,11 @@ import { flushSync } from 'svelte'; export default test({ async test({ assert, target, errors }) { const btn = target.querySelector('button'); + flushSync(() => { btn?.click(); }); + assert.htmlEqual(target.innerHTML, '1:1 '); assert.deepEqual(errors, []); } }); diff --git a/packages/svelte/tests/runtime-runes/samples/abort-signal-derived-set-state/main.svelte b/packages/svelte/tests/runtime-runes/samples/abort-signal-derived-set-state/main.svelte index ebefe38fb2..e76cecbac9 100644 --- a/packages/svelte/tests/runtime-runes/samples/abort-signal-derived-set-state/main.svelte +++ b/packages/svelte/tests/runtime-runes/samples/abort-signal-derived-set-state/main.svelte @@ -9,9 +9,9 @@ const signal = getAbortSignal(); signal.addEventListener("abort", () => { - try{ + try { aborted++; - }catch(e){ + } catch(e) { console.error(e); } }); @@ -19,6 +19,6 @@ }) -{der} +{der}:{aborted} - \ No newline at end of file + From edeef1f9f713ab1ffdba698ff6d92cce8da1c5b4 Mon Sep 17 00:00:00 2001 From: Simon H <5968653+dummdidumm@users.noreply.github.com> Date: Tue, 14 Jul 2026 11:41:20 +0200 Subject: [PATCH 104/222] chore: add some failing tests (#18528) During my explorations I collected these new tests which are currently failing. Two of them work on the incremental-batches branch, all of them (adjusted for new behavior) pass on my uncommitted entangle batches branch, and all of them also pass on another uncommitted experimental branch of overlaying deriveds which will likely not land. --- .../_config.js | 42 +++++++++++++++++++ .../main.svelte | 23 ++++++++++ .../async-derived-not-overfiring/_config.js | 35 ++++++++++++++++ .../async-derived-not-overfiring/main.svelte | 29 +++++++++++++ .../async-derived-not-underfiring/_config.js | 28 +++++++++++++ .../async-derived-not-underfiring/main.svelte | 22 ++++++++++ 6 files changed, 179 insertions(+) create mode 100644 packages/svelte/tests/runtime-runes/samples/async-derived-log-outside-reactivity/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-derived-log-outside-reactivity/main.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/async-derived-not-overfiring/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-derived-not-overfiring/main.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/async-derived-not-underfiring/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-derived-not-underfiring/main.svelte diff --git a/packages/svelte/tests/runtime-runes/samples/async-derived-log-outside-reactivity/_config.js b/packages/svelte/tests/runtime-runes/samples/async-derived-log-outside-reactivity/_config.js new file mode 100644 index 0000000000..1a7ed0eb92 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-derived-log-outside-reactivity/_config.js @@ -0,0 +1,42 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + skip: true, // TODO fix + async test({ assert, target, logs }) { + await tick(); + + const [a, b, log, resolve] = target.querySelectorAll('button'); + const [p] = target.querySelectorAll('p'); + + a.click(); + await tick(); + assert.htmlEqual(p.innerHTML, '0 0 0'); + + b.click(); + await tick(); + assert.htmlEqual(p.innerHTML, '0 0 0'); + + log.click(); + await tick(); + assert.deepEqual(logs, [0, 2]); + + resolve.click(); + await tick(); + assert.htmlEqual(p.innerHTML, '1 0 1'); + assert.deepEqual(logs, [0, 2, 1]); + + log.click(); + await tick(); + assert.deepEqual(logs, [0, 2, 1, 2]); + + resolve.click(); + await tick(); + assert.htmlEqual(p.innerHTML, '1 1 2'); + assert.deepEqual(logs, [0, 2, 1, 2, 2]); + + log.click(); + await tick(); + assert.deepEqual(logs, [0, 2, 1, 2, 2, 2]); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-derived-log-outside-reactivity/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-derived-log-outside-reactivity/main.svelte new file mode 100644 index 0000000000..bf543ba595 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-derived-log-outside-reactivity/main.svelte @@ -0,0 +1,23 @@ + + + + + + + +

{await push(a)} {await push(b)} {d}

diff --git a/packages/svelte/tests/runtime-runes/samples/async-derived-not-overfiring/_config.js b/packages/svelte/tests/runtime-runes/samples/async-derived-not-overfiring/_config.js new file mode 100644 index 0000000000..b959ba5407 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-derived-not-overfiring/_config.js @@ -0,0 +1,35 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + skip: true, // TODO fix + async test({ assert, target, logs }) { + await tick(); + + const [a, b, log, resolve] = target.querySelectorAll('button'); + const [div] = target.querySelectorAll('div'); + + assert.deepEqual(logs, ['e1 0', 'e2 0']); + logs.length = 0; + + a.click(); + await tick(); + assert.htmlEqual(div.innerHTML, '

0

0

0

'); + + b.click(); + await tick(); + assert.htmlEqual(div.innerHTML, '

0

1

1

'); + + log.click(); + await tick(); + assert.deepEqual(logs, ['e1 1', 'e2 1', 'runs 2']); // ideally it's only 2 runs, one or two more would also be acceptable but not the 8 that it's today + logs.length = 0; + + resolve.click(); + await tick(); + log.click(); + await tick(); + assert.htmlEqual(div.innerHTML, '

1

2

2

'); + assert.deepEqual(logs, ['e1 2', 'e2 2', 'runs 3']); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-derived-not-overfiring/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-derived-not-overfiring/main.svelte new file mode 100644 index 0000000000..784932ab5d --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-derived-not-overfiring/main.svelte @@ -0,0 +1,29 @@ + + + + + + + +
+

{await push(a)}

+

{d}

+

{d}

+
diff --git a/packages/svelte/tests/runtime-runes/samples/async-derived-not-underfiring/_config.js b/packages/svelte/tests/runtime-runes/samples/async-derived-not-underfiring/_config.js new file mode 100644 index 0000000000..7d31d30b46 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-derived-not-underfiring/_config.js @@ -0,0 +1,28 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + skip: true, // TODO fix + async test({ assert, target, logs }) { + await tick(); + + const [a, b, resolve] = target.querySelectorAll('button'); + const [p] = target.querySelectorAll('p'); + + assert.deepEqual(logs, ['b: 0, d: 0']); + logs.length = 0; + + a.click(); + await tick(); + b.click(); + await tick(); + assert.htmlEqual(p.innerHTML, '0 1'); + assert.deepEqual(logs, ['b: 1, d: 0']); + logs.length = 0; + + resolve.click(); + await tick(); + assert.htmlEqual(p.innerHTML, '2 1'); + assert.deepEqual(logs, ['b: 1, d: 2']); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-derived-not-underfiring/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-derived-not-underfiring/main.svelte new file mode 100644 index 0000000000..97d91ae994 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-derived-not-underfiring/main.svelte @@ -0,0 +1,22 @@ + + + + + + +

{await push(d)} {b}

From 199ffebca375b29b65fcac53fc1825b452175a53 Mon Sep 17 00:00:00 2001 From: prashantg-pixi Date: Tue, 14 Jul 2026 15:21:13 +0530 Subject: [PATCH 105/222] fix: clear previous_task reference after abort in Tween (#18541) avoids memory leaks --- .changeset/tween-previous-task-leak.md | 5 +++++ packages/svelte/src/motion/tweened.js | 1 + 2 files changed, 6 insertions(+) create mode 100644 .changeset/tween-previous-task-leak.md diff --git a/.changeset/tween-previous-task-leak.md b/.changeset/tween-previous-task-leak.md new file mode 100644 index 0000000000..6208e7c87a --- /dev/null +++ b/.changeset/tween-previous-task-leak.md @@ -0,0 +1,5 @@ +--- +"svelte": patch +--- + +fix: clear previous_task reference after abort in Tween to prevent memory leak on interrupted tweens diff --git a/packages/svelte/src/motion/tweened.js b/packages/svelte/src/motion/tweened.js index a24148d075..460db0df64 100644 --- a/packages/svelte/src/motion/tweened.js +++ b/packages/svelte/src/motion/tweened.js @@ -275,6 +275,7 @@ export class Tween { } previous_task?.abort(); + previous_task = null; } const elapsed = now - start; From a4fd67e361914df612ae432ff75283c2d332ae18 Mon Sep 17 00:00:00 2001 From: adiGuba Date: Tue, 14 Jul 2026 11:54:13 +0200 Subject: [PATCH 106/222] fix: $state.eager() is sometimes incorrect in SSR (#18530) Fix #18529 A simple fix : `context.visit()` was missing on the argument of `$state.eager()`, so the generated code can be incorrect in some case : --- .changeset/long-buttons-hunt.md | 5 +++++ .../phases/3-transform/server/visitors/CallExpression.js | 2 +- .../samples/state-eager/_expected.html | 2 ++ .../server-side-rendering/samples/state-eager/main.svelte | 7 +++++++ 4 files changed, 15 insertions(+), 1 deletion(-) create mode 100644 .changeset/long-buttons-hunt.md create mode 100644 packages/svelte/tests/server-side-rendering/samples/state-eager/_expected.html create mode 100644 packages/svelte/tests/server-side-rendering/samples/state-eager/main.svelte diff --git a/.changeset/long-buttons-hunt.md b/.changeset/long-buttons-hunt.md new file mode 100644 index 0000000000..68d0b60ff4 --- /dev/null +++ b/.changeset/long-buttons-hunt.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: ensure `$state.eager()` is correctly transormed for SSR output diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/CallExpression.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/CallExpression.js index 8525fb6366..ac78f55da9 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/CallExpression.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/CallExpression.js @@ -46,7 +46,7 @@ export function CallExpression(node, context) { } if (rune === '$state.eager') { - return node.arguments[0]; + return context.visit(node.arguments[0]); } if (rune === '$state.snapshot') { diff --git a/packages/svelte/tests/server-side-rendering/samples/state-eager/_expected.html b/packages/svelte/tests/server-side-rendering/samples/state-eager/_expected.html new file mode 100644 index 0000000000..9d3ea2f5b4 --- /dev/null +++ b/packages/svelte/tests/server-side-rendering/samples/state-eager/_expected.html @@ -0,0 +1,2 @@ +
value=0
+
eager=0
\ No newline at end of file diff --git a/packages/svelte/tests/server-side-rendering/samples/state-eager/main.svelte b/packages/svelte/tests/server-side-rendering/samples/state-eager/main.svelte new file mode 100644 index 0000000000..d144914f72 --- /dev/null +++ b/packages/svelte/tests/server-side-rendering/samples/state-eager/main.svelte @@ -0,0 +1,7 @@ + + +
value={value}
+
eager={$state.eager(value)}
From 602a873b6b82bba4e6edc91b039e2f6defbe3fc4 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 14 Jul 2026 13:59:06 +0200 Subject: [PATCH 107/222] Version Packages (#18497) 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.5 ### Patch Changes - chore: drop dead code that make TSGO fail ([#18496](https://github.com/sveltejs/svelte/pull/18496)) - fix: don't (re)connect deriveds when read inside branch/root effects ([#18527](https://github.com/sveltejs/svelte/pull/18527)) - fix: skip unnecessary derived effect in earlier batch ([#18525](https://github.com/sveltejs/svelte/pull/18525)) - fix: avoid declaration tag warning in event handlers ([#18500](https://github.com/sveltejs/svelte/pull/18500)) - fix: abort deriveds own AbortSignal when it disconnects ([#18400](https://github.com/sveltejs/svelte/pull/18400)) - fix: ensure `$state.eager()` is correctly transormed for SSR output ([#18530](https://github.com/sveltejs/svelte/pull/18530)) - fix: correctly transform declaration tags during SSR ([#18492](https://github.com/sveltejs/svelte/pull/18492)) - fix: transform computed keys in keyed `{#each}` destructuring patterns ([#18521](https://github.com/sveltejs/svelte/pull/18521)) - fix: chain preprocessor sourcemaps with an empty `sources[0]` instead of dropping them ([#18518](https://github.com/sveltejs/svelte/pull/18518)) - fix: clear previous_task reference after abort in Tween to prevent memory leak on interrupted tweens ([#18541](https://github.com/sveltejs/svelte/pull/18541)) - fix: don't treat declaration tags as parts inside each blocks ([#18507](https://github.com/sveltejs/svelte/pull/18507)) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .changeset/big-months-shout.md | 5 ----- .changeset/common-ways-deny.md | 5 ----- .changeset/curly-wasps-hide.md | 5 ----- .changeset/dull-oranges-fry.md | 5 ----- .changeset/early-crabs-rest.md | 5 ----- .changeset/long-buttons-hunt.md | 5 ----- .changeset/rich-jokes-attack.md | 5 ----- .changeset/shiny-keys-dance.md | 5 ----- .changeset/tame-donkeys-jump.md | 5 ----- .changeset/tween-previous-task-leak.md | 5 ----- .changeset/young-doodles-beam.md | 5 ----- packages/svelte/CHANGELOG.md | 26 ++++++++++++++++++++++++++ packages/svelte/package.json | 2 +- packages/svelte/src/version.js | 2 +- 14 files changed, 28 insertions(+), 57 deletions(-) delete mode 100644 .changeset/big-months-shout.md delete mode 100644 .changeset/common-ways-deny.md delete mode 100644 .changeset/curly-wasps-hide.md delete mode 100644 .changeset/dull-oranges-fry.md delete mode 100644 .changeset/early-crabs-rest.md delete mode 100644 .changeset/long-buttons-hunt.md delete mode 100644 .changeset/rich-jokes-attack.md delete mode 100644 .changeset/shiny-keys-dance.md delete mode 100644 .changeset/tame-donkeys-jump.md delete mode 100644 .changeset/tween-previous-task-leak.md delete mode 100644 .changeset/young-doodles-beam.md diff --git a/.changeset/big-months-shout.md b/.changeset/big-months-shout.md deleted file mode 100644 index 3d8ca0045e..0000000000 --- a/.changeset/big-months-shout.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -chore: drop dead code that make TSGO fail diff --git a/.changeset/common-ways-deny.md b/.changeset/common-ways-deny.md deleted file mode 100644 index 75e9375965..0000000000 --- a/.changeset/common-ways-deny.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: don't (re)connect deriveds when read inside branch/root effects diff --git a/.changeset/curly-wasps-hide.md b/.changeset/curly-wasps-hide.md deleted file mode 100644 index 7e55d77ba0..0000000000 --- a/.changeset/curly-wasps-hide.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: skip unnecessary derived effect in earlier batch diff --git a/.changeset/dull-oranges-fry.md b/.changeset/dull-oranges-fry.md deleted file mode 100644 index 0efcc6fa2d..0000000000 --- a/.changeset/dull-oranges-fry.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: avoid declaration tag warning in event handlers diff --git a/.changeset/early-crabs-rest.md b/.changeset/early-crabs-rest.md deleted file mode 100644 index d3f290a5c2..0000000000 --- a/.changeset/early-crabs-rest.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: abort deriveds own AbortSignal when it disconnects diff --git a/.changeset/long-buttons-hunt.md b/.changeset/long-buttons-hunt.md deleted file mode 100644 index 68d0b60ff4..0000000000 --- a/.changeset/long-buttons-hunt.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: ensure `$state.eager()` is correctly transormed for SSR output diff --git a/.changeset/rich-jokes-attack.md b/.changeset/rich-jokes-attack.md deleted file mode 100644 index 46e7d2d8c8..0000000000 --- a/.changeset/rich-jokes-attack.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: correctly transform declaration tags during SSR diff --git a/.changeset/shiny-keys-dance.md b/.changeset/shiny-keys-dance.md deleted file mode 100644 index 20cf754212..0000000000 --- a/.changeset/shiny-keys-dance.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: transform computed keys in keyed `{#each}` destructuring patterns diff --git a/.changeset/tame-donkeys-jump.md b/.changeset/tame-donkeys-jump.md deleted file mode 100644 index aae38910db..0000000000 --- a/.changeset/tame-donkeys-jump.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: chain preprocessor sourcemaps with an empty `sources[0]` instead of dropping them diff --git a/.changeset/tween-previous-task-leak.md b/.changeset/tween-previous-task-leak.md deleted file mode 100644 index 6208e7c87a..0000000000 --- a/.changeset/tween-previous-task-leak.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"svelte": patch ---- - -fix: clear previous_task reference after abort in Tween to prevent memory leak on interrupted tweens diff --git a/.changeset/young-doodles-beam.md b/.changeset/young-doodles-beam.md deleted file mode 100644 index b68f55d241..0000000000 --- a/.changeset/young-doodles-beam.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: don't treat declaration tags as parts inside each blocks diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index 80b61facb0..d80da025c9 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,31 @@ # svelte +## 5.56.5 + +### Patch Changes + +- chore: drop dead code that make TSGO fail ([#18496](https://github.com/sveltejs/svelte/pull/18496)) + +- fix: don't (re)connect deriveds when read inside branch/root effects ([#18527](https://github.com/sveltejs/svelte/pull/18527)) + +- fix: skip unnecessary derived effect in earlier batch ([#18525](https://github.com/sveltejs/svelte/pull/18525)) + +- fix: avoid declaration tag warning in event handlers ([#18500](https://github.com/sveltejs/svelte/pull/18500)) + +- fix: abort deriveds own AbortSignal when it disconnects ([#18400](https://github.com/sveltejs/svelte/pull/18400)) + +- fix: ensure `$state.eager()` is correctly transormed for SSR output ([#18530](https://github.com/sveltejs/svelte/pull/18530)) + +- fix: correctly transform declaration tags during SSR ([#18492](https://github.com/sveltejs/svelte/pull/18492)) + +- fix: transform computed keys in keyed `{#each}` destructuring patterns ([#18521](https://github.com/sveltejs/svelte/pull/18521)) + +- fix: chain preprocessor sourcemaps with an empty `sources[0]` instead of dropping them ([#18518](https://github.com/sveltejs/svelte/pull/18518)) + +- fix: clear previous_task reference after abort in Tween to prevent memory leak on interrupted tweens ([#18541](https://github.com/sveltejs/svelte/pull/18541)) + +- fix: don't treat declaration tags as parts inside each blocks ([#18507](https://github.com/sveltejs/svelte/pull/18507)) + ## 5.56.4 ### Patch Changes diff --git a/packages/svelte/package.json b/packages/svelte/package.json index 34fa68c8a7..a26a3e4a76 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.56.4", + "version": "5.56.5", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index ef468a2f79..80b38c225a 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.56.4'; +export const VERSION = '5.56.5'; export const PUBLIC_VERSION = '5'; From 22e0adb75a07442015feb0465cee1d3a61729e90 Mon Sep 17 00:00:00 2001 From: Simon H <5968653+dummdidumm@users.noreply.github.com> Date: Thu, 16 Jul 2026 10:42:31 +0200 Subject: [PATCH 108/222] fix: rerun derived that had an abort controller on reconnection (#18551) Follow-up to #18400 - we need to mark a derived with an abort signal that is frozen as dirty so it is guaranteed to rerun when it reconnects/is re-requested. Else you could return a stale value, or worse, you returned a promise from the derived which you aborted, and it's now in the rejected state until you update one of its dependencies. --- .changeset/lucky-dolls-yell.md | 5 +++ .../svelte/src/internal/client/runtime.js | 2 ++ .../_config.js | 32 +++++++++++++++++++ .../main.svelte | 32 +++++++++++++++++++ 4 files changed, 71 insertions(+) create mode 100644 .changeset/lucky-dolls-yell.md create mode 100644 packages/svelte/tests/runtime-runes/samples/abort-signal-derived-rerun-on-reconnect/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/abort-signal-derived-rerun-on-reconnect/main.svelte diff --git a/.changeset/lucky-dolls-yell.md b/.changeset/lucky-dolls-yell.md new file mode 100644 index 0000000000..d158f376d1 --- /dev/null +++ b/.changeset/lucky-dolls-yell.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: rerun derived that had an abort controller on reconnection diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index 2ebce07916..4458595d35 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -413,6 +413,8 @@ function remove_reaction(signal, dependency) { without_reactive_context(() => { /** @type {AbortController} */ (derived.ac).abort(STALE_REACTION); derived.ac = null; + // ensure it reruns right away next time instead of potentially returning a rejected promise as its value + set_signal_status(derived, DIRTY); }); } diff --git a/packages/svelte/tests/runtime-runes/samples/abort-signal-derived-rerun-on-reconnect/_config.js b/packages/svelte/tests/runtime-runes/samples/abort-signal-derived-rerun-on-reconnect/_config.js new file mode 100644 index 0000000000..7caf765e3b --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/abort-signal-derived-rerun-on-reconnect/_config.js @@ -0,0 +1,32 @@ +import { test } from '../../test'; +import { tick } from 'svelte'; + +export default test({ + async test({ assert, target }) { + const [increment, toggle, resolve] = target.querySelectorAll('button'); + const [div] = target.querySelectorAll('div'); + + assert.htmlEqual(div.innerHTML, 'loading'); + resolve.click(); + await tick(); + assert.htmlEqual(div.innerHTML, '0'); + + increment.click(); + await tick(); + assert.htmlEqual(div.innerHTML, 'loading'); + + toggle.click(); + await tick(); + assert.htmlEqual(div.innerHTML, ''); + + toggle.click(); + await tick(); + assert.htmlEqual(div.innerHTML, 'loading'); + + resolve.click(); // this one's for clearing the obsolete/aborted one from the queue + await tick(); + resolve.click(); + await tick(); + assert.htmlEqual(div.innerHTML, '2'); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/abort-signal-derived-rerun-on-reconnect/main.svelte b/packages/svelte/tests/runtime-runes/samples/abort-signal-derived-rerun-on-reconnect/main.svelte new file mode 100644 index 0000000000..a047afdd44 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/abort-signal-derived-rerun-on-reconnect/main.svelte @@ -0,0 +1,32 @@ + + + + + + +
+ {#if show} + {#await double} + loading + {:then value} + {value} + {:catch} + error + {/await} + {/if} +
From d0dbe1a48013b9c405b518511f8330baf3a27bc9 Mon Sep 17 00:00:00 2001 From: Joe Schafer Date: Thu, 16 Jul 2026 04:35:37 -0700 Subject: [PATCH 109/222] perf: skip quadratic blocker analysis when no top-level await (#18548) Skip function reference tracing in `calculate_blockers` when a component has no top-level `await`. Blockers only represent dependencies on top-level async statements. Without a top-level `await`, no binding can have a blocker, so tracing every top-level function cannot affect the generated output. In large components with many functions and transitive assignments, that unnecessary work can become quadratic. --- .changeset/fast-cats-compile.md | 5 +++++ packages/svelte/src/compiler/phases/2-analyze/index.js | 4 ++++ 2 files changed, 9 insertions(+) create mode 100644 .changeset/fast-cats-compile.md diff --git a/.changeset/fast-cats-compile.md b/.changeset/fast-cats-compile.md new file mode 100644 index 0000000000..585ce0ed1d --- /dev/null +++ b/.changeset/fast-cats-compile.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +perf: skip unnecessary blocker analysis when compiling components without top-level await diff --git a/packages/svelte/src/compiler/phases/2-analyze/index.js b/packages/svelte/src/compiler/phases/2-analyze/index.js index ef20049697..67e9030188 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/index.js +++ b/packages/svelte/src/compiler/phases/2-analyze/index.js @@ -1221,6 +1221,10 @@ function calculate_blockers(instance, analysis) { } } + // With no top-level await, no binding can have a blocker and function tracing + // cannot affect the output. + if (!awaited) return; + flush_sync_group(); for (const fn of functions) { From 4a6a85b5f149cc96514ed3bf5e59083b9246d394 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 17 Jul 2026 00:21:52 +0200 Subject: [PATCH 110/222] Version Packages (#18552) 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.6 ### Patch Changes - perf: skip unnecessary blocker analysis when compiling components without top-level await ([#18548](https://github.com/sveltejs/svelte/pull/18548)) - fix: rerun derived that had an abort controller on reconnection ([#18551](https://github.com/sveltejs/svelte/pull/18551)) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .changeset/fast-cats-compile.md | 5 ----- .changeset/lucky-dolls-yell.md | 5 ----- packages/svelte/CHANGELOG.md | 8 ++++++++ packages/svelte/package.json | 2 +- packages/svelte/src/version.js | 2 +- 5 files changed, 10 insertions(+), 12 deletions(-) delete mode 100644 .changeset/fast-cats-compile.md delete mode 100644 .changeset/lucky-dolls-yell.md diff --git a/.changeset/fast-cats-compile.md b/.changeset/fast-cats-compile.md deleted file mode 100644 index 585ce0ed1d..0000000000 --- a/.changeset/fast-cats-compile.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -perf: skip unnecessary blocker analysis when compiling components without top-level await diff --git a/.changeset/lucky-dolls-yell.md b/.changeset/lucky-dolls-yell.md deleted file mode 100644 index d158f376d1..0000000000 --- a/.changeset/lucky-dolls-yell.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: rerun derived that had an abort controller on reconnection diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index d80da025c9..0f26b571d3 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,13 @@ # svelte +## 5.56.6 + +### Patch Changes + +- perf: skip unnecessary blocker analysis when compiling components without top-level await ([#18548](https://github.com/sveltejs/svelte/pull/18548)) + +- fix: rerun derived that had an abort controller on reconnection ([#18551](https://github.com/sveltejs/svelte/pull/18551)) + ## 5.56.5 ### Patch Changes diff --git a/packages/svelte/package.json b/packages/svelte/package.json index a26a3e4a76..855ba92a8d 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.56.5", + "version": "5.56.6", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index 80b38c225a..05d88ef904 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.56.5'; +export const VERSION = '5.56.6'; export const PUBLIC_VERSION = '5'; From b791cac54e2db43317f2bc6c8e41e694245c551e Mon Sep 17 00:00:00 2001 From: Manuel <30698007+manuel3108@users.noreply.github.com> Date: Sat, 18 Jul 2026 14:47:30 +0200 Subject: [PATCH 111/222] chore: provide `indent` option for `print` (#18474) Relevant for https://github.com/sveltejs/cli/pull/1138. This is basically just an option from `esrap` that we pass through. That will allow tools like `sv migrate` to provide a guessed indent based on the other file contents and therefore allow us to produce way smaller diffs. Since we are just passing an option, there is no need for a test here. Technically a `feat:` but i dont think this is relevant enough. ### 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. - [ ] 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` --- .changeset/short-otters-find.md | 5 +++++ packages/svelte/src/compiler/print/index.js | 5 ++++- packages/svelte/src/compiler/print/types.d.ts | 1 + packages/svelte/types/index.d.ts | 1 + 4 files changed, 11 insertions(+), 1 deletion(-) create mode 100644 .changeset/short-otters-find.md diff --git a/.changeset/short-otters-find.md b/.changeset/short-otters-find.md new file mode 100644 index 0000000000..57194653b6 --- /dev/null +++ b/.changeset/short-otters-find.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +chore: provide `indent` option for `print` diff --git a/packages/svelte/src/compiler/print/index.js b/packages/svelte/src/compiler/print/index.js index fbd7e86f12..a5fbc96fc8 100644 --- a/packages/svelte/src/compiler/print/index.js +++ b/packages/svelte/src/compiler/print/index.js @@ -30,7 +30,10 @@ export function print(ast, options = undefined) { }), ...svelte_visitors(comments), ...css_visitors - }) + }), + { + indent: options?.indent + } ); } diff --git a/packages/svelte/src/compiler/print/types.d.ts b/packages/svelte/src/compiler/print/types.d.ts index d0ff909525..9eccebb6f3 100644 --- a/packages/svelte/src/compiler/print/types.d.ts +++ b/packages/svelte/src/compiler/print/types.d.ts @@ -4,4 +4,5 @@ import type ts from 'esrap/languages/ts'; export type Options = { getLeadingComments?: NonNullable[0]>['getLeadingComments'] | undefined; getTrailingComments?: NonNullable[0]>['getTrailingComments'] | undefined; + indent?: string; // default tab }; diff --git a/packages/svelte/types/index.d.ts b/packages/svelte/types/index.d.ts index 5f41fabf60..d758022ae4 100644 --- a/packages/svelte/types/index.d.ts +++ b/packages/svelte/types/index.d.ts @@ -1854,6 +1854,7 @@ declare module 'svelte/compiler' { type Options = { getLeadingComments?: NonNullable[0]>['getLeadingComments'] | undefined; getTrailingComments?: NonNullable[0]>['getTrailingComments'] | undefined; + indent?: string; // default tab }; export {}; From d9e40d17cf751e22dbc3ec42b9cb50920e2d1229 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sat, 18 Jul 2026 21:00:02 -0400 Subject: [PATCH 112/222] docs: update link on hooks page (#18564) this docs page updated recently and broke everything --- documentation/docs/05-special-elements/01-svelte-boundary.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/documentation/docs/05-special-elements/01-svelte-boundary.md b/documentation/docs/05-special-elements/01-svelte-boundary.md index e1ad00a50b..41ebee1564 100644 --- a/documentation/docs/05-special-elements/01-svelte-boundary.md +++ b/documentation/docs/05-special-elements/01-svelte-boundary.md @@ -109,7 +109,7 @@ By default, error boundaries have no effect on the server — if an error occurs Since 5.51 you can control this behaviour for boundaries with a `failed` snippet, by calling [`render(...)`](imperative-component-api#render) with a `transformError` function. -> [!NOTE] If you're using Svelte via a framework such as SvelteKit, you most likely don't have direct access to the `render(...)` call — the framework must configure `transformError` on your behalf. SvelteKit will add support for this in the near future, via the [`handleError`](../kit/hooks#Shared-hooks-handleError) hook. +> [!NOTE] If you're using Svelte via a framework such as SvelteKit, you most likely don't have direct access to the `render(...)` call — the framework must configure `transformError` on your behalf. SvelteKit will add support for this in the near future, via the [`handleError`](../kit/hooks#handleError) hook. The `transformError` function must return a JSON-stringifiable object which will be used to render the `failed` snippet. This object will be serialized and used to hydrate the snippet in the browser: From b29d7002ecf9bc0036b18647c0b7677a7cb0a914 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 20 Jul 2026 15:23:49 -0400 Subject: [PATCH 113/222] Version Packages (#18560) 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.7 ### Patch Changes - chore: provide `indent` option for `print` ([#18474](https://github.com/sveltejs/svelte/pull/18474)) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .changeset/short-otters-find.md | 5 ----- packages/svelte/CHANGELOG.md | 6 ++++++ packages/svelte/package.json | 2 +- packages/svelte/src/version.js | 2 +- 4 files changed, 8 insertions(+), 7 deletions(-) delete mode 100644 .changeset/short-otters-find.md diff --git a/.changeset/short-otters-find.md b/.changeset/short-otters-find.md deleted file mode 100644 index 57194653b6..0000000000 --- a/.changeset/short-otters-find.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -chore: provide `indent` option for `print` diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index 0f26b571d3..a9b93f5341 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,11 @@ # svelte +## 5.56.7 + +### Patch Changes + +- chore: provide `indent` option for `print` ([#18474](https://github.com/sveltejs/svelte/pull/18474)) + ## 5.56.6 ### Patch Changes diff --git a/packages/svelte/package.json b/packages/svelte/package.json index 855ba92a8d..67f349be71 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.56.6", + "version": "5.56.7", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index 05d88ef904..e9737ec13c 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.56.6'; +export const VERSION = '5.56.7'; export const PUBLIC_VERSION = '5'; From 2bace308e37ac1def958be750bd699ed302bb715 Mon Sep 17 00:00:00 2001 From: Floze <88098863+floze-the-genius@users.noreply.github.com> Date: Wed, 22 Jul 2026 16:17:27 +0400 Subject: [PATCH 114/222] fix: preserve select selection with spread attributes (#18561) Fixes #18557 A ` + + + {#if show_extra} + + {/if} + From 3dde011d3a9e7b9145169da0b75dcd607a378c0e Mon Sep 17 00:00:00 2001 From: Nic Polumeyv Date: Thu, 23 Jul 2026 17:45:50 -0400 Subject: [PATCH 115/222] fix: call `onerror` and provide a working `reset` when hydrating a failed boundary (#18556) Fixes #18555 A boundary that failed during SSR hydrates via `#hydrate_failed_content`, which never calls `onerror` and passes the `failed` snippet a no-op `reset`. Both came in with #17672, whose docs say `onerror` "will be called upon hydration with the deserialized error object". Once hydrated as failed, the boundary can never leave that state. Downstream this is what keeps SvelteKit's `+error.svelte` mounted after navigating away from a server-rendered error page (sveltejs/kit#16345). This extracts the reset/onerror machinery from `#handle_error` into `#create_reset` and uses it in the hydration path too. `onerror` is invoked in a microtask because it may mutate state, which is disallowed while hydrating. `#handle_error` already invokes it asynchronously, so the timing matches the error path. The tests flip a `recovered` flag before calling `reset`, since the child would otherwise throw again. That is the intended retry pattern, and the same one kit uses when it resets route boundaries on navigation. Verified against kit end to end, hydrating a server-rendered error page and navigating away now tears it down with no kit changes needed. --- .changeset/hydrated-boundary-reset.md | 5 ++ .../internal/client/dom/blocks/boundary.js | 89 ++++++++++++------- .../error-boundary-hydrate-1/_config.js | 19 ++++ .../error-boundary-hydrate-1/child.svelte | 3 + .../error-boundary-hydrate-1/main.svelte | 29 ++++++ .../error-boundary-hydrate-2/_config.js | 16 ++++ .../error-boundary-hydrate-2/child.svelte | 3 + .../error-boundary-hydrate-2/main.svelte | 22 +++++ 8 files changed, 152 insertions(+), 34 deletions(-) create mode 100644 .changeset/hydrated-boundary-reset.md create mode 100644 packages/svelte/tests/runtime-runes/samples/error-boundary-hydrate-1/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/error-boundary-hydrate-1/child.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/error-boundary-hydrate-1/main.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/error-boundary-hydrate-2/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/error-boundary-hydrate-2/child.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/error-boundary-hydrate-2/main.svelte diff --git a/.changeset/hydrated-boundary-reset.md b/.changeset/hydrated-boundary-reset.md new file mode 100644 index 0000000000..799a57fd9b --- /dev/null +++ b/.changeset/hydrated-boundary-reset.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: call `onerror` and provide a working `reset` when hydrating a failed boundary diff --git a/packages/svelte/src/internal/client/dom/blocks/boundary.js b/packages/svelte/src/internal/client/dom/blocks/boundary.js index 0c0903ff52..4f655a3695 100644 --- a/packages/svelte/src/internal/client/dom/blocks/boundary.js +++ b/packages/svelte/src/internal/client/dom/blocks/boundary.js @@ -199,17 +199,68 @@ export class Boundary { */ #hydrate_failed_content(error) { const failed = this.#props.failed; + const { reset, invoke_onerror } = this.#create_reset(error); + + // `onerror` may mutate state, which is disallowed while hydrating + queue_micro_task(invoke_onerror); + if (!failed) return; this.#failed_effect = branch(() => { failed( this.#anchor, () => error, - () => () => {} + () => reset ); }); } + /** + * Creates the `reset` function for a failed boundary, along with a function + * that invokes `onerror` with it (if provided) + * @param {unknown} error + * @returns {{ reset: () => void, invoke_onerror: () => void }} + */ + #create_reset(error) { + var did_reset = false; + var calling_on_error = false; + + const reset = () => { + if (did_reset) { + w.svelte_boundary_reset_noop(); + return; + } + + did_reset = true; + + if (calling_on_error) { + e.svelte_boundary_reset_onerror(); + } + + if (this.#failed_effect !== null) { + pause_effect(this.#failed_effect, () => { + this.#failed_effect = null; + }); + } + + this.#run(() => { + this.#render(); + }); + }; + + const invoke_onerror = () => { + try { + calling_on_error = true; + this.#props.onerror?.(error, reset); + calling_on_error = false; + } catch (err) { + invoke_error_boundary(err, this.#effect && this.#effect.parent); + } + }; + + return { reset, invoke_onerror }; + } + #hydrate_pending_content() { const pending = this.#props.pending; if (!pending) return; @@ -429,43 +480,13 @@ export class Boundary { set_hydrate_node(skip_nodes()); } - var onerror = this.#props.onerror; let failed = this.#props.failed; - var did_reset = false; - var calling_on_error = false; - - const reset = () => { - if (did_reset) { - w.svelte_boundary_reset_noop(); - return; - } - - did_reset = true; - - if (calling_on_error) { - e.svelte_boundary_reset_onerror(); - } - - if (this.#failed_effect !== null) { - pause_effect(this.#failed_effect, () => { - this.#failed_effect = null; - }); - } - - this.#run(() => { - this.#render(); - }); - }; /** @param {unknown} transformed_error */ const handle_error_result = (transformed_error) => { - try { - calling_on_error = true; - onerror?.(transformed_error, reset); - calling_on_error = false; - } catch (error) { - invoke_error_boundary(error, this.#effect && this.#effect.parent); - } + const { reset, invoke_onerror } = this.#create_reset(transformed_error); + + invoke_onerror(); if (failed) { this.#failed_effect = this.#run(() => { diff --git a/packages/svelte/tests/runtime-runes/samples/error-boundary-hydrate-1/_config.js b/packages/svelte/tests/runtime-runes/samples/error-boundary-hydrate-1/_config.js new file mode 100644 index 0000000000..f0aef7e92f --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/error-boundary-hydrate-1/_config.js @@ -0,0 +1,19 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + mode: ['hydrate'], + ssrHtml: '

failed: error

', + transformError: () => 'error', + + test({ assert, target, logs }) { + // `onerror` is called upon hydration with the deserialized error + assert.deepEqual(logs, ['onerror: error']); + + const btn = target.querySelector('button'); + btn?.click(); + flushSync(); + + assert.htmlEqual(target.innerHTML, '

recovered

'); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/error-boundary-hydrate-1/child.svelte b/packages/svelte/tests/runtime-runes/samples/error-boundary-hydrate-1/child.svelte new file mode 100644 index 0000000000..e93c7bb0fa --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/error-boundary-hydrate-1/child.svelte @@ -0,0 +1,3 @@ + diff --git a/packages/svelte/tests/runtime-runes/samples/error-boundary-hydrate-1/main.svelte b/packages/svelte/tests/runtime-runes/samples/error-boundary-hydrate-1/main.svelte new file mode 100644 index 0000000000..e84198473b --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/error-boundary-hydrate-1/main.svelte @@ -0,0 +1,29 @@ + + + { + console.log(`onerror: ${error}`); + reset_fn = reset; + }} +> + {#if recovered} +

recovered

+ {:else} + + {/if} + + {#snippet failed(error)} +

failed: {error}

+ {/snippet} +
+ + diff --git a/packages/svelte/tests/runtime-runes/samples/error-boundary-hydrate-2/_config.js b/packages/svelte/tests/runtime-runes/samples/error-boundary-hydrate-2/_config.js new file mode 100644 index 0000000000..e5c52eb19c --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/error-boundary-hydrate-2/_config.js @@ -0,0 +1,16 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + mode: ['hydrate'], + ssrHtml: '

failed: error

', + transformError: () => 'error', + + test({ assert, target }) { + const btn = target.querySelector('button'); + btn?.click(); + flushSync(); + + assert.htmlEqual(target.innerHTML, '

recovered

'); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/error-boundary-hydrate-2/child.svelte b/packages/svelte/tests/runtime-runes/samples/error-boundary-hydrate-2/child.svelte new file mode 100644 index 0000000000..e93c7bb0fa --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/error-boundary-hydrate-2/child.svelte @@ -0,0 +1,3 @@ + diff --git a/packages/svelte/tests/runtime-runes/samples/error-boundary-hydrate-2/main.svelte b/packages/svelte/tests/runtime-runes/samples/error-boundary-hydrate-2/main.svelte new file mode 100644 index 0000000000..1642a94e7a --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/error-boundary-hydrate-2/main.svelte @@ -0,0 +1,22 @@ + + + + {#if recovered} +

recovered

+ {:else} + + {/if} + + {#snippet failed(error, reset)} +

failed: {error}

+ + {/snippet} +
From 23bc246a99c649c0c325509debb93e062948e240 Mon Sep 17 00:00:00 2001 From: Elliott Johnson Date: Fri, 24 Jul 2026 14:14:11 -0600 Subject: [PATCH 116/222] chore: Supply chain hardening (#18253) `svelte` Supply-chain hardening pass. ## Changes - Bump `packageManager` from `pnpm@10.4.0` to `pnpm@10.33.4` with `+sha512` integrity suffix. - Add to `pnpm-workspace.yaml`: - `minimumReleaseAge: 1440` - `minimumReleaseAgeExclude: ['@sveltejs/*', svelte, esrap, devalue]` - `blockExoticSubdeps: true` - Remove `pkg.pr.new.yml` workflow in favor of `pkg.svelte.dev` - Pin all third-party GitHub Actions to full SHA with `# vX.Y.Z` comments across `ci.yml`, `autofix.yml`, `release.yml`, `ecosystem-ci-trigger.yml` - Upgrade `pnpm/action-setup` references in `ci.yml`, `autofix.yml`, `release.yml` from a SHA that was incorrectly labeled `# v4` (actually `v5.0.0` / `v4.4.0`) to the current `v6.0.8` SHA so the version label matches reality. --- .github/workflows/autofix.yml | 8 +- .github/workflows/ci.yml | 30 +-- .github/workflows/ecosystem-ci-trigger.yml | 8 +- .github/workflows/pkg.pr.new.yml | 229 --------------------- .github/workflows/release.yml | 6 +- package.json | 2 +- pnpm-workspace.yaml | 12 ++ 7 files changed, 39 insertions(+), 256 deletions(-) delete mode 100644 .github/workflows/pkg.pr.new.yml diff --git a/.github/workflows/autofix.yml b/.github/workflows/autofix.yml index b6c26e0792..5cdbd34e5b 100644 --- a/.github/workflows/autofix.yml +++ b/.github/workflows/autofix.yml @@ -28,7 +28,7 @@ jobs: - name: Get PR ref if: github.event_name != 'workflow_dispatch' id: pr - uses: actions/github-script@v8 + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: script: | const { data: pull } = await github.rest.pulls.get({ @@ -46,12 +46,12 @@ jobs: core.setFailed('PR is from a fork'); } core.setOutput('ref', pull.head.ref); - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 if: github.event_name == 'workflow_dispatch' || steps.pr.outcome == 'success' with: ref: ${{ github.event_name == 'workflow_dispatch' && github.ref || steps.pr.outputs.ref }} - - uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4.3.0 - - uses: actions/setup-node@v6 + - uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6.0.8 + - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6 with: node-version: 24 cache: pnpm diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 365717755e..3846690135 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -32,9 +32,9 @@ jobs: os: ubuntu-latest steps: - - uses: actions/checkout@v6 - - uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v4 - - uses: actions/setup-node@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + - uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6.0.8 + - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6 with: node-version: ${{ matrix.node-version }} cache: pnpm @@ -48,9 +48,9 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 10 steps: - - uses: actions/checkout@v6 - - uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v4 - - uses: actions/setup-node@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + - uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6.0.8 + - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6 with: node-version: 22 cache: pnpm @@ -65,9 +65,9 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 5 steps: - - uses: actions/checkout@v6 - - uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v4 - - uses: actions/setup-node@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + - uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6.0.8 + - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6 with: node-version: 24 cache: pnpm @@ -82,9 +82,9 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 10 steps: - - uses: actions/checkout@v6 - - uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v4 - - uses: actions/setup-node@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + - uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6.0.8 + - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6 with: node-version: 24 cache: pnpm @@ -105,9 +105,9 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 15 steps: - - uses: actions/checkout@v6 - - uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v4 - - uses: actions/setup-node@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + - uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6.0.8 + - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6 with: node-version: 24 cache: pnpm diff --git a/.github/workflows/ecosystem-ci-trigger.yml b/.github/workflows/ecosystem-ci-trigger.yml index 8a6d1bf345..8691a64ca4 100644 --- a/.github/workflows/ecosystem-ci-trigger.yml +++ b/.github/workflows/ecosystem-ci-trigger.yml @@ -17,7 +17,7 @@ jobs: contents: read # to clone the repo steps: - name: Check User Permissions - uses: actions/github-script@v8 + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 id: check-permissions with: script: | @@ -56,7 +56,7 @@ jobs: } - name: Get PR Data - uses: actions/github-script@v8 + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 id: get-pr-data with: script: | @@ -106,7 +106,7 @@ jobs: - name: Generate Token id: generate-token - uses: actions/create-github-app-token@v2 + uses: actions/create-github-app-token@fee1f7d63c2ff003460e3d139729b119787bc349 # v2 with: app-id: ${{ secrets.ECOSYSTEM_CI_GITHUB_APP_ID }} private-key: ${{ secrets.ECOSYSTEM_CI_GITHUB_APP_PRIVATE_KEY }} @@ -115,7 +115,7 @@ jobs: svelte-ecosystem-ci - name: Trigger Downstream Workflow - uses: actions/github-script@v8 + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 id: trigger env: COMMENT: ${{ github.event.comment.body }} diff --git a/.github/workflows/pkg.pr.new.yml b/.github/workflows/pkg.pr.new.yml deleted file mode 100644 index 0fcda5a778..0000000000 --- a/.github/workflows/pkg.pr.new.yml +++ /dev/null @@ -1,229 +0,0 @@ -name: pkg.pr.new -on: - pull_request_target: - types: [opened, synchronize] - push: - branches: [main] - workflow_dispatch: - inputs: - sha: - description: 'Commit SHA to build' - required: true - type: string - pr: - description: 'PR number to comment on' - required: true - type: number - -permissions: {} - -jobs: - build: - # Skip pull_request_target events from forks — maintainers can use workflow_dispatch instead - if: > - github.event_name != 'pull_request_target' || - github.event.pull_request.head.repo.full_name == github.repository - runs-on: ubuntu-latest - # No permissions — this job runs user-controlled code - permissions: {} - - steps: - - uses: actions/checkout@v6 - with: - # For pull_request_target, check out the PR head. - # For workflow_dispatch, check out the manually specified SHA. - # For push, fall back to the push SHA. - ref: ${{ github.event.pull_request.head.sha || inputs.sha || github.sha }} - - - uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v4 - - uses: actions/setup-node@v6 - with: - node-version: 22.x - cache: pnpm - - - name: Install dependencies - run: pnpm install --frozen-lockfile - - - name: Build - run: pnpm build - - - run: pnpx pkg-pr-new publish --comment=off --json output.json --compact --no-template './packages/svelte' - - - name: Upload output - uses: actions/upload-artifact@v4 - with: - name: output - path: ./output.json - - # Sanitizes the untrusted output from the build job before it's consumed by - # jobs with elevated permissions. This ensures that only known package names - # and valid SHA prefixes make it through. - sanitize: - needs: build - runs-on: ubuntu-latest - - permissions: {} - - steps: - - name: Download artifact - uses: actions/download-artifact@v7 - with: - name: output - - - name: Sanitize output - uses: actions/github-script@v8 - with: - script: | - const fs = require('fs'); - const raw = JSON.parse(fs.readFileSync('output.json', 'utf8')); - - const ALLOWED_PACKAGES = new Set(['svelte']); - const SHA_PATTERN = /^[0-9a-f]{7}$/; - - const packages = (raw.packages || []) - .filter(p => { - if (!ALLOWED_PACKAGES.has(p.name)) { - console.log(`Skipping unexpected package: ${JSON.stringify(p.name)}`); - return false; - } - const sha = p.url?.replace(/^.+@([^@]+)$/, '$1'); - if (!sha || !SHA_PATTERN.test(sha)) { - console.log(`Skipping package with invalid SHA: ${JSON.stringify(p.url)}`); - return false; - } - return true; - }) - .map(p => ({ - name: p.name, - sha: p.url.replace(/^.+@([^@]+)$/, '$1'), - })); - - fs.writeFileSync('sanitized-output.json', JSON.stringify({ packages }), 'utf8'); - - - name: Upload sanitized output - uses: actions/upload-artifact@v4 - with: - name: sanitized-output - path: ./sanitized-output.json - - comment: - needs: sanitize - if: github.event_name == 'pull_request_target' || github.event_name == 'workflow_dispatch' - runs-on: ubuntu-latest - - permissions: - contents: read - pull-requests: write - - steps: - - name: Download sanitized artifact - uses: actions/download-artifact@v7 - with: - name: sanitized-output - - - name: Resolve PR number - id: pr - uses: actions/github-script@v8 - with: - script: | - if (context.eventName === 'pull_request_target') { - core.setOutput('number', context.issue.number); - return; - } - - // For workflow_dispatch, use the explicitly provided PR number. - // We can't use listPullRequestsAssociatedWithCommit because fork - // commits don't exist in the base repo, so the API returns nothing. - const pr = Number('${{ inputs.pr }}'); - if (!pr || isNaN(pr)) { - core.setFailed('workflow_dispatch requires a valid pr input'); - return; - } - - core.setOutput('number', pr); - - - name: Post or update comment - uses: actions/github-script@v8 - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - script: | - const fs = require('fs'); - const { packages } = JSON.parse(fs.readFileSync('sanitized-output.json', 'utf8')); - - if (packages.length === 0) { - console.log('No valid packages found. Skipping comment.'); - return; - } - - const issue_number = parseInt('${{ steps.pr.outputs.number }}', 10); - - const bot_comment_identifier = ``; - - const body = `${bot_comment_identifier} - - [Playground](https://svelte.dev/playground?version=pr-${issue_number}) - - \`\`\` - ${packages.map(p => `pnpm add https://pkg.pr.new/${p.name}@${issue_number}`).join('\n')} - \`\`\` - `; - - const comments = await github.rest.issues.listComments({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number, - }); - const existing = comments.data.find(c => c.body.includes(bot_comment_identifier)); - - if (existing) { - await github.rest.issues.updateComment({ - owner: context.repo.owner, - repo: context.repo.repo, - comment_id: existing.id, - body, - }); - } else { - await github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number, - body, - }); - } - - log: - needs: sanitize - if: github.event_name == 'push' - runs-on: ubuntu-latest - - permissions: {} - - steps: - - name: Download sanitized artifact - uses: actions/download-artifact@v7 - with: - name: sanitized-output - - - name: Log publish info - uses: actions/github-script@v8 - with: - script: | - const fs = require('fs'); - const { packages } = JSON.parse(fs.readFileSync('sanitized-output.json', 'utf8')); - - if (packages.length === 0) { - console.log('No valid packages found.'); - return; - } - - console.log('\n' + '='.repeat(50)); - console.log('Publish Information'); - console.log('='.repeat(50)); - for (const p of packages) { - console.log(`${p.name} - pnpm add https://pkg.pr.new/${p.name}@${p.sha}`); - } - const svelte = packages.find(p => p.name === 'svelte'); - if (svelte) { - console.log(`\nPlayground: https://svelte.dev/playground?version=commit-${svelte.sha}`); - } - console.log('='.repeat(50)); diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 359fcb7eea..12fe17582e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -23,13 +23,13 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout Repo - uses: actions/checkout@v6 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 with: # This makes Actions fetch all Git history so that Changesets can generate changelogs with the correct commits fetch-depth: 0 - - uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v4 + - uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6.0.8 - name: Setup Node.js - uses: actions/setup-node@v6 + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6 with: node-version: 24.x cache: pnpm diff --git a/package.json b/package.json index 0d2fbc7fd6..a178d31698 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "private": true, "type": "module", "license": "MIT", - "packageManager": "pnpm@10.4.0", + "packageManager": "pnpm@10.33.4+sha512.1c67b3b359b2d408119ba1ed289f34b8fc3c6873412bec6fd264fbdc82489e510fcbecb9ce9d22dae7f3b76269d8441046014bdca53b9979cd7a561ad631b800", "engines": { "pnpm": ">=9.0.0" }, diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index f94dac7cc7..8c81497078 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,3 +1,15 @@ +minimumReleaseAge: 2880 +minimumReleaseAgeExclude: + - '@sveltejs/*' + - svelte + - esrap + - devalue + - zimmerframe + - prettier-plugin-svelte + - svelte-check + - esm-env +blockExoticSubdeps: true + packages: - 'packages/*' - 'playgrounds/*' From 44a7813730579b94004e182e5a67aab27aa9d2a6 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 25 Jul 2026 00:04:41 +0200 Subject: [PATCH 117/222] Version Packages (#18572) 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.8 ### Patch Changes - fix: call `onerror` and provide a working `reset` when hydrating a failed boundary ([#18556](https://github.com/sveltejs/svelte/pull/18556)) - fix: preserve select selection when spread attributes omit value ([#18561](https://github.com/sveltejs/svelte/pull/18561)) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .changeset/hydrated-boundary-reset.md | 5 ----- .changeset/tidy-select-spreads.md | 5 ----- packages/svelte/CHANGELOG.md | 8 ++++++++ packages/svelte/package.json | 2 +- packages/svelte/src/version.js | 2 +- 5 files changed, 10 insertions(+), 12 deletions(-) delete mode 100644 .changeset/hydrated-boundary-reset.md delete mode 100644 .changeset/tidy-select-spreads.md diff --git a/.changeset/hydrated-boundary-reset.md b/.changeset/hydrated-boundary-reset.md deleted file mode 100644 index 799a57fd9b..0000000000 --- a/.changeset/hydrated-boundary-reset.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: call `onerror` and provide a working `reset` when hydrating a failed boundary diff --git a/.changeset/tidy-select-spreads.md b/.changeset/tidy-select-spreads.md deleted file mode 100644 index 3b14dc5d34..0000000000 --- a/.changeset/tidy-select-spreads.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: preserve select selection when spread attributes omit value diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index a9b93f5341..722fc21052 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,13 @@ # svelte +## 5.56.8 + +### Patch Changes + +- fix: call `onerror` and provide a working `reset` when hydrating a failed boundary ([#18556](https://github.com/sveltejs/svelte/pull/18556)) + +- fix: preserve select selection when spread attributes omit value ([#18561](https://github.com/sveltejs/svelte/pull/18561)) + ## 5.56.7 ### Patch Changes diff --git a/packages/svelte/package.json b/packages/svelte/package.json index 67f349be71..82dd4faf61 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.56.7", + "version": "5.56.8", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index e9737ec13c..8fb86f3398 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.56.7'; +export const VERSION = '5.56.8'; export const PUBLIC_VERSION = '5'; From 26786e92985842a6d563c67049a43bc5857bf792 Mon Sep 17 00:00:00 2001 From: Sankalp Thakur <31366524+sankalpsthakur@users.noreply.github.com> Date: Sat, 8 Aug 2026 01:07:07 +0530 Subject: [PATCH 118/222] fix: skip controlled each fast path while another batch is pending (#18625) Fixes #18610 ### Problem In async mode, a controlled keyed `{#each}` throws `TypeError: Cannot read properties of undefined (reading 'e')` when its collection becomes empty while an earlier batch is still pending on the same block. The fast path in `pause_effects` cleared `state.items` and then called `destroy_effects`, which walks pending batch keys and reads `state.items.get(key).e`. Those EachItems are still needed for the pending batch (preserved offscreen), so clearing the map makes the dereference throw and aborts the commit mid-flight. ### Fix Only take the controlled-each fast path when `state.pending.size === 0`, so pending batches keep their items until they commit or discard. --- .changeset/async-each-controlled-pending.md | 5 ++ .../src/internal/client/dom/blocks/each.js | 8 ++- .../_config.js | 50 +++++++++++++++++ .../main.svelte | 54 +++++++++++++++++++ 4 files changed, 115 insertions(+), 2 deletions(-) create mode 100644 .changeset/async-each-controlled-pending.md create mode 100644 packages/svelte/tests/runtime-runes/samples/async-each-controlled-empty-pending/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-each-controlled-empty-pending/main.svelte diff --git a/.changeset/async-each-controlled-pending.md b/.changeset/async-each-controlled-pending.md new file mode 100644 index 0000000000..c37f914d13 --- /dev/null +++ b/.changeset/async-each-controlled-pending.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: skip controlled each fast path while another batch is pending diff --git a/packages/svelte/src/internal/client/dom/blocks/each.js b/packages/svelte/src/internal/client/dom/blocks/each.js index 2df1d6ffa1..9a2504f887 100644 --- a/packages/svelte/src/internal/client/dom/blocks/each.js +++ b/packages/svelte/src/internal/client/dom/blocks/each.js @@ -103,8 +103,12 @@ function pause_effects(state, to_destroy, controlled_anchor) { if (remaining === 0) { // If we're in a controlled each block (i.e. the block is the only child of an // element), and we are removing all items, _and_ there are no out transitions, - // we can use the fast path — emptying the element and replacing the anchor - var fast_path = transitions.length === 0 && controlled_anchor !== null; + // we can use the fast path — emptying the element and replacing the anchor. + // Skip the fast path when another batch is still pending on this each block: + // that batch's keys still reference EachItems in `state.items`, which + // `destroy_effects` needs to preserve offscreen (see #18610). + var fast_path = + transitions.length === 0 && controlled_anchor !== null && state.pending.size === 0; if (fast_path) { var anchor = /** @type {Element} */ (controlled_anchor); diff --git a/packages/svelte/tests/runtime-runes/samples/async-each-controlled-empty-pending/_config.js b/packages/svelte/tests/runtime-runes/samples/async-each-controlled-empty-pending/_config.js new file mode 100644 index 0000000000..29d374deec --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-each-controlled-empty-pending/_config.js @@ -0,0 +1,50 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +// Regression for #18610: emptying a controlled keyed {#each} while another +// batch is still pending must not take the fast path that clears state.items +// before destroy_effects walks pending keys. +export default test({ + mode: ['client'], + + async test({ assert, target }) { + await tick(); + + assert.htmlEqual( + target.innerHTML, + ` + + + +

A0/B0

+
12
+ ` + ); + + const [startA, startB, settleB] = target.querySelectorAll('button'); + + // Batch A: add key 9, then block forever on gate A. + startA.click(); + await tick(); + + // Batch B: empty the collection, then block on gate B. + startB.click(); + await tick(); + + // Settle B first so B commits while A is still pending. + // Without the fix this throws reading `.e` of undefined and leaves a/b stuck. + settleB.click(); + await tick(); + + assert.htmlEqual( + target.innerHTML, + ` + + + +

A0/B1

+
+ ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-each-controlled-empty-pending/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-each-controlled-empty-pending/main.svelte new file mode 100644 index 0000000000..68c3937c25 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-each-controlled-empty-pending/main.svelte @@ -0,0 +1,54 @@ + + + + + + +

{a}/{b}

+ + +
+ {#each items as item (item)} + {item} + {/each} +
From 3ed9db4ba78f61792b875b7651755a9c0a018037 Mon Sep 17 00:00:00 2001 From: Simon H <5968653+dummdidumm@users.noreply.github.com> Date: Wed, 12 Aug 2026 17:40:28 +0200 Subject: [PATCH 119/222] fix: don't duplicate comments in attributes (#18636) An attribute with a comment would be printed again atop the following attribute because we didn't take that case into account. --- .changeset/modern-otters-pick.md | 5 +++++ packages/svelte/src/compiler/print/index.js | 18 ++++++++++++------ .../comment-inside-attribute/input.svelte | 7 +++++++ .../comment-inside-attribute/output.svelte | 7 +++++++ 4 files changed, 31 insertions(+), 6 deletions(-) create mode 100644 .changeset/modern-otters-pick.md create mode 100644 packages/svelte/tests/print/samples/comment-inside-attribute/input.svelte create mode 100644 packages/svelte/tests/print/samples/comment-inside-attribute/output.svelte diff --git a/.changeset/modern-otters-pick.md b/.changeset/modern-otters-pick.md new file mode 100644 index 0000000000..de5803e19f --- /dev/null +++ b/.changeset/modern-otters-pick.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: don't duplicate comments in attributes diff --git a/packages/svelte/src/compiler/print/index.js b/packages/svelte/src/compiler/print/index.js index a5fbc96fc8..fe76f31ad3 100644 --- a/packages/svelte/src/compiler/print/index.js +++ b/packages/svelte/src/compiler/print/index.js @@ -82,6 +82,7 @@ function attributes(node, attributes, context, comments) { } const separator = context.new(); + let previous_attribute_end = node.start; const children = attributes.map((attribute) => { const child_context = context.new(); @@ -90,12 +91,16 @@ function attributes(node, attributes, context, comments) { const comment = comments[comment_index]; if (comment.start < attribute.start) { - if (comment.type === 'Line') { - child_context.write('//' + comment.value); - child_context.newline(); - } else { - child_context.write('/*' + comment.value + '*/'); // TODO match indentation? - child_context.append(separator); + // Inside a previous attribute's value can be comments which don't + // advance comment_index, therefore this additional check + if (comment.start >= previous_attribute_end) { + if (comment.type === 'Line') { + child_context.write('//' + comment.value); + child_context.newline(); + } else { + child_context.write('/*' + comment.value + '*/'); // TODO match indentation? + child_context.append(separator); + } } comment_index += 1; @@ -105,6 +110,7 @@ function attributes(node, attributes, context, comments) { } child_context.visit(attribute); + previous_attribute_end = attribute.end; length += child_context.measure() + 1; diff --git a/packages/svelte/tests/print/samples/comment-inside-attribute/input.svelte b/packages/svelte/tests/print/samples/comment-inside-attribute/input.svelte new file mode 100644 index 0000000000..4575abf75a --- /dev/null +++ b/packages/svelte/tests/print/samples/comment-inside-attribute/input.svelte @@ -0,0 +1,7 @@ +` +}); diff --git a/packages/svelte/tests/runtime-runes/samples/derived-destructured-from-derived/main.svelte b/packages/svelte/tests/runtime-runes/samples/derived-destructured-from-derived/main.svelte new file mode 100644 index 0000000000..b1882243f4 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/derived-destructured-from-derived/main.svelte @@ -0,0 +1,8 @@ + + + + From 8835003d41d23751d64e99492b7548ca554abd76 Mon Sep 17 00:00:00 2001 From: Marwan <145616984+marwan562@users.noreply.github.com> Date: Thu, 20 Aug 2026 00:13:06 +0300 Subject: [PATCH 133/222] fix: preserve CSS escape sequences when printing selectors (#18667) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes #18664 `print()` was writing selector names back out verbatim. The parser decodes CSS escape sequences when it reads them — `\31` becomes `1`, `\a` becomes a newline — so the printer produced selectors that either failed to re-parse (`#123`) or silently meant something different (`#line\nbreak` turns into a descendant combinator). A small re-escaper (`escape_identifier`) is now used when printing type, class, id, pseudo, attribute and at-rule names. Also fixes a backslash parser bug in the process. --------- Co-authored-by: Simon Holthausen --- .changeset/calm-foxes-reprint.md | 5 ++ .../src/compiler/phases/1-parse/read/style.js | 3 +- packages/svelte/src/compiler/print/index.js | 76 +++++++++++++++-- .../samples/css-escape-sequences/input.svelte | 25 ++++++ .../css-escape-sequences/output.svelte | 83 +++++++++++++++++++ packages/svelte/tests/print/test.ts | 4 + 6 files changed, 188 insertions(+), 8 deletions(-) create mode 100644 .changeset/calm-foxes-reprint.md create mode 100644 packages/svelte/tests/print/samples/css-escape-sequences/input.svelte create mode 100644 packages/svelte/tests/print/samples/css-escape-sequences/output.svelte diff --git a/.changeset/calm-foxes-reprint.md b/.changeset/calm-foxes-reprint.md new file mode 100644 index 0000000000..08ea964bfe --- /dev/null +++ b/.changeset/calm-foxes-reprint.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: preserve CSS escape sequences when printing selectors \ No newline at end of file diff --git a/packages/svelte/src/compiler/phases/1-parse/read/style.js b/packages/svelte/src/compiler/phases/1-parse/read/style.js index 5eb8c2b3e9..02e6d8d064 100644 --- a/packages/svelte/src/compiler/phases/1-parse/read/style.js +++ b/packages/svelte/src/compiler/phases/1-parse/read/style.js @@ -614,7 +614,8 @@ function read_identifier(parser) { if (char === '\\') { const sequence = parser.match_regex(REGEX_UNICODE_SEQUENCE); if (sequence) { - identifier += String.fromCodePoint(parseInt(sequence.slice(1), 16)); + const character = String.fromCodePoint(parseInt(sequence.slice(1), 16)); + identifier += character === '\\' ? '\\\\' : character; parser.index += sequence.length; } else { identifier += '\\' + parser.template[parser.index + 1]; diff --git a/packages/svelte/src/compiler/print/index.js b/packages/svelte/src/compiler/print/index.js index 8759fe3013..c6964fc0b7 100644 --- a/packages/svelte/src/compiler/print/index.js +++ b/packages/svelte/src/compiler/print/index.js @@ -7,6 +7,68 @@ import { is_void } from '../../utils.js'; /** Threshold for when content should be formatted on separate lines */ const LINE_BREAK_THRESHOLD = 50; +/** Characters that are valid in a CSS identifier without escaping */ +const REGEX_IDENTIFIER_CHAR = /^[a-zA-Z0-9_-]$/; + +/** Hex digits — a backslash followed by one of these is read as a hex escape */ +const REGEX_HEX_DIGIT = /[0-9a-fA-F]/; + +/** + * Re-escape a CSS identifier name so that it prints as valid CSS. + * + * `parse` decodes CSS escape sequences when building the AST — `\31` becomes `1`, + * `\a` becomes a newline — but keeps single-character escapes such as `\.` and + * escaped backslashes intact. When printing we therefore only need to escape the + * characters that would be illegal in a bare identifier: a leading digit, `-` + * followed by a digit, whitespace and control characters, and anything else that + * is not already escaped. + * @param {string} name + */ +function escape_identifier(name) { + let escaped = ''; + let i = 0; + + while (i < name.length) { + const char = name[i]; + + if (char === '\\') { + const next = name.charAt(i + 1); + if (next === '' || REGEX_HEX_DIGIT.test(next)) { + // A literal backslash in a name must itself be escaped: `\5c ` + // re-parses to a backslash, whereas a backslash followed by a hex + // digit (or by nothing) would be read back as a hex escape. + escaped += '\\5c '; + i += 1; + continue; + } + + // Already escaped — copy the backslash and the escaped character as-is. + escaped += '\\' + next; + i += 2; + continue; + } + + const code = /** @type {number} */ (char.codePointAt(0)); + const is_leading_digit = i === 0 && char >= '0' && char <= '9'; + const is_leading_hyphen_digit = + i === 0 && char === '-' && name.charAt(i + 1) >= '0' && name.charAt(i + 1) <= '9'; + + if ( + is_leading_digit || + is_leading_hyphen_digit || + !(REGEX_IDENTIFIER_CHAR.test(char) || code >= 160) + ) { + escaped += `\\${code.toString(16)} `; + } else { + escaped += char; + } + + i += 1; + } + + return escaped; +} + /** * `print` converts a Svelte AST node back into Svelte source code. * It is primarily intended for tools that parse and transform components using the compiler’s modern AST representation. @@ -324,7 +386,7 @@ function css_visitors(comments, js_comments) { return { Atrule(node, context) { - context.write(`@${node.name}`); + context.write(`@${escape_identifier(node.name)}`); const prelude_end = node.block?.start ?? node.end; if (node.prelude || has_comment_before(prelude_end)) { @@ -341,7 +403,7 @@ function css_visitors(comments, js_comments) { }, AttributeSelector(node, context) { - context.write(`[${node.name}`); + context.write(`[${escape_identifier(node.name)}`); if (node.matcher) { context.write(node.matcher); context.write(`"${node.value}"`); @@ -365,7 +427,7 @@ function css_visitors(comments, js_comments) { }, ClassSelector(node, context) { - context.write(`.${node.name}`); + context.write(`.${escape_identifier(node.name)}`); }, ComplexSelector(node, context) { @@ -379,7 +441,7 @@ function css_visitors(comments, js_comments) { }, IdSelector(node, context) { - context.write(`#${node.name}`); + context.write(`#${escape_identifier(node.name)}`); }, NestingSelector(node, context) { @@ -395,7 +457,7 @@ function css_visitors(comments, js_comments) { }, PseudoClassSelector(node, context) { - context.write(`:${node.name}`); + context.write(`:${escape_identifier(node.name)}`); if (node.args) { context.write('('); @@ -409,7 +471,7 @@ function css_visitors(comments, js_comments) { }, PseudoElementSelector(node, context) { - context.write(`::${node.name}`); + context.write(`::${escape_identifier(node.name)}`); if (node.args) { context.write('('); context.visit(node.args); @@ -458,7 +520,7 @@ function css_visitors(comments, js_comments) { }, TypeSelector(node, context) { - context.write(node.name); + context.write(node.name === '*' ? node.name : escape_identifier(node.name)); } }; } diff --git a/packages/svelte/tests/print/samples/css-escape-sequences/input.svelte b/packages/svelte/tests/print/samples/css-escape-sequences/input.svelte new file mode 100644 index 0000000000..db8e3e16a5 --- /dev/null +++ b/packages/svelte/tests/print/samples/css-escape-sequences/input.svelte @@ -0,0 +1,25 @@ +
+ + diff --git a/packages/svelte/tests/print/samples/css-escape-sequences/output.svelte b/packages/svelte/tests/print/samples/css-escape-sequences/output.svelte new file mode 100644 index 0000000000..532d0a2562 --- /dev/null +++ b/packages/svelte/tests/print/samples/css-escape-sequences/output.svelte @@ -0,0 +1,83 @@ +
+ + diff --git a/packages/svelte/tests/print/test.ts b/packages/svelte/tests/print/test.ts index aa007a7a54..4caecf90ae 100644 --- a/packages/svelte/tests/print/test.ts +++ b/packages/svelte/tests/print/test.ts @@ -12,6 +12,10 @@ const { test, run } = suite(async (config, cwd) => { const output = print(ast); const outputCode = output.code.endsWith('\n') ? output.code : output.code + '\n'; + // the printed output must itself be valid Svelte — `print` should never emit + // code that `parse` cannot read back (e.g. CSS escape sequences must round-trip) + parse(outputCode, { modern: true }); + // run `UPDATE_SNAPSHOTS=true pnpm test print` to update print tests if (process.env.UPDATE_SNAPSHOTS) { fs.writeFileSync(`${cwd}/output.svelte`, outputCode); From 19aa51d443474fd89592a55c3d3aa0ccc5c240e2 Mon Sep 17 00:00:00 2001 From: Madan kumar Date: Thu, 20 Aug 2026 02:46:23 +0530 Subject: [PATCH 134/222] fix: print `{#await ... catch}` et al correctly (#18645) needs to check the error value, not the then value --------- Co-authored-by: Simon H <5968653+dummdidumm@users.noreply.github.com> --- .changeset/eighty-pugs-shave.md | 5 ++++ packages/svelte/src/compiler/print/index.js | 2 +- .../print/samples/await-block/input.svelte | 26 +++++++++++++++++++ .../print/samples/await-block/output.svelte | 26 +++++++++++++++++++ 4 files changed, 58 insertions(+), 1 deletion(-) create mode 100644 .changeset/eighty-pugs-shave.md diff --git a/.changeset/eighty-pugs-shave.md b/.changeset/eighty-pugs-shave.md new file mode 100644 index 0000000000..0f0eaf9a6c --- /dev/null +++ b/.changeset/eighty-pugs-shave.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: correctly print `{#await ... catch x}` et al diff --git a/packages/svelte/src/compiler/print/index.js b/packages/svelte/src/compiler/print/index.js index c6964fc0b7..a3e10085de 100644 --- a/packages/svelte/src/compiler/print/index.js +++ b/packages/svelte/src/compiler/print/index.js @@ -763,7 +763,7 @@ const svelte_visitors = (comments, state) => ({ } if (node.catch) { - context.write(node.value ? 'catch ' : 'catch'); + context.write(node.error ? 'catch ' : 'catch'); if (node.error) context.visit(node.error); context.write('}'); diff --git a/packages/svelte/tests/print/samples/await-block/input.svelte b/packages/svelte/tests/print/samples/await-block/input.svelte index 10f0b3fb9e..ba0ba8c882 100644 --- a/packages/svelte/tests/print/samples/await-block/input.svelte +++ b/packages/svelte/tests/print/samples/await-block/input.svelte @@ -8,3 +8,29 @@

Something went wrong: {error.message}

{/await} + +{#await promise catch error} +

The error is {error}

+{/await} + +{#await promise then value} +

The value is {value}

+{/await} + +{#await promise catch} +

Something went wrong

+{/await} + +{#await promise} +

waiting for the promise to resolve...

+{:then} +

the promise resolved

+{:catch error} +

The error is {error}

+{/await} + +{#await promise} +

waiting for the promise to resolve...

+{:catch} +

Something went wrong

+{/await} diff --git a/packages/svelte/tests/print/samples/await-block/output.svelte b/packages/svelte/tests/print/samples/await-block/output.svelte index 10f0b3fb9e..ba0ba8c882 100644 --- a/packages/svelte/tests/print/samples/await-block/output.svelte +++ b/packages/svelte/tests/print/samples/await-block/output.svelte @@ -8,3 +8,29 @@

Something went wrong: {error.message}

{/await} + +{#await promise catch error} +

The error is {error}

+{/await} + +{#await promise then value} +

The value is {value}

+{/await} + +{#await promise catch} +

Something went wrong

+{/await} + +{#await promise} +

waiting for the promise to resolve...

+{:then} +

the promise resolved

+{:catch error} +

The error is {error}

+{/await} + +{#await promise} +

waiting for the promise to resolve...

+{:catch} +

Something went wrong

+{/await} From 9c2494b134717141186887fab011972b9293d4ea Mon Sep 17 00:00:00 2001 From: "svelte-triage-bot[bot]" <316883489+svelte-triage-bot[bot]@users.noreply.github.com> Date: Thu, 20 Aug 2026 10:42:04 +0200 Subject: [PATCH 135/222] fix: preserve namespaces in CSS type selectors (#18678) Fixes #18677. - preserve namespace prefixes on CSS `TypeSelector` AST nodes - accept wildcard local names in `svg|*` and `*|*` - retain namespaces when printing modern ASTs - keep namespaced universal selectors intact while adding scoped CSS selectors - add compile and print regressions covering all four namespace forms --------- Co-authored-by: svelte-triage-bot Co-authored-by: Simon H <5968653+dummdidumm@users.noreply.github.com> --- .changeset/pink-chairs-matter.md | 5 +++++ .../src/compiler/phases/1-parse/read/style.js | 14 +++++++++---- .../compiler/phases/3-transform/css/index.js | 6 +++++- packages/svelte/src/compiler/print/index.js | 4 ++++ packages/svelte/src/compiler/types/css.d.ts | 1 + .../namespaced-type-selector/expected.css | 17 +++++++++++++++ .../namespaced-type-selector/input.svelte | 21 +++++++++++++++++++ .../css-namespaced-type-selector/input.svelte | 10 +++++++++ .../output.svelte | 12 +++++++++++ packages/svelte/types/index.d.ts | 1 + 10 files changed, 86 insertions(+), 5 deletions(-) create mode 100644 .changeset/pink-chairs-matter.md create mode 100644 packages/svelte/tests/css/samples/namespaced-type-selector/expected.css create mode 100644 packages/svelte/tests/css/samples/namespaced-type-selector/input.svelte create mode 100644 packages/svelte/tests/print/samples/css-namespaced-type-selector/input.svelte create mode 100644 packages/svelte/tests/print/samples/css-namespaced-type-selector/output.svelte diff --git a/.changeset/pink-chairs-matter.md b/.changeset/pink-chairs-matter.md new file mode 100644 index 0000000000..6a8a6e75f3 --- /dev/null +++ b/.changeset/pink-chairs-matter.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: preserve namespaces in CSS type selectors diff --git a/packages/svelte/src/compiler/phases/1-parse/read/style.js b/packages/svelte/src/compiler/phases/1-parse/read/style.js index 02e6d8d064..a07d7f6fea 100644 --- a/packages/svelte/src/compiler/phases/1-parse/read/style.js +++ b/packages/svelte/src/compiler/phases/1-parse/read/style.js @@ -204,15 +204,18 @@ function read_selector(parser, inside_pseudo_class = false) { }); } else if (parser.eat('*')) { let name = '*'; + /** @type {string | undefined} */ + let namespace; if (parser.eat('|')) { - // * is the namespace (which we ignore) - name = read_identifier(parser); + namespace = name; + name = parser.eat('*') ? '*' : read_identifier(parser); } relative_selector.selectors.push({ type: 'TypeSelector', name, + ...(namespace !== undefined && { namespace }), start, end: parser.index }); @@ -314,15 +317,18 @@ function read_selector(parser, inside_pseudo_class = false) { }); } else if (!parser.match_regex(REGEX_COMBINATOR)) { let name = read_identifier(parser); + /** @type {string | undefined} */ + let namespace; if (parser.eat('|')) { - // we ignore the namespace when trying to find matching element classes - name = read_identifier(parser); + namespace = name; + name = parser.eat('*') ? '*' : read_identifier(parser); } relative_selector.selectors.push({ type: 'TypeSelector', name, + ...(namespace !== undefined && { namespace }), start, end: parser.index }); diff --git a/packages/svelte/src/compiler/phases/3-transform/css/index.js b/packages/svelte/src/compiler/phases/3-transform/css/index.js index 537ab60a0e..bfeb426493 100644 --- a/packages/svelte/src/compiler/phases/3-transform/css/index.js +++ b/packages/svelte/src/compiler/phases/3-transform/css/index.js @@ -355,7 +355,11 @@ const visitors = { continue; } - if (selector.type === 'TypeSelector' && selector.name === '*') { + if ( + selector.type === 'TypeSelector' && + selector.name === '*' && + selector.namespace === undefined + ) { context.state.code.update(selector.start, selector.end, modifier); } else { context.state.code.appendLeft(selector.end, modifier); diff --git a/packages/svelte/src/compiler/print/index.js b/packages/svelte/src/compiler/print/index.js index a3e10085de..4a78854e1e 100644 --- a/packages/svelte/src/compiler/print/index.js +++ b/packages/svelte/src/compiler/print/index.js @@ -520,6 +520,10 @@ function css_visitors(comments, js_comments) { }, TypeSelector(node, context) { + if (node.namespace !== undefined) { + context.write(node.namespace === '*' ? '*' : escape_identifier(node.namespace)); + context.write('|'); + } context.write(node.name === '*' ? node.name : escape_identifier(node.name)); } }; diff --git a/packages/svelte/src/compiler/types/css.d.ts b/packages/svelte/src/compiler/types/css.d.ts index b0a763b6a1..96b2bc3a7d 100644 --- a/packages/svelte/src/compiler/types/css.d.ts +++ b/packages/svelte/src/compiler/types/css.d.ts @@ -121,6 +121,7 @@ export namespace _CSS { export interface TypeSelector extends BaseNode { type: 'TypeSelector'; name: string; + namespace?: string; } export interface IdSelector extends BaseNode { diff --git a/packages/svelte/tests/css/samples/namespaced-type-selector/expected.css b/packages/svelte/tests/css/samples/namespaced-type-selector/expected.css new file mode 100644 index 0000000000..7672a34dcf --- /dev/null +++ b/packages/svelte/tests/css/samples/namespaced-type-selector/expected.css @@ -0,0 +1,17 @@ + @namespace svg url(http://www.w3.org/2000/svg); + + svg|circle.svelte-xyz { + fill: red; + } + + *|circle.svelte-xyz { + stroke: blue; + } + + svg|*.svelte-xyz { + color: green; + } + + *|*.svelte-xyz { + opacity: 0.5; + } diff --git a/packages/svelte/tests/css/samples/namespaced-type-selector/input.svelte b/packages/svelte/tests/css/samples/namespaced-type-selector/input.svelte new file mode 100644 index 0000000000..17586eab6f --- /dev/null +++ b/packages/svelte/tests/css/samples/namespaced-type-selector/input.svelte @@ -0,0 +1,21 @@ + + + diff --git a/packages/svelte/tests/print/samples/css-namespaced-type-selector/input.svelte b/packages/svelte/tests/print/samples/css-namespaced-type-selector/input.svelte new file mode 100644 index 0000000000..5e170ceb2a --- /dev/null +++ b/packages/svelte/tests/print/samples/css-namespaced-type-selector/input.svelte @@ -0,0 +1,10 @@ + diff --git a/packages/svelte/tests/print/samples/css-namespaced-type-selector/output.svelte b/packages/svelte/tests/print/samples/css-namespaced-type-selector/output.svelte new file mode 100644 index 0000000000..3723659b14 --- /dev/null +++ b/packages/svelte/tests/print/samples/css-namespaced-type-selector/output.svelte @@ -0,0 +1,12 @@ + + + diff --git a/packages/svelte/types/index.d.ts b/packages/svelte/types/index.d.ts index b89f61959f..aab3f4c0b4 100644 --- a/packages/svelte/types/index.d.ts +++ b/packages/svelte/types/index.d.ts @@ -1774,6 +1774,7 @@ declare module 'svelte/compiler' { export interface TypeSelector extends BaseNode { type: 'TypeSelector'; name: string; + namespace?: string; } export interface IdSelector extends BaseNode { From ee1249b43cb702d1ea97274ba7849fb8153bd4e2 Mon Sep 17 00:00:00 2001 From: Nathan Asdarjian Date: Thu, 20 Aug 2026 03:02:07 -0700 Subject: [PATCH 136/222] fix: preserve renderer type in copy() during SSR (#18616) Fixes #18404 `` combined with two or more components that each use `bind:` on a child component's prop corrupts a renderer's `.type` during SSR, misplacing the head hydration marker in `` instead of ``. Reason: `copy()` (`packages/svelte/src/internal/server/renderer.js`) rebuilds via `new Renderer(this.global, this.#parent)`. That constructor defaults `.type` from the *parent's* current type, not from `this.type`. To fix it we therefore reassign it (like already do for the boundary elsewhere) --- .changeset/hydration-head-renderer-type.md | 5 +++++ packages/svelte/src/internal/server/renderer.js | 1 + .../head-with-multiple-binding-components/Foo.svelte | 3 +++ .../Wrapper.svelte | 6 ++++++ .../_expected.html | 0 .../_expected_head.html | 4 ++++ .../main.svelte | 12 ++++++++++++ 7 files changed, 31 insertions(+) create mode 100644 .changeset/hydration-head-renderer-type.md create mode 100644 packages/svelte/tests/server-side-rendering/samples/head-with-multiple-binding-components/Foo.svelte create mode 100644 packages/svelte/tests/server-side-rendering/samples/head-with-multiple-binding-components/Wrapper.svelte create mode 100644 packages/svelte/tests/server-side-rendering/samples/head-with-multiple-binding-components/_expected.html create mode 100644 packages/svelte/tests/server-side-rendering/samples/head-with-multiple-binding-components/_expected_head.html create mode 100644 packages/svelte/tests/server-side-rendering/samples/head-with-multiple-binding-components/main.svelte diff --git a/.changeset/hydration-head-renderer-type.md b/.changeset/hydration-head-renderer-type.md new file mode 100644 index 0000000000..bc566c158d --- /dev/null +++ b/.changeset/hydration-head-renderer-type.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: don't corrupt renderer type during SSR's legacy `bind:` retry loop diff --git a/packages/svelte/src/internal/server/renderer.js b/packages/svelte/src/internal/server/renderer.js index 35aac64721..ba6a361a90 100644 --- a/packages/svelte/src/internal/server/renderer.js +++ b/packages/svelte/src/internal/server/renderer.js @@ -451,6 +451,7 @@ export class Renderer { */ copy() { const copy = new Renderer(this.global, this.#parent); + copy.type = this.type; copy.#out = this.#out.map((item) => (item instanceof Renderer ? item.copy() : item)); copy.promise = this.promise; return copy; diff --git a/packages/svelte/tests/server-side-rendering/samples/head-with-multiple-binding-components/Foo.svelte b/packages/svelte/tests/server-side-rendering/samples/head-with-multiple-binding-components/Foo.svelte new file mode 100644 index 0000000000..b991cdf968 --- /dev/null +++ b/packages/svelte/tests/server-side-rendering/samples/head-with-multiple-binding-components/Foo.svelte @@ -0,0 +1,3 @@ + diff --git a/packages/svelte/tests/server-side-rendering/samples/head-with-multiple-binding-components/Wrapper.svelte b/packages/svelte/tests/server-side-rendering/samples/head-with-multiple-binding-components/Wrapper.svelte new file mode 100644 index 0000000000..d48c90fe59 --- /dev/null +++ b/packages/svelte/tests/server-side-rendering/samples/head-with-multiple-binding-components/Wrapper.svelte @@ -0,0 +1,6 @@ + + + diff --git a/packages/svelte/tests/server-side-rendering/samples/head-with-multiple-binding-components/_expected.html b/packages/svelte/tests/server-side-rendering/samples/head-with-multiple-binding-components/_expected.html new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/svelte/tests/server-side-rendering/samples/head-with-multiple-binding-components/_expected_head.html b/packages/svelte/tests/server-side-rendering/samples/head-with-multiple-binding-components/_expected_head.html new file mode 100644 index 0000000000..904fa1d9eb --- /dev/null +++ b/packages/svelte/tests/server-side-rendering/samples/head-with-multiple-binding-components/_expected_head.html @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/packages/svelte/tests/server-side-rendering/samples/head-with-multiple-binding-components/main.svelte b/packages/svelte/tests/server-side-rendering/samples/head-with-multiple-binding-components/main.svelte new file mode 100644 index 0000000000..b68ccd07fe --- /dev/null +++ b/packages/svelte/tests/server-side-rendering/samples/head-with-multiple-binding-components/main.svelte @@ -0,0 +1,12 @@ + + + + + + + + + + From a4c60ccdbb4b68469444af216bdf5f565ddc2916 Mon Sep 17 00:00:00 2001 From: Sander Machado Date: Thu, 20 Aug 2026 14:48:02 +0200 Subject: [PATCH 137/222] fix: append_styles resolving to `document.head` in WC (#18614) Fixes #18288 Check the surrounding branch effect's start node to retrieve the correct root node instead of just the anchor, since the latter could come from each.js/branch.js and be a text node that is never going to get connected --------- Co-authored-by: Simon H <5968653+dummdidumm@users.noreply.github.com> Co-authored-by: Simon Holthausen --- .changeset/free-signs-ask.md | 5 ++++ .../svelte/src/internal/client/dom/css.js | 7 +++++- .../deferred-nested-styles/Child.svelte | 7 ++++++ .../deferred-nested-styles/_config.js | 24 +++++++++++++++++++ .../deferred-nested-styles/main.svelte | 18 ++++++++++++++ 5 files changed, 60 insertions(+), 1 deletion(-) create mode 100644 .changeset/free-signs-ask.md create mode 100644 packages/svelte/tests/runtime-browser/custom-elements-samples/deferred-nested-styles/Child.svelte create mode 100644 packages/svelte/tests/runtime-browser/custom-elements-samples/deferred-nested-styles/_config.js create mode 100644 packages/svelte/tests/runtime-browser/custom-elements-samples/deferred-nested-styles/main.svelte diff --git a/.changeset/free-signs-ask.md b/.changeset/free-signs-ask.md new file mode 100644 index 0000000000..7e6c925526 --- /dev/null +++ b/.changeset/free-signs-ask.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: reliably resolve append_style to its correct root diff --git a/packages/svelte/src/internal/client/dom/css.js b/packages/svelte/src/internal/client/dom/css.js index 74bf2d49f9..23f4eff4f7 100644 --- a/packages/svelte/src/internal/client/dom/css.js +++ b/packages/svelte/src/internal/client/dom/css.js @@ -2,14 +2,19 @@ import { DEV } from 'esm-env'; import { register_style } from '../dev/css.js'; import { effect } from '../reactivity/effects.js'; import { create_element } from './operations.js'; +import { active_effect } from '../runtime.js'; /** * @param {Node} anchor * @param {{ hash: string, code: string }} css */ export function append_styles(anchor, css) { - // Use `queue_micro_task` to ensure `anchor` is in the DOM, otherwise getRootNode() will yield wrong results + // Use an effect to ensure `anchor` is in the DOM, otherwise getRootNode() will yield wrong results effect(() => { + // Bit of a hack: branches.js/each.js use offscreen fragments with temporary text nodes that will + // never be connected to the real dom. Therfore walk up to the branch that has created the component + // whose styles we want to append, and check its node instead. It will be connected by the time we get here. + anchor = active_effect?.parent?.nodes?.start ?? anchor; var root = anchor.getRootNode(); var target = /** @type {ShadowRoot} */ (root).host diff --git a/packages/svelte/tests/runtime-browser/custom-elements-samples/deferred-nested-styles/Child.svelte b/packages/svelte/tests/runtime-browser/custom-elements-samples/deferred-nested-styles/Child.svelte new file mode 100644 index 0000000000..86064ab3b1 --- /dev/null +++ b/packages/svelte/tests/runtime-browser/custom-elements-samples/deferred-nested-styles/Child.svelte @@ -0,0 +1,7 @@ +

child

+ + diff --git a/packages/svelte/tests/runtime-browser/custom-elements-samples/deferred-nested-styles/_config.js b/packages/svelte/tests/runtime-browser/custom-elements-samples/deferred-nested-styles/_config.js new file mode 100644 index 0000000000..739921ebf5 --- /dev/null +++ b/packages/svelte/tests/runtime-browser/custom-elements-samples/deferred-nested-styles/_config.js @@ -0,0 +1,24 @@ +import { assert_ok, test } from '../../assert'; + +const tick = () => Promise.resolve(); + +export default test({ + async test({ assert, target }) { + target.innerHTML = ''; + + // wait for the initial mount, the `onMount` reveal and the deferred re-render + await tick(); + await tick(); + await tick(); + await tick(); + + /** @type {any} */ + const el = target.querySelector('my-app'); + const p = el.shadowRoot.querySelector('p'); + assert_ok(p); + + // The child's scoped styles must be injected into the shadow root, not `document.head` + assert_ok(el.shadowRoot.querySelector('style')); + assert.equal(getComputedStyle(p).color, 'rgb(255, 0, 0)'); + } +}); diff --git a/packages/svelte/tests/runtime-browser/custom-elements-samples/deferred-nested-styles/main.svelte b/packages/svelte/tests/runtime-browser/custom-elements-samples/deferred-nested-styles/main.svelte new file mode 100644 index 0000000000..2bd0456fc1 --- /dev/null +++ b/packages/svelte/tests/runtime-browser/custom-elements-samples/deferred-nested-styles/main.svelte @@ -0,0 +1,18 @@ + + + + +{#each items as item (item)}{/each} From a166761b041922978b35126f60a7b4092fcc1148 Mon Sep 17 00:00:00 2001 From: Magnar Ovedal Myrtveit Date: Thu, 20 Aug 2026 15:15:27 +0200 Subject: [PATCH 138/222] fix: treat concise arrow function bodies as implicit returns when calculating blockers (#18613) Fixes #18612. A concise arrow body does not contain a `ReturnStatement` which we traverse in full calculate_blockers (else we bail on functions), so short-cut to `touch` there. --------- Co-authored-by: Simon Holthausen --- .changeset/light-pandas-attack.md | 5 +++++ .../svelte/src/compiler/phases/2-analyze/index.js | 15 +++++++++------ .../async-bind-factory-function-remote/_config.js | 4 ++-- .../main.svelte | 5 +++++ 4 files changed, 21 insertions(+), 8 deletions(-) create mode 100644 .changeset/light-pandas-attack.md diff --git a/.changeset/light-pandas-attack.md b/.changeset/light-pandas-attack.md new file mode 100644 index 0000000000..e6eface376 --- /dev/null +++ b/.changeset/light-pandas-attack.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: treat concise arrow function bodies as implicit returns when calculating blockers diff --git a/packages/svelte/src/compiler/phases/2-analyze/index.js b/packages/svelte/src/compiler/phases/2-analyze/index.js index 67e9030188..d81053a08d 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/index.js +++ b/packages/svelte/src/compiler/phases/2-analyze/index.js @@ -1235,12 +1235,15 @@ function calculate_blockers(instance, analysis) { ? /** @type {ESTree.FunctionExpression | ESTree.ArrowFunctionExpression} */ (fn.init) : fn; - trace_references( - init.body, - reads_writes, - reads_writes, - /** @type {Scope} */ (instance.scopes.get(init)) - ); + const fn_scope = /** @type {Scope} */ (instance.scopes.get(init)); + + if (init.body.type === 'BlockStatement') { + trace_references(init.body, reads_writes, reads_writes, fn_scope); + } else { + // A concise arrow body is an implicit return, so treat it like the + // `ReturnStatement` visitor in `trace_references` would. + touch(init.body, fn_scope, reads_writes); + } const max = [...reads_writes].reduce((max, binding) => { if (binding.blocker) { diff --git a/packages/svelte/tests/runtime-runes/samples/async-bind-factory-function-remote/_config.js b/packages/svelte/tests/runtime-runes/samples/async-bind-factory-function-remote/_config.js index 080e2d278c..60e58b27a1 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-bind-factory-function-remote/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/async-bind-factory-function-remote/_config.js @@ -3,12 +3,12 @@ import { test } from '../../test'; export default test({ mode: ['async-server', 'client', 'hydrate'], - ssrHtml: 'true true true true true', + ssrHtml: 'true true true true true true', async test({ assert, target }) { await new Promise((resolve) => setTimeout(resolve, 10)); await tick(); - assert.htmlEqual(target.innerHTML, 'true true true true true'); + assert.htmlEqual(target.innerHTML, 'true true true true true true'); } }); diff --git a/packages/svelte/tests/runtime-runes/samples/async-bind-factory-function-remote/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-bind-factory-function-remote/main.svelte index 5f79a14830..4f8f2f97b8 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-bind-factory-function-remote/main.svelte +++ b/packages/svelte/tests/runtime-runes/samples/async-bind-factory-function-remote/main.svelte @@ -21,6 +21,8 @@ const indirect = () => checkedFactory()(); return indirect; } + + const arrow = () => () => checked; @@ -39,3 +41,6 @@ {#if true} {indirectChecked2()()} {/if} +{#if true} + {arrow()()} +{/if} From 24130c18e210dac351710fcdeb878235d2449bd9 Mon Sep 17 00:00:00 2001 From: kdelay <90545043+kdelay@users.noreply.github.com> Date: Thu, 20 Aug 2026 22:16:47 +0900 Subject: [PATCH 139/222] fix: parse nth-child `of` syntax without whitespace after `of` (#18611) Fixes #18609 A ` of ` CSS statement does not need whitespace after `of` if it's followed by a CSS-known syntax (like a `.` which marks a class identifier). Adjust the regex accordingly. --- .changeset/chilled-lions-parse.md | 5 + .../src/compiler/phases/1-parse/read/style.js | 5 +- .../samples/css-nth-of-minified/input.svelte | 28 + .../samples/css-nth-of-minified/output.json | 799 ++++++++++++++++++ 4 files changed, 836 insertions(+), 1 deletion(-) create mode 100644 .changeset/chilled-lions-parse.md create mode 100644 packages/svelte/tests/parser-modern/samples/css-nth-of-minified/input.svelte create mode 100644 packages/svelte/tests/parser-modern/samples/css-nth-of-minified/output.json diff --git a/.changeset/chilled-lions-parse.md b/.changeset/chilled-lions-parse.md new file mode 100644 index 0000000000..1f2e4a0937 --- /dev/null +++ b/.changeset/chilled-lions-parse.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: parse `:nth-child(2n of.foo)` where `of` is not followed by whitespace diff --git a/packages/svelte/src/compiler/phases/1-parse/read/style.js b/packages/svelte/src/compiler/phases/1-parse/read/style.js index a07d7f6fea..df036d0d33 100644 --- a/packages/svelte/src/compiler/phases/1-parse/read/style.js +++ b/packages/svelte/src/compiler/phases/1-parse/read/style.js @@ -7,8 +7,11 @@ const REGEX_CLOSING_BRACKET = /[\s\]]/; const REGEX_ATTRIBUTE_FLAGS = /[a-zA-Z]+/y; // only `i` and `s` are valid today, but make it future-proof const REGEX_COMBINATOR = /(\+|~|>|\|\|)/y; const REGEX_PERCENTAGE = /\d+(\.\d+)?%/y; +// `of` must be preceded by whitespace, otherwise it would be part of the `` token +// (`2nof` is a single dimension token). It does not need to be followed by whitespace, +// because a `.`, `#`, `[`, `*`, `:` or `&` already ends the `of` identifier — minifiers rely on that const REGEX_NTH_OF = - /(even|odd|\+?(\d+|\d*n(\s*[+-]\s*\d+)?)|-\d*n(\s*\+\s*\d+))((?=\s*[,)])|\s+of\s+)/y; + /(even|odd|\+?(\d+|\d*n(\s*[+-]\s*\d+)?)|-\d*n(\s*\+\s*\d+))((?=\s*[,)])|\s+of(\s+|(?=[.#[*:&])))/y; const REGEX_WHITESPACE_OR_COLON = /[\s:]/; const REGEX_LEADING_HYPHEN_OR_DIGIT = /-?\d/y; const REGEX_VALID_IDENTIFIER_CHAR = /[a-zA-Z0-9_-]/; diff --git a/packages/svelte/tests/parser-modern/samples/css-nth-of-minified/input.svelte b/packages/svelte/tests/parser-modern/samples/css-nth-of-minified/input.svelte new file mode 100644 index 0000000000..3aeda2abc7 --- /dev/null +++ b/packages/svelte/tests/parser-modern/samples/css-nth-of-minified/input.svelte @@ -0,0 +1,28 @@ + + +
  • Foo
  • diff --git a/packages/svelte/tests/parser-modern/samples/css-nth-of-minified/output.json b/packages/svelte/tests/parser-modern/samples/css-nth-of-minified/output.json new file mode 100644 index 0000000000..04de9a5e0a --- /dev/null +++ b/packages/svelte/tests/parser-modern/samples/css-nth-of-minified/output.json @@ -0,0 +1,799 @@ +{ + "css": { + "type": "StyleSheet", + "start": 0, + "end": 511, + "attributes": [], + "children": [ + { + "type": "Rule", + "prelude": { + "type": "SelectorList", + "start": 104, + "end": 133, + "children": [ + { + "type": "ComplexSelector", + "start": 104, + "end": 133, + "children": [ + { + "type": "RelativeSelector", + "combinator": null, + "selectors": [ + { + "type": "TypeSelector", + "name": "li", + "start": 104, + "end": 106 + }, + { + "type": "PseudoClassSelector", + "name": "nth-child", + "args": { + "type": "SelectorList", + "start": 117, + "end": 132, + "children": [ + { + "type": "ComplexSelector", + "start": 117, + "end": 132, + "children": [ + { + "type": "RelativeSelector", + "combinator": null, + "selectors": [ + { + "type": "Nth", + "value": "2n of", + "start": 117, + "end": 122 + }, + { + "type": "ClassSelector", + "name": "important", + "start": 122, + "end": 132 + } + ], + "start": 117, + "end": 132 + } + ] + } + ] + }, + "start": 106, + "end": 133 + } + ], + "start": 104, + "end": 133 + } + ] + } + ] + }, + "block": { + "type": "Block", + "start": 134, + "end": 157, + "children": [ + { + "type": "Declaration", + "start": 138, + "end": 153, + "property": "background", + "value": "red" + } + ] + }, + "start": 104, + "end": 157 + }, + { + "type": "Rule", + "prelude": { + "type": "SelectorList", + "start": 159, + "end": 186, + "children": [ + { + "type": "ComplexSelector", + "start": 159, + "end": 186, + "children": [ + { + "type": "RelativeSelector", + "combinator": null, + "selectors": [ + { + "type": "TypeSelector", + "name": "li", + "start": 159, + "end": 161 + }, + { + "type": "PseudoClassSelector", + "name": "nth-child", + "args": { + "type": "SelectorList", + "start": 172, + "end": 185, + "children": [ + { + "type": "ComplexSelector", + "start": 172, + "end": 185, + "children": [ + { + "type": "RelativeSelector", + "combinator": null, + "selectors": [ + { + "type": "Nth", + "value": "even of", + "start": 172, + "end": 179 + }, + { + "type": "IdSelector", + "name": "first", + "start": 179, + "end": 185 + } + ], + "start": 172, + "end": 185 + } + ] + } + ] + }, + "start": 161, + "end": 186 + } + ], + "start": 159, + "end": 186 + } + ] + } + ] + }, + "block": { + "type": "Block", + "start": 187, + "end": 210, + "children": [ + { + "type": "Declaration", + "start": 191, + "end": 206, + "property": "background", + "value": "red" + } + ] + }, + "start": 159, + "end": 210 + }, + { + "type": "Rule", + "prelude": { + "type": "SelectorList", + "start": 212, + "end": 251, + "children": [ + { + "type": "ComplexSelector", + "start": 212, + "end": 251, + "children": [ + { + "type": "RelativeSelector", + "combinator": null, + "selectors": [ + { + "type": "TypeSelector", + "name": "li", + "start": 212, + "end": 214 + }, + { + "type": "PseudoClassSelector", + "name": "nth-last-child", + "args": { + "type": "SelectorList", + "start": 230, + "end": 250, + "children": [ + { + "type": "ComplexSelector", + "start": 230, + "end": 250, + "children": [ + { + "type": "RelativeSelector", + "combinator": null, + "selectors": [ + { + "type": "Nth", + "value": "-n+3 of", + "start": 230, + "end": 237 + }, + { + "type": "AttributeSelector", + "start": 237, + "end": 250, + "name": "data-active", + "matcher": null, + "value": null, + "flags": null + } + ], + "start": 230, + "end": 250 + } + ] + } + ] + }, + "start": 214, + "end": 251 + } + ], + "start": 212, + "end": 251 + } + ] + } + ] + }, + "block": { + "type": "Block", + "start": 252, + "end": 275, + "children": [ + { + "type": "Declaration", + "start": 256, + "end": 271, + "property": "background", + "value": "red" + } + ] + }, + "start": 212, + "end": 275 + }, + { + "type": "Rule", + "prelude": { + "type": "SelectorList", + "start": 277, + "end": 297, + "children": [ + { + "type": "ComplexSelector", + "start": 277, + "end": 297, + "children": [ + { + "type": "RelativeSelector", + "combinator": null, + "selectors": [ + { + "type": "TypeSelector", + "name": "li", + "start": 277, + "end": 279 + }, + { + "type": "PseudoClassSelector", + "name": "nth-child", + "args": { + "type": "SelectorList", + "start": 290, + "end": 296, + "children": [ + { + "type": "ComplexSelector", + "start": 290, + "end": 296, + "children": [ + { + "type": "RelativeSelector", + "combinator": null, + "selectors": [ + { + "type": "Nth", + "value": "2n of", + "start": 290, + "end": 295 + }, + { + "type": "TypeSelector", + "name": "*", + "start": 295, + "end": 296 + } + ], + "start": 290, + "end": 296 + } + ] + } + ] + }, + "start": 279, + "end": 297 + } + ], + "start": 277, + "end": 297 + } + ] + } + ] + }, + "block": { + "type": "Block", + "start": 298, + "end": 321, + "children": [ + { + "type": "Declaration", + "start": 302, + "end": 317, + "property": "background", + "value": "red" + } + ] + }, + "start": 277, + "end": 321 + }, + { + "type": "Rule", + "prelude": { + "type": "SelectorList", + "start": 323, + "end": 357, + "children": [ + { + "type": "ComplexSelector", + "start": 323, + "end": 357, + "children": [ + { + "type": "RelativeSelector", + "combinator": null, + "selectors": [ + { + "type": "TypeSelector", + "name": "li", + "start": 323, + "end": 325 + }, + { + "type": "PseudoClassSelector", + "name": "nth-child", + "args": { + "type": "SelectorList", + "start": 336, + "end": 356, + "children": [ + { + "type": "ComplexSelector", + "start": 336, + "end": 356, + "children": [ + { + "type": "RelativeSelector", + "combinator": null, + "selectors": [ + { + "type": "Nth", + "value": "2n of", + "start": 336, + "end": 341 + }, + { + "type": "PseudoClassSelector", + "name": "not", + "args": { + "type": "SelectorList", + "start": 346, + "end": 355, + "children": [ + { + "type": "ComplexSelector", + "start": 346, + "end": 355, + "children": [ + { + "type": "RelativeSelector", + "combinator": null, + "selectors": [ + { + "type": "ClassSelector", + "name": "excluded", + "start": 346, + "end": 355 + } + ], + "start": 346, + "end": 355 + } + ] + } + ] + }, + "start": 341, + "end": 356 + } + ], + "start": 336, + "end": 356 + } + ] + } + ] + }, + "start": 325, + "end": 357 + } + ], + "start": 323, + "end": 357 + } + ] + } + ] + }, + "block": { + "type": "Block", + "start": 358, + "end": 381, + "children": [ + { + "type": "Declaration", + "start": 362, + "end": 377, + "property": "background", + "value": "red" + } + ] + }, + "start": 323, + "end": 381 + }, + { + "type": "Rule", + "prelude": { + "type": "SelectorList", + "start": 383, + "end": 408, + "children": [ + { + "type": "ComplexSelector", + "start": 383, + "end": 408, + "children": [ + { + "type": "RelativeSelector", + "combinator": null, + "selectors": [ + { + "type": "TypeSelector", + "name": "li", + "start": 383, + "end": 385 + }, + { + "type": "PseudoClassSelector", + "name": "nth-child", + "args": { + "type": "SelectorList", + "start": 396, + "end": 407, + "children": [ + { + "type": "ComplexSelector", + "start": 396, + "end": 403, + "children": [ + { + "type": "RelativeSelector", + "combinator": null, + "selectors": [ + { + "type": "Nth", + "value": "2n of", + "start": 396, + "end": 401 + }, + { + "type": "ClassSelector", + "name": "a", + "start": 401, + "end": 403 + } + ], + "start": 396, + "end": 403 + } + ] + }, + { + "type": "ComplexSelector", + "start": 405, + "end": 407, + "children": [ + { + "type": "RelativeSelector", + "combinator": null, + "selectors": [ + { + "type": "ClassSelector", + "name": "b", + "start": 405, + "end": 407 + } + ], + "start": 405, + "end": 407 + } + ] + } + ] + }, + "start": 385, + "end": 408 + } + ], + "start": 383, + "end": 408 + } + ] + } + ] + }, + "block": { + "type": "Block", + "start": 409, + "end": 432, + "children": [ + { + "type": "Declaration", + "start": 413, + "end": 428, + "property": "background", + "value": "red" + } + ] + }, + "start": 383, + "end": 432 + }, + { + "type": "Rule", + "prelude": { + "type": "SelectorList", + "start": 434, + "end": 436, + "children": [ + { + "type": "ComplexSelector", + "start": 434, + "end": 436, + "children": [ + { + "type": "RelativeSelector", + "combinator": null, + "selectors": [ + { + "type": "TypeSelector", + "name": "ul", + "start": 434, + "end": 436 + } + ], + "start": 434, + "end": 436 + } + ] + } + ] + }, + "block": { + "type": "Block", + "start": 437, + "end": 502, + "children": [ + { + "type": "Rule", + "prelude": { + "type": "SelectorList", + "start": 441, + "end": 473, + "children": [ + { + "type": "ComplexSelector", + "start": 441, + "end": 473, + "children": [ + { + "type": "RelativeSelector", + "combinator": null, + "selectors": [ + { + "type": "NestingSelector", + "name": "&", + "start": 441, + "end": 442 + } + ], + "start": 441, + "end": 442 + }, + { + "type": "RelativeSelector", + "combinator": { + "type": "Combinator", + "name": " ", + "start": 442, + "end": 443 + }, + "selectors": [ + { + "type": "TypeSelector", + "name": "li", + "start": 443, + "end": 445 + }, + { + "type": "PseudoClassSelector", + "name": "nth-child", + "args": { + "type": "SelectorList", + "start": 456, + "end": 472, + "children": [ + { + "type": "ComplexSelector", + "start": 456, + "end": 472, + "children": [ + { + "type": "RelativeSelector", + "combinator": null, + "selectors": [ + { + "type": "Nth", + "value": "2n of", + "start": 456, + "end": 461 + }, + { + "type": "NestingSelector", + "name": "&", + "start": 461, + "end": 462 + }, + { + "type": "ClassSelector", + "name": "important", + "start": 462, + "end": 472 + } + ], + "start": 456, + "end": 472 + } + ] + } + ] + }, + "start": 445, + "end": 473 + } + ], + "start": 442, + "end": 473 + } + ] + } + ] + }, + "block": { + "type": "Block", + "start": 474, + "end": 499, + "children": [ + { + "type": "Declaration", + "start": 479, + "end": 494, + "property": "background", + "value": "red" + } + ] + }, + "start": 441, + "end": 499 + } + ] + }, + "start": 434, + "end": 502 + } + ], + "comments": [ + { + "type": "CSSComment", + "value": " minifiers drop the whitespace after `of`, since the following token ends the identifier ", + "start": 9, + "end": 102 + } + ], + "content": { + "start": 7, + "end": 503, + "styles": "\n\t/* minifiers drop the whitespace after `of`, since the following token ends the identifier */\n\tli:nth-child(2n of.important) {\n\t\tbackground: red;\n\t}\n\tli:nth-child(even of#first) {\n\t\tbackground: red;\n\t}\n\tli:nth-last-child(-n+3 of[data-active]) {\n\t\tbackground: red;\n\t}\n\tli:nth-child(2n of*) {\n\t\tbackground: red;\n\t}\n\tli:nth-child(2n of:not(.excluded)) {\n\t\tbackground: red;\n\t}\n\tli:nth-child(2n of.a, .b) {\n\t\tbackground: red;\n\t}\n\tul {\n\t\t& li:nth-child(2n of&.important) {\n\t\t\tbackground: red;\n\t\t}\n\t}\n", + "comment": null + } + }, + "js": [], + "start": 0, + "end": 525, + "type": "Root", + "fragment": { + "type": "Fragment", + "nodes": [ + { + "type": "Text", + "start": 511, + "end": 513, + "raw": "\n\n", + "data": "\n\n" + }, + { + "type": "RegularElement", + "start": 513, + "end": 525, + "name": "li", + "name_loc": { + "start": { + "line": 28, + "column": 1, + "character": 514 + }, + "end": { + "line": 28, + "column": 3, + "character": 516 + } + }, + "attributes": [], + "fragment": { + "type": "Fragment", + "nodes": [ + { + "type": "Text", + "start": 517, + "end": 520, + "raw": "Foo", + "data": "Foo" + } + ] + } + } + ] + }, + "options": null, + "comments": [] +} From 388840b1b29d9f02aebc649f705f4a01ca1c4b51 Mon Sep 17 00:00:00 2001 From: Rudevin <101231071+Rudevin17@users.noreply.github.com> Date: Thu, 20 Aug 2026 21:23:11 +0800 Subject: [PATCH 140/222] docs: note that transitions bypass CSS prefers-reduced-motion (#18597) --------- Co-authored-by: Simon H <5968653+dummdidumm@users.noreply.github.com> --- documentation/docs/03-template-syntax/14-transition.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/documentation/docs/03-template-syntax/14-transition.md b/documentation/docs/03-template-syntax/14-transition.md index 9b14b7b25b..5f722259b5 100644 --- a/documentation/docs/03-template-syntax/14-transition.md +++ b/documentation/docs/03-template-syntax/14-transition.md @@ -53,6 +53,12 @@ Transitions can have parameters. {/if} ``` +## Accessibility + +Transitions are driven by the [Web Animations API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Animations_API) rather than by CSS. A global `@media (prefers-reduced-motion: reduce)` rule that zeroes `transition-duration` and `animation-duration` therefore has no effect on them. + +Use [`prefersReducedMotion`](svelte-motion#prefersReducedMotion) to adjust (or completely disable) the transition accordingly for devices who request reduced motion. + ## Custom transition functions ```js From 3feb34a9922bf64610d009d44b0865a7ae88c2ef Mon Sep 17 00:00:00 2001 From: Nic Polumeyv Date: Thu, 20 Aug 2026 09:26:06 -0400 Subject: [PATCH 141/222] chore: deduplicate client/server context helpers (#18580) The server copy of `createContext` shipped without the `missing_context` throw and #17580 had to hand-mirror the client body back in, so this duplication has already cost a bug. This moves the realm-independent parts, the `createContext` tuple, `get_parent_context`, and `get_or_init_context_map`, into `internal/shared/context.js`, and each realm keeps its public context functions as thin wrappers over its own state. --- .changeset/shared-context-helpers.md | 5 ++ .../svelte/src/internal/client/context.js | 52 +++--------------- .../svelte/src/internal/server/context.js | 55 +++---------------- .../svelte/src/internal/shared/context.js | 52 ++++++++++++++++++ 4 files changed, 73 insertions(+), 91 deletions(-) create mode 100644 .changeset/shared-context-helpers.md create mode 100644 packages/svelte/src/internal/shared/context.js diff --git a/.changeset/shared-context-helpers.md b/.changeset/shared-context-helpers.md new file mode 100644 index 0000000000..ecdfcb9a0e --- /dev/null +++ b/.changeset/shared-context-helpers.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +chore: deduplicate client and server context helpers diff --git a/packages/svelte/src/internal/client/context.js b/packages/svelte/src/internal/client/context.js index 0baef5c63e..f4890d0409 100644 --- a/packages/svelte/src/internal/client/context.js +++ b/packages/svelte/src/internal/client/context.js @@ -6,6 +6,7 @@ import { create_user_effect } from './reactivity/effects.js'; import { async_mode_flag, legacy_mode_flag } from '../flags/index.js'; import { FILENAME } from '../../constants.js'; import { BRANCH_EFFECT } from './constants.js'; +import { create_context, get_or_init_context_map } from '../shared/context.js'; /** @type {ComponentContext | null} */ export let component_context = null; @@ -79,18 +80,9 @@ export function set_dev_current_component_function(fn) { * @since 5.40.0 */ export function createContext() { - const key = {}; - - return [ - () => { - if (!hasContext(key)) { - e.missing_context(); - } - - return getContext(key); - }, - (context) => setContext(key, context) - ]; + return /** @type {[() => T, (context: T) => T]} */ ( + create_context(getContext, setContext, hasContext) + ); } /** @@ -104,7 +96,7 @@ export function createContext() { * @returns {T} */ export function getContext(key) { - const context_map = get_or_init_context_map('getContext'); + const context_map = get_or_init_context_map(component_context, 'getContext'); const result = /** @type {T} */ (context_map.get(key)); return result; } @@ -124,7 +116,7 @@ export function getContext(key) { * @returns {T} */ export function setContext(key, context) { - const context_map = get_or_init_context_map('setContext'); + const context_map = get_or_init_context_map(component_context, 'setContext'); if (async_mode_flag) { var flags = /** @type {Effect} */ (active_effect).f; @@ -151,7 +143,7 @@ export function setContext(key, context) { * @returns {boolean} */ export function hasContext(key) { - const context_map = get_or_init_context_map('hasContext'); + const context_map = get_or_init_context_map(component_context, 'hasContext'); return context_map.has(key); } @@ -164,7 +156,7 @@ export function hasContext(key) { * @returns {T} */ export function getAllContexts() { - const context_map = get_or_init_context_map('getAllContexts'); + const context_map = get_or_init_context_map(component_context, 'getAllContexts'); return /** @type {T} */ (context_map); } @@ -229,31 +221,3 @@ export function pop(component) { export function is_runes() { return !legacy_mode_flag || (component_context !== null && component_context.l === null); } - -/** - * @param {string} name - * @returns {Map} - */ -function get_or_init_context_map(name) { - if (component_context === null) { - e.lifecycle_outside_component(name); - } - - return (component_context.c ??= new Map(get_parent_context(component_context) || undefined)); -} - -/** - * @param {ComponentContext} component_context - * @returns {Map | null} - */ -function get_parent_context(component_context) { - let parent = component_context.p; - while (parent !== null) { - const context_map = parent.c; - if (context_map !== null) { - return context_map; - } - parent = parent.p; - } - return null; -} diff --git a/packages/svelte/src/internal/server/context.js b/packages/svelte/src/internal/server/context.js index 6a7dc1f883..de11c11282 100644 --- a/packages/svelte/src/internal/server/context.js +++ b/packages/svelte/src/internal/server/context.js @@ -1,6 +1,6 @@ /** @import { SSRContext } from '#server' */ import { DEV } from 'esm-env'; -import * as e from './errors.js'; +import { create_context, get_or_init_context_map } from '../shared/context.js'; /** @type {SSRContext | null} */ export var ssr_context = null; @@ -16,18 +16,9 @@ export function set_ssr_context(v) { * @since 5.40.0 */ export function createContext() { - const key = {}; - - return [ - () => { - if (!hasContext(key)) { - e.missing_context(); - } - - return getContext(key); - }, - (context) => setContext(key, context) - ]; + return /** @type {[() => T, (context: T) => T]} */ ( + create_context(getContext, setContext, hasContext) + ); } /** @@ -36,7 +27,7 @@ export function createContext() { * @returns {T} */ export function getContext(key) { - const context_map = get_or_init_context_map('getContext'); + const context_map = get_or_init_context_map(ssr_context, 'getContext'); const result = /** @type {T} */ (context_map.get(key)); return result; @@ -49,7 +40,7 @@ export function getContext(key) { * @returns {T} */ export function setContext(key, context) { - get_or_init_context_map('setContext').set(key, context); + get_or_init_context_map(ssr_context, 'setContext').set(key, context); return context; } @@ -58,24 +49,12 @@ export function setContext(key, context) { * @returns {boolean} */ export function hasContext(key) { - return get_or_init_context_map('hasContext').has(key); + return get_or_init_context_map(ssr_context, 'hasContext').has(key); } /** @returns {Map} */ export function getAllContexts() { - return get_or_init_context_map('getAllContexts'); -} - -/** - * @param {string} name - * @returns {Map} - */ -function get_or_init_context_map(name) { - if (ssr_context === null) { - e.lifecycle_outside_component(name); - } - - return (ssr_context.c ??= new Map(get_parent_context(ssr_context) || undefined)); + return get_or_init_context_map(ssr_context, 'getAllContexts'); } /** @@ -94,24 +73,6 @@ export function pop() { ssr_context = /** @type {SSRContext} */ (ssr_context).p; } -/** - * @param {SSRContext} ssr_context - * @returns {Map | null} - */ -function get_parent_context(ssr_context) { - let parent = ssr_context.p; - - while (parent !== null) { - const context_map = parent.c; - if (context_map !== null) { - return context_map; - } - parent = parent.p; - } - - return null; -} - /** * Wraps an `await` expression in such a way that the component context that was * active before the expression evaluated can be reapplied afterwards — diff --git a/packages/svelte/src/internal/shared/context.js b/packages/svelte/src/internal/shared/context.js new file mode 100644 index 0000000000..943e15e415 --- /dev/null +++ b/packages/svelte/src/internal/shared/context.js @@ -0,0 +1,52 @@ +import { lifecycle_outside_component, missing_context } from './errors.js'; + +/** + * @template T + * @param {(key: object) => T} get_context + * @param {(key: object, context: T) => T} set_context + * @param {(key: object) => boolean} has_context + * @returns {[() => T, (context: T) => T]} + */ +export function create_context(get_context, set_context, has_context) { + const key = {}; + + return [ + () => { + if (!has_context(key)) { + missing_context(); + } + + return get_context(key); + }, + (context) => set_context(key, context) + ]; +} + +/** + * @typedef {{ p: Context | null, c: Map | null }} Context + */ + +/** + * @param {Context} context + * @returns {Map | null} + */ +function get_parent_context(context) { + let parent = context.p; + while (parent !== null && parent.c === null) { + parent = parent.p; + } + return parent?.c ?? null; +} + +/** + * @param {Context | null} context + * @param {string} name + * @returns {Map} + */ +export function get_or_init_context_map(context, name) { + if (context === null) { + lifecycle_outside_component(name); + } + + return (context.c ??= new Map(get_parent_context(context) || undefined)); +} From 2f684fefd79f6c9ac7975cc18410ba95319a34d6 Mon Sep 17 00:00:00 2001 From: Nic Polumeyv Date: Thu, 20 Aug 2026 09:45:42 -0400 Subject: [PATCH 142/222] docs: clarify that context lookup includes the current component and all ancestors (#18581) Fixes #7916. Context lookup starts at the current component, not the closest parent. `setContext` followed by `getContext` with the same key in the same component returns the value. --------- Co-authored-by: Simon H <5968653+dummdidumm@users.noreply.github.com> --- .changeset/context-docs-wording.md | 5 +++++ .../98-reference/.generated/shared-errors.md | 4 ++-- .../svelte/messages/shared-errors/errors.md | 4 ++-- .../svelte/src/internal/client/context.js | 21 +++++++++++-------- packages/svelte/src/internal/shared/errors.js | 4 ++-- packages/svelte/types/index.d.ts | 21 +++++++++++-------- 6 files changed, 35 insertions(+), 24 deletions(-) create mode 100644 .changeset/context-docs-wording.md diff --git a/.changeset/context-docs-wording.md b/.changeset/context-docs-wording.md new file mode 100644 index 0000000000..6ec9d47ed9 --- /dev/null +++ b/.changeset/context-docs-wording.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +docs: clarify that context lookup includes the current component and all ancestors diff --git a/documentation/docs/98-reference/.generated/shared-errors.md b/documentation/docs/98-reference/.generated/shared-errors.md index 739bd58b35..d7596fc068 100644 --- a/documentation/docs/98-reference/.generated/shared-errors.md +++ b/documentation/docs/98-reference/.generated/shared-errors.md @@ -75,10 +75,10 @@ Certain lifecycle methods can only be used during component initialisation. To f ### missing_context ``` -Context was not set in a parent component +Context was not set in the current component or any of its ancestors ``` -The [`createContext()`](svelte#createContext) utility returns a `[get, set]` pair of functions. `get` will throw an error if `set` was not used to set the context in a parent component. +The [`createContext()`](svelte#createContext) utility returns a `[get, set]` pair of functions. `get` will throw an error if `set` was not used to set the context in the current component or any of its ancestors. ### snippet_without_render_tag diff --git a/packages/svelte/messages/shared-errors/errors.md b/packages/svelte/messages/shared-errors/errors.md index 3ff474768e..43dca57ac8 100644 --- a/packages/svelte/messages/shared-errors/errors.md +++ b/packages/svelte/messages/shared-errors/errors.md @@ -62,9 +62,9 @@ Certain lifecycle methods can only be used during component initialisation. To f ## missing_context -> Context was not set in a parent component +> Context was not set in the current component or any of its ancestors -The [`createContext()`](svelte#createContext) utility returns a `[get, set]` pair of functions. `get` will throw an error if `set` was not used to set the context in a parent component. +The [`createContext()`](svelte#createContext) utility returns a `[get, set]` pair of functions. `get` will throw an error if `set` was not used to set the context in the current component or any of its ancestors. ## snippet_without_render_tag diff --git a/packages/svelte/src/internal/client/context.js b/packages/svelte/src/internal/client/context.js index f4890d0409..0a7b2cdf31 100644 --- a/packages/svelte/src/internal/client/context.js +++ b/packages/svelte/src/internal/client/context.js @@ -73,7 +73,8 @@ export function set_dev_current_component_function(fn) { /** * Returns a `[get, set]` pair of functions for working with context in a type-safe way. * - * `get` will throw an error if no parent component called `set`. + * `get` will throw an error if `set` has not yet been called in the current component or any of + * its ancestors. * * @template T * @returns {[() => T, (context: T) => T]} @@ -86,7 +87,9 @@ export function createContext() { } /** - * Retrieves the context that belongs to the closest parent component with the specified `key`. + * Retrieves the context set with the specified `key` in the current component or any of its + * ancestors. If multiple components set the same key, the value from the closest one is returned. + * A `setContext` call in the current component is only visible to `getContext` calls that run after it. * Must be called during component initialisation. * * [`createContext`](https://svelte.dev/docs/svelte/svelte#createContext) is a type-safe alternative. @@ -103,8 +106,8 @@ export function getContext(key) { /** * Associates an arbitrary `context` object with the current component and the specified `key` - * and returns that object. The context is then available to children of the component - * (including slotted content) with `getContext`. + * and returns that object. The context is then available to the component itself and all of its + * descendants (including slotted content) with `getContext`. * * Like lifecycle functions, this must be called during component initialisation. * @@ -136,8 +139,8 @@ export function setContext(key, context) { } /** - * Checks whether a given `key` has been set in the context of a parent component. - * Must be called during component initialisation. + * Checks whether a given `key` has been set in the context of the current component or any of + * its ancestors. Must be called during component initialisation. * * @param {any} key * @returns {boolean} @@ -148,9 +151,9 @@ export function hasContext(key) { } /** - * Retrieves the whole context map that belongs to the closest parent component. - * Must be called during component initialisation. Useful, for example, if you - * programmatically create a component and want to pass the existing context to it. + * Retrieves the whole context map that belongs to the current component, including entries + * inherited from its ancestors. Must be called during component initialisation. Useful, for + * example, if you programmatically create a component and want to pass the existing context to it. * * @template {Map} [T=Map] * @returns {T} diff --git a/packages/svelte/src/internal/shared/errors.js b/packages/svelte/src/internal/shared/errors.js index a4ad8cecff..9e41788dc1 100644 --- a/packages/svelte/src/internal/shared/errors.js +++ b/packages/svelte/src/internal/shared/errors.js @@ -86,12 +86,12 @@ export function lifecycle_outside_component(name) { } /** - * Context was not set in a parent component + * Context was not set in the current component or any of its ancestors * @returns {never} */ export function missing_context() { if (DEV) { - const error = new Error(`missing_context\nContext was not set in a parent component\nhttps://svelte.dev/e/missing_context`); + const error = new Error(`missing_context\nContext was not set in the current component or any of its ancestors\nhttps://svelte.dev/e/missing_context`); error.name = 'Svelte error'; diff --git a/packages/svelte/types/index.d.ts b/packages/svelte/types/index.d.ts index aab3f4c0b4..b752ef5e07 100644 --- a/packages/svelte/types/index.d.ts +++ b/packages/svelte/types/index.d.ts @@ -494,13 +494,16 @@ declare module 'svelte' { /** * Returns a `[get, set]` pair of functions for working with context in a type-safe way. * - * `get` will throw an error if no parent component called `set`. + * `get` will throw an error if `set` has not yet been called in the current component or any of + * its ancestors. * * @since 5.40.0 */ export function createContext(): [() => T, (context: T) => T]; /** - * Retrieves the context that belongs to the closest parent component with the specified `key`. + * Retrieves the context set with the specified `key` in the current component or any of its + * ancestors. If multiple components set the same key, the value from the closest one is returned. + * A `setContext` call in the current component is only visible to `getContext` calls that run after it. * Must be called during component initialisation. * * [`createContext`](https://svelte.dev/docs/svelte/svelte#createContext) is a type-safe alternative. @@ -509,8 +512,8 @@ declare module 'svelte' { export function getContext(key: any): T; /** * Associates an arbitrary `context` object with the current component and the specified `key` - * and returns that object. The context is then available to children of the component - * (including slotted content) with `getContext`. + * and returns that object. The context is then available to the component itself and all of its + * descendants (including slotted content) with `getContext`. * * Like lifecycle functions, this must be called during component initialisation. * @@ -519,15 +522,15 @@ declare module 'svelte' { * */ export function setContext(key: any, context: T): T; /** - * Checks whether a given `key` has been set in the context of a parent component. - * Must be called during component initialisation. + * Checks whether a given `key` has been set in the context of the current component or any of + * its ancestors. Must be called during component initialisation. * * */ export function hasContext(key: any): boolean; /** - * Retrieves the whole context map that belongs to the closest parent component. - * Must be called during component initialisation. Useful, for example, if you - * programmatically create a component and want to pass the existing context to it. + * Retrieves the whole context map that belongs to the current component, including entries + * inherited from its ancestors. Must be called during component initialisation. Useful, for + * example, if you programmatically create a component and want to pass the existing context to it. * * */ export function getAllContexts = Map>(): T; From f159e14de767ed333c89d1f42380d80eb2978140 Mon Sep 17 00:00:00 2001 From: "jyc.dev" Date: Thu, 20 Aug 2026 15:59:35 +0200 Subject: [PATCH 143/222] chore: switch to @changesets/changelog-github (#18570) Switch `@svitejs/changesets-changelog-github-compact` to `@changesets/changelog-github` (its new `template` option reproduces the same compact output). --- .changeset/config.json | 2 +- package.json | 2 +- pnpm-lock.yaml | 88 +++++++++++++----------------------------- 3 files changed, 28 insertions(+), 64 deletions(-) diff --git a/.changeset/config.json b/.changeset/config.json index b56077a922..785aa01e70 100644 --- a/.changeset/config.json +++ b/.changeset/config.json @@ -1,6 +1,6 @@ { "$schema": "https://unpkg.com/@changesets/config@2.3.0/schema.json", - "changelog": ["@svitejs/changesets-changelog-github-compact", { "repo": "sveltejs/svelte" }], + "changelog": ["@changesets/changelog-github", { "repo": "sveltejs/svelte", "template": "\n- {summary} {ref}" }], "commit": false, "fixed": [], "linked": [], diff --git a/package.json b/package.json index a178d31698..16fc26bc32 100644 --- a/package.json +++ b/package.json @@ -26,10 +26,10 @@ "bench:debug": "NODE_ENV=production node --allow-natives-syntax --inspect-brk ./benchmarking/run.js" }, "devDependencies": { + "@changesets/changelog-github": "1.0.0-next.6", "@changesets/cli": "^2.29.8", "@eslint/js": "^10.0.0", "@sveltejs/eslint-config": "^9.0.0", - "@svitejs/changesets-changelog-github-compact": "^1.1.0", "@types/node": "^20.11.5", "@types/picomatch": "^4.0.2", "@vitest/coverage-v8": "^4.1.7", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6331fd0789..095d166118 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,6 +8,9 @@ importers: .: devDependencies: + '@changesets/changelog-github': + specifier: 1.0.0-next.6 + version: 1.0.0-next.6 '@changesets/cli': specifier: ^2.29.8 version: 2.29.8(@types/node@20.19.17) @@ -17,9 +20,6 @@ importers: '@sveltejs/eslint-config': specifier: ^9.0.0 version: 9.0.0(@eslint/js@10.0.1(eslint@10.0.0))(@stylistic/eslint-plugin-js@1.8.0(eslint@10.0.0))(eslint-config-prettier@9.1.0(eslint@10.0.0))(eslint-plugin-n@17.24.0(eslint@10.0.0)(typescript@5.5.4))(eslint-plugin-svelte@3.15.0(eslint@10.0.0)(svelte@packages+svelte))(eslint@10.0.0)(typescript-eslint@8.56.0(eslint@10.0.0)(typescript@5.5.4))(typescript@5.5.4) - '@svitejs/changesets-changelog-github-compact': - specifier: ^1.1.0 - version: 1.1.0 '@types/node': specifier: ^20.11.5 version: 20.19.17 @@ -236,6 +236,10 @@ packages: '@changesets/changelog-git@0.2.1': resolution: {integrity: sha512-x/xEleCFLH28c3bQeQIyeZf8lFXyDFVn1SgcBiR2Tw/r4IAWlk1fzxCEZ6NxQAjF2Nwtczoen3OA2qR+UawQ8Q==} + '@changesets/changelog-github@1.0.0-next.6': + resolution: {integrity: sha512-0ShCWgNt50xP0mryAYT8wtSkMUinG8uhxXgCgwHZ4UvJnR2f4ns8OsGAxYu8FIds7kHFdLOz7qX4YQ6AX4tVUA==} + engines: {node: ^22.11 || ^24 || >=26} + '@changesets/cli@2.29.8': resolution: {integrity: sha512-1weuGZpP63YWUYjay/E84qqwcnt5yJMM0tep10Up7Q5cS/DGe2IZ0Uj3HNMxGhCINZuR7aO9WBMdKnPit5ZDPA==} hasBin: true @@ -249,8 +253,9 @@ packages: '@changesets/get-dependents-graph@2.1.3': resolution: {integrity: sha512-gphr+v0mv2I3Oxt19VdWRRUxq3sseyUpX9DaHpTUmLj92Y10AGy+XOtV+kbM6L/fDcpx7/ISDFK6T8A/P3lOdQ==} - '@changesets/get-github-info@0.5.2': - resolution: {integrity: sha512-JppheLu7S114aEs157fOZDjFqUDpm7eHdq5E8SSR0gUBTEK0cNSHsrSR5a66xs0z3RWuo46QvA3vawp8BxDHvg==} + '@changesets/get-github-info@1.0.0-next.4': + resolution: {integrity: sha512-Bosh+XOoFvLMzAj301tg6phbrEggBtvEccrnceEvYsKSM7PjcIa32Fy22SU5g+501HZywkdwcAlybdWF+e/zzw==} + engines: {node: ^22.11 || ^24 || >=26} '@changesets/get-release-plan@4.0.14': resolution: {integrity: sha512-yjZMHpUHgl4Xl5gRlolVuxDkm4HgSJqT93Ri1Uz8kGrQb+5iJ8dkXJ20M2j/Y4iV5QzS2c5SeTxVSKX+2eMI0g==} @@ -282,6 +287,10 @@ packages: '@changesets/types@6.1.0': resolution: {integrity: sha512-rKQcJ+o1nKNgeoYRHKOS07tAMNd3YSN0uHaJOZYjBAgxfV7TUE7JE+z4BzZdQwb5hKaYbayKN5KrYV7ODb2rAA==} + '@changesets/types@7.0.0-next.7': + resolution: {integrity: sha512-1XyshLw+lRCg2DWxi1Qt3hbezjBostHzXvGXVgSzAeZ+fL+r2ikcMbpaQ98D9ivwKhYFf1FBbKbEc+XsBZsIJQ==} + engines: {node: ^22.11 || ^24 || >=26} + '@changesets/write@0.4.0': resolution: {integrity: sha512-CdTLvIOPiCNuH71pyDu3rA+Q0n65cmAbXnwWH84rKGiFumFzkmHNT8KHTMEchcxN+Kl8I54xGUhJ7l3E7X396Q==} @@ -933,11 +942,6 @@ packages: svelte: ^5.0.0 vite: ^6.3.0 || ^7.0.0 - '@svitejs/changesets-changelog-github-compact@1.1.0': - resolution: {integrity: sha512-qhUGGDHcpbY2zpjW3SwqchuW8J/5EzlPFud7xNntHKA7f3a/mx5+g+ruJKFHSAiVZYo30PALt+AyhmPUNKH/Og==} - engines: {node: ^14.13.1 || ^16.0.0 || >=18} - deprecated: unmaintained - '@types/aria-query@5.0.4': resolution: {integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==} @@ -1229,8 +1233,8 @@ packages: resolution: {integrity: sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==} engines: {node: '>=18'} - dataloader@1.4.0: - resolution: {integrity: sha512-68s5jYdlvasItOJnCuI2Q9s4q98g0pCyL3HrcKJu8KNugUl8ahgmZYg38ysLTgQjjXX3H8CJLkAvWrclWfcalw==} + dataloader@2.2.3: + resolution: {integrity: sha512-y2krtASINtPFS1rSDjacrFgn1dcUuoREVabwlOGOe4SdxenREqwjwjElAdwvbGM7kgZz9a3KVicWR7vcz8rnzA==} debug@4.4.3: resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} @@ -1283,10 +1287,6 @@ packages: resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} engines: {node: '>=8'} - dotenv@16.3.2: - resolution: {integrity: sha512-HTlk5nmhkm8F6JcdXvHIzaorzCoziNQT9mGxLPVXW8wJF1TiGSL60ZGB4gHWabHOaMmWmhvk2/lPHfnBiT78AQ==} - engines: {node: '>=12'} - dts-buddy@0.5.5: resolution: {integrity: sha512-Mu5PJuP7C+EqZIwDtW/bG1tVli1UFhRIyW/dERBVBYk28OviTkribu9S2LpDQ0HF2MbkqnjQIkbbE6HnepdNTQ==} hasBin: true @@ -1853,15 +1853,6 @@ packages: natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} - node-fetch@2.7.0: - resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} - engines: {node: 4.x || >=6.0.0} - peerDependencies: - encoding: ^0.1.0 - peerDependenciesMeta: - encoding: - optional: true - normalize-path@3.0.0: resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} engines: {node: '>=0.10.0'} @@ -2237,9 +2228,6 @@ packages: resolution: {integrity: sha512-FRKsF7cz96xIIeMZ82ehjC3xW2E+O2+v11udrDYewUbszngYhsGa8z6YUMMzO9QJZzzyd0nGGXnML/TReX6W8Q==} engines: {node: '>=16'} - tr46@0.0.3: - resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} - tr46@5.0.0: resolution: {integrity: sha512-tk2G5R2KRwBd+ZN0zaEXpmzdKyOYksXwywulIX95MBODjSzMIuQnQ3m8JxgbhnL1LeVo7lqQKsYa1O3Htl7K5g==} engines: {node: '>=18'} @@ -2430,9 +2418,6 @@ packages: web-features@3.29.0: resolution: {integrity: sha512-r8m0Xj77/PSHXEbv2U3UuLhw5gE4U+YAPek5hCor5DMsS9Qnwtxl5FSfam3dXQo7Gl0gxK9BRrcQLlOBZZZXpw==} - webidl-conversions@3.0.1: - resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} - webidl-conversions@7.0.0: resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} engines: {node: '>=12'} @@ -2450,9 +2435,6 @@ packages: resolution: {integrity: sha512-1lfMEm2IEr7RIV+f4lUNPOqfFL+pO+Xw3fJSqmjX9AbXcXcYOkCe1P6+9VBZB6n94af16NfZf+sSk0JCBZC9aw==} engines: {node: '>=18'} - whatwg-url@5.0.0: - resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} - which@2.0.2: resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} engines: {node: '>= 8'} @@ -2547,6 +2529,11 @@ snapshots: dependencies: '@changesets/types': 6.1.0 + '@changesets/changelog-github@1.0.0-next.6': + dependencies: + '@changesets/get-github-info': 1.0.0-next.4 + '@changesets/types': 7.0.0-next.7 + '@changesets/cli@2.29.8(@types/node@20.19.17)': dependencies: '@changesets/apply-release-plan': 7.0.14 @@ -2601,12 +2588,9 @@ snapshots: picocolors: 1.1.1 semver: 7.7.4 - '@changesets/get-github-info@0.5.2': + '@changesets/get-github-info@1.0.0-next.4': dependencies: - dataloader: 1.4.0 - node-fetch: 2.7.0 - transitivePeerDependencies: - - encoding + dataloader: 2.2.3 '@changesets/get-release-plan@4.0.14': dependencies: @@ -2662,6 +2646,8 @@ snapshots: '@changesets/types@6.1.0': {} + '@changesets/types@7.0.0-next.7': {} + '@changesets/write@0.4.0': dependencies: '@changesets/types': 6.1.0 @@ -3111,13 +3097,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@svitejs/changesets-changelog-github-compact@1.1.0': - dependencies: - '@changesets/get-github-info': 0.5.2 - dotenv: 16.3.2 - transitivePeerDependencies: - - encoding - '@types/aria-query@5.0.4': {} '@types/chai@5.2.3': @@ -3439,7 +3418,7 @@ snapshots: whatwg-mimetype: 4.0.0 whatwg-url: 14.0.0 - dataloader@1.4.0: {} + dataloader@2.2.3: {} debug@4.4.3: dependencies: @@ -3473,8 +3452,6 @@ snapshots: dependencies: path-type: 4.0.0 - dotenv@16.3.2: {} - dts-buddy@0.5.5(typescript@5.5.4): dependencies: '@jridgewell/source-map': 0.3.6 @@ -4095,10 +4072,6 @@ snapshots: natural-compare@1.4.0: {} - node-fetch@2.7.0: - dependencies: - whatwg-url: 5.0.0 - normalize-path@3.0.0: optional: true @@ -4435,8 +4408,6 @@ snapshots: dependencies: tldts: 6.1.64 - tr46@0.0.3: {} - tr46@5.0.0: dependencies: punycode: 2.3.1 @@ -4595,8 +4566,6 @@ snapshots: web-features@3.29.0: {} - webidl-conversions@3.0.1: {} - webidl-conversions@7.0.0: {} whatwg-encoding@3.1.1: @@ -4610,11 +4579,6 @@ snapshots: tr46: 5.0.0 webidl-conversions: 7.0.0 - whatwg-url@5.0.0: - dependencies: - tr46: 0.0.3 - webidl-conversions: 3.0.1 - which@2.0.2: dependencies: isexe: 2.0.0 From ffc0e6e7ef5961ec81a9df90cd166e5d06ec33fd Mon Sep 17 00:00:00 2001 From: randalmurphal <49694326+randalmurphal@users.noreply.github.com> Date: Thu, 20 Aug 2026 10:19:42 -0500 Subject: [PATCH 144/222] fix: release last_propagated_event after event propagation settles (#18569) Fixes #18568 `last_propagated_event` was added in #16527 to stop Firefox from garbage collecting the event wrapper mid-propagation. The problem with this is that the even is now retained until the next event, which could be a while. This removes it after a macrotask. Co-authored-by: rmurphy Co-authored-by: Claude Fable 5 --- .changeset/spotty-ducks-refuse.md | 5 +++++ .../internal/client/dom/elements/events.js | 20 ++++++++++++++++++- 2 files changed, 24 insertions(+), 1 deletion(-) create mode 100644 .changeset/spotty-ducks-refuse.md diff --git a/.changeset/spotty-ducks-refuse.md b/.changeset/spotty-ducks-refuse.md new file mode 100644 index 0000000000..6faba7e89f --- /dev/null +++ b/.changeset/spotty-ducks-refuse.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: release `last_propagated_event` after event propagation settles so it no longer retains the last event's target subtree diff --git a/packages/svelte/src/internal/client/dom/elements/events.js b/packages/svelte/src/internal/client/dom/elements/events.js index 45e042a8c6..068c64b9ab 100644 --- a/packages/svelte/src/internal/client/dom/elements/events.js +++ b/packages/svelte/src/internal/client/dom/elements/events.js @@ -159,12 +159,15 @@ export function delegate(events) { } // used to store the reference to the currently propagated event -// to prevent garbage collection between microtasks in Firefox +// to prevent garbage collection between microtasks in Firefox (<= 141) // If the event object is GCed too early, the expando __root property // set on the event object is lost, causing the event delegation // to process the event twice let last_propagated_event = null; +// whether a task is already queued to clear `last_propagated_event` +let last_propagated_event_clear_scheduled = false; + /** * @this {EventTarget} * @param {Event} event @@ -179,6 +182,21 @@ export function handle_event_propagation(event) { last_propagated_event = event; + // The reference is only needed while the event can still reach another + // delegated root, i.e. during the current (synchronous) dispatch and its + // microtask checkpoints. Clearing it in a later task preserves the + // Firefox workaround while making sure the slot doesn't retain the last + // event forever — through `event.target` it would otherwise keep the + // entire detached subtree of whatever the user last clicked in alive + // until the next delegated event happens to arrive. + if (!last_propagated_event_clear_scheduled) { + last_propagated_event_clear_scheduled = true; + setTimeout(() => { + last_propagated_event_clear_scheduled = false; + last_propagated_event = null; + }); + } + // composedPath contains list of nodes the event has propagated through. // We check `event_symbol` to skip all nodes below it in case this is a // parent of the `event_symbol` node, which indicates that there's nested From 950e2a837c344fa2d656cb98f471eba9a15ec4a4 Mon Sep 17 00:00:00 2001 From: Mukund Sarma <23266464+dnukumamras@users.noreply.github.com> Date: Thu, 20 Aug 2026 08:22:27 -0700 Subject: [PATCH 145/222] fix: strip comments from inline style values in linear time (#18553) `to_style()` strips CSS comments from an inline `style` value with `/\s*\/\*.*?\*\/\s*/g`. The leading `\s*` makes the match retry from every position, so a long run of whitespace backtracks in O(n^2). When a dynamic `style={value}` sits on an element that also has a `style:` directive, that regex runs on `value`, so a large whitespace string can stall rendering (server) or the main thread (client). ### Fix Drop the surrounding `\s*` and match only the comment: `/\/\*.*?\*\//g` to prevent quadratic regex. The surrounding whitespace was already removed by the `.trim()` --------- Co-authored-by: Claude Opus 4.8 Co-authored-by: Simon H <5968653+dummdidumm@users.noreply.github.com> --- .changeset/strip-style-comments.md | 5 +++++ packages/svelte/src/internal/shared/attributes.js | 3 ++- 2 files changed, 7 insertions(+), 1 deletion(-) create mode 100644 .changeset/strip-style-comments.md diff --git a/.changeset/strip-style-comments.md b/.changeset/strip-style-comments.md new file mode 100644 index 0000000000..6a27e431df --- /dev/null +++ b/.changeset/strip-style-comments.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: strip comments from inline `style` values in linear time diff --git a/packages/svelte/src/internal/shared/attributes.js b/packages/svelte/src/internal/shared/attributes.js index 487a40baf3..bb1abf22c1 100644 --- a/packages/svelte/src/internal/shared/attributes.js +++ b/packages/svelte/src/internal/shared/attributes.js @@ -142,8 +142,9 @@ export function to_style(value, styles) { } if (value) { + // strip comments; surrounding whitespace is handled by the trims below (which is much faster than doing it through regex) value = String(value) - .replaceAll(/\s*\/\*.*?\*\/\s*/g, '') + .replaceAll(/\/\*.*?\*\//g, '') .trim(); /** @type {boolean | '"' | "'"} */ From 545205b4206bb5e7c070c55ae6b0600437291646 Mon Sep 17 00:00:00 2001 From: Joe Schafer Date: Thu, 20 Aug 2026 08:41:55 -0700 Subject: [PATCH 146/222] perf: make async blocker analysis linear (#18549) `trace_references` recreated a fresh seen set for every CallExpression, so `touch` re-walked the same transitive assignment graph once per call. For N calls reaching an N-deep binding chain, this was $O(N^2)$. Share one seen set per trace_references invocation. Use separate sets for the write-directed CallExpression touches and the read-directed ReturnStatement touches. The shared set ensures each assignment-value expression is walked at most once, making the traversal linear. `touch` only ever adds to a fixed target set, so skipping an already-seen expression never drops a binding: compiler output is byte-identical. Complements #18548. --------- Co-authored-by: Simon H <5968653+dummdidumm@users.noreply.github.com> --- .changeset/tidy-melons-attack.md | 5 +++++ .../src/compiler/phases/2-analyze/index.js | 21 ++++++++++--------- 2 files changed, 16 insertions(+), 10 deletions(-) create mode 100644 .changeset/tidy-melons-attack.md diff --git a/.changeset/tidy-melons-attack.md b/.changeset/tidy-melons-attack.md new file mode 100644 index 0000000000..87e62a1c4d --- /dev/null +++ b/.changeset/tidy-melons-attack.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +perf: make async blocker analysis scale linearly with the number of top-level references diff --git a/packages/svelte/src/compiler/phases/2-analyze/index.js b/packages/svelte/src/compiler/phases/2-analyze/index.js index d81053a08d..f9cf2f1d89 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/index.js +++ b/packages/svelte/src/compiler/phases/2-analyze/index.js @@ -958,7 +958,7 @@ function calculate_blockers(instance, analysis) { * @param {Set} touched * @param {Set} seen */ - const touch = (expression, scope, touched, seen = new Set()) => { + const touch = (expression, scope, touched, seen) => { if (seen.has(expression)) return; seen.add(expression); @@ -1015,6 +1015,13 @@ function calculate_blockers(instance, analysis) { } } + // Share seen nodes across calls so transitive assignments are only visited once. + // Keep separate read/write state because the target sets can differ. + /** @type {Set} */ + const writes_seen = new Set(); + /** @type {Set} */ + const reads_seen = new Set(); + walk( node, { scope }, @@ -1044,13 +1051,7 @@ function calculate_blockers(instance, analysis) { const rune = get_rune(node, context.state.scope); if (rune === '$effect') return; - /** @type {Set} */ - const touched = new Set(); - touch(node, context.state.scope, touched); - - for (const b of touched) { - writes.add(b); - } + touch(node, context.state.scope, writes, writes_seen); }, Identifier(node, context) { const parent = /** @type {ESTree.Node} */ (context.path.at(-1)); @@ -1066,7 +1067,7 @@ function calculate_blockers(instance, analysis) { // might be called immediately, so we have to touch all references within it. Example: // function foo() { return () => blocker; } foo(); // blocker is touched if (node.argument) { - touch(node.argument, context.state.scope, reads); + touch(node.argument, context.state.scope, reads, reads_seen); } }, // don't look inside functions until they are called @@ -1242,7 +1243,7 @@ function calculate_blockers(instance, analysis) { } else { // A concise arrow body is an implicit return, so treat it like the // `ReturnStatement` visitor in `trace_references` would. - touch(init.body, fn_scope, reads_writes); + touch(init.body, fn_scope, reads_writes, new Set()); } const max = [...reads_writes].reduce((max, binding) => { From 5ccdfe355739229573b20a2fa2ee49e38e3868fe Mon Sep 17 00:00:00 2001 From: Scott Wu Date: Fri, 21 Aug 2026 00:05:45 +0800 Subject: [PATCH 147/222] docs: note comments within tags possibility (#18482) Co-authored-by: Simon H <5968653+dummdidumm@users.noreply.github.com> --- .../docs/03-template-syntax/01-basic-markup.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/documentation/docs/03-template-syntax/01-basic-markup.md b/documentation/docs/03-template-syntax/01-basic-markup.md index feecfe033e..436f307f84 100644 --- a/documentation/docs/03-template-syntax/01-basic-markup.md +++ b/documentation/docs/03-template-syntax/01-basic-markup.md @@ -215,3 +215,14 @@ You can add a special comment starting with `@component` that will show up when ```` + +You can also put JavaScript-style comments within tags between attributes: + +```svelte +
    + foo bar +
    +``` From 56a036f4ce873a24ee6631a06d03d372523d7a9b Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 20 Aug 2026 15:13:38 -0400 Subject: [PATCH 148/222] Version Packages (#18640) 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.10 ### Patch Changes - fix: preserve CSS escape sequences when printing selectors ([#18667](https://github.com/sveltejs/svelte/pull/18667)) - fix: parse `:nth-child(2n of.foo)` where `of` is not followed by whitespace ([#18611](https://github.com/sveltejs/svelte/pull/18611)) - fix: transform expressions inside labeled statements during server compilation ([#18617](https://github.com/sveltejs/svelte/pull/18617)) - docs: clarify that context lookup includes the current component and all ancestors ([#18581](https://github.com/sveltejs/svelte/pull/18581)) - fix: apply CSS custom properties with falsy values on components ([#18634](https://github.com/sveltejs/svelte/pull/18634)) - fix: correctly print `{#await ... catch x}` et al ([#18645](https://github.com/sveltejs/svelte/pull/18645)) - fix: ignore comments of Program node during migration script ([#18656](https://github.com/sveltejs/svelte/pull/18656)) - fix: reliably resolve append_style to its correct root ([#18614](https://github.com/sveltejs/svelte/pull/18614)) - fix: clean up removed capture event handlers from spread attributes ([#18618](https://github.com/sveltejs/svelte/pull/18618)) - fix: don't corrupt renderer type during SSR's legacy `bind:` retry loop ([#18616](https://github.com/sveltejs/svelte/pull/18616)) - fix: treat concise arrow function bodies as implicit returns when calculating blockers ([#18613](https://github.com/sveltejs/svelte/pull/18613)) - fix: give effect teardowns the value from before the first write in a flush ([#18620](https://github.com/sveltejs/svelte/pull/18620)) - fix: avoid double-calling a derived reference when destructuring `$derived` of another `$derived` during server-side rendering ([#18668](https://github.com/sveltejs/svelte/pull/18668)) - fix: preserve namespaces in CSS type selectors ([#18678](https://github.com/sveltejs/svelte/pull/18678)) - fix: increment private state fields through a non-`this` receiver ([#18622](https://github.com/sveltejs/svelte/pull/18622)) - chore: deduplicate client and server context helpers ([#18580](https://github.com/sveltejs/svelte/pull/18580)) - fix: release `last_propagated_event` after event propagation settles so it no longer retains the last event's target subtree ([#18569](https://github.com/sveltejs/svelte/pull/18569)) - fix: allow custom elements to receive async values as props ([#18661](https://github.com/sveltejs/svelte/pull/18661)) - fix: strip comments from inline `style` values in linear time ([#18553](https://github.com/sveltejs/svelte/pull/18553)) - fix: prevent declaration comments from breaking server derived references ([#18641](https://github.com/sveltejs/svelte/pull/18641)) - perf: make async blocker analysis scale linearly with the number of top-level references ([#18549](https://github.com/sveltejs/svelte/pull/18549)) - fix: preserve short-circuiting for logical assignments to private state fields ([#18594](https://github.com/sveltejs/svelte/pull/18594)) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .changeset/calm-foxes-reprint.md | 5 -- .changeset/chilled-lions-parse.md | 5 -- .changeset/clear-badgers-type.md | 5 -- .changeset/context-docs-wording.md | 5 -- .changeset/css-props-falsy-values.md | 5 -- .changeset/eighty-pugs-shave.md | 5 -- .changeset/fix-migrate-comment-no-position.md | 5 -- .changeset/free-signs-ask.md | 5 -- .changeset/happy-spoons-talk.md | 5 -- .changeset/hydration-head-renderer-type.md | 5 -- .changeset/light-pandas-attack.md | 5 -- .changeset/lucky-pears-smoke.md | 5 -- .changeset/nervous-dolls-clean.md | 5 -- .changeset/pink-chairs-matter.md | 5 -- .changeset/quiet-donkeys-clap.md | 5 -- .changeset/shared-context-helpers.md | 5 -- .changeset/spotty-ducks-refuse.md | 5 -- .changeset/spotty-files-trade.md | 5 -- .changeset/strip-style-comments.md | 5 -- .changeset/tidy-cats-return.md | 5 -- .changeset/tidy-melons-attack.md | 5 -- .changeset/tidy-ravens-assign.md | 5 -- packages/svelte/CHANGELOG.md | 48 +++++++++++++++++++ packages/svelte/package.json | 2 +- packages/svelte/src/version.js | 2 +- 25 files changed, 50 insertions(+), 112 deletions(-) delete mode 100644 .changeset/calm-foxes-reprint.md delete mode 100644 .changeset/chilled-lions-parse.md delete mode 100644 .changeset/clear-badgers-type.md delete mode 100644 .changeset/context-docs-wording.md delete mode 100644 .changeset/css-props-falsy-values.md delete mode 100644 .changeset/eighty-pugs-shave.md delete mode 100644 .changeset/fix-migrate-comment-no-position.md delete mode 100644 .changeset/free-signs-ask.md delete mode 100644 .changeset/happy-spoons-talk.md delete mode 100644 .changeset/hydration-head-renderer-type.md delete mode 100644 .changeset/light-pandas-attack.md delete mode 100644 .changeset/lucky-pears-smoke.md delete mode 100644 .changeset/nervous-dolls-clean.md delete mode 100644 .changeset/pink-chairs-matter.md delete mode 100644 .changeset/quiet-donkeys-clap.md delete mode 100644 .changeset/shared-context-helpers.md delete mode 100644 .changeset/spotty-ducks-refuse.md delete mode 100644 .changeset/spotty-files-trade.md delete mode 100644 .changeset/strip-style-comments.md delete mode 100644 .changeset/tidy-cats-return.md delete mode 100644 .changeset/tidy-melons-attack.md delete mode 100644 .changeset/tidy-ravens-assign.md diff --git a/.changeset/calm-foxes-reprint.md b/.changeset/calm-foxes-reprint.md deleted file mode 100644 index 08ea964bfe..0000000000 --- a/.changeset/calm-foxes-reprint.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: preserve CSS escape sequences when printing selectors \ No newline at end of file diff --git a/.changeset/chilled-lions-parse.md b/.changeset/chilled-lions-parse.md deleted file mode 100644 index 1f2e4a0937..0000000000 --- a/.changeset/chilled-lions-parse.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: parse `:nth-child(2n of.foo)` where `of` is not followed by whitespace diff --git a/.changeset/clear-badgers-type.md b/.changeset/clear-badgers-type.md deleted file mode 100644 index 3668aa7644..0000000000 --- a/.changeset/clear-badgers-type.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: transform expressions inside labeled statements during server compilation diff --git a/.changeset/context-docs-wording.md b/.changeset/context-docs-wording.md deleted file mode 100644 index 6ec9d47ed9..0000000000 --- a/.changeset/context-docs-wording.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -docs: clarify that context lookup includes the current component and all ancestors diff --git a/.changeset/css-props-falsy-values.md b/.changeset/css-props-falsy-values.md deleted file mode 100644 index 677594ddaf..0000000000 --- a/.changeset/css-props-falsy-values.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: apply CSS custom properties with falsy values on components diff --git a/.changeset/eighty-pugs-shave.md b/.changeset/eighty-pugs-shave.md deleted file mode 100644 index 0f0eaf9a6c..0000000000 --- a/.changeset/eighty-pugs-shave.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: correctly print `{#await ... catch x}` et al diff --git a/.changeset/fix-migrate-comment-no-position.md b/.changeset/fix-migrate-comment-no-position.md deleted file mode 100644 index fa9981dc60..0000000000 --- a/.changeset/fix-migrate-comment-no-position.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: ignore comments of Program node during migration script diff --git a/.changeset/free-signs-ask.md b/.changeset/free-signs-ask.md deleted file mode 100644 index 7e6c925526..0000000000 --- a/.changeset/free-signs-ask.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: reliably resolve append_style to its correct root diff --git a/.changeset/happy-spoons-talk.md b/.changeset/happy-spoons-talk.md deleted file mode 100644 index cf165320db..0000000000 --- a/.changeset/happy-spoons-talk.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: clean up removed capture event handlers from spread attributes diff --git a/.changeset/hydration-head-renderer-type.md b/.changeset/hydration-head-renderer-type.md deleted file mode 100644 index bc566c158d..0000000000 --- a/.changeset/hydration-head-renderer-type.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: don't corrupt renderer type during SSR's legacy `bind:` retry loop diff --git a/.changeset/light-pandas-attack.md b/.changeset/light-pandas-attack.md deleted file mode 100644 index e6eface376..0000000000 --- a/.changeset/light-pandas-attack.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: treat concise arrow function bodies as implicit returns when calculating blockers diff --git a/.changeset/lucky-pears-smoke.md b/.changeset/lucky-pears-smoke.md deleted file mode 100644 index 816a37dca5..0000000000 --- a/.changeset/lucky-pears-smoke.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: give effect teardowns the value from before the first write in a flush diff --git a/.changeset/nervous-dolls-clean.md b/.changeset/nervous-dolls-clean.md deleted file mode 100644 index 5b31bdd3c7..0000000000 --- a/.changeset/nervous-dolls-clean.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: avoid double-calling a derived reference when destructuring `$derived` of another `$derived` during server-side rendering diff --git a/.changeset/pink-chairs-matter.md b/.changeset/pink-chairs-matter.md deleted file mode 100644 index 6a8a6e75f3..0000000000 --- a/.changeset/pink-chairs-matter.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: preserve namespaces in CSS type selectors diff --git a/.changeset/quiet-donkeys-clap.md b/.changeset/quiet-donkeys-clap.md deleted file mode 100644 index dc9df3b7e1..0000000000 --- a/.changeset/quiet-donkeys-clap.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: increment private state fields through a non-`this` receiver diff --git a/.changeset/shared-context-helpers.md b/.changeset/shared-context-helpers.md deleted file mode 100644 index ecdfcb9a0e..0000000000 --- a/.changeset/shared-context-helpers.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -chore: deduplicate client and server context helpers diff --git a/.changeset/spotty-ducks-refuse.md b/.changeset/spotty-ducks-refuse.md deleted file mode 100644 index 6faba7e89f..0000000000 --- a/.changeset/spotty-ducks-refuse.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: release `last_propagated_event` after event propagation settles so it no longer retains the last event's target subtree diff --git a/.changeset/spotty-files-trade.md b/.changeset/spotty-files-trade.md deleted file mode 100644 index 39975b7a9a..0000000000 --- a/.changeset/spotty-files-trade.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: allow custom elements to receive async values as props diff --git a/.changeset/strip-style-comments.md b/.changeset/strip-style-comments.md deleted file mode 100644 index 6a27e431df..0000000000 --- a/.changeset/strip-style-comments.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: strip comments from inline `style` values in linear time diff --git a/.changeset/tidy-cats-return.md b/.changeset/tidy-cats-return.md deleted file mode 100644 index 0c041196ef..0000000000 --- a/.changeset/tidy-cats-return.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: prevent declaration comments from breaking server derived references diff --git a/.changeset/tidy-melons-attack.md b/.changeset/tidy-melons-attack.md deleted file mode 100644 index 87e62a1c4d..0000000000 --- a/.changeset/tidy-melons-attack.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -perf: make async blocker analysis scale linearly with the number of top-level references diff --git a/.changeset/tidy-ravens-assign.md b/.changeset/tidy-ravens-assign.md deleted file mode 100644 index 312adde9b5..0000000000 --- a/.changeset/tidy-ravens-assign.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: preserve short-circuiting for logical assignments to private state fields diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index 5ec2101bf7..e66b3db109 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,53 @@ # svelte +## 5.56.10 + +### Patch Changes + +- fix: preserve CSS escape sequences when printing selectors ([#18667](https://github.com/sveltejs/svelte/pull/18667)) + +- fix: parse `:nth-child(2n of.foo)` where `of` is not followed by whitespace ([#18611](https://github.com/sveltejs/svelte/pull/18611)) + +- fix: transform expressions inside labeled statements during server compilation ([#18617](https://github.com/sveltejs/svelte/pull/18617)) + +- docs: clarify that context lookup includes the current component and all ancestors ([#18581](https://github.com/sveltejs/svelte/pull/18581)) + +- fix: apply CSS custom properties with falsy values on components ([#18634](https://github.com/sveltejs/svelte/pull/18634)) + +- fix: correctly print `{#await ... catch x}` et al ([#18645](https://github.com/sveltejs/svelte/pull/18645)) + +- fix: ignore comments of Program node during migration script ([#18656](https://github.com/sveltejs/svelte/pull/18656)) + +- fix: reliably resolve append_style to its correct root ([#18614](https://github.com/sveltejs/svelte/pull/18614)) + +- fix: clean up removed capture event handlers from spread attributes ([#18618](https://github.com/sveltejs/svelte/pull/18618)) + +- fix: don't corrupt renderer type during SSR's legacy `bind:` retry loop ([#18616](https://github.com/sveltejs/svelte/pull/18616)) + +- fix: treat concise arrow function bodies as implicit returns when calculating blockers ([#18613](https://github.com/sveltejs/svelte/pull/18613)) + +- fix: give effect teardowns the value from before the first write in a flush ([#18620](https://github.com/sveltejs/svelte/pull/18620)) + +- fix: avoid double-calling a derived reference when destructuring `$derived` of another `$derived` during server-side rendering ([#18668](https://github.com/sveltejs/svelte/pull/18668)) + +- fix: preserve namespaces in CSS type selectors ([#18678](https://github.com/sveltejs/svelte/pull/18678)) + +- fix: increment private state fields through a non-`this` receiver ([#18622](https://github.com/sveltejs/svelte/pull/18622)) + +- chore: deduplicate client and server context helpers ([#18580](https://github.com/sveltejs/svelte/pull/18580)) + +- fix: release `last_propagated_event` after event propagation settles so it no longer retains the last event's target subtree ([#18569](https://github.com/sveltejs/svelte/pull/18569)) + +- fix: allow custom elements to receive async values as props ([#18661](https://github.com/sveltejs/svelte/pull/18661)) + +- fix: strip comments from inline `style` values in linear time ([#18553](https://github.com/sveltejs/svelte/pull/18553)) + +- fix: prevent declaration comments from breaking server derived references ([#18641](https://github.com/sveltejs/svelte/pull/18641)) + +- perf: make async blocker analysis scale linearly with the number of top-level references ([#18549](https://github.com/sveltejs/svelte/pull/18549)) + +- fix: preserve short-circuiting for logical assignments to private state fields ([#18594](https://github.com/sveltejs/svelte/pull/18594)) + ## 5.56.9 ### Patch Changes diff --git a/packages/svelte/package.json b/packages/svelte/package.json index 0bd1580969..2da4ee428d 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.56.9", + "version": "5.56.10", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index e3b4c740f0..b9f1637abb 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.56.9'; +export const VERSION = '5.56.10'; export const PUBLIC_VERSION = '5'; From a7ba9e85aa47583f84a19a622667bc4c97514c2f Mon Sep 17 00:00:00 2001 From: Ben McCann <322311+benmccann@users.noreply.github.com> Date: Thu, 20 Aug 2026 13:34:02 -0700 Subject: [PATCH 149/222] docs: update mobile FAQ (#18665) Updated to mention Symbiote Native --------- Co-authored-by: vercel[bot] <35613825+vercel[bot]@users.noreply.github.com> --- documentation/docs/07-misc/99-faq.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/documentation/docs/07-misc/99-faq.md b/documentation/docs/07-misc/99-faq.md index 035629e5ae..a9067b0703 100644 --- a/documentation/docs/07-misc/99-faq.md +++ b/documentation/docs/07-misc/99-faq.md @@ -99,7 +99,9 @@ However, you can use any router library. A sampling of available routers are hig While most mobile apps are written without using JavaScript, if you'd like to leverage your existing Svelte components and knowledge of Svelte when building mobile apps, you can turn a [SvelteKit SPA](https://kit.svelte.dev/docs/single-page-apps) into a mobile app with [Tauri](https://v2.tauri.app/start/frontend/sveltekit/) or [Capacitor](https://capacitorjs.com/solution/svelte). Mobile features like the camera, geolocation, and push notifications are available via plugins for both platforms. -Some work has been completed towards [custom renderer support in Svelte 5](https://github.com/sveltejs/svelte/issues/15470), but this feature is not yet available. The custom rendering API would support additional mobile frameworks like Lynx JS and Svelte Native. Svelte Native was an option available for Svelte 4, but Svelte 5 does not currently support it. Svelte Native lets you write NativeScript apps using Svelte components that contain [NativeScript UI components](https://docs.nativescript.org/ui/) rather than DOM elements, which may be familiar for users coming from React Native. +You can also write apps in Svelte that compiles to native components by using [Symbiote Native](https://docs.symbiote-native.dev/), which leverages the infrastructure provided by React Native. + +Work has been completed towards [custom renderer support in Svelte 5](https://github.com/sveltejs/svelte/issues/15470), but this feature is not yet merged. The custom rendering API will allow support in additional mobile frameworks like Lynx JS and Svelte Native. Symbiote Native will also adopt this API. Svelte Native was an option available for Svelte 4, but Svelte 5 does not currently support it. Svelte Native lets you write NativeScript apps using Svelte components that contain [NativeScript UI components](https://docs.nativescript.org/ui/) rather than DOM elements. ## Can I tell Svelte not to remove my unused styles? From faf1e103c53675cbe71860f331d4fedd84c8d612 Mon Sep 17 00:00:00 2001 From: Siddhesh Kabra <146343711+Xsidz@users.noreply.github.com> Date: Fri, 21 Aug 2026 02:20:44 +0530 Subject: [PATCH 150/222] fix: prevent onoutroend from firing twice when compilerOptions.hmr is true (#18655) Fixes #18440 `hmr()` in `packages/svelte/src/internal/client/dev/hmr.js` wrapped a component in a `block` effect containing a `branch` effect. It forwarded the inner effect's `nodes` object to the outer block: This shared the same reference, meaning `outer_block.nodes === inner_branch.nodes`. When `pause_children` collected transitions for unmounting, Each collected transition had `out(check)` called multiple times, starting multiple animations and firing `outroend` multiple times. Fix by only copying `start`/`end` (the DOM range info the outer block needs) without sharing the transitions array: --- .changeset/fix-hmr-onoutroend-double-fire.md | 5 +++++ .../svelte/src/internal/client/dev/hmr.js | 19 +++++++++++++++---- 2 files changed, 20 insertions(+), 4 deletions(-) create mode 100644 .changeset/fix-hmr-onoutroend-double-fire.md diff --git a/.changeset/fix-hmr-onoutroend-double-fire.md b/.changeset/fix-hmr-onoutroend-double-fire.md new file mode 100644 index 0000000000..0eafbfa731 --- /dev/null +++ b/.changeset/fix-hmr-onoutroend-double-fire.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: prevent onoutroend from firing twice when compilerOptions.hmr is true diff --git a/packages/svelte/src/internal/client/dev/hmr.js b/packages/svelte/src/internal/client/dev/hmr.js index 73dba95f9b..0e988fe66d 100644 --- a/packages/svelte/src/internal/client/dev/hmr.js +++ b/packages/svelte/src/internal/client/dev/hmr.js @@ -57,10 +57,21 @@ export function hmr(fn) { if (ran) set_should_intro(true); }); - // Forward the nodes from the inner effect to the outer active effect which would - // get them if the HMR wrapper wasn't there. Do this inside the block not outside - // so that HMR updates to the component will also update the nodes on the active effect. - /** @type {Effect} */ (active_effect).nodes = effect.nodes; + // Forward the start/end DOM nodes from the inner effect to the outer active effect + // which would get them if the HMR wrapper wasn't there. Do this inside the block not + // outside so that HMR updates to the component will also update the nodes on the + // active effect. We copy only start/end, not the full nodes object, so that + // pause_children does not collect transitions from both effects and fire outroend twice. + var inner_nodes = effect.nodes; + if (inner_nodes) { + var ae = /** @type {Effect} */ (active_effect); + if (ae.nodes) { + ae.nodes.start = inner_nodes.start; + ae.nodes.end = inner_nodes.end; + } else { + ae.nodes = { start: inner_nodes.start, end: inner_nodes.end, a: null, t: null }; + } + } }, EFFECT_TRANSPARENT); ran = true; From cac27c783ed0c12011fa223825b6877dc6760825 Mon Sep 17 00:00:00 2001 From: Nic Polumeyv Date: Thu, 20 Aug 2026 16:51:54 -0400 Subject: [PATCH 151/222] feat: export RenderOutput, SyncRenderOutput, Csp and Sha256Source from svelte/server (#18648) `render` returns `RenderOutput` and takes `csp: Csp`, but both are declared in `src/internal/server/types.d.ts` and never exported. The generated `svelte/server` block has them without `export`, so nothing outside the repo can import them and the docs page names `RenderOutput` as the return type with nothing on the page defining it. Moves them into `src/server/public.d.ts` the way `svelte/motion` went in #17967, rather than re-exporting internals. --- .changeset/olive-jars-repeat.md | 5 +++++ packages/svelte/src/internal/server/index.js | 2 +- .../svelte/src/internal/server/renderer.js | 3 ++- packages/svelte/src/internal/server/types.d.ts | 18 ------------------ packages/svelte/src/legacy/legacy-server.js | 2 +- packages/svelte/src/server/index.d.ts | 4 +++- packages/svelte/src/server/public.d.ts | 17 +++++++++++++++++ .../svelte/tests/server-side-rendering/test.ts | 3 +-- packages/svelte/types/index.d.ts | 8 ++++---- 9 files changed, 34 insertions(+), 28 deletions(-) create mode 100644 .changeset/olive-jars-repeat.md create mode 100644 packages/svelte/src/server/public.d.ts diff --git a/.changeset/olive-jars-repeat.md b/.changeset/olive-jars-repeat.md new file mode 100644 index 0000000000..815b5adf71 --- /dev/null +++ b/.changeset/olive-jars-repeat.md @@ -0,0 +1,5 @@ +--- +'svelte': minor +--- + +feat: export `RenderOutput`, `SyncRenderOutput`, `Csp` and `Sha256Source` from `svelte/server` diff --git a/packages/svelte/src/internal/server/index.js b/packages/svelte/src/internal/server/index.js index 12f76f188e..3d8ec5fe5e 100644 --- a/packages/svelte/src/internal/server/index.js +++ b/packages/svelte/src/internal/server/index.js @@ -1,5 +1,5 @@ /** @import { ComponentType, SvelteComponent, Component } from 'svelte' */ -/** @import { Csp, RenderOutput } from '#server' */ +/** @import { Csp, RenderOutput } from '../../server/public.js' */ /** @import { Store } from '#shared' */ export { FILENAME, HMR } from '../../constants.js'; import { attr, clsx, to_class, to_style } from '../shared/attributes.js'; diff --git a/packages/svelte/src/internal/server/renderer.js b/packages/svelte/src/internal/server/renderer.js index ba6a361a90..5fb0bb86d5 100644 --- a/packages/svelte/src/internal/server/renderer.js +++ b/packages/svelte/src/internal/server/renderer.js @@ -1,5 +1,6 @@ /** @import { Component } from 'svelte' */ -/** @import { Csp, HydratableContext, RenderOutput, SSRContext, SyncRenderOutput, Sha256Source } from './types.js' */ +/** @import { HydratableContext, SSRContext } from './types.js' */ +/** @import { Csp, RenderOutput, SyncRenderOutput, Sha256Source } from '../../server/public.js' */ /** @import { MaybePromise } from '#shared' */ import { async_mode_flag } from '../flags/index.js'; import { abort } from './abort-signal.js'; diff --git a/packages/svelte/src/internal/server/types.d.ts b/packages/svelte/src/internal/server/types.d.ts index ea6282c176..899255d366 100644 --- a/packages/svelte/src/internal/server/types.d.ts +++ b/packages/svelte/src/internal/server/types.d.ts @@ -15,8 +15,6 @@ export interface SSRContext { element?: Element; } -export type Csp = { nonce?: string; hash?: boolean }; - export interface HydratableLookupEntry { value: unknown; serialized: string; @@ -34,19 +32,3 @@ export interface HydratableContext { export interface RenderContext { hydratable: HydratableContext; } - -export type Sha256Source = `sha256-${string}`; - -export interface SyncRenderOutput { - /** HTML that goes into the `` */ - head: string; - /** @deprecated use `body` instead */ - html: string; - /** HTML that goes somewhere into the `` */ - body: string; - hashes: { - script: Sha256Source[]; - }; -} - -export type RenderOutput = SyncRenderOutput & PromiseLike; diff --git a/packages/svelte/src/legacy/legacy-server.js b/packages/svelte/src/legacy/legacy-server.js index 2c43aab6ac..913f2dccb2 100644 --- a/packages/svelte/src/legacy/legacy-server.js +++ b/packages/svelte/src/legacy/legacy-server.js @@ -1,5 +1,5 @@ /** @import { SvelteComponent } from '../index.js' */ -/** @import { Csp } from '#server' */ +/** @import { Csp } from '../server/public.js' */ import { asClassComponent as as_class_component, createClassComponent } from './legacy-client.js'; import { render } from '../internal/server/index.js'; import { async_mode_flag } from '../internal/flags/index.js'; diff --git a/packages/svelte/src/server/index.d.ts b/packages/svelte/src/server/index.d.ts index db75afb9ed..7297c57490 100644 --- a/packages/svelte/src/server/index.d.ts +++ b/packages/svelte/src/server/index.d.ts @@ -1,6 +1,8 @@ -import type { Csp, RenderOutput } from '#server'; +import type { Csp, RenderOutput } from './public.js'; import type { ComponentProps, Component, SvelteComponent, ComponentType } from 'svelte'; +export type { Csp, RenderOutput, SyncRenderOutput, Sha256Source } from './public.js'; + /** * Only available on the server and when compiling with the `server` option. * Takes a component and returns an object with `body` and `head` properties on it, which you can use to populate the HTML when server-rendering your app. diff --git a/packages/svelte/src/server/public.d.ts b/packages/svelte/src/server/public.d.ts new file mode 100644 index 0000000000..8ef98aef9c --- /dev/null +++ b/packages/svelte/src/server/public.d.ts @@ -0,0 +1,17 @@ +export type Csp = { nonce?: string; hash?: boolean }; + +export type Sha256Source = `sha256-${string}`; + +export interface SyncRenderOutput { + /** HTML that goes into the `` */ + head: string; + /** @deprecated use `body` instead */ + html: string; + /** HTML that goes somewhere into the `` */ + body: string; + hashes: { + script: Sha256Source[]; + }; +} + +export type RenderOutput = SyncRenderOutput & PromiseLike; diff --git a/packages/svelte/tests/server-side-rendering/test.ts b/packages/svelte/tests/server-side-rendering/test.ts index 2dcaa85708..16f7c422b8 100644 --- a/packages/svelte/tests/server-side-rendering/test.ts +++ b/packages/svelte/tests/server-side-rendering/test.ts @@ -6,13 +6,12 @@ import * as fs from 'node:fs'; import { assert } from 'vitest'; -import { render } from 'svelte/server'; +import { render, type SyncRenderOutput } from 'svelte/server'; import { compile_directory, should_update_expected, try_read_file } from '../helpers.js'; import { assert_html_equal_with_options } from '../html_equal.js'; import { suite_with_variants, type BaseTest } from '../suite.js'; import type { CompileOptions } from '#compiler'; import { seen } from '../../src/internal/server/dev.js'; -import type { SyncRenderOutput } from '#server'; interface SSRTest extends BaseTest { mode?: ('sync' | 'async')[]; diff --git a/packages/svelte/types/index.d.ts b/packages/svelte/types/index.d.ts index b752ef5e07..5fcae1b2e0 100644 --- a/packages/svelte/types/index.d.ts +++ b/packages/svelte/types/index.d.ts @@ -2616,11 +2616,11 @@ declare module 'svelte/server' { } ] ): RenderOutput; - type Csp = { nonce?: string; hash?: boolean }; + export type Csp = { nonce?: string; hash?: boolean }; - type Sha256Source = `sha256-${string}`; + export type Sha256Source = `sha256-${string}`; - interface SyncRenderOutput { + export interface SyncRenderOutput { /** HTML that goes into the `` */ head: string; /** @deprecated use `body` instead */ @@ -2632,7 +2632,7 @@ declare module 'svelte/server' { }; } - type RenderOutput = SyncRenderOutput & PromiseLike; + export type RenderOutput = SyncRenderOutput & PromiseLike; export {}; } From f5f70343df452f32c6a1ea918bb458d8117706ce Mon Sep 17 00:00:00 2001 From: Liam O'Dea Date: Thu, 20 Aug 2026 23:11:14 +0200 Subject: [PATCH 152/222] =?UTF-8?q?perf:=20O(n=C2=B2)=E2=86=92O(n)=20Map?= =?UTF-8?q?=20lookups=20for=20legacy=20reactive=20statements=20(#18602)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .changeset/legacy-reactive-map-lookups.md | 5 +++++ packages/svelte/src/compiler/phases/2-analyze/index.js | 3 ++- .../compiler/phases/3-transform/client/transform-client.js | 4 ++-- .../compiler/phases/3-transform/server/transform-server.js | 4 ++-- 4 files changed, 11 insertions(+), 5 deletions(-) create mode 100644 .changeset/legacy-reactive-map-lookups.md diff --git a/.changeset/legacy-reactive-map-lookups.md b/.changeset/legacy-reactive-map-lookups.md new file mode 100644 index 0000000000..ec6f4fbf9d --- /dev/null +++ b/.changeset/legacy-reactive-map-lookups.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +perf: O(n²)→O(n) Map lookups for legacy `$:` reactive statement ordering diff --git a/packages/svelte/src/compiler/phases/2-analyze/index.js b/packages/svelte/src/compiler/phases/2-analyze/index.js index f9cf2f1d89..1fd83e65b9 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/index.js +++ b/packages/svelte/src/compiler/phases/2-analyze/index.js @@ -1318,7 +1318,8 @@ function order_reactive_statements(unsorted_reactive_declarations) { * @returns */ const add_declaration = (node, declaration) => { - if ([...reactive_declarations.values()].includes(declaration)) return; + // Visited set: each ReactiveStatement is stored under exactly one LabeledStatement node + if (reactive_declarations.has(node)) return; for (const binding of declaration.dependencies) { if (declaration.assignments.has(binding)) continue; diff --git a/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js b/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js index 552fe89960..cbb5f2eac7 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js @@ -247,11 +247,11 @@ export function client_component(analysis, options) { } for (const [node] of analysis.reactive_statements) { - const statement = [...state.legacy_reactive_statements].find(([n]) => n === node); + const statement = state.legacy_reactive_statements.get(node); if (statement === undefined) { throw new Error('Could not find reactive statement'); } - instance.body.push(statement[1]); + instance.body.push(statement); } if (analysis.reactive_statements.size > 0) { diff --git a/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js b/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js index 24ca58bb41..0a533aec37 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js @@ -148,7 +148,7 @@ export function server_component(analysis, options) { const legacy_reactive_declarations = []; for (const [node] of analysis.reactive_statements) { - const statement = [...state.legacy_reactive_statements].find(([n]) => n === node); + const statement = state.legacy_reactive_statements.get(node); if (statement === undefined) { throw new Error('Could not find reactive statement'); } @@ -165,7 +165,7 @@ export function server_component(analysis, options) { } } - instance.body.push(statement[1]); + instance.body.push(statement); } if (legacy_reactive_declarations.length > 0) { From f4918d86f3f82cb03535836c329457b1a57e1fdb Mon Sep 17 00:00:00 2001 From: Jared Dunham Date: Thu, 20 Aug 2026 16:30:04 -0500 Subject: [PATCH 153/222] docs: added generic type parameter example for $state (#18494) --- documentation/docs/07-misc/03-typescript.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/documentation/docs/07-misc/03-typescript.md b/documentation/docs/07-misc/03-typescript.md index b7f49f9c6f..daa73b5e98 100644 --- a/documentation/docs/07-misc/03-typescript.md +++ b/documentation/docs/07-misc/03-typescript.md @@ -170,6 +170,12 @@ If you don't give `$state` an initial value, part of its types will be `undefine let count: number = $state(); ``` +You can pass the type directly as a generic parameter to safely handle this. TypeScript will infer the variable as `number | undefined`. + +```ts +let count = $state(); +``` + If you know that the variable _will_ be defined before you first use it, use an `as` casting. This is especially useful in the context of classes: ```ts From 38ef714f1a2fa0e74b60e74ca46a8977ddac26b7 Mon Sep 17 00:00:00 2001 From: Kamil Jakubus Date: Thu, 20 Aug 2026 23:30:19 +0200 Subject: [PATCH 154/222] fix: scope SSR boundary failed snippets to their boundary (#18593) Each `` with: ```svelte {#snippet failed(error)} ... {/snippet} ``` generated a function named failed. Sibling boundaries placed both functions in the same SSR scope: ```js { function failed() {} function failed() {} // Identifier `failed` has already been declared } ``` Resulting in a `[PARSE_ERROR] Identifier `failed` has already been declared` error during build time. A let declaration such as `{const x = 0}` can create a nested lexical block, exposing the collision. The fix gives each boundary its own scope: ```js { function failed() {} $$renderer.boundary({ failed }, ...); } { function failed() {} $$renderer.boundary({ failed }, ...); } `` --- .changeset/young-papers-hide.md | 5 +++++ .../3-transform/server/visitors/SvelteBoundary.js | 13 +++++++++---- .../_expected.html | 1 + .../boundary-duplicate-failed-snippets/main.svelte | 11 +++++++++++ 4 files changed, 26 insertions(+), 4 deletions(-) create mode 100644 .changeset/young-papers-hide.md create mode 100644 packages/svelte/tests/server-side-rendering/samples/boundary-duplicate-failed-snippets/_expected.html create mode 100644 packages/svelte/tests/server-side-rendering/samples/boundary-duplicate-failed-snippets/main.svelte diff --git a/.changeset/young-papers-hide.md b/.changeset/young-papers-hide.md new file mode 100644 index 0000000000..7df1f114b9 --- /dev/null +++ b/.changeset/young-papers-hide.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: scope SSR boundary failed snippets to their boundary diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/SvelteBoundary.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/SvelteBoundary.js index 47a07a1312..feef4bd1a9 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/SvelteBoundary.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/SvelteBoundary.js @@ -1,4 +1,4 @@ -/** @import { BlockStatement } from 'estree' */ +/** @import { BlockStatement, Statement } from 'estree' */ /** @import { AST } from '#compiler' */ /** @import { ComponentContext } from '../types' */ import * as b from '#compiler/builders'; @@ -84,6 +84,9 @@ export function SvelteBoundary(node, context) { } const props = b.object([]); + /** @type {Statement[]} */ + const init = []; + if (failed_attribute && !failed_snippet) { const failed_callee = build_attribute_value( failed_attribute.value, @@ -95,13 +98,15 @@ export function SvelteBoundary(node, context) { props.properties.push(b.init('failed', failed_callee)); } else if (failed_snippet) { - context.visit(failed_snippet, context.state); + context.visit(failed_snippet, { ...context.state, init }); props.properties.push(b.init('failed', failed_snippet.expression)); } - context.state.template.push( - b.stmt(b.call('$$renderer.boundary', props, b.arrow([b.id('$$renderer')], children_body))) + const boundary = b.stmt( + b.call('$$renderer.boundary', props, b.arrow([b.id('$$renderer')], children_body)) ); + + context.state.template.push(init.length > 0 ? b.block([...init, boundary]) : boundary); } /** diff --git a/packages/svelte/tests/server-side-rendering/samples/boundary-duplicate-failed-snippets/_expected.html b/packages/svelte/tests/server-side-rendering/samples/boundary-duplicate-failed-snippets/_expected.html new file mode 100644 index 0000000000..7c89b545c5 --- /dev/null +++ b/packages/svelte/tests/server-side-rendering/samples/boundary-duplicate-failed-snippets/_expected.html @@ -0,0 +1 @@ +
    diff --git a/packages/svelte/tests/server-side-rendering/samples/boundary-duplicate-failed-snippets/main.svelte b/packages/svelte/tests/server-side-rendering/samples/boundary-duplicate-failed-snippets/main.svelte new file mode 100644 index 0000000000..126c9ea304 --- /dev/null +++ b/packages/svelte/tests/server-side-rendering/samples/boundary-duplicate-failed-snippets/main.svelte @@ -0,0 +1,11 @@ +
    + {const x = 0} + + + {#snippet failed()}{/snippet} + + + + {#snippet failed()}{/snippet} + +
    From 07dda4056b56024dd263eb93c8d7d1c16c9a72d6 Mon Sep 17 00:00:00 2001 From: AminBouchareb <93881530+el-amin-dev@users.noreply.github.com> Date: Thu, 20 Aug 2026 22:30:33 +0100 Subject: [PATCH 155/222] docs: document rune_missing_parentheses compiler error (#18589) --- .../docs/98-reference/.generated/compile-errors.md | 12 ++++++++++++ packages/svelte/messages/compile-errors/script.md | 12 ++++++++++++ 2 files changed, 24 insertions(+) diff --git a/documentation/docs/98-reference/.generated/compile-errors.md b/documentation/docs/98-reference/.generated/compile-errors.md index 0d25173711..02e762dc48 100644 --- a/documentation/docs/98-reference/.generated/compile-errors.md +++ b/documentation/docs/98-reference/.generated/compile-errors.md @@ -821,6 +821,18 @@ Cannot use `%rune%` rune in non-runes mode Cannot use rune without parentheses ``` +Runes are keywords rather than values — they can't be assigned to a variable or passed to a function, only called. Referencing one without parentheses is therefore an error... + +```js +let count = $state; +``` + +...whether it's a rune like `$state` or one reached through a property, like `$derived.by`. Add the parentheses, along with any arguments the rune expects: + +```js +let count = $state(0); +``` + ### rune_removed ``` diff --git a/packages/svelte/messages/compile-errors/script.md b/packages/svelte/messages/compile-errors/script.md index 743fe31fde..9980db4c19 100644 --- a/packages/svelte/messages/compile-errors/script.md +++ b/packages/svelte/messages/compile-errors/script.md @@ -186,6 +186,18 @@ This turned out to be buggy and unpredictable, particularly when working with de > Cannot use rune without parentheses +Runes are keywords rather than values — they can't be assigned to a variable or passed to a function, only called. Referencing one without parentheses is therefore an error... + +```js +let count = $state; +``` + +...whether it's a rune like `$state` or one reached through a property, like `$derived.by`. Add the parentheses, along with any arguments the rune expects: + +```js +let count = $state(0); +``` + ## rune_removed > The `%name%` rune has been removed From 793e309d4e2c2442ad62e5b44d5ded5ee530fd2a Mon Sep 17 00:00:00 2001 From: john gravois Date: Thu, 20 Aug 2026 14:41:46 -0700 Subject: [PATCH 156/222] docs: fix typo in basic markup doc (#18434) the current example in the docs conflates two different (commonly associated) words. https://www.reddit.com/r/namenerds/comments/1m6yiw6/what_do_we_think_of_the_name_aretha/ --- documentation/docs/03-template-syntax/01-basic-markup.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/documentation/docs/03-template-syntax/01-basic-markup.md b/documentation/docs/03-template-syntax/01-basic-markup.md index 436f307f84..89e01cf1ed 100644 --- a/documentation/docs/03-template-syntax/01-basic-markup.md +++ b/documentation/docs/03-template-syntax/01-basic-markup.md @@ -202,7 +202,7 @@ You can add a special comment starting with `@component` that will show up when - You can also use code blocks here. - Usage: ```html -
    +
    ``` --> + + + + diff --git a/packages/svelte/tests/validator/samples/global-event-reference-special-elements/warnings.json b/packages/svelte/tests/validator/samples/global-event-reference-special-elements/warnings.json new file mode 100644 index 0000000000..e69a45ab3e --- /dev/null +++ b/packages/svelte/tests/validator/samples/global-event-reference-special-elements/warnings.json @@ -0,0 +1,38 @@ +[ + { + "code": "attribute_global_event_reference", + "message": "You are referencing `globalThis.onresize`. Did you forget to declare a variable with that name?", + "start": { + "column": 27, + "line": 5 + }, + "end": { + "column": 37, + "line": 5 + } + }, + { + "code": "attribute_global_event_reference", + "message": "You are referencing `globalThis.onvisibilitychange`. Did you forget to declare a variable with that name?", + "start": { + "column": 17, + "line": 6 + }, + "end": { + "column": 37, + "line": 6 + } + }, + { + "code": "attribute_global_event_reference", + "message": "You are referencing `globalThis.onfocus`. Did you forget to declare a variable with that name?", + "start": { + "column": 13, + "line": 7 + }, + "end": { + "column": 22, + "line": 7 + } + } +] From 2d2e5df26e0defc3ad5d0feb3b61e86a7145633f Mon Sep 17 00:00:00 2001 From: Khaled Waleed Date: Fri, 21 Aug 2026 13:58:47 +0300 Subject: [PATCH 159/222] fix: don't turn component instances stored in $state into state proxies (#18646) Fixes #18416. We previously said that we don't want to handle component instances specifically when they're wrapped with state in #16747 - though the use case presented back then was much more arcane than the one in #18416. Therefore we now don't proxify component instances anymore, which also makes the dev time proxy warning obsolete. --------- Co-authored-by: Claude Co-authored-by: Simon Holthausen Co-authored-by: Simon H <5968653+dummdidumm@users.noreply.github.com> --- .changeset/olive-crabs-refuse.md | 5 +++++ .../.generated/client-warnings.md | 21 ------------------- .../messages/client-warnings/warnings.md | 19 ----------------- .../svelte/src/internal/client/constants.js | 2 ++ .../svelte/src/internal/client/context.js | 14 +++++++++++-- .../client/dom/elements/bindings/this.js | 9 ++++++-- packages/svelte/src/internal/client/proxy.js | 11 +++++++--- packages/svelte/src/internal/client/render.js | 12 ++++------- .../svelte/src/internal/client/warnings.js | 11 ---------- .../Child.svelte | 7 +++++++ .../_config.js | 15 +++++++++++++ .../bind-this-component-in-state-dev/data.js | 1 + .../main.svelte | 11 ++++++++++ .../bind-this-component-in-state/Child.svelte | 7 +++++++ .../bind-this-component-in-state/_config.js | 11 ++++++++++ .../bind-this-component-in-state/data.js | 1 + .../bind-this-component-in-state/main.svelte | 11 ++++++++++ 17 files changed, 102 insertions(+), 66 deletions(-) create mode 100644 .changeset/olive-crabs-refuse.md create mode 100644 packages/svelte/tests/runtime-runes/samples/bind-this-component-in-state-dev/Child.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/bind-this-component-in-state-dev/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/bind-this-component-in-state-dev/data.js create mode 100644 packages/svelte/tests/runtime-runes/samples/bind-this-component-in-state-dev/main.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/bind-this-component-in-state/Child.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/bind-this-component-in-state/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/bind-this-component-in-state/data.js create mode 100644 packages/svelte/tests/runtime-runes/samples/bind-this-component-in-state/main.svelte diff --git a/.changeset/olive-crabs-refuse.md b/.changeset/olive-crabs-refuse.md new file mode 100644 index 0000000000..a928ae7f19 --- /dev/null +++ b/.changeset/olive-crabs-refuse.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: don't turn component instances stored in `$state` into state proxies diff --git a/documentation/docs/98-reference/.generated/client-warnings.md b/documentation/docs/98-reference/.generated/client-warnings.md index 73c97c253c..75eff181f9 100644 --- a/documentation/docs/98-reference/.generated/client-warnings.md +++ b/documentation/docs/98-reference/.generated/client-warnings.md @@ -339,27 +339,6 @@ Reactive `$state(...)` proxies and the values they proxy have different identiti To resolve this, ensure you're comparing values where both values were created with `$state(...)`, or neither were. Note that `$state.raw(...)` will _not_ create a state proxy. -### state_proxy_unmount - -``` -Tried to unmount a state proxy, rather than a component -``` - -`unmount` was called with a state proxy: - -```js -import { mount, unmount } from 'svelte'; -import Component from './Component.svelte'; -let target = document.body; -// ---cut--- -let component = $state(mount(Component, { target })); - -// later... -unmount(component); -``` - -Avoid using `$state` here. If `component` _does_ need to be reactive for some reason, use `$state.raw` instead. - ### svelte_boundary_reset_noop ``` diff --git a/packages/svelte/messages/client-warnings/warnings.md b/packages/svelte/messages/client-warnings/warnings.md index 58d00f3933..ac2e103ffe 100644 --- a/packages/svelte/messages/client-warnings/warnings.md +++ b/packages/svelte/messages/client-warnings/warnings.md @@ -295,25 +295,6 @@ To silence the warning, ensure that `value`: To resolve this, ensure you're comparing values where both values were created with `$state(...)`, or neither were. Note that `$state.raw(...)` will _not_ create a state proxy. -## state_proxy_unmount - -> Tried to unmount a state proxy, rather than a component - -`unmount` was called with a state proxy: - -```js -import { mount, unmount } from 'svelte'; -import Component from './Component.svelte'; -let target = document.body; -// ---cut--- -let component = $state(mount(Component, { target })); - -// later... -unmount(component); -``` - -Avoid using `$state` here. If `component` _does_ need to be reactive for some reason, use `$state.raw` instead. - ## svelte_boundary_reset_noop > A `` `reset` function only resets the boundary the first time it is called diff --git a/packages/svelte/src/internal/client/constants.js b/packages/svelte/src/internal/client/constants.js index 043b50b4b2..b92ea8598b 100644 --- a/packages/svelte/src/internal/client/constants.js +++ b/packages/svelte/src/internal/client/constants.js @@ -60,6 +60,8 @@ export const ASYNC = 1 << 22; export const ERROR_VALUE = 1 << 23; export const STATE_SYMBOL = Symbol('$state'); +/** Marks component export objects, so that `proxy(...)` leaves them untouched */ +export const COMPONENT_SYMBOL = Symbol('component'); export const LEGACY_PROPS = Symbol('legacy props'); export const LOADING_ATTR_SYMBOL = Symbol(''); export const PROXY_PATH_SYMBOL = Symbol('proxy path'); diff --git a/packages/svelte/src/internal/client/context.js b/packages/svelte/src/internal/client/context.js index 0a7b2cdf31..e1571862ed 100644 --- a/packages/svelte/src/internal/client/context.js +++ b/packages/svelte/src/internal/client/context.js @@ -5,7 +5,8 @@ import { active_effect, active_reaction } from './runtime.js'; import { create_user_effect } from './reactivity/effects.js'; import { async_mode_flag, legacy_mode_flag } from '../flags/index.js'; import { FILENAME } from '../../constants.js'; -import { BRANCH_EFFECT } from './constants.js'; +import { BRANCH_EFFECT, COMPONENT_SYMBOL } from './constants.js'; +import { define_property } from '../shared/utils.js'; import { create_context, get_or_init_context_map } from '../shared/context.js'; /** @type {ComponentContext | null} */ @@ -217,7 +218,16 @@ export function pop(component) { dev_current_component_function = component_context?.function ?? null; } - return component ?? /** @type {T} */ ({}); + return mark_as_component(component); +} + +/** + * Add a symbol to the object (or create one if undefined) to mark it as a component so it isn't proxified. + * @param {any} component + */ +export function mark_as_component(component = {}) { + define_property(component, COMPONENT_SYMBOL, { value: true }); + return component; } /** @returns {boolean} */ diff --git a/packages/svelte/src/internal/client/dom/elements/bindings/this.js b/packages/svelte/src/internal/client/dom/elements/bindings/this.js index 52f0c213d3..705adc99ec 100644 --- a/packages/svelte/src/internal/client/dom/elements/bindings/this.js +++ b/packages/svelte/src/internal/client/dom/elements/bindings/this.js @@ -1,6 +1,6 @@ /** @import { ComponentContext, Effect } from '#client' */ import { DESTROYING, STATE_SYMBOL } from '#client/constants'; -import { component_context } from '../../../context.js'; +import { component_context, mark_as_component } from '../../../context.js'; import { effect, render_effect } from '../../../reactivity/effects.js'; import { active_effect, untrack } from '../../../runtime.js'; @@ -23,7 +23,12 @@ function is_bound_this(bound_value, element_or_component) { * returns all the parts of the each block context that are used in the expression * @returns {void} */ -export function bind_this(element_or_component = {}, update, get_value, get_parts) { +export function bind_this( + element_or_component = mark_as_component(), + update, + get_value, + get_parts +) { var component_effect = /** @type {ComponentContext} */ (component_context).r; var parent = /** @type {Effect} */ (active_effect); diff --git a/packages/svelte/src/internal/client/proxy.js b/packages/svelte/src/internal/client/proxy.js index 51333e597f..91d82f8903 100644 --- a/packages/svelte/src/internal/client/proxy.js +++ b/packages/svelte/src/internal/client/proxy.js @@ -22,7 +22,7 @@ import { flush_eager_effects, set_eager_effects_deferred } from './reactivity/sources.js'; -import { PROXY_PATH_SYMBOL, STATE_SYMBOL } from '#client/constants'; +import { COMPONENT_SYMBOL, PROXY_PATH_SYMBOL, STATE_SYMBOL } from '#client/constants'; import { UNINITIALIZED } from '../../constants.js'; import * as e from './errors.js'; import { tag } from './dev/tracing.js'; @@ -38,8 +38,13 @@ const regex_is_valid_identifier = /^[a-zA-Z_$][a-zA-Z_$0-9]*$/; * @returns {T} */ export function proxy(value) { - // if non-proxyable, or is already a proxy, return `value` - if (typeof value !== 'object' || value === null || STATE_SYMBOL in value) { + // if non-proxyable, a component instance, or already a proxy, return `value` + if ( + typeof value !== 'object' || + value === null || + STATE_SYMBOL in value || + COMPONENT_SYMBOL in value + ) { return value; } diff --git a/packages/svelte/src/internal/client/render.js b/packages/svelte/src/internal/client/render.js index 50832fb3ff..2abedb3a55 100644 --- a/packages/svelte/src/internal/client/render.js +++ b/packages/svelte/src/internal/client/render.js @@ -10,7 +10,7 @@ import { } from './dom/operations.js'; import { HYDRATION_END, HYDRATION_ERROR, HYDRATION_START } from '../../constants.js'; import { active_effect } from './runtime.js'; -import { push, pop, component_context } from './context.js'; +import { push, pop, component_context, mark_as_component } from './context.js'; import { component_root } from './reactivity/effects.js'; import { hydrate_node, hydrating, set_hydrate_node, set_hydrating } from './dom/hydration.js'; import { array_from } from '../shared/utils.js'; @@ -23,7 +23,7 @@ import * as w from './warnings.js'; import * as e from './errors.js'; import { assign_nodes } from './dom/template.js'; import { is_passive_event } from '../../utils.js'; -import { COMMENT_NODE, STATE_SYMBOL, TEXT_CACHE } from './constants.js'; +import { COMMENT_NODE, TEXT_CACHE } from './constants.js'; import { boundary } from './dom/blocks/boundary.js'; /** @@ -193,7 +193,7 @@ function _mount( should_intro = intro; // @ts-expect-error the public typings are not what the actual function looks like - component = Component(anchor_node, props) || {}; + component = Component(anchor_node, props) || mark_as_component(); should_intro = true; if (hydrating) { @@ -323,11 +323,7 @@ export function unmount(component, options) { } if (DEV) { - if (STATE_SYMBOL in component) { - w.state_proxy_unmount(); - } else { - w.lifecycle_double_unmount(); - } + w.lifecycle_double_unmount(); } return Promise.resolve(); diff --git a/packages/svelte/src/internal/client/warnings.js b/packages/svelte/src/internal/client/warnings.js index f4e605ac96..ced1a8b2c3 100644 --- a/packages/svelte/src/internal/client/warnings.js +++ b/packages/svelte/src/internal/client/warnings.js @@ -247,17 +247,6 @@ export function state_proxy_equality_mismatch(operator) { } } -/** - * Tried to unmount a state proxy, rather than a component - */ -export function state_proxy_unmount() { - if (DEV) { - console.warn(`%c[svelte] state_proxy_unmount\n%cTried to unmount a state proxy, rather than a component\nhttps://svelte.dev/e/state_proxy_unmount`, bold, normal); - } else { - console.warn(`https://svelte.dev/e/state_proxy_unmount`); - } -} - /** * A `` `reset` function only resets the boundary the first time it is called */ diff --git a/packages/svelte/tests/runtime-runes/samples/bind-this-component-in-state-dev/Child.svelte b/packages/svelte/tests/runtime-runes/samples/bind-this-component-in-state-dev/Child.svelte new file mode 100644 index 0000000000..2eb83744c4 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/bind-this-component-in-state-dev/Child.svelte @@ -0,0 +1,7 @@ + + +

    child

    diff --git a/packages/svelte/tests/runtime-runes/samples/bind-this-component-in-state-dev/_config.js b/packages/svelte/tests/runtime-runes/samples/bind-this-component-in-state-dev/_config.js new file mode 100644 index 0000000000..daf97f7da6 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/bind-this-component-in-state-dev/_config.js @@ -0,0 +1,15 @@ +import { test } from '../../test'; +import { items } from './data.js'; + +export default test({ + compileOptions: { + dev: true + }, + + html: `

    child

    `, + + test({ assert, instance }) { + // ensure component instance doesn't get proxified (https://github.com/sveltejs/svelte/issues/18416) + assert.ok(instance.get_first().myArr === items); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/bind-this-component-in-state-dev/data.js b/packages/svelte/tests/runtime-runes/samples/bind-this-component-in-state-dev/data.js new file mode 100644 index 0000000000..06c15d09e4 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/bind-this-component-in-state-dev/data.js @@ -0,0 +1 @@ +export const items = [{ id: 5, name: 'John' }]; diff --git a/packages/svelte/tests/runtime-runes/samples/bind-this-component-in-state-dev/main.svelte b/packages/svelte/tests/runtime-runes/samples/bind-this-component-in-state-dev/main.svelte new file mode 100644 index 0000000000..5f5ecddab2 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/bind-this-component-in-state-dev/main.svelte @@ -0,0 +1,11 @@ + + + diff --git a/packages/svelte/tests/runtime-runes/samples/bind-this-component-in-state/Child.svelte b/packages/svelte/tests/runtime-runes/samples/bind-this-component-in-state/Child.svelte new file mode 100644 index 0000000000..2eb83744c4 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/bind-this-component-in-state/Child.svelte @@ -0,0 +1,7 @@ + + +

    child

    diff --git a/packages/svelte/tests/runtime-runes/samples/bind-this-component-in-state/_config.js b/packages/svelte/tests/runtime-runes/samples/bind-this-component-in-state/_config.js new file mode 100644 index 0000000000..e845e6932e --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/bind-this-component-in-state/_config.js @@ -0,0 +1,11 @@ +import { test } from '../../test'; +import { items } from './data.js'; + +export default test({ + html: `

    child

    `, + + test({ assert, instance }) { + // ensure component instance doesn't get proxified (https://github.com/sveltejs/svelte/issues/18416) + assert.ok(instance.get_first().myArr === items); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/bind-this-component-in-state/data.js b/packages/svelte/tests/runtime-runes/samples/bind-this-component-in-state/data.js new file mode 100644 index 0000000000..06c15d09e4 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/bind-this-component-in-state/data.js @@ -0,0 +1 @@ +export const items = [{ id: 5, name: 'John' }]; diff --git a/packages/svelte/tests/runtime-runes/samples/bind-this-component-in-state/main.svelte b/packages/svelte/tests/runtime-runes/samples/bind-this-component-in-state/main.svelte new file mode 100644 index 0000000000..5f5ecddab2 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/bind-this-component-in-state/main.svelte @@ -0,0 +1,11 @@ + + + From 9d0062d60784474ac9e618733e51d8ef6315d13c Mon Sep 17 00:00:00 2001 From: Nic Polumeyv Date: Fri, 21 Aug 2026 07:08:14 -0400 Subject: [PATCH 160/222] fix: make template store subscriptions wait for the promise that assigns the store (#18582) Blockers didn't include analyzing implicit store subscriptions, which could also only happen in the template. Also needs to defer store unsubscribe until after the async template has settled in case the store value is read after an async blocker, in which case unsubscribe synchronously is too soon. Fixes https://github.com/sveltejs/kit/issues/15119 --- .changeset/async-store-sub-blocker.md | 5 ++++ .../src/compiler/phases/2-analyze/index.js | 17 +++++++++++++ .../3-transform/server/transform-server.js | 25 +++++++++++++------ .../async-store-sub-blocker/_config.js | 14 +++++++++++ .../async-store-sub-blocker/main.svelte | 16 ++++++++++++ .../async-store-sub-teardown/_config.js | 18 +++++++++++++ .../async-store-sub-teardown/main.svelte | 11 ++++++++ .../samples/async-store-sub-teardown/store.js | 23 +++++++++++++++++ 8 files changed, 122 insertions(+), 7 deletions(-) create mode 100644 .changeset/async-store-sub-blocker.md create mode 100644 packages/svelte/tests/runtime-runes/samples/async-store-sub-blocker/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-store-sub-blocker/main.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/async-store-sub-teardown/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-store-sub-teardown/main.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/async-store-sub-teardown/store.js diff --git a/.changeset/async-store-sub-blocker.md b/.changeset/async-store-sub-blocker.md new file mode 100644 index 0000000000..0222dac846 --- /dev/null +++ b/.changeset/async-store-sub-blocker.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: block template store subscriptions on the promise that assigns the store diff --git a/packages/svelte/src/compiler/phases/2-analyze/index.js b/packages/svelte/src/compiler/phases/2-analyze/index.js index 1fd83e65b9..45f703901d 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/index.js +++ b/packages/svelte/src/compiler/phases/2-analyze/index.js @@ -1228,6 +1228,23 @@ function calculate_blockers(instance, analysis) { flush_sync_group(); + // a store subscription must wait on whatever blocks the store itself; this must happen + // before function tracing so that functions reading `$store` inherit the blocker + for (const [name, binding] of instance.scope.declarations) { + if (binding.kind !== 'store_sub') continue; + + const store_blocker = instance.scope.get(name.slice(1))?.blocker; + if (!store_blocker) continue; + + if ( + !binding.blocker || + /** @type {ESTree.SimpleLiteral & { value: number }} */ (binding.blocker.property).value < + /** @type {ESTree.SimpleLiteral & { value: number }} */ (store_blocker.property).value + ) { + binding.blocker = store_blocker; + } + } + for (const fn of functions) { /** @type {Set} */ const reads_writes = new Set(); diff --git a/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js b/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js index 0a533aec37..44690a1efe 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js @@ -210,14 +210,25 @@ export function server_component(analysis, options) { ]; } - if ( - [...analysis.instance.scope.declarations.values()].some( - (binding) => binding.kind === 'store_sub' - ) - ) { + const store_subs = [...analysis.instance.scope.declarations.values()].filter( + (binding) => binding.kind === 'store_sub' + ); + + // a blocked subscription is only created once its promise resolves, so its teardown must wait until the render is done + const defer_store_teardown = store_subs.some((binding) => binding.blocker); + + if (store_subs.length > 0) { instance.body.unshift(b.var('$$store_subs')); + + const unsubscribe = b.if( + b.id('$$store_subs'), + b.stmt(b.call('$.unsubscribe_stores', b.id('$$store_subs'))) + ); + template.body.push( - b.if(b.id('$$store_subs'), b.stmt(b.call('$.unsubscribe_stores', b.id('$$store_subs')))) + defer_store_teardown + ? b.stmt(b.call('$$renderer.on_destroy', b.arrow([], b.block([unsubscribe])))) + : unsubscribe ); } @@ -257,7 +268,7 @@ export function server_component(analysis, options) { ); } - let should_inject_context = dev || analysis.needs_context; + let should_inject_context = dev || analysis.needs_context || defer_store_teardown; if (should_inject_context) { component_block = b.block([ diff --git a/packages/svelte/tests/runtime-runes/samples/async-store-sub-blocker/_config.js b/packages/svelte/tests/runtime-runes/samples/async-store-sub-blocker/_config.js new file mode 100644 index 0000000000..896385e609 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-store-sub-blocker/_config.js @@ -0,0 +1,14 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +// Tests that a store subscription only present in the template waits for the +// promise that assigns the store instead of subscribing to `undefined`, +// including when the subscription is read through a function. +export default test({ + mode: ['client', 'hydrate', 'async-server'], + ssrHtml: '

    hello

    hello

    ', + async test({ assert, target }) { + await tick(); + assert.htmlEqual(target.innerHTML, '

    hello

    hello

    '); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-store-sub-blocker/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-store-sub-blocker/main.svelte new file mode 100644 index 0000000000..04c707c66d --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-store-sub-blocker/main.svelte @@ -0,0 +1,16 @@ + + +

    {$store}

    +

    {read()}

    diff --git a/packages/svelte/tests/runtime-runes/samples/async-store-sub-teardown/_config.js b/packages/svelte/tests/runtime-runes/samples/async-store-sub-teardown/_config.js new file mode 100644 index 0000000000..ac60694648 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-store-sub-teardown/_config.js @@ -0,0 +1,18 @@ +import { test } from '../../test'; +import { counts, reset } from './store.js'; + +// A blocked store subscription is created after the synchronous part of the +// render has finished, so the teardown must wait for the async work. +export default test({ + mode: ['async-server'], + + before_test() { + reset(); + }, + + ssrHtml: '

    hello

    ', + + test_ssr({ assert }) { + assert.deepEqual(counts, { subscribes: 1, unsubscribes: 1 }); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-store-sub-teardown/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-store-sub-teardown/main.svelte new file mode 100644 index 0000000000..8d17e701d3 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-store-sub-teardown/main.svelte @@ -0,0 +1,11 @@ + + +

    {$s}

    diff --git a/packages/svelte/tests/runtime-runes/samples/async-store-sub-teardown/store.js b/packages/svelte/tests/runtime-runes/samples/async-store-sub-teardown/store.js new file mode 100644 index 0000000000..4922c1b027 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-store-sub-teardown/store.js @@ -0,0 +1,23 @@ +import { writable } from 'svelte/store'; + +export const counts = { subscribes: 0, unsubscribes: 0 }; + +const inner = writable('hello'); + +export const store = { + /** @param {(value: string) => void} fn */ + subscribe(fn) { + counts.subscribes += 1; + const unsubscribe = inner.subscribe(fn); + + return () => { + counts.unsubscribes += 1; + unsubscribe(); + }; + } +}; + +export function reset() { + counts.subscribes = 0; + counts.unsubscribes = 0; +} From 6266debb21f0a7e6971ac9a117c9180e480022e3 Mon Sep 17 00:00:00 2001 From: Nic Polumeyv Date: Fri, 21 Aug 2026 07:35:44 -0400 Subject: [PATCH 161/222] docs: clarify when `$effect.pre` runs relative to DOM updates (#18534) Related to #16648 - describe the `$effect.pre` in more detail --------- Co-authored-by: Simon H <5968653+dummdidumm@users.noreply.github.com> --- .changeset/witty-donuts-explain.md | 5 +++++ documentation/docs/02-runes/04-$effect.md | 2 ++ documentation/docs/06-runtime/03-lifecycle-hooks.md | 2 +- packages/svelte/src/ambient.d.ts | 2 +- packages/svelte/types/index.d.ts | 2 +- 5 files changed, 10 insertions(+), 3 deletions(-) create mode 100644 .changeset/witty-donuts-explain.md diff --git a/.changeset/witty-donuts-explain.md b/.changeset/witty-donuts-explain.md new file mode 100644 index 0000000000..6ee8f66da6 --- /dev/null +++ b/.changeset/witty-donuts-explain.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +chore: clarify when `$effect.pre` runs relative to DOM updates diff --git a/documentation/docs/02-runes/04-$effect.md b/documentation/docs/02-runes/04-$effect.md index a13fc7bc46..de18add08f 100644 --- a/documentation/docs/02-runes/04-$effect.md +++ b/documentation/docs/02-runes/04-$effect.md @@ -205,6 +205,8 @@ In rare cases, you may need to run code _before_ the DOM updates. For this we ca ``` +`$effect.pre` runs before DOM updates that are scheduled after it, not before every DOM mutation in the flush - DOM of parent components may already be updated. When using [await expressions](await-expressions), block updates like `{#if ...}` and `{#each ...}` in the same component also run before `$effect.pre`. + Apart from the timing, `$effect.pre` works exactly like `$effect`. ## `$effect.tracking` diff --git a/documentation/docs/06-runtime/03-lifecycle-hooks.md b/documentation/docs/06-runtime/03-lifecycle-hooks.md index 95e1c260c1..c714dc2e52 100644 --- a/documentation/docs/06-runtime/03-lifecycle-hooks.md +++ b/documentation/docs/06-runtime/03-lifecycle-hooks.md @@ -102,7 +102,7 @@ To implement a chat window that autoscrolls to the bottom when new messages appe In Svelte 4, we do this with `beforeUpdate`, but this is a flawed approach — it fires before _every_ update, whether it's relevant or not. In the example below, we need to introduce checks like `updatingMessages` to make sure we don't mess with the scroll position when someone toggles dark mode. -With runes, we can use `$effect.pre`, which behaves the same as `$effect` but runs before the DOM is updated. As long as we explicitly reference `messages` inside the effect body, it will run whenever `messages` changes, but _not_ when `theme` changes. +With runes, we can use `$effect.pre`, which behaves the same as `$effect` but runs before DOM updates scheduled after it (see [$effect.pre]($effect#$effect.pre) for the exact ordering). As long as we explicitly reference `messages` inside the effect body, it will run whenever `messages` changes, but _not_ when `theme` changes. `beforeUpdate`, and its equally troublesome counterpart `afterUpdate`, are therefore deprecated in Svelte 5. diff --git a/packages/svelte/src/ambient.d.ts b/packages/svelte/src/ambient.d.ts index ed0a004fa1..a9b2cebe1a 100644 --- a/packages/svelte/src/ambient.d.ts +++ b/packages/svelte/src/ambient.d.ts @@ -261,7 +261,7 @@ declare function $effect(fn: () => void | (() => void)): void; declare namespace $effect { /** * Runs code right before a component is mounted to the DOM, and then whenever its dependencies change, i.e. `$state` or `$derived` values. - * The timing of the execution is right before the DOM is updated. + * The timing of the execution is right before the DOM that comes after it is updated; parent DOM may already have been updated by the time it runs. * * Example: * ```ts diff --git a/packages/svelte/types/index.d.ts b/packages/svelte/types/index.d.ts index 5fcae1b2e0..96be34ef9e 100644 --- a/packages/svelte/types/index.d.ts +++ b/packages/svelte/types/index.d.ts @@ -3481,7 +3481,7 @@ declare function $effect(fn: () => void | (() => void)): void; declare namespace $effect { /** * Runs code right before a component is mounted to the DOM, and then whenever its dependencies change, i.e. `$state` or `$derived` values. - * The timing of the execution is right before the DOM is updated. + * The timing of the execution is right before the DOM that comes after it is updated; parent DOM may already have been updated by the time it runs. * * Example: * ```ts From b2a24b0426f1a302a3d741526a33555fde28f269 Mon Sep 17 00:00:00 2001 From: Nic Polumeyv Date: Fri, 21 Aug 2026 07:51:00 -0400 Subject: [PATCH 162/222] fix: run `onDestroy` callbacks when a server render throws (#18585) Fixes #18584. There's multiple parts to this - abort signal was buggy. It wasn't scoped per render, so cross-talk was possible. Fix by scoping to renderer - onDestroy callbacks were skipped when something throws. Fix by carefully aborting the rest of the tree, waiting for settle, collect all callbacks and then call them --------- Co-authored-by: Simon H <5968653+dummdidumm@users.noreply.github.com> Co-authored-by: Simon Holthausen --- .changeset/great-hoops-tickle.md | 5 + .../src/internal/server/abort-signal.js | 17 +- .../svelte/src/internal/server/renderer.js | 190 +++++++++++++----- .../src/internal/server/renderer.test.ts | 173 ++++++++++++++++ 4 files changed, 331 insertions(+), 54 deletions(-) create mode 100644 .changeset/great-hoops-tickle.md diff --git a/.changeset/great-hoops-tickle.md b/.changeset/great-hoops-tickle.md new file mode 100644 index 0000000000..94b32588f1 --- /dev/null +++ b/.changeset/great-hoops-tickle.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: run `onDestroy` callbacks when a server render throws diff --git a/packages/svelte/src/internal/server/abort-signal.js b/packages/svelte/src/internal/server/abort-signal.js index a769a46e3d..16047bdbb1 100644 --- a/packages/svelte/src/internal/server/abort-signal.js +++ b/packages/svelte/src/internal/server/abort-signal.js @@ -1,13 +1,12 @@ -import { STALE_REACTION } from '#client/constants'; +import { ssr_context } from './context.js'; -/** @type {AbortController | null} */ -let controller = null; +export function getAbortSignal() { + let context = ssr_context; -export function abort() { - controller?.abort(STALE_REACTION); - controller = null; -} + while (context !== null) { + if (context.r !== null) return context.r.global.get_abort_signal(); + context = context.p; + } -export function getAbortSignal() { - return (controller ??= new AbortController()).signal; + return new AbortController().signal; } diff --git a/packages/svelte/src/internal/server/renderer.js b/packages/svelte/src/internal/server/renderer.js index 5fb0bb86d5..1a5199d18e 100644 --- a/packages/svelte/src/internal/server/renderer.js +++ b/packages/svelte/src/internal/server/renderer.js @@ -3,7 +3,7 @@ /** @import { Csp, RenderOutput, SyncRenderOutput, Sha256Source } from '../../server/public.js' */ /** @import { MaybePromise } from '#shared' */ import { async_mode_flag } from '../flags/index.js'; -import { abort } from './abort-signal.js'; +import { STALE_REACTION } from '../client/constants.js'; import { pop, push, set_ssr_context, ssr_context } from './context.js'; import * as e from './errors.js'; import * as w from './warnings.js'; @@ -180,7 +180,7 @@ export class Renderer { // prevent unhandled rejections, and attach the promise to the renderer instance // so that rejections correctly cause rendering to fail promise.catch(noop); - this.promise = promise; + this.promise = this.global.track(promise); return promises; } @@ -225,7 +225,7 @@ export class Renderer { e.await_invalid(); } - child.promise = result; + child.promise = child.global.track(result); } return child; @@ -273,7 +273,7 @@ export class Renderer { e.await_invalid(); } result.catch(noop); - child.promise = result; + child.promise = child.global.track(result); } } catch (error) { // synchronous errors are handled here, async errors will be handled in #collect_content_async @@ -293,12 +293,14 @@ export class Renderer { e.await_invalid(); } - child.promise = /** @type {Promise} */ (result).then((transformed) => { - set_ssr_context(parent_context); - child.#out.push(Renderer.#serialize_failed_boundary(transformed)); - failed_snippet(child, transformed, noop); - child.#out.push(BLOCK_CLOSE); - }); + child.promise = child.global.track( + /** @type {Promise} */ (result).then((transformed) => { + set_ssr_context(parent_context); + child.#out.push(Renderer.#serialize_failed_boundary(transformed)); + failed_snippet(child, transformed, noop); + child.#out.push(BLOCK_CLOSE); + }) + ); child.promise.catch(noop); } else { child.#out.push(Renderer.#serialize_failed_boundary(result)); @@ -317,8 +319,11 @@ export class Renderer { */ component(fn, component_fn) { push(component_fn); - const child = this.child(fn); - child.#is_component_body = true; + // mark before running so `onDestroy` callbacks are still collected if `fn` throws + this.child((renderer) => { + renderer.#is_component_body = true; + return fn(renderer); + }); pop(); } @@ -626,6 +631,49 @@ export class Renderer { } } + /** + * Runs every `onDestroy` callback in this renderer tree. On a failed render, + * cleanup errors are suppressed so they do not mask the render error. + * @param {boolean} suppress_errors + */ + #run_on_destroy(suppress_errors) { + let first_error; + let has_error = false; + + for (const cleanup of this.#collect_on_destroy()) { + try { + cleanup(); + } catch (error) { + if (!suppress_errors && !has_error) { + first_error = error; + has_error = true; + } + } + } + + if (has_error) throw first_error; + } + + /** + * @param {'sync' | 'async'} mode + * @param {{ idPrefix?: string; csp?: Csp; transformError?: (error: unknown) => unknown }} options + * @returns {Renderer} + */ + static #create(mode, options) { + if (options.idPrefix?.includes('--')) { + e.invalid_id_prefix(); + } + + return new Renderer( + new SSRState( + mode, + options.idPrefix ? options.idPrefix + '-' : '', + options.csp, + options.transformError + ) + ); + } + /** * Render a component. Throws if any of the children are performing asynchronous work. * @@ -636,13 +684,27 @@ export class Renderer { */ static #render(component, options) { var previous_context = ssr_context; + const renderer = Renderer.#create('sync', options); + /** @type {AccumulatedContent | undefined} */ + let result; + let render_error; + let failed = false; + try { - const renderer = Renderer.#open_render('sync', component, options); + try { + Renderer.#open_render(renderer, component, options); + result = Renderer.#close_render(renderer.#collect_content(), renderer); + } catch (error) { + render_error = error; + failed = true; + } + + renderer.#run_on_destroy(failed); + if (failed) throw render_error; - const content = renderer.#collect_content(); - return Renderer.#close_render(content, renderer); + return /** @type {AccumulatedContent} */ (result); } finally { - abort(); + renderer.global.abort(); set_ssr_context(previous_context); } } @@ -657,18 +719,35 @@ export class Renderer { */ static async #render_async(component, options) { const previous_context = ssr_context; + const renderer = Renderer.#create('async', options); + /** @type {(AccumulatedContent & { hashes: { script: Sha256Source[] } }) | undefined} */ + let result; + let render_error; + let failed = false; try { - const renderer = Renderer.#open_render('async', component, options); - const content = await renderer.#collect_content_async(); - const hydratables = await renderer.#collect_hydratables(); - if (hydratables !== null) { - content.head = hydratables + content.head; + try { + Renderer.#open_render(renderer, component, options); + const content = await renderer.#collect_content_async(); + const hydratables = await renderer.#collect_hydratables(); + if (hydratables !== null) { + content.head = hydratables + content.head; + } + result = Renderer.#close_render(content, renderer); + } catch (error) { + render_error = error; + failed = true; + renderer.global.abort(); + await renderer.global.settle(); } - return Renderer.#close_render(content, renderer); + + renderer.#run_on_destroy(failed); + if (failed) throw render_error; + + return /** @type {AccumulatedContent & { hashes: { script: Sha256Source[] } }} */ (result); } finally { set_ssr_context(previous_context); - abort(); + renderer.global.abort(); } } @@ -760,28 +839,15 @@ export class Renderer { /** * @template {Record} Props - * @param {'sync' | 'async'} mode + * @param {Renderer} renderer * @param {import('svelte').Component} component * @param {{ props?: Omit; context?: Map; idPrefix?: string; csp?: Csp; transformError?: (error: unknown) => unknown }} options - * @returns {Renderer} + * @returns {void} */ - static #open_render(mode, component, options) { - if (options.idPrefix?.includes('--')) { - e.invalid_id_prefix(); - } - + static #open_render(renderer, component, options) { var previous_context = ssr_context; try { - const renderer = new Renderer( - new SSRState( - mode, - options.idPrefix ? options.idPrefix + '-' : '', - options.csp, - options.transformError - ) - ); - /** @type {SSRContext} */ const context = { p: null, c: options.context ?? null, r: renderer }; set_ssr_context(context); @@ -790,8 +856,6 @@ export class Renderer { // @ts-expect-error component(renderer, options.props ?? {}); renderer.push(BLOCK_CLOSE); - - return renderer; } finally { set_ssr_context(previous_context); } @@ -803,10 +867,6 @@ export class Renderer { * @returns {AccumulatedContent & { hashes: { script: Sha256Source[] } }} */ static #close_render(content, renderer) { - for (const cleanup of renderer.#collect_on_destroy()) { - cleanup(); - } - let head = content.head + renderer.global.get_title(); let body = content.body; @@ -890,6 +950,14 @@ export class SSRState { /** @readonly @type {Set<{ hash: string; code: string }>} */ css = new Set(); + /** @type {Set>} */ + #pending = new Set(); + + /** @type {AbortController | null} */ + #controller = null; + + #aborted = false; + /** * `transformError` passed to `render`. Called when an error boundary catches an error. * Throws by default if unset in `render`. @@ -920,6 +988,38 @@ export class SSRState { this.uid = () => `${id_prefix}s${uid++}`; } + /** + * @template T + * @param {Promise} promise + * @returns {Promise} + */ + track(promise) { + this.#pending.add(promise); + promise.then( + () => this.#pending.delete(promise), + () => this.#pending.delete(promise) + ); + return promise; + } + + async settle() { + while (this.#pending.size > 0) { + await Promise.allSettled([...this.#pending]); + } + } + + abort() { + if (this.#aborted) return; + this.#aborted = true; + this.#controller?.abort(STALE_REACTION); + } + + get_abort_signal() { + const controller = (this.#controller ??= new AbortController()); + if (this.#aborted) controller.abort(STALE_REACTION); + return controller.signal; + } + get_title() { return this.#title.value; } diff --git a/packages/svelte/src/internal/server/renderer.test.ts b/packages/svelte/src/internal/server/renderer.test.ts index 8e98c41796..1adfdda64c 100644 --- a/packages/svelte/src/internal/server/renderer.test.ts +++ b/packages/svelte/src/internal/server/renderer.test.ts @@ -2,6 +2,7 @@ import { afterAll, beforeAll, describe, expect, test } from 'vitest'; import { Renderer, SSRState } from './renderer.js'; import type { Component } from 'svelte'; import { disable_async_mode_flag, enable_async_mode_flag } from '../flags/index.js'; +import { getAbortSignal } from './abort-signal.js'; test('collects synchronous body content by default', () => { const component = (renderer: Renderer) => { @@ -466,4 +467,176 @@ describe('async', () => { await Renderer.render(component as unknown as Component); expect(destroyed).toEqual(['c', 'e', 'a', 'b', 'b*', 'd']); }); + + test('on_destroy callbacks run when a sync render throws', () => { + const destroyed: string[] = []; + const component = (renderer: Renderer) => { + renderer.component((renderer) => { + renderer.on_destroy(() => destroyed.push('a')); + renderer.child(() => { + throw new Error('boom'); + }); + }); + }; + + expect(() => Renderer.render(component as unknown as Component).body).toThrow('boom'); + expect(destroyed).toEqual(['a']); + }); + + test('on_destroy callbacks run when an async render rejects', async () => { + const destroyed: string[] = []; + const component = (renderer: Renderer) => { + renderer.component((renderer) => { + renderer.on_destroy(() => destroyed.push('a')); + renderer.child(async () => { + await Promise.resolve(); + throw new Error('boom'); + }); + }); + }; + + await expect(Renderer.render(component as unknown as Component)).rejects.toThrow('boom'); + expect(destroyed).toEqual(['a']); + }); + + test('on_destroy waits for in-flight renderers when an async render rejects', async () => { + const events: string[] = []; + let initialised = false; + + const component = (renderer: Renderer) => { + renderer.component((renderer) => { + // rejects while the sibling component below is still in flight + renderer.child(async () => { + await Promise.resolve(); + throw new Error('boom'); + }); + + renderer.component((renderer) => { + renderer.on_destroy(() => events.push(`before-await (initialised: ${initialised})`)); + renderer.child(async () => { + await new Promise((f) => setTimeout(f, 10)); + initialised = true; + renderer.on_destroy(() => events.push('after-await')); + }); + }); + }); + }; + + await expect(Renderer.render(component as unknown as Component)).rejects.toThrow('boom'); + expect(events).toEqual(['before-await (initialised: true)', 'after-await']); + }); + + test('aborts in-flight renderers before waiting for them', async () => { + const events: string[] = []; + const component = (renderer: Renderer) => { + renderer.component((renderer) => { + renderer.child(async () => { + await Promise.resolve(); + throw new Error('boom'); + }); + + renderer.component((renderer) => { + renderer.on_destroy(() => events.push('destroyed')); + renderer.child(async () => { + const signal = getAbortSignal(); + await new Promise((_, reject) => { + signal.addEventListener('abort', () => reject(signal.reason), { once: true }); + }); + }); + }); + }); + }; + + await expect(Renderer.render(component as unknown as Component)).rejects.toThrow('boom'); + expect(events).toEqual(['destroyed']); + }); + + test('on_destroy waits for every run invocation when an async render rejects', async () => { + const events: string[] = []; + let initialised = false; + const component = (renderer: Renderer) => { + renderer.component((renderer) => { + renderer.on_destroy(() => events.push(`destroyed (initialised: ${initialised})`)); + renderer.run([ + async () => { + await new Promise((resolve) => setTimeout(resolve, 10)); + initialised = true; + renderer.on_destroy(() => events.push('destroyed after await')); + } + ]); + renderer.run([ + async () => { + await Promise.resolve(); + throw new Error('boom'); + } + ]); + }); + }; + + await expect(Renderer.render(component as unknown as Component)).rejects.toThrow('boom'); + expect(events).toEqual(['destroyed (initialised: true)', 'destroyed after await']); + }); + + test('abort signals are scoped to a render', async () => { + let signal: AbortSignal; + let start!: () => void; + let resume!: () => void; + const started = new Promise((resolve) => (start = resolve)); + const resumed = new Promise((resolve) => (resume = resolve)); + const component = (renderer: Renderer) => { + renderer.child(async () => { + signal = getAbortSignal(); + start(); + await resumed; + }); + }; + + const first_render = Promise.resolve(Renderer.render(component as unknown as Component)); + await started; + await Renderer.render((() => {}) as unknown as Component); + expect(signal!.aborted).toBe(false); + + resume(); + await first_render; + expect(signal!.aborted).toBe(true); + }); + + test('a throwing on_destroy callback does not mask a sync render error', () => { + const destroyed: string[] = []; + const component = (renderer: Renderer) => { + renderer.component((renderer) => { + renderer.on_destroy(() => { + destroyed.push('a'); + throw new Error('cleanup failed'); + }); + renderer.on_destroy(() => destroyed.push('b')); + renderer.child(() => { + throw new Error('boom'); + }); + }); + }; + + expect(() => Renderer.render(component as unknown as Component).body).toThrow('boom'); + expect(destroyed).toEqual(['a', 'b']); + }); + + test('a throwing on_destroy callback does not mask an async render error', async () => { + const destroyed: string[] = []; + const component = (renderer: Renderer) => { + renderer.component((renderer) => { + renderer.on_destroy(() => { + destroyed.push('a'); + throw new Error('cleanup failed'); + }); + renderer.on_destroy(() => destroyed.push('b')); + renderer.child(async () => { + await Promise.resolve(); + throw new Error('boom'); + }); + }); + }; + + await expect(Renderer.render(component as unknown as Component)).rejects.toThrow('boom'); + expect(destroyed).toEqual(['a', 'b']); + }); }); From 06c9b7929a1cb5294e7f00faa3d24b7a65556a58 Mon Sep 17 00:00:00 2001 From: Paolo Ricciuti Date: Fri, 21 Aug 2026 14:16:19 +0200 Subject: [PATCH 163/222] fix: prevent `selectedcontent` mutation from changing the selected option (#18495) Basically, in a situation like this ```svelte {}} /> ``` what happens is that the `oninput` is delegated and so it registers the global listener (which means it listen on every `oninput` not just the one from the input). When the select change, the `input` event is dispatched first, the listener runs, doesn't find an `__input` handler and returns. Now before the `change` event is emitted, the `MutationObserver` in `init_select` is triggered by the browser updating `selectedcontent` and invokes `select_option` with `select.__value`. However, since the `change` event has yet to fire, `select.__value` still has the old value, so we "reselect" that. When the change event runs, the selected option is effectively the old one and the whole thing breaks. I had to add the test in `runtime-browser` because JSDom doesn't support `selectedcontent` --- .changeset/tidy-pandas-refuse.md | 5 +++ .../client/dom/elements/bindings/select.js | 32 ++++++++++++++++--- .../_config.js | 22 +++++++++++++ .../main.svelte | 17 ++++++++++ 4 files changed, 71 insertions(+), 5 deletions(-) create mode 100644 .changeset/tidy-pandas-refuse.md create mode 100644 packages/svelte/tests/runtime-browser/samples/selectedcontent-oninput-interfere/_config.js create mode 100644 packages/svelte/tests/runtime-browser/samples/selectedcontent-oninput-interfere/main.svelte diff --git a/.changeset/tidy-pandas-refuse.md b/.changeset/tidy-pandas-refuse.md new file mode 100644 index 0000000000..7034a3e267 --- /dev/null +++ b/.changeset/tidy-pandas-refuse.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: prevent `selectedcontent` mutation from changing the selected option diff --git a/packages/svelte/src/internal/client/dom/elements/bindings/select.js b/packages/svelte/src/internal/client/dom/elements/bindings/select.js index e390e23ea0..a408613104 100644 --- a/packages/svelte/src/internal/client/dom/elements/bindings/select.js +++ b/packages/svelte/src/internal/client/dom/elements/bindings/select.js @@ -55,11 +55,14 @@ export function select_option(select, value, mounting = false) { * @param {HTMLSelectElement} select */ export function init_select(select) { - var observer = new MutationObserver(() => { - if ('__value' in select) { - // @ts-ignore - select_option(select, select.__value); - } + var observer = new MutationObserver((entries) => { + // Mutations related to `` can never affect the option list. + // Reacting to them could revert a user-initiated selection change, because the + // records are delivered as soon as any listener returns (e.g. a delegated `input` + // handler), which can happen before the `change` handler has updated `__value` + if (entries.every(is_selectedcontent_mutation) || !('__value' in select)) return; + // @ts-ignore + select_option(select, select.__value); // Deliberately don't update the potential binding value, // the model should be preserved unless explicitly changed }); @@ -164,3 +167,22 @@ function get_option_value(option) { return option.value; } } + +/** + * Returns `true` if the mutation stems from the browser mirroring the selected + * option's content into ``, or from us replacing the + * `` element with a clone of itself + * @param {MutationRecord} entry + */ +function is_selectedcontent_mutation(entry) { + if (/** @type {Element} */ (entry.target).closest('selectedcontent') !== null) { + return true; + } + + if (entry.type === 'childList') { + var nodes = [...entry.addedNodes, ...entry.removedNodes]; + return nodes.length > 0 && nodes.every((node) => node.nodeName === 'SELECTEDCONTENT'); + } + + return false; +} diff --git a/packages/svelte/tests/runtime-browser/samples/selectedcontent-oninput-interfere/_config.js b/packages/svelte/tests/runtime-browser/samples/selectedcontent-oninput-interfere/_config.js new file mode 100644 index 0000000000..d153eceeb7 --- /dev/null +++ b/packages/svelte/tests/runtime-browser/samples/selectedcontent-oninput-interfere/_config.js @@ -0,0 +1,22 @@ +import { flushSync } from 'svelte'; +import { ok, test } from '../../assert'; + +export default test({ + async test({ target, assert }) { + const select = target.querySelector('select'); + ok(select); + + select.value = 'B'; + select.dispatchEvent(new Event('input', { bubbles: true })); + + // because another element has a delegated `oninput` handler, a global `input` + // listener runs and, once it returns, a microtask checkpoint delivers the + // mutation records *before* the `change` event — we emulate that checkpoint here + await Promise.resolve(); + + select.dispatchEvent(new Event('change', { bubbles: true })); + flushSync(); + + assert.equal(select.value, 'B'); + } +}); diff --git a/packages/svelte/tests/runtime-browser/samples/selectedcontent-oninput-interfere/main.svelte b/packages/svelte/tests/runtime-browser/samples/selectedcontent-oninput-interfere/main.svelte new file mode 100644 index 0000000000..97634042ff --- /dev/null +++ b/packages/svelte/tests/runtime-browser/samples/selectedcontent-oninput-interfere/main.svelte @@ -0,0 +1,17 @@ + + + {}} /> + + + \ No newline at end of file From 8bf9ec87000fffd3805ebaac3586a0502cf92900 Mon Sep 17 00:00:00 2001 From: Barry <91018388+barry166@users.noreply.github.com> Date: Fri, 21 Aug 2026 21:45:41 +0800 Subject: [PATCH 164/222] docs: clarify $bindable fallback binding requirement (#18477) --- documentation/docs/02-runes/06-$bindable.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/documentation/docs/02-runes/06-$bindable.md b/documentation/docs/02-runes/06-$bindable.md index 3675a56b16..a3be662785 100644 --- a/documentation/docs/02-runes/06-$bindable.md +++ b/documentation/docs/02-runes/06-$bindable.md @@ -52,3 +52,5 @@ In this case, you can specify a fallback value for when no prop is passed at all /// file: FancyInput.svelte let { value = $bindable('fallback'), ...props } = $props(); ``` + +When a bindable prop has a fallback value, the parent must pass a value other than `undefined` if it uses `bind:`. This avoids ambiguity about which value should apply, since the parent and child should share the same value for a binding. From 224fcadbd5034f0ac6d89b6f8525a58acde8eed4 Mon Sep 17 00:00:00 2001 From: Rupankar Dutta Date: Fri, 21 Aug 2026 19:24:15 +0530 Subject: [PATCH 165/222] fix: route $derived teardown errors through invoke_error_boundary (#18486) Fixes #18485 Two parts to this: 1. explicitly invoke error boundary during teardown errors, else they go missing/bubble up outside the render tree 2. skip destroying/destroyed boundaries will searching a handler --------- Co-authored-by: Claude Sonnet 4.6 Co-authored-by: Simon Holthausen Co-authored-by: Simon H <5968653+dummdidumm@users.noreply.github.com> --- .changeset/boundary-teardown-error-routing.md | 5 +++ .../src/internal/client/error-handling.js | 12 +++++- .../internal/client/error-handling.test.ts | 39 +++++++++++++++++++ .../src/internal/client/reactivity/effects.js | 6 +++ .../samples/error-boundary-28/Trigger.svelte | 12 ++++++ .../samples/error-boundary-28/_config.js | 23 +++++++++++ .../samples/error-boundary-28/main.svelte | 24 ++++++++++++ 7 files changed, 119 insertions(+), 2 deletions(-) create mode 100644 .changeset/boundary-teardown-error-routing.md create mode 100644 packages/svelte/src/internal/client/error-handling.test.ts create mode 100644 packages/svelte/tests/runtime-runes/samples/error-boundary-28/Trigger.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/error-boundary-28/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/error-boundary-28/main.svelte diff --git a/.changeset/boundary-teardown-error-routing.md b/.changeset/boundary-teardown-error-routing.md new file mode 100644 index 0000000000..0b2249bf22 --- /dev/null +++ b/.changeset/boundary-teardown-error-routing.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: route $derived teardown errors through invoke_error_boundary diff --git a/packages/svelte/src/internal/client/error-handling.js b/packages/svelte/src/internal/client/error-handling.js index 7c404dd933..a46281d36c 100644 --- a/packages/svelte/src/internal/client/error-handling.js +++ b/packages/svelte/src/internal/client/error-handling.js @@ -3,7 +3,14 @@ import { DEV } from 'esm-env'; import { FILENAME } from '../../constants.js'; import { is_firefox } from './dom/operations.js'; -import { ERROR_VALUE, BOUNDARY_EFFECT, REACTION_RAN, EFFECT, DESTROYED } from './constants.js'; +import { + ERROR_VALUE, + BOUNDARY_EFFECT, + REACTION_RAN, + EFFECT, + DESTROYED, + DESTROYING +} from './constants.js'; import { define_property, get_descriptor } from '../shared/utils.js'; import { active_effect, active_reaction } from './runtime.js'; @@ -50,7 +57,8 @@ export function invoke_error_boundary(error, effect) { } while (effect !== null) { - if ((effect.f & BOUNDARY_EFFECT) !== 0) { + // Skip boundaries that are destroyed/destroying and cannot meaningfully handle the error. + if ((effect.f & BOUNDARY_EFFECT) !== 0 && (effect.f & (DESTROYED | DESTROYING)) === 0) { if ((effect.f & REACTION_RAN) === 0) { // we are still creating the boundary effect throw error; diff --git a/packages/svelte/src/internal/client/error-handling.test.ts b/packages/svelte/src/internal/client/error-handling.test.ts new file mode 100644 index 0000000000..2eb6093a2c --- /dev/null +++ b/packages/svelte/src/internal/client/error-handling.test.ts @@ -0,0 +1,39 @@ +import { assert, test } from 'vitest'; +import { BOUNDARY_EFFECT, DESTROYED, REACTION_RAN } from './constants'; +import { invoke_error_boundary } from './error-handling'; +import type { Effect } from './types'; + +test('ignores errors from a destroyed entry effect', () => { + const error = new Error('original'); + let handled = null; + const boundary = { + f: BOUNDARY_EFFECT | REACTION_RAN, + b: { error: (error: unknown) => (handled = error) }, + parent: null + } as unknown as Effect; + const effect = { f: DESTROYED, parent: boundary } as Effect; + + invoke_error_boundary(error, effect); + + assert.equal(handled, null); +}); + +test('skips destroyed boundary ancestors without masking the error', () => { + const error = new Error('original'); + let handled = null; + const live_boundary = { + f: BOUNDARY_EFFECT | REACTION_RAN, + b: { error: (error: unknown) => (handled = error) }, + parent: null + } as unknown as Effect; + const destroyed_boundary = { + f: BOUNDARY_EFFECT | DESTROYED | REACTION_RAN, + b: null, + parent: live_boundary + } as unknown as Effect; + const effect = { f: 0, parent: destroyed_boundary } as Effect; + + invoke_error_boundary(error, effect); + + assert.equal(handled, error); +}); diff --git a/packages/svelte/src/internal/client/reactivity/effects.js b/packages/svelte/src/internal/client/reactivity/effects.js index c5d195dfae..7e53138aae 100644 --- a/packages/svelte/src/internal/client/reactivity/effects.js +++ b/packages/svelte/src/internal/client/reactivity/effects.js @@ -36,6 +36,7 @@ import { MANAGED_EFFECT, DESTROYING } from '#client/constants'; +import { invoke_error_boundary } from '../error-handling.js'; import * as e from '../errors.js'; import { DEV } from 'esm-env'; import { define_property } from '../../shared/utils.js'; @@ -449,6 +450,11 @@ export function execute_effect_teardown(effect) { set_active_reaction(null); try { teardown.call(null); + } catch (error) { + // Route teardown errors through the boundary system so that a live + // ancestor can handle them. Boundaries that are + // themselves mid-teardown are skipped by invoke_error_boundary. + invoke_error_boundary(error, effect.parent); } finally { set_is_destroying_effect(previously_destroying_effect); set_active_reaction(previous_reaction); diff --git a/packages/svelte/tests/runtime-runes/samples/error-boundary-28/Trigger.svelte b/packages/svelte/tests/runtime-runes/samples/error-boundary-28/Trigger.svelte new file mode 100644 index 0000000000..46b4bd1c30 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/error-boundary-28/Trigger.svelte @@ -0,0 +1,12 @@ + + +trigger diff --git a/packages/svelte/tests/runtime-runes/samples/error-boundary-28/_config.js b/packages/svelte/tests/runtime-runes/samples/error-boundary-28/_config.js new file mode 100644 index 0000000000..4802089124 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/error-boundary-28/_config.js @@ -0,0 +1,23 @@ +import { flushSync, tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + // Regression test for https://github.com/sveltejs/svelte/issues/18485. + // A $derived that re-executes and throws during teardown should route the + // error to a live ancestor boundary. + mode: ['client'], + async test({ assert, target }) { + const [break_it, unmount] = target.querySelectorAll('button'); + + break_it.click(); + flushSync(); + + assert.doesNotThrow(() => { + unmount.click(); + flushSync(); + }); + + await tick(); + assert.htmlEqual(target.innerHTML, '

    caught

    '); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/error-boundary-28/main.svelte b/packages/svelte/tests/runtime-runes/samples/error-boundary-28/main.svelte new file mode 100644 index 0000000000..e100be2169 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/error-boundary-28/main.svelte @@ -0,0 +1,24 @@ + + + + + + + {#if mounted} + appContext} /> + {/if} + + {#snippet failed()} +

    caught

    + {/snippet} +
    From a1ec9c8eb7e5fd7e4e02de22e2c18a51ae7aadc7 Mon Sep 17 00:00:00 2001 From: Giorgio Maria Federico Birnthaler <129273127+Dodothereal@users.noreply.github.com> Date: Fri, 21 Aug 2026 16:22:14 +0200 Subject: [PATCH 166/222] chore: fix typo in read_version JSDoc (#18470) --- packages/svelte/src/internal/client/runtime.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index 4458595d35..e0914b8f70 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -133,7 +133,7 @@ export function set_untracked_writes(value) { **/ export let write_version = 1; -/** @type {number} Used to version each read of a source of derived to avoid duplicating depedencies inside a reaction */ +/** @type {number} Used to version each read of a source of derived to avoid duplicating dependencies inside a reaction */ let read_version = 0; export let update_version = read_version; From 3405b5e735e46aaff057bfa8e5fcdb21391ca89a Mon Sep 17 00:00:00 2001 From: adiGuba Date: Fri, 21 Aug 2026 17:05:32 +0200 Subject: [PATCH 167/222] fix: distinct memoizer on style/class directives (#18466) Fix #18465 The style and classe directives are generated using an unique memoizer for all the class/style directive, which means if you have multiple of the same time on one element you have overly broad invalidation/reruns. This fixes it by having one memoizer call per directive instead of collecting them. --------- Co-authored-by: Simon Holthausen Co-authored-by: Simon H <5968653+dummdidumm@users.noreply.github.com> --- .changeset/lemon-bikes-marry.md | 5 +++ .../client/visitors/RegularElement.js | 22 ++-------- .../class-directive-memoize/_config.js | 37 ++++++++++++++++ .../class-directive-memoize/main.svelte | 10 +++++ .../style-directive-memoize/_config.js | 42 +++++++++++++++++++ .../style-directive-memoize/main.svelte | 10 +++++ 6 files changed, 108 insertions(+), 18 deletions(-) create mode 100644 .changeset/lemon-bikes-marry.md create mode 100644 packages/svelte/tests/runtime-runes/samples/class-directive-memoize/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/class-directive-memoize/main.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/style-directive-memoize/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/style-directive-memoize/main.svelte diff --git a/.changeset/lemon-bikes-marry.md b/.changeset/lemon-bikes-marry.md new file mode 100644 index 0000000000..6b1c431a30 --- /dev/null +++ b/.changeset/lemon-bikes-marry.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: distinct memoizer on style/class directives diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js index 1cf5abeb69..de09d3ef60 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js @@ -4,7 +4,6 @@ /** @import { Scope } from '../../../scope' */ import { cannot_be_set_statically, - is_boolean_attribute, is_dom_property, is_load_error_element } from '../../../../../utils.js'; @@ -13,7 +12,6 @@ import { is_event_attribute, is_text_attribute } from '../../../../utils/ast.js' import * as b from '#compiler/builders'; import { create_attribute, - ExpressionMetadata, is_custom_element_node, is_customizable_select_element } from '../../../nodes.js'; @@ -528,18 +526,12 @@ export function build_class_directives_object( ) { let properties = []; - const metadata = new ExpressionMetadata(); - for (const d of class_directives) { - metadata.merge(d.metadata.expression); - const expression = /** @type Expression */ (context.visit(d.expression)); - properties.push(b.init(d.name, expression)); + properties.push(b.init(d.name, memoizer.add(expression, d.metadata.expression))); } - const directives = b.object(properties); - - return memoizer.add(directives, metadata); + return b.object(properties); } /** @@ -555,23 +547,17 @@ export function build_style_directives_object( const normal = b.object([]); const important = b.object([]); - const metadata = new ExpressionMetadata(); - for (const d of style_directives) { - metadata.merge(d.metadata.expression); - const expression = d.value === true ? build_getter(b.id(d.name), context.state) : build_attribute_value(d.value, context).value; const object = d.modifiers.includes('important') ? important : normal; - object.properties.push(b.init(d.name, expression)); + object.properties.push(b.init(d.name, memoizer.add(expression, d.metadata.expression))); } - const directives = important.properties.length ? b.array([normal, important]) : normal; - - return memoizer.add(directives, metadata); + return important.properties.length ? b.array([normal, important]) : normal; } /** diff --git a/packages/svelte/tests/runtime-runes/samples/class-directive-memoize/_config.js b/packages/svelte/tests/runtime-runes/samples/class-directive-memoize/_config.js new file mode 100644 index 0000000000..964993787f --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/class-directive-memoize/_config.js @@ -0,0 +1,37 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +// This test counts mutations on hydration +// set_style() should not mutate style on hydration, except if mismatch +export default test({ + mode: ['server', 'hydrate', 'client'], + + ssrHtml: ` + +
    + `, + + html: ` + +
    + `, + + test({ target, assert, logs }) { + flushSync(); + + assert.deepEqual(logs, ['is_red()']); + + const btn = target.querySelector('button'); + const div = target.querySelector('div'); + + assert.equal(div?.className, 'red'); + + btn?.click(); + + flushSync(); + + assert.equal(div?.className, 'red active'); + // only one call here + assert.deepEqual(logs, ['is_red()']); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/class-directive-memoize/main.svelte b/packages/svelte/tests/runtime-runes/samples/class-directive-memoize/main.svelte new file mode 100644 index 0000000000..3a6d02e626 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/class-directive-memoize/main.svelte @@ -0,0 +1,10 @@ + + +
    \ No newline at end of file diff --git a/packages/svelte/tests/runtime-runes/samples/style-directive-memoize/_config.js b/packages/svelte/tests/runtime-runes/samples/style-directive-memoize/_config.js new file mode 100644 index 0000000000..5638a8ee13 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/style-directive-memoize/_config.js @@ -0,0 +1,42 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +// This test counts mutations on hydration +// set_style() should not mutate style on hydration, except if mismatch +export default test({ + mode: ['server', 'hydrate', 'client'], + + ssrHtml: ` + +
    + `, + + html: ` + +
    + `, + + test({ target, assert, logs }) { + flushSync(); + + assert.deepEqual(logs, ['makeColor()']); + + const btn = target.querySelector('button'); + const div = target.querySelector('div'); + + // Note : we cannot compare HTML because set_style() use dom.style.cssText + // which can alter the format of the attribute... + + assert.equal(div?.style.backgroundColor, 'red'); + assert.equal(div?.style.fontSize, '1em'); + + btn?.click(); + + flushSync(); + + assert.equal(div?.style.backgroundColor, 'red'); + assert.equal(div?.style.fontSize, '2em'); + // only one call here + assert.deepEqual(logs, ['makeColor()']); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/style-directive-memoize/main.svelte b/packages/svelte/tests/runtime-runes/samples/style-directive-memoize/main.svelte new file mode 100644 index 0000000000..444d686745 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/style-directive-memoize/main.svelte @@ -0,0 +1,10 @@ + + +
    \ No newline at end of file From 3b559bbf96cffb48e782e89aca251e0ca4ccc5bc Mon Sep 17 00:00:00 2001 From: Nic Polumeyv Date: Fri, 21 Aug 2026 16:08:53 -0400 Subject: [PATCH 168/222] fix: avoid NaN keyframe values in slide transition (#18430) Fixes #14205 When a `slide` transition runs on an element without a layout box, for example inside a `display: none` parent, `getComputedStyle` returns values like `auto` for the animated dimensions. These were passed through `parseFloat` unguarded, so the generated css contained `height: NaNpx` and the browser rejected the keyframe property with an "Invalid keyframe value" warning on every transition. Fix it by omitting properties if they have unparseable numbers - it's the same outcome but you don't see a warning. --------- Co-authored-by: Simon Holthausen --- .changeset/tough-pandas-shout.md | 5 ++++ packages/svelte/src/transition/index.js | 25 ++++++++++++----- .../transition-slide-hidden-parent/_config.js | 27 +++++++++++++++++++ .../main.svelte | 13 +++++++++ 4 files changed, 63 insertions(+), 7 deletions(-) create mode 100644 .changeset/tough-pandas-shout.md create mode 100644 packages/svelte/tests/runtime-browser/samples/transition-slide-hidden-parent/_config.js create mode 100644 packages/svelte/tests/runtime-browser/samples/transition-slide-hidden-parent/main.svelte diff --git a/.changeset/tough-pandas-shout.md b/.changeset/tough-pandas-shout.md new file mode 100644 index 0000000000..b0f6ea64fd --- /dev/null +++ b/.changeset/tough-pandas-shout.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: avoid `NaN` keyframe values in `slide` transition for elements without a layout box diff --git a/packages/svelte/src/transition/index.js b/packages/svelte/src/transition/index.js index 898a929eb1..31e8fbde2b 100644 --- a/packages/svelte/src/transition/index.js +++ b/packages/svelte/src/transition/index.js @@ -28,6 +28,17 @@ function split_css_unit(value) { return split ? [parseFloat(split[1]), split[2] || 'px'] : [/** @type {number} */ (value), 'px']; } +/** + * Omits unresolved dimensions rather than passing invalid values to Web Animations. + * + * @param {string} property + * @param {number} value + * @param {number} t + */ +function css_dimension(property, value, t) { + return Number.isNaN(value) ? '' : `${property}: ${t * value}px;`; +} + /** * Animates a `blur` filter alongside an element's opacity. * @@ -138,13 +149,13 @@ export function slide(node, { delay = 0, duration = 400, easing = cubic_out, axi css: (t) => 'overflow: hidden;' + `opacity: ${Math.min(t * 20, 1) * opacity};` + - `${primary_property}: ${t * primary_property_value}px;` + - `padding-${secondary_properties[0]}: ${t * padding_start_value}px;` + - `padding-${secondary_properties[1]}: ${t * padding_end_value}px;` + - `margin-${secondary_properties[0]}: ${t * margin_start_value}px;` + - `margin-${secondary_properties[1]}: ${t * margin_end_value}px;` + - `border-${secondary_properties[0]}-width: ${t * border_width_start_value}px;` + - `border-${secondary_properties[1]}-width: ${t * border_width_end_value}px;` + + css_dimension(primary_property, primary_property_value, t) + + css_dimension(`padding-${secondary_properties[0]}`, padding_start_value, t) + + css_dimension(`padding-${secondary_properties[1]}`, padding_end_value, t) + + css_dimension(`margin-${secondary_properties[0]}`, margin_start_value, t) + + css_dimension(`margin-${secondary_properties[1]}`, margin_end_value, t) + + css_dimension(`border-${secondary_properties[0]}-width`, border_width_start_value, t) + + css_dimension(`border-${secondary_properties[1]}-width`, border_width_end_value, t) + `min-${primary_property}: 0` }; } diff --git a/packages/svelte/tests/runtime-browser/samples/transition-slide-hidden-parent/_config.js b/packages/svelte/tests/runtime-browser/samples/transition-slide-hidden-parent/_config.js new file mode 100644 index 0000000000..a8c1e07111 --- /dev/null +++ b/packages/svelte/tests/runtime-browser/samples/transition-slide-hidden-parent/_config.js @@ -0,0 +1,27 @@ +import { ok, test } from '../../assert'; + +export default test({ + async test({ assert, window }) { + window.document.querySelector('button')?.click(); + await new Promise((r) => setTimeout(r, 100)); + + const p = window.document.querySelector('p'); + const animations = /** @type {HTMLElement} */ (p).getAnimations(); + assert.equal(animations.length, 1); + + // when the element has no layout box, computed dimensions resolve to 'auto', + // which must not end up as NaN values that the browser rejects (#14205) + const effect = /** @type {KeyframeEffect} */ (animations[0].effect); + const keyframes = effect.getKeyframes(); + ok(keyframes.length > 0); + assert.equal(effect.getTiming().duration, 400); + + for (const keyframe of keyframes) { + ok(!('height' in keyframe), 'unresolved height should be omitted'); + + for (const value of Object.values(keyframe)) { + ok(!String(value).includes('NaN'), `unexpected NaN in keyframe: ${value}`); + } + } + } +}); diff --git a/packages/svelte/tests/runtime-browser/samples/transition-slide-hidden-parent/main.svelte b/packages/svelte/tests/runtime-browser/samples/transition-slide-hidden-parent/main.svelte new file mode 100644 index 0000000000..e1a7c54bdb --- /dev/null +++ b/packages/svelte/tests/runtime-browser/samples/transition-slide-hidden-parent/main.svelte @@ -0,0 +1,13 @@ + + + + +
    + {#if visible} +

    hello

    + {/if} +
    From a6153f1d2a9168f8371149fbd775fa428c718a9b Mon Sep 17 00:00:00 2001 From: "svelte-triage-bot[bot]" <316883489+svelte-triage-bot[bot]@users.noreply.github.com> Date: Fri, 21 Aug 2026 22:16:30 +0200 Subject: [PATCH 169/222] fix: preserve whitespace after inline elements when printing (#18685) Fixes #18683. The printer previously trimmed leading whitespace after an inline element and failed to replace it when the fragment stayed on one line. Track that whitespace while grouping fragment nodes and emit a space for inline output, while retaining newline-based separation for multiline output. Adds regression coverage for whitespace following an inline element and updates existing print snapshots that exposed the same behavior. --- .changeset/gentle-spaces-print.md | 5 +++ packages/svelte/src/compiler/print/index.js | 37 +++++++++++-------- .../tests/print/samples/comment/output.svelte | 2 +- .../samples/expression-tag/output.svelte | 2 +- .../inline-element-whitespace/input.svelte | 1 + .../inline-element-whitespace/output.svelte | 1 + .../samples/regular-element/output.svelte | 2 +- 7 files changed, 31 insertions(+), 19 deletions(-) create mode 100644 .changeset/gentle-spaces-print.md create mode 100644 packages/svelte/tests/print/samples/inline-element-whitespace/input.svelte create mode 100644 packages/svelte/tests/print/samples/inline-element-whitespace/output.svelte diff --git a/.changeset/gentle-spaces-print.md b/.changeset/gentle-spaces-print.md new file mode 100644 index 0000000000..70b337fcd7 --- /dev/null +++ b/.changeset/gentle-spaces-print.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: preserve whitespace after inline elements when printing diff --git a/packages/svelte/src/compiler/print/index.js b/packages/svelte/src/compiler/print/index.js index 4a78854e1e..554e788654 100644 --- a/packages/svelte/src/compiler/print/index.js +++ b/packages/svelte/src/compiler/print/index.js @@ -587,15 +587,19 @@ const svelte_visitors = (comments, state) => ({ last?.type === 'Text' && /\s$/.test(last.data); - /** @type {AST.SvelteNode[][]} */ + /** @type {{ nodes: AST.SvelteNode[]; leading_whitespace: boolean }[]} */ const items = []; /** @type {AST.SvelteNode[]} */ let sequence = []; + let leading_whitespace = false; const flush = () => { - items.push(sequence); - sequence = []; + if (sequence.length > 0) { + items.push({ nodes: sequence, leading_whitespace }); + sequence = []; + leading_whitespace = false; + } }; for (let i = 0; i < node.nodes.length; i += 1) { @@ -624,6 +628,7 @@ const svelte_visitors = (comments, state) => ({ if (child_node.data.startsWith(' ') && prev && prev.type !== 'ExpressionTag') { flush(); + leading_whitespace = true; child_node.data = child_node.data.trimStart(); } @@ -662,20 +667,18 @@ const svelte_visitors = (comments, state) => ({ let multiline = false; let width = 0; - const child_contexts = items - .filter((x) => x.length > 0) - .map((sequence) => { - const child_context = context.new(); + const child_contexts = items.map(({ nodes, leading_whitespace }) => { + const child_context = context.new(); - for (const node of sequence) { - child_context.visit(node); - multiline ||= child_context.multiline; - } + for (const node of nodes) { + child_context.visit(node); + multiline ||= child_context.multiline; + } - width += child_context.measure(); + width += child_context.measure() + (leading_whitespace ? 1 : 0); - return child_context; - }); + return { context: child_context, leading_whitespace }; + }); multiline ||= width > LINE_BREAK_THRESHOLD; // Normally context.newline() also makes context.multiline true, but the below loop only @@ -687,14 +690,16 @@ const svelte_visitors = (comments, state) => ({ const prev = child_contexts[i]; const next = child_contexts[i + 1]; - context.append(prev); + context.append(prev.context); if (next) { - if (prev.multiline || next.multiline) { + if (prev.context.multiline || next.context.multiline) { context.margin(); context.newline(); } else if (multiline) { context.newline(); + } else if (next.leading_whitespace) { + context.write(' '); } } } diff --git a/packages/svelte/tests/print/samples/comment/output.svelte b/packages/svelte/tests/print/samples/comment/output.svelte index f2f97b65be..2cac5f4048 100644 --- a/packages/svelte/tests/print/samples/comment/output.svelte +++ b/packages/svelte/tests/print/samples/comment/output.svelte @@ -1,3 +1,3 @@ - diff --git a/packages/svelte/tests/print/samples/expression-tag/output.svelte b/packages/svelte/tests/print/samples/expression-tag/output.svelte index 9142a59631..5af5746d87 100644 --- a/packages/svelte/tests/print/samples/expression-tag/output.svelte +++ b/packages/svelte/tests/print/samples/expression-tag/output.svelte @@ -1 +1 @@ -{name}{count + 1} +{name} {count + 1} diff --git a/packages/svelte/tests/print/samples/inline-element-whitespace/input.svelte b/packages/svelte/tests/print/samples/inline-element-whitespace/input.svelte new file mode 100644 index 0000000000..e442ae954c --- /dev/null +++ b/packages/svelte/tests/print/samples/inline-element-whitespace/input.svelte @@ -0,0 +1 @@ +

    Hello bold world

    diff --git a/packages/svelte/tests/print/samples/inline-element-whitespace/output.svelte b/packages/svelte/tests/print/samples/inline-element-whitespace/output.svelte new file mode 100644 index 0000000000..e442ae954c --- /dev/null +++ b/packages/svelte/tests/print/samples/inline-element-whitespace/output.svelte @@ -0,0 +1 @@ +

    Hello bold world

    diff --git a/packages/svelte/tests/print/samples/regular-element/output.svelte b/packages/svelte/tests/print/samples/regular-element/output.svelte index 0cf7c2472f..f5fd3c8f42 100644 --- a/packages/svelte/tests/print/samples/regular-element/output.svelte +++ b/packages/svelte/tests/print/samples/regular-element/output.svelte @@ -1 +1 @@ -
    +
    From 78979c87c96e6071502e8111a2fc5f312ad586b8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 21 Aug 2026 22:24:11 +0200 Subject: [PATCH 170/222] chore(deps-dev): bump vite from 7.3.2 to 7.3.5 (#18444) --- playgrounds/sandbox/package.json | 2 +- pnpm-lock.yaml | 366 ++++++++++++++++++++++++++----- 2 files changed, 315 insertions(+), 53 deletions(-) diff --git a/playgrounds/sandbox/package.json b/playgrounds/sandbox/package.json index 12676777cb..bb7f5f0c81 100644 --- a/playgrounds/sandbox/package.json +++ b/playgrounds/sandbox/package.json @@ -21,7 +21,7 @@ "polka": "^1.0.0-next.25", "svelte": "workspace:*", "tinyglobby": "^0.2.12", - "vite": "^7.3.2", + "vite": "^7.3.5", "vite-plugin-devtools-json": "^1.0.0", "vite-plugin-inspect": "^11.3.3" } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 095d166118..70dcf70e5b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -64,7 +64,7 @@ importers: version: 1.2.5 vitest: specifier: ^4.1.7 - version: 4.1.7(@types/node@20.19.17)(@vitest/coverage-v8@4.1.7)(jsdom@25.0.1)(vite@7.3.2(@types/node@20.19.17)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0)) + version: 4.1.7(@types/node@20.19.17)(@vitest/coverage-v8@4.1.7)(jsdom@25.0.1)(vite@7.3.5(@types/node@20.19.17)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0)) packages/svelte: dependencies: @@ -164,7 +164,7 @@ importers: version: 5.5.4 vitest: specifier: ^4.1.7 - version: 4.1.7(@types/node@20.19.17)(@vitest/coverage-v8@4.1.7)(jsdom@25.0.1)(vite@7.3.2(@types/node@20.19.17)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0)) + version: 4.1.7(@types/node@20.19.17)(@vitest/coverage-v8@4.1.7)(jsdom@25.0.1)(vite@7.3.5(@types/node@20.19.17)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0)) web-features: specifier: ^3.29.0 version: 3.29.0 @@ -173,7 +173,7 @@ importers: devDependencies: '@sveltejs/vite-plugin-svelte': specifier: ^6.2.0 - version: 6.2.0(svelte@packages+svelte)(vite@7.3.2(@types/node@24.5.2)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0)) + version: 6.2.0(svelte@packages+svelte)(vite@7.3.5(@types/node@24.5.2)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0)) '@types/node': specifier: ^24.5.2 version: 24.5.2 @@ -187,14 +187,14 @@ importers: specifier: ^0.2.12 version: 0.2.15 vite: - specifier: ^7.3.2 - version: 7.3.2(@types/node@24.5.2)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0) + specifier: ^7.3.5 + version: 7.3.5(@types/node@24.5.2)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0) vite-plugin-devtools-json: specifier: ^1.0.0 - version: 1.0.0(vite@7.3.2(@types/node@24.5.2)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0)) + version: 1.0.0(vite@7.3.5(@types/node@24.5.2)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0)) vite-plugin-inspect: specifier: ^11.3.3 - version: 11.3.3(vite@7.3.2(@types/node@24.5.2)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0)) + version: 11.3.3(vite@7.3.5(@types/node@24.5.2)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0)) packages: @@ -781,126 +781,251 @@ packages: cpu: [arm] os: [android] + '@rollup/rollup-android-arm-eabi@4.62.2': + resolution: {integrity: sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==} + cpu: [arm] + os: [android] + '@rollup/rollup-android-arm64@4.60.1': resolution: {integrity: sha512-YjG/EwIDvvYI1YvYbHvDz/BYHtkY4ygUIXHnTdLhG+hKIQFBiosfWiACWortsKPKU/+dUwQQCKQM3qrDe8c9BA==} cpu: [arm64] os: [android] + '@rollup/rollup-android-arm64@4.62.2': + resolution: {integrity: sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw==} + cpu: [arm64] + os: [android] + '@rollup/rollup-darwin-arm64@4.60.1': resolution: {integrity: sha512-mjCpF7GmkRtSJwon+Rq1N8+pI+8l7w5g9Z3vWj4T7abguC4Czwi3Yu/pFaLvA3TTeMVjnu3ctigusqWUfjZzvw==} cpu: [arm64] os: [darwin] + '@rollup/rollup-darwin-arm64@4.62.2': + resolution: {integrity: sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A==} + cpu: [arm64] + os: [darwin] + '@rollup/rollup-darwin-x64@4.60.1': resolution: {integrity: sha512-haZ7hJ1JT4e9hqkoT9R/19XW2QKqjfJVv+i5AGg57S+nLk9lQnJ1F/eZloRO3o9Scy9CM3wQ9l+dkXtcBgN5Ew==} cpu: [x64] os: [darwin] + '@rollup/rollup-darwin-x64@4.62.2': + resolution: {integrity: sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA==} + cpu: [x64] + os: [darwin] + '@rollup/rollup-freebsd-arm64@4.60.1': resolution: {integrity: sha512-czw90wpQq3ZsAVBlinZjAYTKduOjTywlG7fEeWKUA7oCmpA8xdTkxZZlwNJKWqILlq0wehoZcJYfBvOyhPTQ6w==} cpu: [arm64] os: [freebsd] + '@rollup/rollup-freebsd-arm64@4.62.2': + resolution: {integrity: sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw==} + cpu: [arm64] + os: [freebsd] + '@rollup/rollup-freebsd-x64@4.60.1': resolution: {integrity: sha512-KVB2rqsxTHuBtfOeySEyzEOB7ltlB/ux38iu2rBQzkjbwRVlkhAGIEDiiYnO2kFOkJp+Z7pUXKyrRRFuFUKt+g==} cpu: [x64] os: [freebsd] + '@rollup/rollup-freebsd-x64@4.62.2': + resolution: {integrity: sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg==} + cpu: [x64] + os: [freebsd] + '@rollup/rollup-linux-arm-gnueabihf@4.60.1': resolution: {integrity: sha512-L+34Qqil+v5uC0zEubW7uByo78WOCIrBvci69E7sFASRl0X7b/MB6Cqd1lky/CtcSVTydWa2WZwFuWexjS5o6g==} cpu: [arm] os: [linux] + '@rollup/rollup-linux-arm-gnueabihf@4.62.2': + resolution: {integrity: sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg==} + cpu: [arm] + os: [linux] + '@rollup/rollup-linux-arm-musleabihf@4.60.1': resolution: {integrity: sha512-n83O8rt4v34hgFzlkb1ycniJh7IR5RCIqt6mz1VRJD6pmhRi0CXdmfnLu9dIUS6buzh60IvACM842Ffb3xd6Gg==} cpu: [arm] os: [linux] + '@rollup/rollup-linux-arm-musleabihf@4.62.2': + resolution: {integrity: sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA==} + cpu: [arm] + os: [linux] + '@rollup/rollup-linux-arm64-gnu@4.60.1': resolution: {integrity: sha512-Nql7sTeAzhTAja3QXeAI48+/+GjBJ+QmAH13snn0AJSNL50JsDqotyudHyMbO2RbJkskbMbFJfIJKWA6R1LCJQ==} cpu: [arm64] os: [linux] + '@rollup/rollup-linux-arm64-gnu@4.62.2': + resolution: {integrity: sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA==} + cpu: [arm64] + os: [linux] + '@rollup/rollup-linux-arm64-musl@4.60.1': resolution: {integrity: sha512-+pUymDhd0ys9GcKZPPWlFiZ67sTWV5UU6zOJat02M1+PiuSGDziyRuI/pPue3hoUwm2uGfxdL+trT6Z9rxnlMA==} cpu: [arm64] os: [linux] + '@rollup/rollup-linux-arm64-musl@4.62.2': + resolution: {integrity: sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ==} + cpu: [arm64] + os: [linux] + '@rollup/rollup-linux-loong64-gnu@4.60.1': resolution: {integrity: sha512-VSvgvQeIcsEvY4bKDHEDWcpW4Yw7BtlKG1GUT4FzBUlEKQK0rWHYBqQt6Fm2taXS+1bXvJT6kICu5ZwqKCnvlQ==} cpu: [loong64] os: [linux] + '@rollup/rollup-linux-loong64-gnu@4.62.2': + resolution: {integrity: sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg==} + cpu: [loong64] + os: [linux] + '@rollup/rollup-linux-loong64-musl@4.60.1': resolution: {integrity: sha512-4LqhUomJqwe641gsPp6xLfhqWMbQV04KtPp7/dIp0nzPxAkNY1AbwL5W0MQpcalLYk07vaW9Kp1PBhdpZYYcEw==} cpu: [loong64] os: [linux] + '@rollup/rollup-linux-loong64-musl@4.62.2': + resolution: {integrity: sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ==} + cpu: [loong64] + os: [linux] + '@rollup/rollup-linux-ppc64-gnu@4.60.1': resolution: {integrity: sha512-tLQQ9aPvkBxOc/EUT6j3pyeMD6Hb8QF2BTBnCQWP/uu1lhc9AIrIjKnLYMEroIz/JvtGYgI9dF3AxHZNaEH0rw==} cpu: [ppc64] os: [linux] + '@rollup/rollup-linux-ppc64-gnu@4.62.2': + resolution: {integrity: sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A==} + cpu: [ppc64] + os: [linux] + '@rollup/rollup-linux-ppc64-musl@4.60.1': resolution: {integrity: sha512-RMxFhJwc9fSXP6PqmAz4cbv3kAyvD1etJFjTx4ONqFP9DkTkXsAMU4v3Vyc5BgzC+anz7nS/9tp4obsKfqkDHg==} cpu: [ppc64] os: [linux] + '@rollup/rollup-linux-ppc64-musl@4.62.2': + resolution: {integrity: sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w==} + cpu: [ppc64] + os: [linux] + '@rollup/rollup-linux-riscv64-gnu@4.60.1': resolution: {integrity: sha512-QKgFl+Yc1eEk6MmOBfRHYF6lTxiiiV3/z/BRrbSiW2I7AFTXoBFvdMEyglohPj//2mZS4hDOqeB0H1ACh3sBbg==} cpu: [riscv64] os: [linux] + '@rollup/rollup-linux-riscv64-gnu@4.62.2': + resolution: {integrity: sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg==} + cpu: [riscv64] + os: [linux] + '@rollup/rollup-linux-riscv64-musl@4.60.1': resolution: {integrity: sha512-RAjXjP/8c6ZtzatZcA1RaQr6O1TRhzC+adn8YZDnChliZHviqIjmvFwHcxi4JKPSDAt6Uhf/7vqcBzQJy0PDJg==} cpu: [riscv64] os: [linux] + '@rollup/rollup-linux-riscv64-musl@4.62.2': + resolution: {integrity: sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q==} + cpu: [riscv64] + os: [linux] + '@rollup/rollup-linux-s390x-gnu@4.60.1': resolution: {integrity: sha512-wcuocpaOlaL1COBYiA89O6yfjlp3RwKDeTIA0hM7OpmhR1Bjo9j31G1uQVpDlTvwxGn2nQs65fBFL5UFd76FcQ==} cpu: [s390x] os: [linux] + '@rollup/rollup-linux-s390x-gnu@4.62.2': + resolution: {integrity: sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg==} + cpu: [s390x] + os: [linux] + '@rollup/rollup-linux-x64-gnu@4.60.1': resolution: {integrity: sha512-77PpsFQUCOiZR9+LQEFg9GClyfkNXj1MP6wRnzYs0EeWbPcHs02AXu4xuUbM1zhwn3wqaizle3AEYg5aeoohhg==} cpu: [x64] os: [linux] + '@rollup/rollup-linux-x64-gnu@4.62.2': + resolution: {integrity: sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A==} + cpu: [x64] + os: [linux] + '@rollup/rollup-linux-x64-musl@4.60.1': resolution: {integrity: sha512-5cIATbk5vynAjqqmyBjlciMJl1+R/CwX9oLk/EyiFXDWd95KpHdrOJT//rnUl4cUcskrd0jCCw3wpZnhIHdD9w==} cpu: [x64] os: [linux] + '@rollup/rollup-linux-x64-musl@4.62.2': + resolution: {integrity: sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg==} + cpu: [x64] + os: [linux] + '@rollup/rollup-openbsd-x64@4.60.1': resolution: {integrity: sha512-cl0w09WsCi17mcmWqqglez9Gk8isgeWvoUZ3WiJFYSR3zjBQc2J5/ihSjpl+VLjPqjQ/1hJRcqBfLjssREQILw==} cpu: [x64] os: [openbsd] + '@rollup/rollup-openbsd-x64@4.62.2': + resolution: {integrity: sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg==} + cpu: [x64] + os: [openbsd] + '@rollup/rollup-openharmony-arm64@4.60.1': resolution: {integrity: sha512-4Cv23ZrONRbNtbZa37mLSueXUCtN7MXccChtKpUnQNgF010rjrjfHx3QxkS2PI7LqGT5xXyYs1a7LbzAwT0iCA==} cpu: [arm64] os: [openharmony] + '@rollup/rollup-openharmony-arm64@4.62.2': + resolution: {integrity: sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA==} + cpu: [arm64] + os: [openharmony] + '@rollup/rollup-win32-arm64-msvc@4.60.1': resolution: {integrity: sha512-i1okWYkA4FJICtr7KpYzFpRTHgy5jdDbZiWfvny21iIKky5YExiDXP+zbXzm3dUcFpkEeYNHgQ5fuG236JPq0g==} cpu: [arm64] os: [win32] + '@rollup/rollup-win32-arm64-msvc@4.62.2': + resolution: {integrity: sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg==} + cpu: [arm64] + os: [win32] + '@rollup/rollup-win32-ia32-msvc@4.60.1': resolution: {integrity: sha512-u09m3CuwLzShA0EYKMNiFgcjjzwqtUMLmuCJLeZWjjOYA3IT2Di09KaxGBTP9xVztWyIWjVdsB2E9goMjZvTQg==} cpu: [ia32] os: [win32] + '@rollup/rollup-win32-ia32-msvc@4.62.2': + resolution: {integrity: sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q==} + cpu: [ia32] + os: [win32] + '@rollup/rollup-win32-x64-gnu@4.60.1': resolution: {integrity: sha512-k+600V9Zl1CM7eZxJgMyTUzmrmhB/0XZnF4pRypKAlAgxmedUA+1v9R+XOFv56W4SlHEzfeMtzujLJD22Uz5zg==} cpu: [x64] os: [win32] + '@rollup/rollup-win32-x64-gnu@4.62.2': + resolution: {integrity: sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg==} + cpu: [x64] + os: [win32] + '@rollup/rollup-win32-x64-msvc@4.60.1': resolution: {integrity: sha512-lWMnixq/QzxyhTV6NjQJ4SFo1J6PvOX8vUx5Wb4bBPsEb+8xZ89Bz6kOXpfXj9ak9AHTQVQzlgzBEc1SyM27xQ==} cpu: [x64] os: [win32] + '@rollup/rollup-win32-x64-msvc@4.62.2': + resolution: {integrity: sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA==} + cpu: [x64] + os: [win32] + '@standard-schema/spec@1.1.0': resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} @@ -1850,6 +1975,11 @@ packages: engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true + nanoid@3.3.13: + resolution: {integrity: sha512-sPdqC6ByMVVGvF1ynvvMo0/o+oD1VX7DaHhijt1bFgjvBkHBib4t49GoNDhf2NDta4oeUNlaGbSt5K7qjZ955Q==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} @@ -1989,6 +2119,10 @@ packages: resolution: {integrity: sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==} engines: {node: '>=4'} + postcss@8.5.15: + resolution: {integrity: sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==} + engines: {node: ^10 || ^12 || >=14} + postcss@8.5.9: resolution: {integrity: sha512-7a70Nsot+EMX9fFU3064K/kdHWZqGVY+BADLyXc8Dfv+mTLLVl6JzJpPaCZ2kQL9gIJvKXSLMHhqdRRjwQeFtw==} engines: {node: ^10 || ^12 || >=14} @@ -2058,6 +2192,11 @@ packages: engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true + rollup@4.62.2: + resolution: {integrity: sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + rrweb-cssom@0.7.1: resolution: {integrity: sha512-TrEMa7JGdVm0UThDJSx7ddw5nVm3UJS9o9CCIZ72B1vSyEZoziDqBYP3XIoi/12lKrJR8rE3jeFHMok2F/Mnsg==} @@ -2092,8 +2231,8 @@ packages: engines: {node: '>=10'} hasBin: true - semver@7.8.4: - resolution: {integrity: sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA==} + semver@7.8.5: + resolution: {integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==} engines: {node: '>=10'} hasBin: true @@ -2205,6 +2344,10 @@ packages: resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} engines: {node: '>=12.0.0'} + tinyglobby@0.2.17: + resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} + engines: {node: '>=12.0.0'} + tinyrainbow@3.1.0: resolution: {integrity: sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==} engines: {node: '>=14.0.0'} @@ -2322,8 +2465,8 @@ packages: '@nuxt/kit': optional: true - vite@7.3.2: - resolution: {integrity: sha512-Bby3NOsna2jsjfLVOHKes8sGwgl4TT0E6vvpYgnAYDIF/tie7MRaFthmKuHx1NSXjiTueXH3do80FMQgvEktRg==} + vite@7.3.5: + resolution: {integrity: sha512-KuOaNhcnGFN2zIPGA7wRmzF+lJA1sea7rHq17aiJ++9lzY1WWG6Jpwqwe1KNbRVPIqHmr8GLYx7jbrQcN/7/ww==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true peerDependencies: @@ -2977,78 +3120,153 @@ snapshots: '@rollup/rollup-android-arm-eabi@4.60.1': optional: true + '@rollup/rollup-android-arm-eabi@4.62.2': + optional: true + '@rollup/rollup-android-arm64@4.60.1': optional: true + '@rollup/rollup-android-arm64@4.62.2': + optional: true + '@rollup/rollup-darwin-arm64@4.60.1': optional: true + '@rollup/rollup-darwin-arm64@4.62.2': + optional: true + '@rollup/rollup-darwin-x64@4.60.1': optional: true + '@rollup/rollup-darwin-x64@4.62.2': + optional: true + '@rollup/rollup-freebsd-arm64@4.60.1': optional: true + '@rollup/rollup-freebsd-arm64@4.62.2': + optional: true + '@rollup/rollup-freebsd-x64@4.60.1': optional: true + '@rollup/rollup-freebsd-x64@4.62.2': + optional: true + '@rollup/rollup-linux-arm-gnueabihf@4.60.1': optional: true + '@rollup/rollup-linux-arm-gnueabihf@4.62.2': + optional: true + '@rollup/rollup-linux-arm-musleabihf@4.60.1': optional: true + '@rollup/rollup-linux-arm-musleabihf@4.62.2': + optional: true + '@rollup/rollup-linux-arm64-gnu@4.60.1': optional: true + '@rollup/rollup-linux-arm64-gnu@4.62.2': + optional: true + '@rollup/rollup-linux-arm64-musl@4.60.1': optional: true + '@rollup/rollup-linux-arm64-musl@4.62.2': + optional: true + '@rollup/rollup-linux-loong64-gnu@4.60.1': optional: true + '@rollup/rollup-linux-loong64-gnu@4.62.2': + optional: true + '@rollup/rollup-linux-loong64-musl@4.60.1': optional: true + '@rollup/rollup-linux-loong64-musl@4.62.2': + optional: true + '@rollup/rollup-linux-ppc64-gnu@4.60.1': optional: true + '@rollup/rollup-linux-ppc64-gnu@4.62.2': + optional: true + '@rollup/rollup-linux-ppc64-musl@4.60.1': optional: true + '@rollup/rollup-linux-ppc64-musl@4.62.2': + optional: true + '@rollup/rollup-linux-riscv64-gnu@4.60.1': optional: true + '@rollup/rollup-linux-riscv64-gnu@4.62.2': + optional: true + '@rollup/rollup-linux-riscv64-musl@4.60.1': optional: true + '@rollup/rollup-linux-riscv64-musl@4.62.2': + optional: true + '@rollup/rollup-linux-s390x-gnu@4.60.1': optional: true + '@rollup/rollup-linux-s390x-gnu@4.62.2': + optional: true + '@rollup/rollup-linux-x64-gnu@4.60.1': optional: true + '@rollup/rollup-linux-x64-gnu@4.62.2': + optional: true + '@rollup/rollup-linux-x64-musl@4.60.1': optional: true + '@rollup/rollup-linux-x64-musl@4.62.2': + optional: true + '@rollup/rollup-openbsd-x64@4.60.1': optional: true + '@rollup/rollup-openbsd-x64@4.62.2': + optional: true + '@rollup/rollup-openharmony-arm64@4.60.1': optional: true + '@rollup/rollup-openharmony-arm64@4.62.2': + optional: true + '@rollup/rollup-win32-arm64-msvc@4.60.1': optional: true + '@rollup/rollup-win32-arm64-msvc@4.62.2': + optional: true + '@rollup/rollup-win32-ia32-msvc@4.60.1': optional: true + '@rollup/rollup-win32-ia32-msvc@4.62.2': + optional: true + '@rollup/rollup-win32-x64-gnu@4.60.1': optional: true + '@rollup/rollup-win32-x64-gnu@4.62.2': + optional: true + '@rollup/rollup-win32-x64-msvc@4.60.1': optional: true + '@rollup/rollup-win32-x64-msvc@4.62.2': + optional: true + '@standard-schema/spec@1.1.0': {} '@stylistic/eslint-plugin-js@1.8.0(eslint@10.0.0)': @@ -3076,24 +3294,24 @@ snapshots: typescript: 5.5.4 typescript-eslint: 8.56.0(eslint@10.0.0)(typescript@5.5.4) - '@sveltejs/vite-plugin-svelte-inspector@5.0.1(@sveltejs/vite-plugin-svelte@6.2.0(svelte@packages+svelte)(vite@7.3.2(@types/node@24.5.2)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0)))(svelte@packages+svelte)(vite@7.3.2(@types/node@24.5.2)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0))': + '@sveltejs/vite-plugin-svelte-inspector@5.0.1(@sveltejs/vite-plugin-svelte@6.2.0(svelte@packages+svelte)(vite@7.3.5(@types/node@24.5.2)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0)))(svelte@packages+svelte)(vite@7.3.5(@types/node@24.5.2)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0))': dependencies: - '@sveltejs/vite-plugin-svelte': 6.2.0(svelte@packages+svelte)(vite@7.3.2(@types/node@24.5.2)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0)) + '@sveltejs/vite-plugin-svelte': 6.2.0(svelte@packages+svelte)(vite@7.3.5(@types/node@24.5.2)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0)) debug: 4.4.3 svelte: link:packages/svelte - vite: 7.3.2(@types/node@24.5.2)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0) + vite: 7.3.5(@types/node@24.5.2)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0) transitivePeerDependencies: - supports-color - '@sveltejs/vite-plugin-svelte@6.2.0(svelte@packages+svelte)(vite@7.3.2(@types/node@24.5.2)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0))': + '@sveltejs/vite-plugin-svelte@6.2.0(svelte@packages+svelte)(vite@7.3.5(@types/node@24.5.2)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0))': dependencies: - '@sveltejs/vite-plugin-svelte-inspector': 5.0.1(@sveltejs/vite-plugin-svelte@6.2.0(svelte@packages+svelte)(vite@7.3.2(@types/node@24.5.2)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0)))(svelte@packages+svelte)(vite@7.3.2(@types/node@24.5.2)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0)) + '@sveltejs/vite-plugin-svelte-inspector': 5.0.1(@sveltejs/vite-plugin-svelte@6.2.0(svelte@packages+svelte)(vite@7.3.5(@types/node@24.5.2)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0)))(svelte@packages+svelte)(vite@7.3.5(@types/node@24.5.2)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0)) debug: 4.4.3 deepmerge: 4.3.1 magic-string: 0.30.17 svelte: link:packages/svelte - vite: 7.3.2(@types/node@24.5.2)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0) - vitefu: 1.1.1(vite@7.3.2(@types/node@24.5.2)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0)) + vite: 7.3.5(@types/node@24.5.2)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0) + vitefu: 1.1.1(vite@7.3.5(@types/node@24.5.2)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0)) transitivePeerDependencies: - supports-color @@ -3207,7 +3425,7 @@ snapshots: debug: 4.4.3 minimatch: 9.0.5 semver: 7.7.4 - tinyglobby: 0.2.15 + tinyglobby: 0.2.17 ts-api-utils: 2.4.0(typescript@5.5.4) typescript: 5.5.4 transitivePeerDependencies: @@ -3241,7 +3459,7 @@ snapshots: obug: 2.1.1 std-env: 4.1.0 tinyrainbow: 3.1.0 - vitest: 4.1.7(@types/node@20.19.17)(@vitest/coverage-v8@4.1.7)(jsdom@25.0.1)(vite@7.3.2(@types/node@20.19.17)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0)) + vitest: 4.1.7(@types/node@20.19.17)(@vitest/coverage-v8@4.1.7)(jsdom@25.0.1)(vite@7.3.5(@types/node@20.19.17)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0)) '@vitest/expect@4.1.7': dependencies: @@ -3252,13 +3470,13 @@ snapshots: chai: 6.2.2 tinyrainbow: 3.1.0 - '@vitest/mocker@4.1.7(vite@7.3.2(@types/node@20.19.17)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0))': + '@vitest/mocker@4.1.7(vite@7.3.5(@types/node@20.19.17)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0))': dependencies: '@vitest/spy': 4.1.7 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 7.3.2(@types/node@20.19.17)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0) + vite: 7.3.5(@types/node@20.19.17)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0) '@vitest/pretty-format@4.1.7': dependencies: @@ -3543,7 +3761,7 @@ snapshots: eslint-compat-utils@0.5.1(eslint@10.0.0): dependencies: eslint: 10.0.0 - semver: 7.8.4 + semver: 7.8.5 eslint-config-prettier@9.1.0(eslint@10.0.0): dependencies: @@ -3570,7 +3788,7 @@ snapshots: globals: 15.15.0 globrex: 0.1.2 ignore: 5.3.2 - semver: 7.8.4 + semver: 7.8.5 ts-declaration-location: 1.0.7(typescript@5.5.4) transitivePeerDependencies: - typescript @@ -4070,6 +4288,8 @@ snapshots: nanoid@3.3.11: {} + nanoid@3.3.13: {} + natural-compare@1.4.0: {} normalize-path@3.0.0: @@ -4175,15 +4395,21 @@ snapshots: dependencies: postcss: 8.5.9 - postcss-scss@4.0.9(postcss@8.5.9): + postcss-scss@4.0.9(postcss@8.5.15): dependencies: - postcss: 8.5.9 + postcss: 8.5.15 postcss-selector-parser@7.1.0: dependencies: cssesc: 3.0.0 util-deprecate: 1.0.2 + postcss@8.5.15: + dependencies: + nanoid: 3.3.13 + picocolors: 1.1.1 + source-map-js: 1.2.1 + postcss@8.5.9: dependencies: nanoid: 3.3.11 @@ -4268,6 +4494,37 @@ snapshots: '@rollup/rollup-win32-x64-msvc': 4.60.1 fsevents: 2.3.3 + rollup@4.62.2: + dependencies: + '@types/estree': 1.0.9 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.62.2 + '@rollup/rollup-android-arm64': 4.62.2 + '@rollup/rollup-darwin-arm64': 4.62.2 + '@rollup/rollup-darwin-x64': 4.62.2 + '@rollup/rollup-freebsd-arm64': 4.62.2 + '@rollup/rollup-freebsd-x64': 4.62.2 + '@rollup/rollup-linux-arm-gnueabihf': 4.62.2 + '@rollup/rollup-linux-arm-musleabihf': 4.62.2 + '@rollup/rollup-linux-arm64-gnu': 4.62.2 + '@rollup/rollup-linux-arm64-musl': 4.62.2 + '@rollup/rollup-linux-loong64-gnu': 4.62.2 + '@rollup/rollup-linux-loong64-musl': 4.62.2 + '@rollup/rollup-linux-ppc64-gnu': 4.62.2 + '@rollup/rollup-linux-ppc64-musl': 4.62.2 + '@rollup/rollup-linux-riscv64-gnu': 4.62.2 + '@rollup/rollup-linux-riscv64-musl': 4.62.2 + '@rollup/rollup-linux-s390x-gnu': 4.62.2 + '@rollup/rollup-linux-x64-gnu': 4.62.2 + '@rollup/rollup-linux-x64-musl': 4.62.2 + '@rollup/rollup-openbsd-x64': 4.62.2 + '@rollup/rollup-openharmony-arm64': 4.62.2 + '@rollup/rollup-win32-arm64-msvc': 4.62.2 + '@rollup/rollup-win32-ia32-msvc': 4.62.2 + '@rollup/rollup-win32-x64-gnu': 4.62.2 + '@rollup/rollup-win32-x64-msvc': 4.62.2 + fsevents: 2.3.3 + rrweb-cssom@0.7.1: {} run-applescript@7.0.0: {} @@ -4297,7 +4554,7 @@ snapshots: semver@7.7.4: {} - semver@7.8.4: {} + semver@7.8.5: {} serialize-javascript@6.0.2: dependencies: @@ -4362,8 +4619,8 @@ snapshots: eslint-scope: 8.4.0 eslint-visitor-keys: 4.2.1 espree: 10.1.0 - postcss: 8.5.9 - postcss-scss: 4.0.9(postcss@8.5.9) + postcss: 8.5.15 + postcss-scss: 4.0.9(postcss@8.5.15) postcss-selector-parser: 7.1.0 optionalDependencies: svelte: link:packages/svelte @@ -4390,6 +4647,11 @@ snapshots: fdir: 6.5.0(picomatch@4.0.4) picomatch: 4.0.4 + tinyglobby@0.2.17: + dependencies: + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 + tinyrainbow@3.1.0: {} tldts-core@6.1.64: {} @@ -4467,22 +4729,22 @@ snapshots: v8-natives@1.2.5: {} - vite-dev-rpc@1.1.0(vite@7.3.2(@types/node@24.5.2)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0)): + vite-dev-rpc@1.1.0(vite@7.3.5(@types/node@24.5.2)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0)): dependencies: birpc: 2.5.0 - vite: 7.3.2(@types/node@24.5.2)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0) - vite-hot-client: 2.1.0(vite@7.3.2(@types/node@24.5.2)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0)) + vite: 7.3.5(@types/node@24.5.2)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0) + vite-hot-client: 2.1.0(vite@7.3.5(@types/node@24.5.2)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0)) - vite-hot-client@2.1.0(vite@7.3.2(@types/node@24.5.2)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0)): + vite-hot-client@2.1.0(vite@7.3.5(@types/node@24.5.2)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0)): dependencies: - vite: 7.3.2(@types/node@24.5.2)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0) + vite: 7.3.5(@types/node@24.5.2)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0) - vite-plugin-devtools-json@1.0.0(vite@7.3.2(@types/node@24.5.2)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0)): + vite-plugin-devtools-json@1.0.0(vite@7.3.5(@types/node@24.5.2)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0)): dependencies: uuid: 11.1.0 - vite: 7.3.2(@types/node@24.5.2)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0) + vite: 7.3.5(@types/node@24.5.2)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0) - vite-plugin-inspect@11.3.3(vite@7.3.2(@types/node@24.5.2)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0)): + vite-plugin-inspect@11.3.3(vite@7.3.5(@types/node@24.5.2)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0)): dependencies: ansis: 4.1.0 debug: 4.4.3 @@ -4492,19 +4754,19 @@ snapshots: perfect-debounce: 2.0.0 sirv: 3.0.2 unplugin-utils: 0.3.0 - vite: 7.3.2(@types/node@24.5.2)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0) - vite-dev-rpc: 1.1.0(vite@7.3.2(@types/node@24.5.2)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0)) + vite: 7.3.5(@types/node@24.5.2)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0) + vite-dev-rpc: 1.1.0(vite@7.3.5(@types/node@24.5.2)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0)) transitivePeerDependencies: - supports-color - vite@7.3.2(@types/node@20.19.17)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0): + vite@7.3.5(@types/node@20.19.17)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0): dependencies: esbuild: 0.27.7 fdir: 6.5.0(picomatch@4.0.4) picomatch: 4.0.4 - postcss: 8.5.9 - rollup: 4.60.1 - tinyglobby: 0.2.15 + postcss: 8.5.15 + rollup: 4.62.2 + tinyglobby: 0.2.17 optionalDependencies: '@types/node': 20.19.17 fsevents: 2.3.3 @@ -4512,14 +4774,14 @@ snapshots: sass: 1.70.0 terser: 5.27.0 - vite@7.3.2(@types/node@24.5.2)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0): + vite@7.3.5(@types/node@24.5.2)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0): dependencies: esbuild: 0.27.7 fdir: 6.5.0(picomatch@4.0.4) picomatch: 4.0.4 - postcss: 8.5.9 - rollup: 4.60.1 - tinyglobby: 0.2.15 + postcss: 8.5.15 + rollup: 4.62.2 + tinyglobby: 0.2.17 optionalDependencies: '@types/node': 24.5.2 fsevents: 2.3.3 @@ -4527,14 +4789,14 @@ snapshots: sass: 1.70.0 terser: 5.27.0 - vitefu@1.1.1(vite@7.3.2(@types/node@24.5.2)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0)): + vitefu@1.1.1(vite@7.3.5(@types/node@24.5.2)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0)): optionalDependencies: - vite: 7.3.2(@types/node@24.5.2)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0) + vite: 7.3.5(@types/node@24.5.2)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0) - vitest@4.1.7(@types/node@20.19.17)(@vitest/coverage-v8@4.1.7)(jsdom@25.0.1)(vite@7.3.2(@types/node@20.19.17)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0)): + vitest@4.1.7(@types/node@20.19.17)(@vitest/coverage-v8@4.1.7)(jsdom@25.0.1)(vite@7.3.5(@types/node@20.19.17)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0)): dependencies: '@vitest/expect': 4.1.7 - '@vitest/mocker': 4.1.7(vite@7.3.2(@types/node@20.19.17)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0)) + '@vitest/mocker': 4.1.7(vite@7.3.5(@types/node@20.19.17)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0)) '@vitest/pretty-format': 4.1.7 '@vitest/runner': 4.1.7 '@vitest/snapshot': 4.1.7 @@ -4551,7 +4813,7 @@ snapshots: tinyexec: 1.1.2 tinyglobby: 0.2.15 tinyrainbow: 3.1.0 - vite: 7.3.2(@types/node@20.19.17)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0) + vite: 7.3.5(@types/node@20.19.17)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0) why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 20.19.17 From a6560bbe08bd25105152e23cb80465a9e7d0c95a Mon Sep 17 00:00:00 2001 From: Nic Polumeyv Date: Fri, 21 Aug 2026 16:29:02 -0400 Subject: [PATCH 171/222] fix: don't resurrect outroing elements when an ancestor block is paused and resumed (#18431) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes #15604 Given two nested blocks that both transition: ```svelte {#if fetching}

    loading

    {:else}
    {#if shown}
    red square
    {/if}
    {/if} ``` and this sequence, where each step happens before the previous fade finished: ```js shown = false; // red square starts fading out fetching = true; // outer block starts fading out too fetching = false; // 50ms later ``` the red square should finish fading out and be removed, since `shown` is still false. Instead it fades back in and stays on screen: resuming the outer block walks the whole subtree, clears `INERT` on every effect and plays `in()` on every transition it finds — including the inner block's, so the square's outro is aborted and the removal callback waiting on it never runs. The inner block has no reason to re-run on its own, `shown` never changed again. The root issue is that `INERT` records no ownership — it can't distinguish "paused by the ancestor currently being resumed" (revive) from "paused by its own block for its own reasons" (leave alone). The pause side already has this restraint: `pause_children` refuses to touch a subtree that is already `INERT`. The resume side had nothing to check. This PR marks the one effect `pause_effect` was actually called on — the root of the paused subtree — with a `PAUSED` flag, and gives resume the same restraint: ```js function resume_children(effect, local) { if ((effect.f & PAUSED) !== 0) return; ``` so a resume can only ever undo its own pause; the flag is only cleared by `resume_effect` on that exact effect. If `shown` flips back to true while the outer block is paused, this still works: the inner block effect carries no `PAUSED` itself, so it is resumed and rescheduled, re-evaluates its condition and revives its own branch. --- .changeset/quiet-falcons-search.md | 5 ++++ .../svelte/src/internal/client/constants.js | 7 +++++ .../src/internal/client/reactivity/effects.js | 9 +++++- .../_config.js | 23 +++++++++++++++ .../main.svelte | 19 ++++++++++++ .../_config.js | 24 +++++++++++++++ .../main.svelte | 19 ++++++++++++ .../_config.js | 29 +++++++++++++++++++ .../main.svelte | 19 ++++++++++++ 9 files changed, 153 insertions(+), 1 deletion(-) create mode 100644 .changeset/quiet-falcons-search.md create mode 100644 packages/svelte/tests/runtime-runes/samples/transition-global-nested-resume-each/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/transition-global-nested-resume-each/main.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/transition-global-nested-resume-flipback/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/transition-global-nested-resume-flipback/main.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/transition-global-nested-resume/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/transition-global-nested-resume/main.svelte diff --git a/.changeset/quiet-falcons-search.md b/.changeset/quiet-falcons-search.md new file mode 100644 index 0000000000..8c5c4dedca --- /dev/null +++ b/.changeset/quiet-falcons-search.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: don't resurrect outroing elements when an ancestor block is paused and resumed diff --git a/packages/svelte/src/internal/client/constants.js b/packages/svelte/src/internal/client/constants.js index b92ea8598b..e0b7a8779b 100644 --- a/packages/svelte/src/internal/client/constants.js +++ b/packages/svelte/src/internal/client/constants.js @@ -15,6 +15,13 @@ export const BLOCK_EFFECT = 1 << 4; export const BRANCH_EFFECT = 1 << 5; export const ROOT_EFFECT = 1 << 6; export const BOUNDARY_EFFECT = 1 << 7; +/** + * Set on the effect that `pause_effect` was called on, i.e. the root of a paused subtree, + * as opposed to its descendants which are merely `INERT`. This allows `resume_effect` on + * an ancestor to skip subtrees that were paused for their own reasons (such as a block + * whose condition is still false) rather than resurrecting them + */ +export const PAUSED = 1 << 8; /** * Indicates that a reaction is connected to an effect root — either it is an effect, * or it is a derived that is depended on by at least one effect. If a derived has diff --git a/packages/svelte/src/internal/client/reactivity/effects.js b/packages/svelte/src/internal/client/reactivity/effects.js index 7e53138aae..e69a639a94 100644 --- a/packages/svelte/src/internal/client/reactivity/effects.js +++ b/packages/svelte/src/internal/client/reactivity/effects.js @@ -34,7 +34,8 @@ import { ASYNC, CONNECTED, MANAGED_EFFECT, - DESTROYING + DESTROYING, + PAUSED } from '#client/constants'; import { invoke_error_boundary } from '../error-handling.js'; import * as e from '../errors.js'; @@ -616,6 +617,7 @@ export function pause_effect(effect, callback, destroy = true) { /** @type {TransitionManager[]} */ var transitions = []; + effect.f |= PAUSED; pause_children(effect, transitions, true); var fn = () => { @@ -683,6 +685,7 @@ function pause_children(effect, transitions, local) { * @param {Effect} effect */ export function resume_effect(effect) { + effect.f &= ~PAUSED; resume_children(effect, true); } @@ -691,6 +694,10 @@ export function resume_effect(effect) { * @param {boolean} local */ function resume_children(effect, local) { + // this subtree was paused for its own reasons (e.g. a block whose condition + // is still false) — its controller will resume or destroy it + if ((effect.f & PAUSED) !== 0) return; + if ((effect.f & INERT) === 0) return; effect.f ^= INERT; diff --git a/packages/svelte/tests/runtime-runes/samples/transition-global-nested-resume-each/_config.js b/packages/svelte/tests/runtime-runes/samples/transition-global-nested-resume-each/_config.js new file mode 100644 index 0000000000..7ccfd72f00 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/transition-global-nested-resume-each/_config.js @@ -0,0 +1,23 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +// #15604 — a removed each item mid-outro must not be resurrected by an ancestor pause/resume +export default test({ + test({ assert, target, raf }) { + const [remove, fetch] = target.querySelectorAll('button'); + + raf.tick(200); + assert.equal(target.querySelectorAll('.item').length, 3); + + flushSync(() => remove.click()); + + flushSync(() => fetch.click()); + raf.tick(30); + + flushSync(() => fetch.click()); + raf.tick(2000); + + const items = [...target.querySelectorAll('.item')].map((el) => el.textContent); + assert.deepEqual(items, ['a', 'c']); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/transition-global-nested-resume-each/main.svelte b/packages/svelte/tests/runtime-runes/samples/transition-global-nested-resume-each/main.svelte new file mode 100644 index 0000000000..36e685e3c8 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/transition-global-nested-resume-each/main.svelte @@ -0,0 +1,19 @@ + + + + + +{#if fetching} +

    loading

    +{:else} +
    + {#each items as item (item)} +
    {item}
    + {/each} +
    +{/if} diff --git a/packages/svelte/tests/runtime-runes/samples/transition-global-nested-resume-flipback/_config.js b/packages/svelte/tests/runtime-runes/samples/transition-global-nested-resume-flipback/_config.js new file mode 100644 index 0000000000..a8e5e99bbf --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/transition-global-nested-resume-flipback/_config.js @@ -0,0 +1,24 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +// #15604 companion — flipping the condition back while the ancestor is paused must still revive the element +export default test({ + test({ assert, target, raf }) { + const [toggle, fetch] = target.querySelectorAll('button'); + + raf.tick(200); + assert.ok(target.querySelector('.red')); + + flushSync(() => toggle.click()); + + flushSync(() => fetch.click()); + raf.tick(30); + + flushSync(() => toggle.click()); + + flushSync(() => fetch.click()); + raf.tick(2000); + + assert.ok(target.querySelector('.red')); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/transition-global-nested-resume-flipback/main.svelte b/packages/svelte/tests/runtime-runes/samples/transition-global-nested-resume-flipback/main.svelte new file mode 100644 index 0000000000..71a3ea2090 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/transition-global-nested-resume-flipback/main.svelte @@ -0,0 +1,19 @@ + + + + + +{#if fetching} +

    loading

    +{:else} +
    + {#if shown} +
    red
    + {/if} +
    +{/if} diff --git a/packages/svelte/tests/runtime-runes/samples/transition-global-nested-resume/_config.js b/packages/svelte/tests/runtime-runes/samples/transition-global-nested-resume/_config.js new file mode 100644 index 0000000000..4009eea204 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/transition-global-nested-resume/_config.js @@ -0,0 +1,29 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +// #15604 — an element mid-outro must not be resurrected when an ancestor +// block is paused and resumed while the element's own condition is still false. +// The outer `transition:fade` matters: it keeps the outer branch alive (pending +// its own outro) so that toggling back takes the resume path +export default test({ + test({ assert, target, raf }) { + const [hide, fetch] = target.querySelectorAll('button'); + + // let the mount intro (dom variant) finish + raf.tick(200); + assert.ok(target.querySelector('.red')); + + // start the outro of the inner block + flushSync(() => hide.click()); + + // pause the outer block while the outro is in flight... + flushSync(() => fetch.click()); + raf.tick(250); + + // ...then resume it before either outro completes + flushSync(() => fetch.click()); + raf.tick(2000); + + assert.equal(target.querySelector('.red'), null); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/transition-global-nested-resume/main.svelte b/packages/svelte/tests/runtime-runes/samples/transition-global-nested-resume/main.svelte new file mode 100644 index 0000000000..0060401b8e --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/transition-global-nested-resume/main.svelte @@ -0,0 +1,19 @@ + + + + + +{#if fetching} +

    loading

    +{:else} +
    + {#if shown} +
    red
    + {/if} +
    +{/if} From 2ac73663918645a20e4fe2294ff1073ca306d973 Mon Sep 17 00:00:00 2001 From: PD Shaheed Ali Khan Date: Sat, 22 Aug 2026 02:18:58 +0530 Subject: [PATCH 172/222] perf: optimize simple object destructuring in @const tags (#18390) This PR resolves a `TODO` in `ConstTag.js` regarding the optimization of simple object pattern matching cases like `{@const { x } = y}`. --------- Co-authored-by: Simon H <5968653+dummdidumm@users.noreply.github.com> --- .changeset/optimize-const-tag.md | 5 ++++ .../src/compiler/phases/1-parse/acorn.js | 2 +- .../3-transform/client/visitors/ConstTag.js | 23 ++++++++++++++----- 3 files changed, 23 insertions(+), 7 deletions(-) create mode 100644 .changeset/optimize-const-tag.md diff --git a/.changeset/optimize-const-tag.md b/.changeset/optimize-const-tag.md new file mode 100644 index 0000000000..90c6a6b5d2 --- /dev/null +++ b/.changeset/optimize-const-tag.md @@ -0,0 +1,5 @@ +--- +"svelte": patch +--- + +perf: optimize simple object destructuring in `@const` tags diff --git a/packages/svelte/src/compiler/phases/1-parse/acorn.js b/packages/svelte/src/compiler/phases/1-parse/acorn.js index fb60c228c5..070183f984 100644 --- a/packages/svelte/src/compiler/phases/1-parse/acorn.js +++ b/packages/svelte/src/compiler/phases/1-parse/acorn.js @@ -59,7 +59,7 @@ export function parse(source, comments, typescript, is_script) { return /** @type {Program} */ (ast); } catch (err) { - // TODO the `return` in necessary for TS<7 due to a bug; otherwise + // TODO the `return` is necessary for TS<7 due to a bug; otherwise // the `finally` block is regarded as unreachable return handle_parse_error(err); } finally { diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/ConstTag.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/ConstTag.js index d05d7a8ed9..63c84a507c 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/ConstTag.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/ConstTag.js @@ -45,18 +45,29 @@ export function ConstTag(node, context) { transform }); - // TODO optimise the simple `{ x } = y` case — we can just return `y` - // instead of destructuring it only to return a new object + const is_simple_object_pattern = + declaration.id.type === 'ObjectPattern' && + declaration.id.properties.every( + (p) => + p.type === 'Property' && + !p.computed && + p.key.type === 'Identifier' && + p.value.type === 'Identifier' && + p.key.name === p.value.name + ); + const init = build_expression( { ...context, state: child_state }, declaration.init, node.metadata.expression ); - const block = b.block([ - b.const(/** @type {Pattern} */ (context.visit(declaration.id, child_state)), init), - b.return(b.object(identifiers.map((node) => b.prop('init', node, node)))) - ]); + const block = is_simple_object_pattern + ? b.block([b.return(init)]) + : b.block([ + b.const(/** @type {Pattern} */ (context.visit(declaration.id, child_state)), init), + b.return(b.object(identifiers.map((node) => b.prop('init', node, node)))) + ]); let expression = create_derived(context.state, block, node.metadata.expression.has_await); From 63b4c3652dfe0d4b692dd9278b5f668a0b67a76b Mon Sep 17 00:00:00 2001 From: sliang-code Date: Sat, 22 Aug 2026 04:59:35 +0800 Subject: [PATCH 173/222] fix: don't apply scoped CSS class to elements inside (#18160) skip elements inside head, they should not get hashes --------- Co-authored-by: Simon H <5968653+dummdidumm@users.noreply.github.com> Co-authored-by: Simon Holthausen --- .changeset/hungry-plums-roll.md | 5 +++++ .../src/compiler/phases/2-analyze/css/css-prune.js | 10 ++++++++++ .../samples/head-css-no-class/_expected_head.html | 1 + .../hydration/samples/head-css-no-class/main.svelte | 11 +++++++++++ 4 files changed, 27 insertions(+) create mode 100644 .changeset/hungry-plums-roll.md create mode 100644 packages/svelte/tests/hydration/samples/head-css-no-class/_expected_head.html create mode 100644 packages/svelte/tests/hydration/samples/head-css-no-class/main.svelte diff --git a/.changeset/hungry-plums-roll.md b/.changeset/hungry-plums-roll.md new file mode 100644 index 0000000000..b5e3688eb5 --- /dev/null +++ b/.changeset/hungry-plums-roll.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: never apply class hash to elements inside `` diff --git a/packages/svelte/src/compiler/phases/2-analyze/css/css-prune.js b/packages/svelte/src/compiler/phases/2-analyze/css/css-prune.js index 39f485a9f7..dff6bb9b5a 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/css/css-prune.js +++ b/packages/svelte/src/compiler/phases/2-analyze/css/css-prune.js @@ -122,6 +122,13 @@ const any_selector = { */ const seen = new Set(); +/** + * @param {Compiler.AST.RegularElement | Compiler.AST.SvelteElement | Compiler.AST.RenderTag | Compiler.AST.Component | Compiler.AST.SvelteComponent | Compiler.AST.SvelteSelf} node + */ +function is_inside_svelte_head(node) { + return node.metadata.path.some((ancestor) => ancestor.type === 'SvelteHead'); +} + /** * * @param {Compiler.AST.CSS.StyleSheet} stylesheet @@ -143,6 +150,9 @@ export function prune(stylesheet, elements) { seen.clear(); if ( + // Elements rendered through are not style-scopable. + // Prevent css hash injection (class="s-...") on tags like , , \ No newline at end of file diff --git a/packages/svelte/tests/hydration/samples/head-css-no-class/main.svelte b/packages/svelte/tests/hydration/samples/head-css-no-class/main.svelte new file mode 100644 index 0000000000..29336a17ba --- /dev/null +++ b/packages/svelte/tests/hydration/samples/head-css-no-class/main.svelte @@ -0,0 +1,11 @@ + + + + + + +
    dummy
    + + From 15720b16a5ef33e3e1f4301c77b94ec375070e73 Mon Sep 17 00:00:00 2001 From: ChaseCopeland <106773491+chasec22@users.noreply.github.com> Date: Fri, 21 Aug 2026 14:32:03 -0700 Subject: [PATCH 174/222] docs: Updated docs to include information on easing functions (#16070) closes #15992 This is a minor update to the easing documents. It adds an overview description for the module and short descriptions for each of the easing functions. --------- Co-authored-by: Simon H <5968653+dummdidumm@users.noreply.github.com> Co-authored-by: vercel[bot] <35613825+vercel[bot]@users.noreply.github.com> Co-authored-by: Simon Holthausen --- .../docs/98-reference/21-svelte-easing.md | 2 + packages/svelte/src/easing/index.js | 62 +++++++ packages/svelte/types/index.d.ts | 154 ++++++++++++++---- 3 files changed, 188 insertions(+), 30 deletions(-) diff --git a/documentation/docs/98-reference/21-svelte-easing.md b/documentation/docs/98-reference/21-svelte-easing.md index 3f47963cde..a2b49ff4a9 100644 --- a/documentation/docs/98-reference/21-svelte-easing.md +++ b/documentation/docs/98-reference/21-svelte-easing.md @@ -2,4 +2,6 @@ title: svelte/easing --- +This module provides a set of functions that allow you to manipulate time values in different ways. It’s particularly useful for animations when combined with the `motion` module. + > MODULE: svelte/easing diff --git a/packages/svelte/src/easing/index.js b/packages/svelte/src/easing/index.js index 08c86933ab..c2f7e63bc0 100644 --- a/packages/svelte/src/easing/index.js +++ b/packages/svelte/src/easing/index.js @@ -4,6 +4,8 @@ Distributed under MIT License https://github.com/mattdesl/eases/blob/master/LICE */ /** + * Returns value as is. + * * @param {number} t * @returns {number} */ @@ -12,6 +14,8 @@ export function linear(t) { } /** + * Rebound effect on start and end of value range. + * * @param {number} t * @returns {number} */ @@ -22,6 +26,8 @@ export function backInOut(t) { } /** + * Rebound effect on start. + * * @param {number} t * @returns {number} */ @@ -31,6 +37,8 @@ export function backIn(t) { } /** + * Rebound effect on end. + * * @param {number} t * @returns {number} */ @@ -40,6 +48,8 @@ export function backOut(t) { } /** + * Bounce effect on end. + * * @param {number} t * @returns {number} */ @@ -61,6 +71,8 @@ export function bounceOut(t) { } /** + * Bounce effect on start and end. + * * @param {number} t * @returns {number} */ @@ -69,6 +81,8 @@ export function bounceInOut(t) { } /** + * Bounce effect on start. + * * @param {number} t * @returns {number} */ @@ -77,6 +91,8 @@ export function bounceIn(t) { } /** + * Circular effect, accelerate on start, decelerate towards end. + * * @param {number} t * @returns {number} */ @@ -86,6 +102,8 @@ export function circInOut(t) { } /** + * Circular effect, accelerate on start. + * * @param {number} t * @returns {number} */ @@ -94,6 +112,8 @@ export function circIn(t) { } /** + * Circular effect, decelerate towards end. + * * @param {number} t * @returns {number} */ @@ -102,6 +122,8 @@ export function circOut(t) { } /** + * Cubic scaling, accelerate on start, decelerate towards end. + * * @param {number} t * @returns {number} */ @@ -110,6 +132,8 @@ export function cubicInOut(t) { } /** + * Cubic scaling, accelerate on start + * * @param {number} t * @returns {number} */ @@ -118,6 +142,8 @@ export function cubicIn(t) { } /** + * Cubic scaling, decelerate towards end. + * * @param {number} t * @returns {number} */ @@ -127,6 +153,8 @@ export function cubicOut(t) { } /** + * Elastic effect on start and end. + * * @param {number} t * @returns {number} */ @@ -140,6 +168,8 @@ export function elasticInOut(t) { } /** + * Elastic effect on start. + * * @param {number} t * @returns {number} */ @@ -148,6 +178,8 @@ export function elasticIn(t) { } /** + * Elastic effect on end. + * * @param {number} t * @returns {number} */ @@ -156,6 +188,8 @@ export function elasticOut(t) { } /** + * Exponential effect on start and end. + * * @param {number} t * @returns {number} */ @@ -168,6 +202,8 @@ export function expoInOut(t) { } /** + * Exponential effect on start. + * * @param {number} t * @returns {number} */ @@ -176,6 +212,8 @@ export function expoIn(t) { } /** + * Exponential effect on end. + * * @param {number} t * @returns {number} */ @@ -184,6 +222,8 @@ export function expoOut(t) { } /** + * Quadratic scaling, accelerate on start, decelerate towards end. + * * @param {number} t * @returns {number} */ @@ -195,6 +235,8 @@ export function quadInOut(t) { } /** + * Quadratic scaling, accelerate on start. + * * @param {number} t * @returns {number} */ @@ -203,6 +245,8 @@ export function quadIn(t) { } /** + * Quadratic scaling, decelerate towards end. + * * @param {number} t * @returns {number} */ @@ -211,6 +255,8 @@ export function quadOut(t) { } /** + * Quartic scaling, accelerate on start, decelerate towards end. + * * @param {number} t * @returns {number} */ @@ -219,6 +265,8 @@ export function quartInOut(t) { } /** + * Quartic scaling, accelerate on start. + * * @param {number} t * @returns {number} */ @@ -227,6 +275,8 @@ export function quartIn(t) { } /** + * Quartic scaling, decelerate towards end. + * * @param {number} t * @returns {number} */ @@ -235,6 +285,8 @@ export function quartOut(t) { } /** + * Quintic scaling, accelerate on start, decelerate towards end. + * * @param {number} t * @returns {number} */ @@ -244,6 +296,8 @@ export function quintInOut(t) { } /** + * Quintic scaling, accelerate on start. + * * @param {number} t * @returns {number} */ @@ -252,6 +306,8 @@ export function quintIn(t) { } /** + * Quintic scaling, decelerate towards end. + * * @param {number} t * @returns {number} */ @@ -260,6 +316,8 @@ export function quintOut(t) { } /** + * Sinusoidal effect, accelerate on start, decelerate towards end. + * * @param {number} t * @returns {number} */ @@ -268,6 +326,8 @@ export function sineInOut(t) { } /** + * Sinusoidal effect, accelerate on start. + * * @param {number} t * @returns {number} */ @@ -278,6 +338,8 @@ export function sineIn(t) { } /** + * Sinusoidal effect, decelerate towards end. + * * @param {number} t * @returns {number} */ diff --git a/packages/svelte/types/index.d.ts b/packages/svelte/types/index.d.ts index 96be34ef9e..dcc6dc5914 100644 --- a/packages/svelte/types/index.d.ts +++ b/packages/svelte/types/index.d.ts @@ -1875,66 +1875,160 @@ declare module 'svelte/compiler' { } declare module 'svelte/easing' { + /** + * Returns value as is. + * + * */ export function linear(t: number): number; - + /** + * Rebound effect on start and end of value range. + * + * */ export function backInOut(t: number): number; - + /** + * Rebound effect on start. + * + * */ export function backIn(t: number): number; - + /** + * Rebound effect on end. + * + * */ export function backOut(t: number): number; - + /** + * Bounce effect on end. + * + * */ export function bounceOut(t: number): number; - + /** + * Bounce effect on start and end. + * + * */ export function bounceInOut(t: number): number; - + /** + * Bounce effect on start. + * + * */ export function bounceIn(t: number): number; - + /** + * Circular effect, accelerate on start, decelerate towards end. + * + * */ export function circInOut(t: number): number; - + /** + * Circular effect, accelerate on start. + * + * */ export function circIn(t: number): number; - + /** + * Circular effect, decelerate towards end. + * + * */ export function circOut(t: number): number; - + /** + * Cubic scaling, accelerate on start, decelerate towards end. + * + * */ export function cubicInOut(t: number): number; - + /** + * Cubic scaling, accelerate on start + * + * */ export function cubicIn(t: number): number; - + /** + * Cubic scaling, decelerate towards end. + * + * */ export function cubicOut(t: number): number; - + /** + * Elastic effect on start and end. + * + * */ export function elasticInOut(t: number): number; - + /** + * Elastic effect on start. + * + * */ export function elasticIn(t: number): number; - + /** + * Elastic effect on end. + * + * */ export function elasticOut(t: number): number; - + /** + * Exponential effect on start and end. + * + * */ export function expoInOut(t: number): number; - + /** + * Exponential effect on start. + * + * */ export function expoIn(t: number): number; - + /** + * Exponential effect on end. + * + * */ export function expoOut(t: number): number; - + /** + * Quadratic scaling, accelerate on start, decelerate towards end. + * + * */ export function quadInOut(t: number): number; - + /** + * Quadratic scaling, accelerate on start. + * + * */ export function quadIn(t: number): number; - + /** + * Quadratic scaling, decelerate towards end. + * + * */ export function quadOut(t: number): number; - + /** + * Quartic scaling, accelerate on start, decelerate towards end. + * + * */ export function quartInOut(t: number): number; - + /** + * Quartic scaling, accelerate on start. + * + * */ export function quartIn(t: number): number; - + /** + * Quartic scaling, decelerate towards end. + * + * */ export function quartOut(t: number): number; - + /** + * Quintic scaling, accelerate on start, decelerate towards end. + * + * */ export function quintInOut(t: number): number; - + /** + * Quintic scaling, accelerate on start. + * + * */ export function quintIn(t: number): number; - + /** + * Quintic scaling, decelerate towards end. + * + * */ export function quintOut(t: number): number; - + /** + * Sinusoidal effect, accelerate on start, decelerate towards end. + * + * */ export function sineInOut(t: number): number; - + /** + * Sinusoidal effect, accelerate on start. + * + * */ export function sineIn(t: number): number; - + /** + * Sinusoidal effect, decelerate towards end. + * + * */ export function sineOut(t: number): number; export {}; From 4b61851ec80054c465b23603aafe2a7d69819438 Mon Sep 17 00:00:00 2001 From: "svelte-triage-bot[bot]" <316883489+svelte-triage-bot[bot]@users.noreply.github.com> Date: Mon, 24 Aug 2026 13:59:50 +0200 Subject: [PATCH 175/222] fix: clean up svelte:head anchors on unmount (#18697) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit include client-created anchors in the head effect’s DOM range remove anchors through the existing HEAD_EFFECT teardown add client and hydration regression coverage for repeated mount/unmount cycles Fixes #18695 --------- Co-authored-by: svelte-triage-bot Co-authored-by: Simon H <5968653+dummdidumm@users.noreply.github.com> --- .changeset/clean-head-anchors.md | 5 +++++ .../internal/client/dom/blocks/svelte-head.js | 8 ++++++++ .../svelte-head-anchor-cleanup/Head.svelte | 3 +++ .../svelte-head-anchor-cleanup/_config.js | 19 +++++++++++++++++++ .../svelte-head-anchor-cleanup/main.svelte | 11 +++++++++++ 5 files changed, 46 insertions(+) create mode 100644 .changeset/clean-head-anchors.md create mode 100644 packages/svelte/tests/runtime-runes/samples/svelte-head-anchor-cleanup/Head.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/svelte-head-anchor-cleanup/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/svelte-head-anchor-cleanup/main.svelte diff --git a/.changeset/clean-head-anchors.md b/.changeset/clean-head-anchors.md new file mode 100644 index 0000000000..d58283f502 --- /dev/null +++ b/.changeset/clean-head-anchors.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: remove `` anchors on unmount diff --git a/packages/svelte/src/internal/client/dom/blocks/svelte-head.js b/packages/svelte/src/internal/client/dom/blocks/svelte-head.js index 5721f7b056..d22d74b275 100644 --- a/packages/svelte/src/internal/client/dom/blocks/svelte-head.js +++ b/packages/svelte/src/internal/client/dom/blocks/svelte-head.js @@ -52,6 +52,14 @@ export function head(hash, render_fn) { block(() => { var e = branch(() => render_fn(anchor)); e.f |= HEAD_EFFECT; + + if (!hydrating) { + if (e.nodes === null) { + e.nodes = { start: anchor, end: anchor, a: null, t: null }; + } else { + e.nodes.end = anchor; + } + } }); } finally { if (was_hydrating) { diff --git a/packages/svelte/tests/runtime-runes/samples/svelte-head-anchor-cleanup/Head.svelte b/packages/svelte/tests/runtime-runes/samples/svelte-head-anchor-cleanup/Head.svelte new file mode 100644 index 0000000000..fa5ae4a349 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/svelte-head-anchor-cleanup/Head.svelte @@ -0,0 +1,3 @@ + + + diff --git a/packages/svelte/tests/runtime-runes/samples/svelte-head-anchor-cleanup/_config.js b/packages/svelte/tests/runtime-runes/samples/svelte-head-anchor-cleanup/_config.js new file mode 100644 index 0000000000..57ef735497 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/svelte-head-anchor-cleanup/_config.js @@ -0,0 +1,19 @@ +import { flushSync } from 'svelte'; +import { ok, test } from '../../test'; + +export default test({ + mode: ['client', 'hydrate'], + + test({ assert, target, window }) { + const initial = window.document.head.childNodes.length; + const button = target.querySelector('button'); + ok(button); + + for (let i = 0; i < 3; i++) { + flushSync(() => button.click()); + flushSync(() => button.click()); + } + + assert.equal(window.document.head.childNodes.length, initial); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/svelte-head-anchor-cleanup/main.svelte b/packages/svelte/tests/runtime-runes/samples/svelte-head-anchor-cleanup/main.svelte new file mode 100644 index 0000000000..f4cfefbadf --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/svelte-head-anchor-cleanup/main.svelte @@ -0,0 +1,11 @@ + + + + +{#if show} + +{/if} From 721244ad3ba75675414521d02af8581dcb5902c6 Mon Sep 17 00:00:00 2001 From: "svelte-triage-bot[bot]" <316883489+svelte-triage-bot[bot]@users.noreply.github.com> Date: Mon, 24 Aug 2026 14:14:18 +0200 Subject: [PATCH 176/222] fix: track SvelteDate snapshots in reactions (#18700) Read the Date value through `getTime()` before cloning it to get reactive updates from `SvelteDate`. Closes #18698 --------- Co-authored-by: svelte-triage-bot Co-authored-by: Simon H <5968653+dummdidumm@users.noreply.github.com> --- .changeset/calm-dates-track.md | 5 +++++ packages/svelte/src/internal/shared/clone.js | 2 ++ .../samples/state-snapshot-svelte-date/_config.js | 13 +++++++++++++ .../samples/state-snapshot-svelte-date/main.svelte | 9 +++++++++ 4 files changed, 29 insertions(+) create mode 100644 .changeset/calm-dates-track.md create mode 100644 packages/svelte/tests/runtime-runes/samples/state-snapshot-svelte-date/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/state-snapshot-svelte-date/main.svelte diff --git a/.changeset/calm-dates-track.md b/.changeset/calm-dates-track.md new file mode 100644 index 0000000000..e7275ae556 --- /dev/null +++ b/.changeset/calm-dates-track.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: track SvelteDate snapshots in reactions diff --git a/packages/svelte/src/internal/shared/clone.js b/packages/svelte/src/internal/shared/clone.js index 1a83adf8c5..ba0c731047 100644 --- a/packages/svelte/src/internal/shared/clone.js +++ b/packages/svelte/src/internal/shared/clone.js @@ -105,6 +105,8 @@ function clone(value, cloned, path, paths, original = null, no_tojson = false) { } if (value instanceof Date) { + // Ensure SvelteDate snapshots are tracked + value.getTime(); return /** @type {Snapshot} */ (structuredClone(value)); } diff --git a/packages/svelte/tests/runtime-runes/samples/state-snapshot-svelte-date/_config.js b/packages/svelte/tests/runtime-runes/samples/state-snapshot-svelte-date/_config.js new file mode 100644 index 0000000000..53d8e1d4ff --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/state-snapshot-svelte-date/_config.js @@ -0,0 +1,13 @@ +import { test } from '../../test'; +import { tick } from 'svelte'; + +export default test({ + async test({ assert, target }) { + assert.htmlEqual(target.innerHTML, '

    1970-01-01T00:00:00.000Z

    '); + + target.querySelector('button')?.click(); + await tick(); + + assert.htmlEqual(target.innerHTML, '

    1970-01-02T00:00:00.000Z

    '); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/state-snapshot-svelte-date/main.svelte b/packages/svelte/tests/runtime-runes/samples/state-snapshot-svelte-date/main.svelte new file mode 100644 index 0000000000..634a7ee3e8 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/state-snapshot-svelte-date/main.svelte @@ -0,0 +1,9 @@ + + + +

    {snapshot.toISOString()}

    From 1be496f0a61bf7c1da912caab62804e5e173ca5b Mon Sep 17 00:00:00 2001 From: Bao Nguyen <39545125+giaBaoJS@users.noreply.github.com> Date: Mon, 24 Aug 2026 20:14:16 +0700 Subject: [PATCH 177/222] fix: accept `onfocusin`/`onfocusout` in `a11y_mouse_events_have_key_events` (#18689) Fixes #8089 focusin/focusout are the bubble variants of focus/blur --------- Co-authored-by: Simon H <5968653+dummdidumm@users.noreply.github.com> --- .changeset/large-schools-smell.md | 5 +++++ .../98-reference/.generated/compile-warnings.md | 2 +- .../svelte/messages/compile-warnings/a11y.md | 2 +- .../2-analyze/visitors/shared/a11y/index.js | 16 ++++++++++++++-- .../input.svelte | 4 ++++ 5 files changed, 25 insertions(+), 4 deletions(-) create mode 100644 .changeset/large-schools-smell.md diff --git a/.changeset/large-schools-smell.md b/.changeset/large-schools-smell.md new file mode 100644 index 0000000000..61ef571f02 --- /dev/null +++ b/.changeset/large-schools-smell.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: accept `onfocusin`/`onfocusout` in `a11y_mouse_events_have_key_events` diff --git a/documentation/docs/98-reference/.generated/compile-warnings.md b/documentation/docs/98-reference/.generated/compile-warnings.md index f372a010a5..237ec105f6 100644 --- a/documentation/docs/98-reference/.generated/compile-warnings.md +++ b/documentation/docs/98-reference/.generated/compile-warnings.md @@ -360,7 +360,7 @@ Enforce that heading elements (`h1`, `h2`, etc.) and anchors have content and th '%event%' event must be accompanied by '%accompanied_by%' event ``` -Enforce that `onmouseover` and `onmouseout` are accompanied by `onfocus` and `onblur`, respectively. This helps to ensure that any functionality triggered by these mouse events is also accessible to keyboard users. +Enforce that `onmouseover` and `onmouseout` are accompanied by `onfocus` (or `onfocusin`) and `onblur` (or `onfocusout`), respectively. This helps to ensure that any functionality triggered by these mouse events is also accessible to keyboard users. ```svelte diff --git a/packages/svelte/messages/compile-warnings/a11y.md b/packages/svelte/messages/compile-warnings/a11y.md index 1f48ef0dd5..915032bb93 100644 --- a/packages/svelte/messages/compile-warnings/a11y.md +++ b/packages/svelte/messages/compile-warnings/a11y.md @@ -300,7 +300,7 @@ Enforce that heading elements (`h1`, `h2`, etc.) and anchors have content and th > '%event%' event must be accompanied by '%accompanied_by%' event -Enforce that `onmouseover` and `onmouseout` are accompanied by `onfocus` and `onblur`, respectively. This helps to ensure that any functionality triggered by these mouse events is also accessible to keyboard users. +Enforce that `onmouseover` and `onmouseout` are accompanied by `onfocus` (or `onfocusin`) and `onblur` (or `onfocusout`), respectively. This helps to ensure that any functionality triggered by these mouse events is also accessible to keyboard users. ```svelte diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/a11y/index.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/a11y/index.js index a6e801cf5a..e09ab018a8 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/a11y/index.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/a11y/index.js @@ -373,11 +373,23 @@ export function check_element(node, context) { } } - if (!has_spread && handlers.has('mouseover') && !handlers.has('focus')) { + // `focus`/`blur` don't bubble, so an element that isn't focusable itself can only be notified + // about focus changes inside it through `focusin`/`focusout` — accept those as well + if ( + !has_spread && + handlers.has('mouseover') && + !handlers.has('focus') && + !handlers.has('focusin') + ) { w.a11y_mouse_events_have_key_events(node, 'mouseover', 'focus'); } - if (!has_spread && handlers.has('mouseout') && !handlers.has('blur')) { + if ( + !has_spread && + handlers.has('mouseout') && + !handlers.has('blur') && + !handlers.has('focusout') + ) { w.a11y_mouse_events_have_key_events(node, 'mouseout', 'blur'); } diff --git a/packages/svelte/tests/validator/samples/a11y-mouse-events-have-key-events/input.svelte b/packages/svelte/tests/validator/samples/a11y-mouse-events-have-key-events/input.svelte index f9fe4f15c1..85e9384c98 100644 --- a/packages/svelte/tests/validator/samples/a11y-mouse-events-have-key-events/input.svelte +++ b/packages/svelte/tests/validator/samples/a11y-mouse-events-have-key-events/input.svelte @@ -17,3 +17,7 @@
    {}} onblur={() => {}}>
    {}} {...otherProps}>
    + +
    {}} onfocusin={() => {}}>
    + +
    {}} onfocusout={() => {}}>
    From f2648b353778493494c6dd3a7d020b1f08eb5def Mon Sep 17 00:00:00 2001 From: Nic Polumeyv Date: Mon, 24 Aug 2026 13:01:48 -0400 Subject: [PATCH 178/222] fix: end a restored reaction context at the end of its synchronous segment (#18694) When an async expression resumes after a pickled `await`, the thunk returned by `save()` in `reactivity/async.js` calls `restore()` to re-arm `active_reaction` for the rest of the expression, then disarms it with `queue_micro_task(unset_context)`. Any microtask already queued before that one runs inside the restored context. If it writes to a source, `set()` throws `state_unsafe_mutation` in production, since the guard is not dev-only. #18453 introduced the queued disarm and noted this case in review as unavoidable. SvelteKit hits it in practice: its fetch continuations write to internal `$state` (sveltejs/kit#16914), and a user's `$derived((await q()).length)` resuming in the same tick makes that write throw and drops the update signal. The context restored by a `save` thunk now ends with the synchronous segment it was restored in. A `restored` flag is set by the thunk and consumed on entry to `save` and `track_reactivity_loss`, so every suspension ends it; once an expression contains a pickled await, the analysis pickles every later await in it too (`has_pickled_await` on `ExpressionMetadata`), so a trailing await compiles to `$.save` rather than a bare `await`. At the end of the body, `async_thunk` in `3-transform/client/utils.js` wraps the return expression in `$.unsave(...)` when the metadata has a pickled await. If the body throws instead, the context is unset by `async_derived`'s existing `finally`, as before. The queued microtask in `save` is removed. Output is unchanged for expressions that pickle nothing (`$derived(await a)` compiles byte for byte the same). Expressions with a pickled await gain one `$.unsave(` call per body, and their trailing await becomes a `$.save`, 4 to 6 bytes gzipped in the added tests. At runtime a boolean write replaces a queued microtask per resume. `bench:compare` shows no difference outside run-to-run noise. Two runtime tests reproduce the throw without any library involved, one in dev and one with the prod `await` shape, and fail on `main`. --------- Co-authored-by: Simon Holthausen --- .changeset/quiet-context-segment.md | 5 + .../src/compiler/phases/2-analyze/index.js | 4 +- .../2-analyze/visitors/AwaitExpression.js | 14 ++- .../2-analyze/visitors/CallExpression.js | 2 +- .../phases/3-transform/client/utils.js | 43 +++++-- .../3-transform/client/visitors/AwaitBlock.js | 12 +- .../3-transform/client/visitors/ConstTag.js | 4 +- .../client/visitors/DeclarationTag.js | 3 +- .../3-transform/client/visitors/EachBlock.js | 5 +- .../3-transform/client/visitors/HtmlTag.js | 3 +- .../3-transform/client/visitors/IfBlock.js | 3 +- .../3-transform/client/visitors/KeyBlock.js | 3 +- .../client/visitors/SvelteElement.js | 3 +- .../client/visitors/VariableDeclaration.js | 15 ++- .../client/visitors/shared/utils.js | 10 +- packages/svelte/src/compiler/phases/nodes.js | 4 + .../svelte/src/compiler/phases/types.d.ts | 2 +- packages/svelte/src/internal/client/index.js | 1 + .../src/internal/client/reactivity/async.js | 26 ++++- .../async-save-await-block-pending/_config.js | 20 ++++ .../main.svelte | 14 +++ .../_config.js | 22 ++++ .../main.svelte | 20 ++++ .../_config.js | 21 ++++ .../main.svelte | 21 ++++ .../async-save-throwing-expression/_config.js | 20 ++++ .../main.svelte | 26 +++++ .../_expected/client/index.svelte.js | 14 ++- .../_expected/client/index.svelte.js | 110 +++++++++++------- .../_expected/client/index.svelte.js | 30 ++++- 30 files changed, 393 insertions(+), 87 deletions(-) create mode 100644 .changeset/quiet-context-segment.md create mode 100644 packages/svelte/tests/runtime-runes/samples/async-save-await-block-pending/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-save-await-block-pending/main.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/async-save-foreign-microtask-mutation-prod/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-save-foreign-microtask-mutation-prod/main.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/async-save-foreign-microtask-mutation/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-save-foreign-microtask-mutation/main.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/async-save-throwing-expression/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-save-throwing-expression/main.svelte diff --git a/.changeset/quiet-context-segment.md b/.changeset/quiet-context-segment.md new file mode 100644 index 0000000000..7af36f8ed3 --- /dev/null +++ b/.changeset/quiet-context-segment.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: end a restored reaction context at the end of its synchronous segment diff --git a/packages/svelte/src/compiler/phases/2-analyze/index.js b/packages/svelte/src/compiler/phases/2-analyze/index.js index 45f703901d..f6fb14a870 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/index.js +++ b/packages/svelte/src/compiler/phases/2-analyze/index.js @@ -285,7 +285,7 @@ export function analyze_module(source, options) { runes: true, immutable: true, tracing: false, - async_deriveds: new Set(), + async_deriveds: new Map(), comments, classes: new Map(), pickled_awaits: new Set() @@ -557,7 +557,7 @@ export function analyze_component(root, source, options) { source, snippet_renderers: new Map(), snippets: new Set(), - async_deriveds: new Set(), + async_deriveds: new Map(), pickled_awaits: new Set(), instance_body: { sync: [], diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/AwaitExpression.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/AwaitExpression.js index 5a54d62471..c29f469ad0 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/AwaitExpression.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/AwaitExpression.js @@ -1,6 +1,7 @@ /** @import { AwaitExpression, Expression, SpreadElement, Property } from 'estree' */ /** @import { Context } from '../types' */ /** @import { AST } from '#compiler' */ +/** @import { ExpressionMetadata } from '../../nodes.js' */ import * as e from '../../../errors.js'; /** @@ -10,15 +11,20 @@ import * as e from '../../../errors.js'; export function AwaitExpression(node, context) { const tla = context.state.ast_type === 'instance' && context.state.function_depth === 1; - // preserve context for awaits that precede other expressions in template or `$derived(...)` if ( is_reactive_expression( context.path, context.state.derived_function_depth === context.state.function_depth - ) && - !is_last_evaluated_expression(context.path, node) + ) ) { - context.state.analysis.pickled_awaits.add(node); + const expression = /** @type {ExpressionMetadata} */ (context.state.expression); + + // preserve context for awaits that precede other expressions in template or `$derived(...)`, + // and for any await that follows one, so the restored context ends at the next suspension + if (expression.has_pickled_await || !is_last_evaluated_expression(context.path, node)) { + context.state.analysis.pickled_awaits.add(node); + expression.has_pickled_await = true; + } } let suspend = tla; diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/CallExpression.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/CallExpression.js index d3524af4db..312a93e5fd 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/CallExpression.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/CallExpression.js @@ -253,7 +253,7 @@ export function CallExpression(node, context) { }); if (expression.has_await) { - context.state.analysis.async_deriveds.add(node); + context.state.analysis.async_deriveds.set(node, expression); } // Tell surrounding declaration tag about metadata for correct calculation of blockers etc diff --git a/packages/svelte/src/compiler/phases/3-transform/client/utils.js b/packages/svelte/src/compiler/phases/3-transform/client/utils.js index d52b438d92..9cdfa5cae1 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/utils.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/utils.js @@ -2,6 +2,7 @@ /** @import { Binding } from '#compiler' */ /** @import { ClientTransformState, ComponentClientTransformState } from './types.js' */ /** @import { Analysis } from '../../types.js' */ +/** @import { ExpressionMetadata } from '../../nodes.js' */ /** @import { Scope } from '../../scope.js' */ import * as b from '#compiler/builders'; import { is_simple_expression, save } from '../../../utils/ast.js'; @@ -164,20 +165,46 @@ export function should_proxy(node, scope) { return true; } +/** + * An async thunk. If an `await` inside restores the reaction context via `$.save`, + * the body exits through `$.unsave` so the context cannot leak into foreign microtasks + * that run before the returned promise settles + * @param {Expression | BlockStatement} body + * @param {ExpressionMetadata} metadata + */ +export function async_thunk(body, metadata) { + if (!metadata.has_pickled_await) { + return b.arrow([], body, true); + } + + const block = body.type === 'BlockStatement' ? body : b.block([b.return(body)]); + + return b.arrow( + [], + b.block([ + { + type: 'TryStatement', + block, + handler: null, + finalizer: b.block([b.stmt(b.call('$.unsave'))]) + } + ]), + true + ); +} + /** * Svelte legacy mode should use safe equals in most places, runes mode shouldn't * @param {ComponentClientTransformState} state * @param {Expression | BlockStatement} expression - * @param {boolean} [async] + * @param {ExpressionMetadata} [metadata] */ -export function create_derived(state, expression, async = false) { - const thunk = b.thunk(expression, async); - - if (async) { - return save(b.call('$.async_derived', thunk)); - } else { - return b.call(state.analysis.runes ? '$.derived' : '$.derived_safe_equal', thunk); +export function create_derived(state, expression, metadata) { + if (metadata?.has_await) { + return save(b.call('$.async_derived', async_thunk(expression, metadata))); } + + return b.call(state.analysis.runes ? '$.derived' : '$.derived_safe_equal', b.thunk(expression)); } /** diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/AwaitBlock.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/AwaitBlock.js index b8def178f2..986cd789d9 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/AwaitBlock.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/AwaitBlock.js @@ -1,9 +1,9 @@ /** @import { BlockStatement, Pattern, Statement } from 'estree' */ /** @import { AST } from '#compiler' */ /** @import { ComponentClientTransformState, ComponentContext } from '../types' */ -import { extract_identifiers, is_expression_async } from '../../../../utils/ast.js'; +import { extract_identifiers } from '../../../../utils/ast.js'; import * as b from '#compiler/builders'; -import { create_derived } from '../utils.js'; +import { async_thunk, create_derived } from '../utils.js'; import { get_value } from './shared/declarations.js'; import { build_expression, add_svelte_meta } from './shared/utils.js'; @@ -15,10 +15,10 @@ export function AwaitBlock(node, context) { context.state.template.push_comment(); // Visit {#await } first to ensure that scopes are in the correct order - const expression = b.thunk( - build_expression(context, node.expression, node.metadata.expression), - node.metadata.expression.has_await - ); + const input = build_expression(context, node.expression, node.metadata.expression); + const expression = node.metadata.expression.has_await + ? async_thunk(input, node.metadata.expression) + : b.thunk(input); let then_block; let catch_block; diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/ConstTag.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/ConstTag.js index 63c84a507c..b3b2358e4c 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/ConstTag.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/ConstTag.js @@ -19,7 +19,7 @@ export function ConstTag(node, context) { if (declaration.id.type === 'Identifier') { const init = build_expression(context, declaration.init, node.metadata.expression); - let expression = create_derived(context.state, init, node.metadata.expression.has_await); + let expression = create_derived(context.state, init, node.metadata.expression); if (dev) { expression = b.call('$.tag', expression, b.literal(declaration.id.name)); @@ -69,7 +69,7 @@ export function ConstTag(node, context) { b.return(b.object(identifiers.map((node) => b.prop('init', node, node)))) ]); - let expression = create_derived(context.state, block, node.metadata.expression.has_await); + let expression = create_derived(context.state, block, node.metadata.expression); if (dev) { expression = b.call('$.tag', expression, b.literal('[@const]')); 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 abec5e828f..3f78b53c90 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 @@ -3,6 +3,7 @@ /** @import { ComponentContext } from '../types' */ import { extract_identifiers, has_await_expression } from '../../../../utils/ast.js'; import * as b from '#compiler/builders'; +import { async_thunk } from '../utils.js'; import { add_state_transformers } from './shared/declarations.js'; /** @@ -85,5 +86,5 @@ export function add_async_declaration(context, metadata, ids, assignments, kind metadata.expression.has_await || assignments.some((assignment) => has_await_expression(assignment)); const body = assignments.length === 1 ? assignments[0].expression : b.block(assignments); - run.thunks.push(b.thunk(body, has_await)); + run.thunks.push(has_await ? async_thunk(body, metadata.expression) : b.thunk(body)); } diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/EachBlock.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/EachBlock.js index b33eddd461..e0bd8d797a 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/EachBlock.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/EachBlock.js @@ -12,6 +12,7 @@ import { import { dev } from '../../../../state.js'; import { extract_paths, object } from '../../../../utils/ast.js'; import * as b from '#compiler/builders'; +import { async_thunk } from '../utils.js'; import { get_value } from './shared/declarations.js'; import { build_expression, add_svelte_meta } from './shared/utils.js'; @@ -313,7 +314,9 @@ export function EachBlock(node, context) { const has_await = node.metadata.expression.has_await; - const get_collection = b.thunk(collection, has_await); + const get_collection = has_await + ? async_thunk(collection, node.metadata.expression) + : b.thunk(collection); const thunk = has_await ? b.thunk(b.call('$.get', b.id('$$collection'))) : get_collection; const render_args = [b.id('$$anchor'), item]; diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/HtmlTag.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/HtmlTag.js index 6c8b7c0354..910f0a1239 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/HtmlTag.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/HtmlTag.js @@ -2,6 +2,7 @@ /** @import { ComponentContext } from '../types' */ import { is_ignored } from '../../../../state.js'; import * as b from '#compiler/builders'; +import { async_thunk } from '../utils.js'; import { build_expression } from './shared/utils.js'; /** @@ -46,7 +47,7 @@ export function HtmlTag(node, context) { '$.async', context.state.node, node.metadata.expression.blockers(), - has_await ? b.array([b.thunk(expression, true)]) : b.void0, + has_await ? b.array([async_thunk(expression, node.metadata.expression)]) : b.void0, b.arrow( has_await ? [context.state.node, b.id('$$html')] : [context.state.node], b.block([statement]) diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/IfBlock.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/IfBlock.js index b0cd51ce3f..5dcb3991c2 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/IfBlock.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/IfBlock.js @@ -2,6 +2,7 @@ /** @import { AST } from '#compiler' */ /** @import { ComponentContext } from '../types' */ import * as b from '#compiler/builders'; +import { async_thunk } from '../utils.js'; import { build_expression, add_svelte_meta } from './shared/utils.js'; /** @@ -117,7 +118,7 @@ export function IfBlock(node, context) { '$.async', context.state.node, node.metadata.expression.blockers(), - has_await ? b.array([b.thunk(expression, true)]) : b.void0, + has_await ? b.array([async_thunk(expression, node.metadata.expression)]) : b.void0, b.arrow( has_await ? [context.state.node, b.id('$$condition')] : [context.state.node], b.block(statements) diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/KeyBlock.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/KeyBlock.js index 143a4e8edd..1831e7abab 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/KeyBlock.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/KeyBlock.js @@ -2,6 +2,7 @@ /** @import { AST } from '#compiler' */ /** @import { ComponentContext } from '../types' */ import * as b from '#compiler/builders'; +import { async_thunk } from '../utils.js'; import { build_expression, add_svelte_meta } from './shared/utils.js'; /** @@ -31,7 +32,7 @@ export function KeyBlock(node, context) { '$.async', context.state.node, node.metadata.expression.blockers(), - has_await ? b.array([b.thunk(expression, true)]) : b.void0, + has_await ? b.array([async_thunk(expression, node.metadata.expression)]) : b.void0, b.arrow( has_await ? [context.state.node, b.id('$$key')] : [context.state.node], b.block([statement]) diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/SvelteElement.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/SvelteElement.js index 10024298fa..909b4e0b71 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/SvelteElement.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/SvelteElement.js @@ -4,6 +4,7 @@ import { dev, locator } from '../../../../state.js'; import { is_text_attribute } from '../../../../utils/ast.js'; import * as b from '#compiler/builders'; +import { async_thunk } from '../utils.js'; import { determine_namespace_for_children } from '../../utils.js'; import { build_attribute_value, @@ -147,7 +148,7 @@ export function SvelteElement(node, context) { '$.async', context.state.node, node.metadata.expression.blockers(), - has_await ? b.array([b.thunk(expression, true)]) : b.void0, + has_await ? b.array([async_thunk(expression, node.metadata.expression)]) : b.void0, b.arrow( has_await ? [context.state.node, b.id('$$tag')] : [context.state.node], b.block(statements) diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/VariableDeclaration.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/VariableDeclaration.js index b9f4690179..246feaccf6 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/VariableDeclaration.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/VariableDeclaration.js @@ -6,7 +6,13 @@ import { extract_paths, save } from '../../../../utils/ast.js'; import * as b from '#compiler/builders'; import * as assert from '../../../../utils/assert.js'; import { get_rune } from '../../../scope.js'; -import { get_prop_source, is_prop_source, is_state_source, should_proxy } from '../utils.js'; +import { + async_thunk, + get_prop_source, + is_prop_source, + is_state_source, + should_proxy +} from '../utils.js'; import { get_value } from './shared/declarations.js'; /** @@ -200,9 +206,10 @@ export function VariableDeclaration(node, context) { } if (rune === '$derived' || rune === '$derived.by') { - const is_async = context.state.analysis.async_deriveds.has( + const metadata = context.state.analysis.async_deriveds.get( /** @type {CallExpression} */ (init) ); + const is_async = metadata !== undefined; if (declarator.id.type === 'Identifier') { let expression = /** @type {Expression} */ (context.visit(value)); @@ -213,7 +220,7 @@ export function VariableDeclaration(node, context) { /** @type {Expression} */ let call = b.call( '$.async_derived', - b.thunk(expression, true), + async_thunk(expression, metadata), dev && b.literal(declarator.id.name), location ? b.literal(location) : undefined ); @@ -246,7 +253,7 @@ export function VariableDeclaration(node, context) { call = b.call( '$.async_derived', - b.thunk(expression, true), + async_thunk(expression, metadata), dev && b.literal( `[$derived ${declarator.id.type === 'ArrayPattern' ? 'iterable' : 'object'}]` diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js index c305d53ea5..64ae573984 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js @@ -8,7 +8,7 @@ import { sanitize_template_string } from '../../../../../utils/sanitize_template import { regex_is_valid_identifier } from '../../../../patterns.js'; import is_reference from 'is-reference'; import { dev, is_ignored, locator, component_name } from '../../../../../state.js'; -import { build_getter, is_state_source } from '../../utils.js'; +import { async_thunk, build_getter, is_state_source } from '../../utils.js'; import { ExpressionMetadata } from '../../../../nodes.js'; /** @@ -16,10 +16,10 @@ import { ExpressionMetadata } from '../../../../nodes.js'; * from templates and replacing them with `$0`, `$1` etc */ export class Memoizer { - /** @type {Array<{ id: Identifier, expression: Expression }>} */ + /** @type {Array<{ id: Identifier, expression: Expression, metadata: ExpressionMetadata }>} */ #sync = []; - /** @type {Array<{ id: Identifier, expression: Expression }>} */ + /** @type {Array<{ id: Identifier, expression: Expression, metadata: ExpressionMetadata }>} */ #async = []; /** @type {Set} */ @@ -43,7 +43,7 @@ export class Memoizer { const id = b.id('#'); // filled in later - (metadata.has_await ? this.#async : this.#sync).push({ id, expression }); + (metadata.has_await ? this.#async : this.#sync).push({ id, expression, metadata }); return id; } @@ -84,7 +84,7 @@ export class Memoizer { if (this.#async.length === 0) return; // use `b.arrow` rather than `b.thunk` so that deferred async/template effects // always read live bindings rather than a possibly stale snapshot. - return b.array(this.#async.map((memo) => b.arrow([], memo.expression, true))); + return b.array(this.#async.map((memo) => async_thunk(memo.expression, memo.metadata))); } sync_values() { diff --git a/packages/svelte/src/compiler/phases/nodes.js b/packages/svelte/src/compiler/phases/nodes.js index da7e73015b..fea447a7c5 100644 --- a/packages/svelte/src/compiler/phases/nodes.js +++ b/packages/svelte/src/compiler/phases/nodes.js @@ -77,6 +77,9 @@ export class ExpressionMetadata { /** True if the expression contains `await` */ has_await = false; + /** True if an `await` restores the reaction context afterwards, so the thunk must end it */ + has_pickled_await = false; + /** True if the expression includes a member expression */ has_member_expression = false; @@ -142,6 +145,7 @@ export class ExpressionMetadata { this.has_state ||= source.has_state; this.has_call ||= source.has_call; this.has_await ||= source.has_await; + this.has_pickled_await ||= source.has_pickled_await; this.has_member_expression ||= source.has_member_expression; this.has_assignment ||= source.has_assignment; this.#blockers = null; // so that blockers are recalculated diff --git a/packages/svelte/src/compiler/phases/types.d.ts b/packages/svelte/src/compiler/phases/types.d.ts index a1a85ce145..38fa80ac4b 100644 --- a/packages/svelte/src/compiler/phases/types.d.ts +++ b/packages/svelte/src/compiler/phases/types.d.ts @@ -63,7 +63,7 @@ export interface Analysis { accessors: boolean; /** A set of deriveds that contain `await` expressions */ - async_deriveds: Set; + async_deriveds: Map; /** Awaits needing context preservation */ pickled_awaits: Set; } diff --git a/packages/svelte/src/internal/client/index.js b/packages/svelte/src/internal/client/index.js index fa6f9dda39..6cdf81c155 100644 --- a/packages/svelte/src/internal/client/index.js +++ b/packages/svelte/src/internal/client/index.js @@ -103,6 +103,7 @@ export { run, save, track_reactivity_loss, + unsave, run_after_blockers, wait } from './reactivity/async.js'; diff --git a/packages/svelte/src/internal/client/reactivity/async.js b/packages/svelte/src/internal/client/reactivity/async.js index 2e7df92dc0..a75f97a54f 100644 --- a/packages/svelte/src/internal/client/reactivity/async.js +++ b/packages/svelte/src/internal/client/reactivity/async.js @@ -8,7 +8,6 @@ import { set_component_context, set_dev_stack } from '../context.js'; -import { Boundary } from '../dom/blocks/boundary.js'; import { invoke_error_boundary } from '../error-handling.js'; import { active_effect, @@ -25,7 +24,6 @@ import { set_reactivity_loss_tracker } from './deriveds.js'; import { aborted } from './effects.js'; -import { queue_micro_task } from '../dom/task.js'; /** * @param {Blocker[]} blockers @@ -156,6 +154,9 @@ export function capture() { }; } +/** `true` between a `save` thunk restoring a context and the end of that synchronous segment */ +var restored = false; + /** * Wraps an `await` expression in such a way that the effect context that was * active before the expression evaluated can be reapplied afterwards — @@ -166,15 +167,32 @@ export function capture() { */ export async function save(promise) { var restore = capture(); + // the context restored by an earlier `save` in this expression must not + // outlive the synchronous segment that is about to end at this `await` + unsave(); var value = await promise; return () => { restore(); - queue_micro_task(unset_context); + restored = true; return value; }; } +/** + * Unset the context if a `save` thunk restored it in the current synchronous segment, + * so that a foreign microtask can never run inside a restored reaction context. + * Called at every suspension point, and at the end of async expression bodies — + * `async () => (await $.save(a))().b` becomes `async () => { try { return (await $.save(a))().b; } finally { $.unsave(); } }` + * @template T + * @param {T} [value] + * @returns {T} + */ +export function unsave(value) { + if (restored) unset_context(); + return /** @type {T} */ (value); +} + /** * Reset `current_async_effect` after the `promise` resolves, so * that we can emit `await_reactivity_loss` warnings @@ -183,6 +201,7 @@ export async function save(promise) { * @returns {Promise<() => T>} */ export async function track_reactivity_loss(promise) { + unsave(); var previous_reactivity_loss_tracker = reactivity_loss_tracker; // Ensure that unrelated reads after an async operation is kicked off don't cause false positives queueMicrotask(() => { @@ -269,6 +288,7 @@ export async function* for_await_track_reactivity_loss(iterable) { } export function unset_context(deactivate_batch = true) { + restored = false; set_active_effect(null); set_active_reaction(null); set_component_context(null); diff --git a/packages/svelte/tests/runtime-runes/samples/async-save-await-block-pending/_config.js b/packages/svelte/tests/runtime-runes/samples/async-save-await-block-pending/_config.js new file mode 100644 index 0000000000..9608e294e7 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-save-await-block-pending/_config.js @@ -0,0 +1,20 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + mode: ['client'], + compileOptions: { + dev: true + }, + + async test({ assert, target, errors }) { + await new Promise((resolve) => setTimeout(resolve, 20)); + await tick(); + + assert.deepEqual( + errors.filter((error) => error.includes('state_unsafe_mutation')), + [] + ); + assert.htmlEqual(target.innerHTML, '

    pending

    1

    '); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-save-await-block-pending/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-save-await-block-pending/main.svelte new file mode 100644 index 0000000000..3545c69451 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-save-await-block-pending/main.svelte @@ -0,0 +1,14 @@ + + +{#await (await input).pending} +

    pending

    +{/await} + +

    {foreign}

    diff --git a/packages/svelte/tests/runtime-runes/samples/async-save-foreign-microtask-mutation-prod/_config.js b/packages/svelte/tests/runtime-runes/samples/async-save-foreign-microtask-mutation-prod/_config.js new file mode 100644 index 0000000000..050ee60e09 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-save-foreign-microtask-mutation-prod/_config.js @@ -0,0 +1,22 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + mode: ['client'], + + compileOptions: { + dev: false + }, + + async test({ assert, target, errors }) { + await tick(); + await tick(); + await tick(); + + assert.deepEqual( + errors.filter((error) => error.includes('state_unsafe_mutation')), + [] + ); + assert.htmlEqual(target.innerHTML, '

    4 1

    '); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-save-foreign-microtask-mutation-prod/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-save-foreign-microtask-mutation-prod/main.svelte new file mode 100644 index 0000000000..ef69eb1154 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-save-foreign-microtask-mutation-prod/main.svelte @@ -0,0 +1,20 @@ + + +

    {total} {foreign}

    diff --git a/packages/svelte/tests/runtime-runes/samples/async-save-foreign-microtask-mutation/_config.js b/packages/svelte/tests/runtime-runes/samples/async-save-foreign-microtask-mutation/_config.js new file mode 100644 index 0000000000..371cca3ff3 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-save-foreign-microtask-mutation/_config.js @@ -0,0 +1,21 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + mode: ['client'], + + compileOptions: { + dev: true + }, + + async test({ assert, target, errors }) { + await tick(); + await tick(); + + assert.deepEqual( + errors.filter((error) => error.includes('state_unsafe_mutation')), + [] + ); + assert.htmlEqual(target.innerHTML, '

    3 1

    '); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-save-foreign-microtask-mutation/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-save-foreign-microtask-mutation/main.svelte new file mode 100644 index 0000000000..c85206e7f1 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-save-foreign-microtask-mutation/main.svelte @@ -0,0 +1,21 @@ + + +

    {length} {foreign}

    diff --git a/packages/svelte/tests/runtime-runes/samples/async-save-throwing-expression/_config.js b/packages/svelte/tests/runtime-runes/samples/async-save-throwing-expression/_config.js new file mode 100644 index 0000000000..5999daa5d2 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-save-throwing-expression/_config.js @@ -0,0 +1,20 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + mode: ['client'], + compileOptions: { + dev: true + }, + + async test({ assert, target, errors }) { + await tick(); + await tick(); + + assert.deepEqual( + errors.filter((error) => error.includes('state_unsafe_mutation')), + [] + ); + assert.htmlEqual(target.innerHTML, '

    failed

    1

    '); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-save-throwing-expression/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-save-throwing-expression/main.svelte new file mode 100644 index 0000000000..e94bf86568 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-save-throwing-expression/main.svelte @@ -0,0 +1,26 @@ + + + {}}> + {#snippet failed()} +

    failed

    + {/snippet} + +

    {(await input).value}

    +
    + +

    {foreign}

    diff --git a/packages/svelte/tests/snapshot/samples/async-const/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/async-const/_expected/client/index.svelte.js index 5c1d27ae54..b3e9e87aca 100644 --- a/packages/svelte/tests/snapshot/samples/async-const/_expected/client/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/async-const/_expected/client/index.svelte.js @@ -14,7 +14,19 @@ export default function Async_const($$anchor) { let b; var promises = $.run([ - async () => a = (await $.save($.async_derived(async () => (await $.save(1))())))(), + async () => { + try { + return a = (await $.save($.async_derived(async () => { + try { + return (await $.save(1))(); + } finally { + $.unsave(); + } + })))(); + } finally { + $.unsave(); + } + }, () => b = $.derived(() => $.get(a) + 1) ]); diff --git a/packages/svelte/tests/snapshot/samples/async-if-chain/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/async-if-chain/_expected/client/index.svelte.js index 8d21c2ceef..ca99d1c0c5 100644 --- a/packages/svelte/tests/snapshot/samples/async-if-chain/_expected/client/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/async-if-chain/_expected/client/index.svelte.js @@ -90,52 +90,78 @@ export default function Async_if_chain($$anchor) { var node_3 = $.sibling(node_1, 2); - $.async(node_3, [$$promises[0]], [async () => (await $.save(foo))() > 10], (node_3, $$condition) => { - var consequent_5 = ($$anchor) => { - var text_7 = $.text('foo'); - - $.append($$anchor, text_7); - }; - - var consequent_6 = ($$anchor) => { - var text_8 = $.text('bar'); - - $.append($$anchor, text_8); - }; - - var alternate_4 = ($$anchor) => { - var fragment_2 = $.comment(); - var node_4 = $.first_child(fragment_2); - - $.async(node_4, [$$promises[0]], [async () => (await $.save(foo))() > 5], (node_4, $$condition) => { - var consequent_7 = ($$anchor) => { - var text_9 = $.text('baz'); - - $.append($$anchor, text_9); - }; - - var alternate_3 = ($$anchor) => { - var text_10 = $.text('else'); - - $.append($$anchor, text_10); - }; - - $.if( + $.async( + node_3, + [$$promises[0]], + [ + async () => { + try { + return (await $.save(foo))() > 10; + } finally { + $.unsave(); + } + } + ], + (node_3, $$condition) => { + var consequent_5 = ($$anchor) => { + var text_7 = $.text('foo'); + + $.append($$anchor, text_7); + }; + + var consequent_6 = ($$anchor) => { + var text_8 = $.text('bar'); + + $.append($$anchor, text_8); + }; + + var alternate_4 = ($$anchor) => { + var fragment_2 = $.comment(); + var node_4 = $.first_child(fragment_2); + + $.async( node_4, - ($$render) => { - if ($.get($$condition)) $$render(consequent_7); else $$render(alternate_3, -1); - }, - true + [$$promises[0]], + [ + async () => { + try { + return (await $.save(foo))() > 5; + } finally { + $.unsave(); + } + } + ], + (node_4, $$condition) => { + var consequent_7 = ($$anchor) => { + var text_9 = $.text('baz'); + + $.append($$anchor, text_9); + }; + + var alternate_3 = ($$anchor) => { + var text_10 = $.text('else'); + + $.append($$anchor, text_10); + }; + + $.if( + node_4, + ($$render) => { + if ($.get($$condition)) $$render(consequent_7); else $$render(alternate_3, -1); + }, + true + ); + } ); - }); - $.append($$anchor, fragment_2); - }; + $.append($$anchor, fragment_2); + }; - $.if(node_3, ($$render) => { - if ($.get($$condition)) $$render(consequent_5); else if (bar) $$render(consequent_6, 1); else $$render(alternate_4, -1); - }); - }); + $.if(node_3, ($$render) => { + if ($.get($$condition)) $$render(consequent_5); else if (bar) $$render(consequent_6, 1); else $$render(alternate_4, -1); + }); + } + ); var node_5 = $.sibling(node_3, 2); diff --git a/packages/svelte/tests/snapshot/samples/async-in-derived/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/async-in-derived/_expected/client/index.svelte.js index 79b8ad0040..327dc4918c 100644 --- a/packages/svelte/tests/snapshot/samples/async-in-derived/_expected/client/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/async-in-derived/_expected/client/index.svelte.js @@ -34,8 +34,34 @@ export default function Async_in_derived($$anchor, $$props) { let no2; var promises = $.run([ - async () => yes1 = (await $.save($.async_derived(async () => (await $.save(1))())))(), - async () => yes2 = (await $.save($.async_derived(async () => foo((await $.save(1))()))))(), + async () => { + try { + return yes1 = (await $.save($.async_derived(async () => { + try { + return (await $.save(1))(); + } finally { + $.unsave(); + } + })))(); + } finally { + $.unsave(); + } + }, + + async () => { + try { + return yes2 = (await $.save($.async_derived(async () => { + try { + return foo((await $.save(1))()); + } finally { + $.unsave(); + } + })))(); + } finally { + $.unsave(); + } + }, + () => no1 = $.derived(() => (async () => { return await 1; })()), From 6f5dd04a0040db09c3171afd9d5e39071db6069d Mon Sep 17 00:00:00 2001 From: Ashok Kaushik <56879395+kaushikashok45@users.noreply.github.com> Date: Tue, 25 Aug 2026 01:45:27 +0530 Subject: [PATCH 179/222] fix: Fixes error thrown on use of select with no options (#18449) Fixes #18445. --- .changeset/every-rabbits-rule.md | 5 +++++ .../phases/3-transform/client/visitors/shared/fragment.js | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 .changeset/every-rabbits-rule.md diff --git a/.changeset/every-rabbits-rule.md b/.changeset/every-rabbits-rule.md new file mode 100644 index 0000000000..e78df191a5 --- /dev/null +++ b/.changeset/every-rabbits-rule.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: prevent malformed AST output for ` + + + From c894afd8c73e4f08a377909f29aa58177a256277 Mon Sep 17 00:00:00 2001 From: Jawad Ali Date: Tue, 25 Aug 2026 13:37:34 +0500 Subject: [PATCH 185/222] fix: sync SvelteURL port when protocol setter clears it (#18705) Per the WHATWG URL spec, assigning a new protocol can clear the URL's port when the current port equals the new scheme's default port. Therefore also `set` the port when the protocol is updated. --- .changeset/svelte-url-protocol-port.md | 5 +++++ packages/svelte/src/reactivity/url.js | 2 ++ packages/svelte/src/reactivity/url.test.ts | 22 ++++++++++++++++++++++ 3 files changed, 29 insertions(+) create mode 100644 .changeset/svelte-url-protocol-port.md diff --git a/.changeset/svelte-url-protocol-port.md b/.changeset/svelte-url-protocol-port.md new file mode 100644 index 0000000000..2c37fce4ab --- /dev/null +++ b/.changeset/svelte-url-protocol-port.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: sync `SvelteURL` port signal when the protocol setter clears the port diff --git a/packages/svelte/src/reactivity/url.js b/packages/svelte/src/reactivity/url.js index 549a20baa7..1e24e774ee 100644 --- a/packages/svelte/src/reactivity/url.js +++ b/packages/svelte/src/reactivity/url.js @@ -163,6 +163,8 @@ export class SvelteURL extends URL { set protocol(value) { super.protocol = value; set(this.#protocol, super.protocol); + // changing the protocol can clear the port when it matches the new scheme's default + set(this.#port, super.port); } get search() { diff --git a/packages/svelte/src/reactivity/url.test.ts b/packages/svelte/src/reactivity/url.test.ts index d698116421..72518d730a 100644 --- a/packages/svelte/src/reactivity/url.test.ts +++ b/packages/svelte/src/reactivity/url.test.ts @@ -240,3 +240,25 @@ test('url.searchParams.forEach re-runs when the search string changes via the UR cleanup(); }); + +test('url.port is updated when the protocol change clears the port', () => { + const url = new SvelteURL('http://example.com:443/'); + const log: any = []; + + const cleanup = effect_root(() => { + render_effect(() => { + log.push(url.port); + }); + }); + + flushSync(() => { + // 443 is the default port for https, so it gets stripped + url.protocol = 'https:'; + }); + + assert.equal(url.port, ''); + assert.equal(url.href, 'https://example.com/'); + assert.deepEqual(log, ['443', '']); + + cleanup(); +}); From 135f1ec004352048454e732202c0bc8332816242 Mon Sep 17 00:00:00 2001 From: Simon H <5968653+dummdidumm@users.noreply.github.com> Date: Tue, 25 Aug 2026 11:24:45 +0200 Subject: [PATCH 186/222] fix: more robust rendering of Svelte custom element slots (#18710) Instead of checking the given nodes at startup and only injecting those into the inner Svelte component constructor, we are now detecting (via the `$$host` property) whether or not the component is rendered as a custom element, and in that case create all slots as real slots right away. Fixes #13638 Fixes #8997 (therefore closes #8999) --- .changeset/open-parents-stare.md | 5 ++++ .../src/internal/client/dom/blocks/slot.js | 19 +++++++++++++ .../$$slot-dynamic-content/_config.js | 8 +++--- .../html-slots/_config.js | 28 +++++++++++++++---- .../no-shadow-dom/_config.js | 3 +- .../no-shadow-dom/main.svelte | 1 + 6 files changed, 53 insertions(+), 11 deletions(-) create mode 100644 .changeset/open-parents-stare.md diff --git a/.changeset/open-parents-stare.md b/.changeset/open-parents-stare.md new file mode 100644 index 0000000000..cb94267ea0 --- /dev/null +++ b/.changeset/open-parents-stare.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: more robust rendering of Svelte custom element slots diff --git a/packages/svelte/src/internal/client/dom/blocks/slot.js b/packages/svelte/src/internal/client/dom/blocks/slot.js index 10b2959582..491f04a5ea 100644 --- a/packages/svelte/src/internal/client/dom/blocks/slot.js +++ b/packages/svelte/src/internal/client/dom/blocks/slot.js @@ -1,4 +1,6 @@ import { hydrate_next, hydrating } from '../hydration.js'; +import { create_element, create_text } from '../operations.js'; +import { append } from '../template.js'; /** * @param {Comment} anchor @@ -12,6 +14,23 @@ export function slot(anchor, $$props, name, slot_props, fallback_fn) { hydrate_next(); } + // Custom element slots are native DOM slots. + // Use the stored reference because the shadow root may be closed. + if ($$props.$$host?.$$shadowRoot) { + const element = create_element('slot'); + if (name !== 'default') element.name = name; + + append(anchor, element); + + if (fallback_fn !== null) { + const fallback_anchor = create_text(); + element.append(fallback_anchor); + fallback_fn(fallback_anchor); + } + + return; + } + var slot_fn = $$props.$$slots?.[name]; // Interop: Can use snippets to fill slots var is_interop = false; diff --git a/packages/svelte/tests/runtime-browser/custom-elements-samples/$$slot-dynamic-content/_config.js b/packages/svelte/tests/runtime-browser/custom-elements-samples/$$slot-dynamic-content/_config.js index 8ade347a79..3fd5a09888 100644 --- a/packages/svelte/tests/runtime-browser/custom-elements-samples/$$slot-dynamic-content/_config.js +++ b/packages/svelte/tests/runtime-browser/custom-elements-samples/$$slot-dynamic-content/_config.js @@ -14,8 +14,8 @@ export default test({ assert.htmlEqual( ce.shadowRoot.innerHTML, ` - -

    named fallback

    + fallback +

    named fallback

    ` ); @@ -23,8 +23,8 @@ export default test({ assert.htmlEqual( ce.shadowRoot.innerHTML, ` - -

    named fallback

    + fallback +

    named fallback

    ` ); } diff --git a/packages/svelte/tests/runtime-browser/custom-elements-samples/html-slots/_config.js b/packages/svelte/tests/runtime-browser/custom-elements-samples/html-slots/_config.js index 287e2987b8..e3e8ce072b 100644 --- a/packages/svelte/tests/runtime-browser/custom-elements-samples/html-slots/_config.js +++ b/packages/svelte/tests/runtime-browser/custom-elements-samples/html-slots/_config.js @@ -3,10 +3,7 @@ const tick = () => Promise.resolve(); export default test({ async test({ assert, target }) { - target.innerHTML = ` - - slotted - `; + target.innerHTML = ''; await tick(); await tick(); @@ -16,7 +13,26 @@ export default test({ const div = el.shadowRoot.children[0]; const [slot0, slot1] = div.children; - assert.equal(slot0.assignedNodes()[1], target.querySelector('strong')); - assert.equal(slot1.innerHTML, 'foo fallback content'); + assert.equal(slot0.localName, 'slot'); + assert.equal(slot0.assignedNodes().length, 0); + assert.equal(slot0.innerHTML, '

    default fallback content

    '); + assert.equal(slot1.localName, 'slot'); + assert.equal(slot1.name, 'foo'); + assert.equal(slot1.assignedNodes().length, 0); + assert.equal(slot1.innerHTML, '

    foo fallback content

    '); + + const default_content = document.createElement('strong'); + default_content.textContent = 'default content'; + el.append(default_content); + + const named_content = document.createElement('strong'); + named_content.slot = 'foo'; + named_content.textContent = 'named content'; + el.append(named_content); + + assert.equal(slot0.assignedNodes().length, 1); + assert.equal(slot0.assignedNodes()[0], default_content); + assert.equal(slot1.assignedNodes().length, 1); + assert.equal(slot1.assignedNodes()[0], named_content); } }); diff --git a/packages/svelte/tests/runtime-browser/custom-elements-samples/no-shadow-dom/_config.js b/packages/svelte/tests/runtime-browser/custom-elements-samples/no-shadow-dom/_config.js index cd8550120a..e83491418a 100644 --- a/packages/svelte/tests/runtime-browser/custom-elements-samples/no-shadow-dom/_config.js +++ b/packages/svelte/tests/runtime-browser/custom-elements-samples/no-shadow-dom/_config.js @@ -3,7 +3,7 @@ const tick = () => Promise.resolve(); export default test({ async test({ assert, target }) { - target.innerHTML = ''; + target.innerHTML = 'slotted'; await tick(); await tick(); @@ -15,5 +15,6 @@ export default test({ assert.equal(el.shadowRoot, null); assert.equal(h1.innerHTML, 'Hello world!'); assert.equal(getComputedStyle(h1).color, 'rgb(255, 0, 0)'); + assert.equal(el.querySelector('slot').innerHTML, ''); } }); diff --git a/packages/svelte/tests/runtime-browser/custom-elements-samples/no-shadow-dom/main.svelte b/packages/svelte/tests/runtime-browser/custom-elements-samples/no-shadow-dom/main.svelte index 36fea3c43e..1c14fb98d8 100644 --- a/packages/svelte/tests/runtime-browser/custom-elements-samples/no-shadow-dom/main.svelte +++ b/packages/svelte/tests/runtime-browser/custom-elements-samples/no-shadow-dom/main.svelte @@ -5,6 +5,7 @@

    Hello {name}!

    +fallback `; + +const typescript_source = `${Array.from( + { length: 1000 }, + (_, i) => `
    Item ${i}
    ` +).join('\n')} +{value as number} +${' \n'.repeat(1000)}`; + +/** + * @param {string} label + * @param {string} source + * @param {number} iterations + */ +function create_parser_benchmark(label, source, iterations) { + return { + label, + fn: async () => { + for (let i = 0; i < iterations; i++) { + parse(source); + } + + return await fastest_test(10, () => { + for (let i = 0; i < iterations; i++) { + parse(source); + } + }); + } + }; +} + +export const parser_benchmarks = [ + create_parser_benchmark('parser_elements', elements_source, 5), + create_parser_benchmark('parser_script_style', script_style_source, 50), + create_parser_benchmark('parser_typescript', typescript_source, 25) +]; diff --git a/benchmarking/compiler-profiling.md b/benchmarking/compiler-profiling.md new file mode 100644 index 0000000000..cc2196958e --- /dev/null +++ b/benchmarking/compiler-profiling.md @@ -0,0 +1,43 @@ +# Compiler CPU profiling + +Run the full test suite with V8 CPU profiling enabled in every Vitest worker: + +```sh +pnpm profile:compiler +``` + +Arguments are passed to `pnpm test`, so a smaller smoke run can target a suite: + +```sh +pnpm profile:compiler snapshot +``` + +Each run is stored under `benchmarking/.profiles/compiler-tests/`. Set +`SVELTE_PROFILE_NAME` to give a run a stable name; existing runs are never overwritten. +`benchmarking/.profiles/compiler-tests/latest.txt` contains the latest successful run name. + +Each run contains: + +- `raw/*.cpuprofile`: the original V8 profile from every Vitest worker +- `flamegraph.speedscope.json`: compiler-active stacks merged for use in + [Speedscope](https://www.speedscope.app/) +- `summary.md`: ranked self/inclusive hotspots and files +- `summary.json`: the same measurements for further analysis +- `manifest.json`: the command, revision, timestamps, and test result + +The merged profile includes downstream callees while a compiler source frame is on the stack. +This captures parser, walker, and printer dependencies without including unrelated test runtime. +V8 reports garbage collection outside the originating stack, so the summary reports process-wide +GC separately rather than attributing it to the compiler. + +The default sampling interval is 5 ms. It can be changed for shorter, focused runs: + +```sh +SVELTE_CPU_PROF_INTERVAL=1000 pnpm profile:compiler snapshot +``` + +Existing raw profiles can be analyzed again without rerunning tests: + +```sh +node benchmarking/analyze-compiler-profile.js benchmarking/.profiles/compiler-tests/ +``` diff --git a/benchmarking/profile-compiler.js b/benchmarking/profile-compiler.js new file mode 100644 index 0000000000..323c2d27ea --- /dev/null +++ b/benchmarking/profile-compiler.js @@ -0,0 +1,94 @@ +import fs from 'node:fs'; +import path from 'node:path'; +import { spawnSync } from 'node:child_process'; +import { fileURLToPath } from 'node:url'; +import { analyze_compiler_profiles } from './analyze-compiler-profile.js'; + +const root = path.resolve(fileURLToPath(import.meta.url), '../..'); +const profiles_root = path.join(root, 'benchmarking/.profiles/compiler-tests'); +const requested_name = process.env.SVELTE_PROFILE_NAME; +const run_name = safe(requested_name || new Date().toISOString().replaceAll(':', '-')); +const run_dir = path.join(profiles_root, run_name); +const raw_dir = path.join(run_dir, 'raw'); +const test_args = process.argv.slice(2); +const sampling_interval = process.env.SVELTE_CPU_PROF_INTERVAL || '5000'; + +if (fs.existsSync(run_dir)) { + console.error(`Profile run already exists: ${path.relative(root, run_dir)}`); + process.exit(1); +} + +fs.mkdirSync(raw_dir, { recursive: true }); + +const revision = spawnSync('git', ['rev-parse', 'HEAD'], { + cwd: root, + encoding: 'utf8' +}); +const started_at = new Date().toISOString(); +const manifest_file = path.join(run_dir, 'manifest.json'); +const command = [ + 'pnpm', + 'test', + '--execArgv=--cpu-prof', + `--execArgv=--cpu-prof-dir=${raw_dir}`, + `--execArgv=--cpu-prof-interval=${sampling_interval}`, + ...test_args +]; + +write_manifest({ + command, + revision: revision.status === 0 ? revision.stdout.trim() : null, + started_at, + status: 'running' +}); + +console.log(`Compiler profiles will be written to ${path.relative(root, run_dir)}`); + +const result = spawnSync(command[0], command.slice(1), { + cwd: root, + stdio: 'inherit' +}); + +let analysis_error = null; + +try { + analyze_compiler_profiles(run_dir); + fs.writeFileSync(path.join(profiles_root, 'latest.txt'), `${run_name}\n`); +} catch (error) { + analysis_error = error instanceof Error ? error.stack || error.message : String(error); + console.error(analysis_error); +} + +write_manifest({ + command, + revision: revision.status === 0 ? revision.stdout.trim() : null, + started_at, + finished_at: new Date().toISOString(), + status: result.status === 0 && analysis_error === null ? 'completed' : 'failed', + test_exit_code: result.status, + test_signal: result.signal, + analysis_error +}); + +if (result.error) { + console.error(result.error); +} + +if (result.status === 0 && analysis_error === null) { + console.log( + `Compiler flame graph: ${path.relative(root, path.join(run_dir, 'flamegraph.speedscope.json'))}` + ); + console.log(`Compiler hotspot summary: ${path.relative(root, path.join(run_dir, 'summary.md'))}`); +} else { + process.exitCode = result.status || 1; +} + +/** @param {string} value */ +function safe(value) { + return value.replace(/[^a-z0-9._-]+/gi, '_'); +} + +/** @param {Record} manifest */ +function write_manifest(manifest) { + fs.writeFileSync(manifest_file, `${JSON.stringify(manifest, null, '\t')}\n`); +} diff --git a/benchmarking/run.js b/benchmarking/run.js index e44e816dd0..322960e866 100644 --- a/benchmarking/run.js +++ b/benchmarking/run.js @@ -1,6 +1,7 @@ import { fork } from 'node:child_process'; import { fileURLToPath } from 'node:url'; import * as $ from '../packages/svelte/src/internal/client/index.js'; +import { compiler_benchmarks } from './benchmarks/compiler/index.js'; import { reactivity_benchmarks } from './benchmarks/reactivity/index.js'; import { ssr_benchmarks } from './benchmarks/ssr/index.js'; import { with_cpu_profile } from './utils.js'; @@ -11,7 +12,9 @@ const single = process.env.BENCH_SINGLE; if (single) { // child mode — run a single benchmark and report the result to the parent - const benchmark = [...reactivity_benchmarks, ...ssr_benchmarks].find((b) => b.label === single); + const benchmark = [...compiler_benchmarks, ...reactivity_benchmarks, ...ssr_benchmarks].find( + (b) => b.label === single + ); if (!benchmark) { throw new Error(`Unknown benchmark ${single}`); @@ -33,6 +36,13 @@ if (single) { const filters = process.argv.slice(2); const suites = [ + // Commenting out because we rarely need to run it + // { + // benchmarks: compiler_benchmarks.filter( + // (b) => filters.length === 0 || filters.some((f) => b.label.includes(f)) + // ), + // name: 'compiler benchmarks' + // }, { benchmarks: reactivity_benchmarks.filter( (b) => filters.length === 0 || filters.some((f) => b.label.includes(f)) diff --git a/package.json b/package.json index 8004698dd1..41f93d526c 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "lint": "eslint && prettier --check .", "format": "prettier --write .", "test": "vitest run", + "profile:compiler": "node ./benchmarking/profile-compiler.js", "changeset:version": "changeset version && pnpm -r generate:version && git add --all", "changeset:publish": "changeset publish", "bench": "NODE_ENV=production node --allow-natives-syntax ./benchmarking/run.js", From 9e695442380a7c3bec5bf90d6c7341c3e1ae9bac Mon Sep 17 00:00:00 2001 From: JY Wey <34165386+JaiWey@users.noreply.github.com> Date: Fri, 4 Sep 2026 02:06:28 +1200 Subject: [PATCH 221/222] fix: in non-async mode, only push variable to current_sources when active_reaction is updating (#18550) fix: #16814 in non-async mode, when the derived inside `SvelteDate` is created and evaluated, the `#reaction` is from the `active_reaction` when the `SvelteDate` instance is created, which could possible in NON_UPDATING state, so the derived should not be added to `current_sources` to prevent re-run. --------- Co-authored-by: Simon H <5968653+dummdidumm@users.noreply.github.com> Co-authored-by: Simon Holthausen --- .changeset/tricky-cooks-help.md | 5 +++ .../svelte/src/internal/client/runtime.js | 6 ++- packages/svelte/tests/signals/test.ts | 39 +++++++++++++++++++ 3 files changed, 49 insertions(+), 1 deletion(-) create mode 100644 .changeset/tricky-cooks-help.md diff --git a/.changeset/tricky-cooks-help.md b/.changeset/tricky-cooks-help.md new file mode 100644 index 0000000000..f61c7944a1 --- /dev/null +++ b/.changeset/tricky-cooks-help.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: in non-async mode, only push variable to current_sources when active_reaction is updating diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index a881403f58..d86c349445 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -100,7 +100,11 @@ export let current_sources = null; /** @param {Value} value */ export function push_reaction_value(value) { - if (active_reaction !== null && (!async_mode_flag || (active_reaction.f & DERIVED) !== 0)) { + if ( + active_reaction !== null && + ((!async_mode_flag && (active_reaction.f & REACTION_IS_UPDATING) !== 0) || + (active_reaction.f & DERIVED) !== 0) + ) { (current_sources ??= new Set()).add(value); } } diff --git a/packages/svelte/tests/signals/test.ts b/packages/svelte/tests/signals/test.ts index c1ebc3ec75..285dc1b3ce 100644 --- a/packages/svelte/tests/signals/test.ts +++ b/packages/svelte/tests/signals/test.ts @@ -1591,4 +1591,43 @@ describe('signals', () => { assert.equal(s.reactions, null); }; }); + + // https://github.com/sveltejs/svelte/issues/16814 + it('does not treat values created for inactive reactions as current', () => { + push({}, true); + + const trigger = state(false); + const value = state(0); + const log: number[] = []; + let inactive_reaction: Effect; + + const destroy = effect_root(() => { + render_effect(() => { + $.get(trigger); + inactive_reaction = $.active_reaction as Effect; + }); + + render_effect(() => { + $.get(trigger); + + const reaction = $.active_reaction; + $.set_active_reaction(inactive_reaction); + $.push_reaction_value(value); + $.set_active_reaction(reaction); + + log.push($.get(value)); + }); + }); + + try { + flushSync(); + flushSync(() => set(trigger, true)); + flushSync(() => set(value, 1)); + + assert.deepEqual(log, [0, 0, 1]); + } finally { + destroy(); + pop(); + } + }); }); From 5895c637b04dc8667020c8d326807c3f3a984472 Mon Sep 17 00:00:00 2001 From: Bao Nguyen <39545125+giaBaoJS@users.noreply.github.com> Date: Thu, 3 Sep 2026 21:31:52 +0700 Subject: [PATCH 222/222] fix: recognise `aria-braillelabel` and `aria-brailleroledescription` as known ARIA attributes (#18765) --- .changeset/wild-cases-smile.md | 5 +++++ .../phases/2-analyze/visitors/shared/a11y/constants.js | 2 +- .../tests/validator/samples/a11y-aria-props/input.svelte | 3 ++- 3 files changed, 8 insertions(+), 2 deletions(-) create mode 100644 .changeset/wild-cases-smile.md diff --git a/.changeset/wild-cases-smile.md b/.changeset/wild-cases-smile.md new file mode 100644 index 0000000000..ac5edacfd1 --- /dev/null +++ b/.changeset/wild-cases-smile.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: recognise `aria-braillelabel` and `aria-brailleroledescription` as known ARIA attributes diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/a11y/constants.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/a11y/constants.js index 49099c2cb1..a2cd91333b 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/a11y/constants.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/a11y/constants.js @@ -4,7 +4,7 @@ import { roles as roles_map, elementRoles } from 'aria-query'; import { AXObjects, elementAXObjects } from 'axobject-query'; export const aria_attributes = - 'activedescendant atomic autocomplete busy checked colcount colindex colspan controls current describedby description details disabled dropeffect errormessage expanded flowto grabbed haspopup hidden invalid keyshortcuts label labelledby level live modal multiline multiselectable orientation owns placeholder posinset pressed readonly relevant required roledescription rowcount rowindex rowspan selected setsize sort valuemax valuemin valuenow valuetext'.split( + 'activedescendant atomic autocomplete braillelabel brailleroledescription busy checked colcount colindex colspan controls current describedby description details disabled dropeffect errormessage expanded flowto grabbed haspopup hidden invalid keyshortcuts label labelledby level live modal multiline multiselectable orientation owns placeholder posinset pressed readonly relevant required roledescription rowcount rowindex rowspan selected setsize sort valuemax valuemin valuenow valuetext'.split( ' ' ); diff --git a/packages/svelte/tests/validator/samples/a11y-aria-props/input.svelte b/packages/svelte/tests/validator/samples/a11y-aria-props/input.svelte index 21c6327960..7e4bf7b3b3 100644 --- a/packages/svelte/tests/validator/samples/a11y-aria-props/input.svelte +++ b/packages/svelte/tests/validator/samples/a11y-aria-props/input.svelte @@ -1 +1,2 @@ - \ No newline at end of file + +