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 01/67] 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 @@
+
+
+ count += 1}>clicks: {count}
+ show = !show}>toggle
+ queued.shift()?.()}>resolve
+
+
+ {#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 02/67] 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 03/67] 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 04/67] 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 05/67] 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 06/67] 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 07/67] fix: preserve select selection with spread attributes
(#18561)
Fixes #18557
A `` with spread attributes initializes the option mutation
observer even when those attributes never provide a `value`. In that
case the element has no internal `__value`, but the observer previously
treated the missing property as an explicit `undefined` value and
deselected every option after an option was added or removed.
Only reapply the programmatic selection when `__value` is actually
present. This preserves the browser's current selection for spreads
without a value while keeping the existing behavior for bindings,
including an explicitly stored `undefined` value.
A runtime-legacy regression fixture covers both adding and removing an
option in client and hydration modes.
### Before submitting
- [x] References the existing issue
- [x] Uses a `fix:` title
- [x] Includes a regression test
- [x] Includes a patch changeset for `svelte`
### Test plan
- [x] `FILTER=select-spread-preserve-selection pnpm test runtime-legacy
--maxWorkers=1` (2 tests passed: client and hydrate)
- [x] `git diff --check`
- [ ] Full `pnpm test` was not run because the shared host was under
resource pressure and broad test processes were explicitly avoided
- [ ] `pnpm lint` and `pnpm check` were not run for the same reason
- [ ] Targeted repository Prettier check was attempted, but could not
start because `prettier-plugin-svelte` required the ungenerated
`packages/svelte/compiler/index.js`; no broad build or dependency
reinstall was run
### AI disclosure
AI-assisted: the implementation, regression test, and pull request text
were prepared with OpenAI Codex and reviewed by the contributor.
Co-authored-by: Paolo Ricciuti
---
.changeset/tidy-select-spreads.md | 5 ++++
.../client/dom/elements/bindings/select.js | 6 +++--
.../_config.js | 25 +++++++++++++++++++
.../main.svelte | 16 ++++++++++++
4 files changed, 50 insertions(+), 2 deletions(-)
create mode 100644 .changeset/tidy-select-spreads.md
create mode 100644 packages/svelte/tests/runtime-legacy/samples/select-spread-preserve-selection/_config.js
create mode 100644 packages/svelte/tests/runtime-legacy/samples/select-spread-preserve-selection/main.svelte
diff --git a/.changeset/tidy-select-spreads.md b/.changeset/tidy-select-spreads.md
new file mode 100644
index 0000000000..3b14dc5d34
--- /dev/null
+++ b/.changeset/tidy-select-spreads.md
@@ -0,0 +1,5 @@
+---
+'svelte': patch
+---
+
+fix: preserve select selection when spread attributes omit value
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 eb26062653..e390e23ea0 100644
--- a/packages/svelte/src/internal/client/dom/elements/bindings/select.js
+++ b/packages/svelte/src/internal/client/dom/elements/bindings/select.js
@@ -56,8 +56,10 @@ export function select_option(select, value, mounting = false) {
*/
export function init_select(select) {
var observer = new MutationObserver(() => {
- // @ts-ignore
- select_option(select, select.__value);
+ if ('__value' in select) {
+ // @ts-ignore
+ select_option(select, select.__value);
+ }
// Deliberately don't update the potential binding value,
// the model should be preserved unless explicitly changed
});
diff --git a/packages/svelte/tests/runtime-legacy/samples/select-spread-preserve-selection/_config.js b/packages/svelte/tests/runtime-legacy/samples/select-spread-preserve-selection/_config.js
new file mode 100644
index 0000000000..8bc2f11e26
--- /dev/null
+++ b/packages/svelte/tests/runtime-legacy/samples/select-spread-preserve-selection/_config.js
@@ -0,0 +1,25 @@
+import { flushSync } from 'svelte';
+import { ok, test } from '../../test';
+
+export default test({
+ mode: ['client', 'hydrate'],
+
+ async test({ assert, component, target }) {
+ const select = target.querySelector('select');
+ ok(select);
+
+ assert.equal(select.selectedIndex, 0);
+
+ component.toggle();
+ flushSync();
+ await Promise.resolve();
+
+ assert.equal(select.selectedIndex, 0);
+
+ component.toggle();
+ flushSync();
+ await Promise.resolve();
+
+ assert.equal(select.selectedIndex, 0);
+ }
+});
diff --git a/packages/svelte/tests/runtime-legacy/samples/select-spread-preserve-selection/main.svelte b/packages/svelte/tests/runtime-legacy/samples/select-spread-preserve-selection/main.svelte
new file mode 100644
index 0000000000..229ce4fb73
--- /dev/null
+++ b/packages/svelte/tests/runtime-legacy/samples/select-spread-preserve-selection/main.svelte
@@ -0,0 +1,16 @@
+
+
+
+ Choose an option
+ First
+ {#if show_extra}
+ 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 08/67] 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
reset ',
+ 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
reset ');
+ }
+});
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}
+
+
+ {
+ recovered = true;
+ reset_fn();
+ }}>reset
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
reset ',
+ 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}
+ {
+ recovered = true;
+ reset();
+ }}>reset
+ {/snippet}
+
From 23bc246a99c649c0c325509debb93e062948e240 Mon Sep 17 00:00:00 2001
From: Elliott Johnson
Date: Fri, 24 Jul 2026 14:14:11 -0600
Subject: [PATCH 09/67] 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 10/67] 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 11/67] 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,
+ `
+ startA
+ startB
+ settleB
+ A0/B0
+ 1 2
+ `
+ );
+
+ 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,
+ `
+ startA
+ startB
+ settleB
+ 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 @@
+
+
+startA
+startB
+settleB
+
+{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 12/67] 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 @@
+ {
+ // belongs to onclick
+ run();
+ }}
+ onkeydown={() => run()}
+/>
diff --git a/packages/svelte/tests/print/samples/comment-inside-attribute/output.svelte b/packages/svelte/tests/print/samples/comment-inside-attribute/output.svelte
new file mode 100644
index 0000000000..4575abf75a
--- /dev/null
+++ b/packages/svelte/tests/print/samples/comment-inside-attribute/output.svelte
@@ -0,0 +1,7 @@
+ {
+ // belongs to onclick
+ run();
+ }}
+ onkeydown={() => run()}
+/>
From a1d5035d1777be6ab177d3749668f599ccefc445 Mon Sep 17 00:00:00 2001
From: Simon H <5968653+dummdidumm@users.noreply.github.com>
Date: Wed, 12 Aug 2026 19:13:47 +0200
Subject: [PATCH 13/67] fix: preserve CSS comments in the AST printer (#18637)
Adds a new `comments` array to the stylesheet node which CSS comments
are added to. Is subsequently used in `print` to see them in the output.
Alternative to #18475
---
.changeset/soft-cats-comment.md | 5 +
packages/svelte/src/compiler/index.js | 3 +-
.../src/compiler/phases/1-parse/index.js | 4 +
.../src/compiler/phases/1-parse/read/style.js | 82 ++--
.../phases/2-analyze/css/css-analyze.js | 3 +
.../compiler/phases/2-analyze/css/css-warn.js | 3 +
.../compiler/phases/3-transform/css/index.js | 3 +
packages/svelte/src/compiler/print/index.js | 361 +++++++++++-------
packages/svelte/src/compiler/types/css.d.ts | 10 +
packages/svelte/tests/css-parse.test.ts | 14 +-
.../parser-legacy/samples/css/output.json | 1 +
.../whitespace-after-style-tag/output.json | 1 +
.../samples/css-nth-syntax/output.json | 11 +-
.../samples/css-pseudo-classes/output.json | 173 ++++++++-
.../script-style-no-markup/output.json | 19 +
.../semicolon-inside-quotes/output.json | 4 +-
.../print/samples/style-comments/input.svelte | 26 ++
.../samples/style-comments/output.svelte | 28 ++
packages/svelte/types/index.d.ts | 10 +
19 files changed, 586 insertions(+), 175 deletions(-)
create mode 100644 .changeset/soft-cats-comment.md
create mode 100644 packages/svelte/tests/print/samples/style-comments/input.svelte
create mode 100644 packages/svelte/tests/print/samples/style-comments/output.svelte
diff --git a/.changeset/soft-cats-comment.md b/.changeset/soft-cats-comment.md
new file mode 100644
index 0000000000..01b07c4d7d
--- /dev/null
+++ b/.changeset/soft-cats-comment.md
@@ -0,0 +1,5 @@
+---
+'svelte': patch
+---
+
+fix: preserve CSS comments in the AST printer
diff --git a/packages/svelte/src/compiler/index.js b/packages/svelte/src/compiler/index.js
index 1d822514b9..599206ac6a 100644
--- a/packages/svelte/src/compiler/index.js
+++ b/packages/svelte/src/compiler/index.js
@@ -141,7 +141,8 @@ export function parseCss(source) {
type: 'StyleSheetFile',
start: 0,
end: source.length,
- children
+ children,
+ comments: parser.css_comments
};
}
diff --git a/packages/svelte/src/compiler/phases/1-parse/index.js b/packages/svelte/src/compiler/phases/1-parse/index.js
index 02d1629c99..fa7b292640 100644
--- a/packages/svelte/src/compiler/phases/1-parse/index.js
+++ b/packages/svelte/src/compiler/phases/1-parse/index.js
@@ -50,6 +50,9 @@ export class Parser {
/** */
index = 0;
+ /** @type {AST.CSS.CSSComment[]} */
+ css_comments = [];
+
/**
* Creates a minimal parser instance for CSS-only parsing.
* Skips Svelte component parsing setup.
@@ -61,6 +64,7 @@ export class Parser {
parser.template = source;
parser.index = 0;
parser.loose = false;
+ parser.css_comments = [];
return parser;
}
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 160e5da277..5eb8c2b3e9 100644
--- a/packages/svelte/src/compiler/phases/1-parse/read/style.js
+++ b/packages/svelte/src/compiler/phases/1-parse/read/style.js
@@ -24,6 +24,7 @@ const REGEX_HTML_COMMENT_CLOSE = /-->/;
*/
export default function read_style(parser, start, attributes) {
const content_start = parser.index;
+ parser.css_comments = [];
const children = read_body(parser, (p) => p.match('= p.template.length);
const content_end = parser.index;
@@ -36,6 +37,7 @@ export default function read_style(parser, start, attributes) {
end: parser.index,
attributes,
children,
+ comments: parser.css_comments,
content: {
start: content_start,
end: content_end,
@@ -229,18 +231,22 @@ function read_selector(parser, inside_pseudo_class = false) {
end: parser.index
});
} else if (parser.eat('::')) {
+ const name = read_identifier(parser);
+ /** @type {AST.CSS.SelectorList | null} */
+ let args = null;
+
+ if (parser.eat('(')) {
+ args = read_selector_list(parser, true);
+ parser.eat(')', true);
+ }
+
relative_selector.selectors.push({
type: 'PseudoElementSelector',
- name: read_identifier(parser),
+ name,
start,
- end: parser.index
+ end: parser.index,
+ ...(args && { args })
});
- // We read the inner selectors of a pseudo element to ensure it parses correctly,
- // but we don't do anything with the result.
- if (parser.eat('(')) {
- read_selector_list(parser, true);
- parser.eat(')', true);
- }
} else if (parser.eat(':')) {
const name = read_identifier(parser);
@@ -323,7 +329,7 @@ function read_selector(parser, inside_pseudo_class = false) {
}
const index = parser.index;
- allow_comment_or_whitespace(parser);
+ allow_comment_or_whitespace(parser, false);
if (parser.match(',') || (inside_pseudo_class ? parser.match(')') : parser.match('{'))) {
// rewind, so we know whether to continue building the selector list
@@ -449,7 +455,7 @@ function read_block_item(parser) {
// read ahead to understand whether we're dealing with a declaration or a nested rule.
// this involves some duplicated work, but avoids a try-catch that would disguise errors
const start = parser.index;
- read_value(parser);
+ read_value(parser, false);
const char = parser.template[parser.index];
parser.index = start;
@@ -492,10 +498,13 @@ function read_declaration(parser) {
/**
* @param {Parser} parser
+ * @param {boolean} [capture_comments]
* @returns {string}
*/
-function read_value(parser) {
+function read_value(parser, capture_comments = true) {
let value = '';
+ /** @type {AST.CSS.CSSComment[]} */
+ const value_comments = [];
let escaped = false;
let in_url = false;
@@ -523,6 +532,13 @@ function read_value(parser) {
} else if (char === '(' && value.slice(-3) === 'url') {
in_url = true;
} else if ((char === ';' || char === '{' || char === '}') && !in_url && !quote_mark) {
+ const leading_whitespace = value.length - value.trimStart().length;
+ for (const comment of value_comments) {
+ comment.position = Math.max(
+ 0,
+ /** @type {number} */ (comment.position) - leading_whitespace
+ );
+ }
return value.trim();
} else if (
char === '/' &&
@@ -530,13 +546,11 @@ function read_value(parser) {
!quote_mark &&
parser.template[parser.index + 1] === '*'
) {
- parser.index += 2;
- while (parser.index < parser.template.length) {
- if (parser.template[parser.index] === '*' && parser.template[parser.index + 1] === '/') {
- parser.index += 2;
- break;
- }
- parser.index++;
+ const comment = read_comment(parser);
+ if (capture_comments) {
+ comment.position = value.length;
+ parser.css_comments.push(comment);
+ value_comments.push(comment);
}
continue;
}
@@ -624,13 +638,16 @@ function read_identifier(parser) {
return identifier;
}
-/** @param {Parser} parser */
-function allow_comment_or_whitespace(parser) {
+/**
+ * @param {Parser} parser
+ * @param {boolean} [capture_comments]
+ */
+function allow_comment_or_whitespace(parser, capture_comments = true) {
parser.allow_whitespace();
while (parser.match('/*') || parser.match('
+
+{result.join('|')}
From fe4a56b0a8b065777c4f01dab63e4fbd260ef758 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Gautier=20Ben=20A=C3=AFm?=
<48261497+GauBen@users.noreply.github.com>
Date: Wed, 19 Aug 2026 16:06:15 +0200
Subject: [PATCH 25/67] fix: avoid double-calling a derived reference when
destructuring (#18668)
Hi! This PR fixes #18666
### 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`
---
.changeset/nervous-dolls-clean.md | 5 +++++
.../3-transform/server/visitors/VariableDeclaration.js | 6 ++++--
.../samples/derived-destructured-from-derived/_config.js | 5 +++++
.../samples/derived-destructured-from-derived/main.svelte | 8 ++++++++
4 files changed, 22 insertions(+), 2 deletions(-)
create mode 100644 .changeset/nervous-dolls-clean.md
create mode 100644 packages/svelte/tests/runtime-runes/samples/derived-destructured-from-derived/_config.js
create mode 100644 packages/svelte/tests/runtime-runes/samples/derived-destructured-from-derived/main.svelte
diff --git a/.changeset/nervous-dolls-clean.md b/.changeset/nervous-dolls-clean.md
new file mode 100644
index 0000000000..5b31bdd3c7
--- /dev/null
+++ b/.changeset/nervous-dolls-clean.md
@@ -0,0 +1,5 @@
+---
+'svelte': patch
+---
+
+fix: avoid double-calling a derived reference when destructuring `$derived` of another `$derived` during server-side rendering
diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/VariableDeclaration.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/VariableDeclaration.js
index 2c7b14afa5..108f65a884 100644
--- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/VariableDeclaration.js
+++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/VariableDeclaration.js
@@ -102,9 +102,11 @@ export function VariableDeclaration(node, context) {
} else {
const call = /** @type {CallExpression} */ (declarator.init);
- let rhs = value;
+ // - cannot be a SpreadElement because refused during analysis
+ // - use args[0] rather than value to avoid visiting twice (above in const value = ... and below in for-ofs)
+ let rhs = /** @type {Expression} */ (call.arguments[0]);
- if (rune !== '$derived' || call.arguments[0].type !== 'Identifier') {
+ if (rune === '$derived.by' || call.arguments[0].type !== 'Identifier') {
const id = b.id(context.state.scope.generate('$$d'));
rhs = b.call(id);
diff --git a/packages/svelte/tests/runtime-runes/samples/derived-destructured-from-derived/_config.js b/packages/svelte/tests/runtime-runes/samples/derived-destructured-from-derived/_config.js
new file mode 100644
index 0000000000..a36c2adf7d
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/derived-destructured-from-derived/_config.js
@@ -0,0 +1,5 @@
+import { test } from '../../test';
+
+export default test({
+ html: `3 `
+});
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 @@
+
+
+
+ (name = 'longer')}>{length}
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 26/67] 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 27/67] 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 28/67] 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 29/67] 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 30/67] 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 31/67] 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 32/67] 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 33/67] 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 34/67] 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 35/67] 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 36/67] 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 37/67] 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 38/67] 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 39/67] 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 40/67] 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 41/67] 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 42/67] 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 43/67] 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 44/67] 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 45/67] =?UTF-8?q?perf:=20O(n=C2=B2)=E2=86=92O(n)=20Map=20l?=
=?UTF-8?q?ookups=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 46/67] 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 47/67] 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 48/67] 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 49/67] 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 52/67] 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 53/67] 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 54/67] 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 55/67] 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 56/67] fix: prevent `selectedcontent` mutation from changing
the selected option (#18495)
Basically, in a situation like this
```svelte
{}} />
A
B
C
```
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 @@
+
+
+ {}} />
+
+
+ A
+ B
+ C
+
+
+
\ 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 57/67] 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 58/67] 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 @@
+
+
+
+ (broken = true)}>break
+ (mounted = false)}>unmount
+
+ {#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 59/67] 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 60/67] 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: `
+ click
+
+ `,
+
+ html: `
+ click
+
+ `,
+
+ 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 @@
+
+ active = true}>click
+
\ 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: `
+ click
+
+ `,
+
+ html: `
+ click
+
+ `,
+
+ 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 @@
+
+ size = '2em'}>click
+
\ 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 61/67] 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 @@
+
+
+ (visible = !visible)}>toggle
+
+
+ {#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 62/67] 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 63/67] 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 64/67] 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 @@
+
+
+ (items = items.filter((i) => i !== 'b'))}>remove
+ (fetching = !fetching)}>fetch
+
+{#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 @@
+
+
+ (shown = !shown)}>toggle
+ (fetching = !fetching)}>fetch
+
+{#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 @@
+
+
+ (shown = false)}>hide
+ (fetching = !fetching)}>fetch
+
+{#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 65/67] 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 66/67] 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 67/67] 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 {};