From eb10a70cf1f003c3e93f3c4ecc9749870e7581c6 Mon Sep 17 00:00:00 2001 From: Simon H <5968653+dummdidumm@users.noreply.github.com> Date: Mon, 18 May 2026 17:36:17 +0200 Subject: [PATCH 1/8] fix: unset context synchronously in `run` (#18236) Our `run` function which executes top level awaits (and synchronous statements in-between/after) did not unset the context in time in case the function returns an async value. In that case the context was still around until the that promise resolves, which can be too late because unrelated things can be intertwined with the batch. The test shows this: Without the fix, the unrelated count incrementation would not update the view until the top level awaits in the child are done. In the test this just shows as a delayed visual update, but it also can result in stale roots as shown in https://github.com/sveltejs/svelte/issues/18221#issuecomment-4470921077 --- .changeset/tangy-icons-dig.md | 5 ++++ .../src/internal/client/reactivity/async.js | 23 ++++++++++------ .../async-run-isolated-batch/Child.svelte | 7 +++++ .../async-run-isolated-batch/_config.js | 26 +++++++++++++++++++ .../async-run-isolated-batch/main.svelte | 23 ++++++++++++++++ 5 files changed, 76 insertions(+), 8 deletions(-) create mode 100644 .changeset/tangy-icons-dig.md create mode 100644 packages/svelte/tests/runtime-runes/samples/async-run-isolated-batch/Child.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/async-run-isolated-batch/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-run-isolated-batch/main.svelte diff --git a/.changeset/tangy-icons-dig.md b/.changeset/tangy-icons-dig.md new file mode 100644 index 0000000000..6d87912bbb --- /dev/null +++ b/.changeset/tangy-icons-dig.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: unset context synchronously in `run` diff --git a/packages/svelte/src/internal/client/reactivity/async.js b/packages/svelte/src/internal/client/reactivity/async.js index c1d4cbcd67..12c5e9baa5 100644 --- a/packages/svelte/src/internal/client/reactivity/async.js +++ b/packages/svelte/src/internal/client/reactivity/async.js @@ -303,15 +303,21 @@ export function run(thunks) { .then(() => { restore(); - if (errored) { - throw errored.error; + try { + if (errored) { + throw errored.error; + } + + if (aborted(active)) { + throw STALE_REACTION; + } + + return fn(); + } finally { + // We gotta unset context directly in case the function returns a promise, in which case + // unset_context in .finally() would be too late ... + unset_context(); } - - if (aborted(active)) { - throw STALE_REACTION; - } - - return fn(); }) .catch(handle_error); @@ -320,6 +326,7 @@ export function run(thunks) { promise.finally(() => { blocker.settled = true; + // ... but we also need it after such a promise has resolved in case it restores our context unset_context(); }); } diff --git a/packages/svelte/tests/runtime-runes/samples/async-run-isolated-batch/Child.svelte b/packages/svelte/tests/runtime-runes/samples/async-run-isolated-batch/Child.svelte new file mode 100644 index 0000000000..6f84dc319e --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-run-isolated-batch/Child.svelte @@ -0,0 +1,7 @@ + + +{x} {y} diff --git a/packages/svelte/tests/runtime-runes/samples/async-run-isolated-batch/_config.js b/packages/svelte/tests/runtime-runes/samples/async-run-isolated-batch/_config.js new file mode 100644 index 0000000000..a0dc38c660 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-run-isolated-batch/_config.js @@ -0,0 +1,26 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target }) { + const [show, resolve, count] = target.querySelectorAll('button'); + + show.click(); + await tick(); + resolve.click(); + await tick(); + count.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ' ' + ); + + resolve.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + '1 2 ' + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-run-isolated-batch/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-run-isolated-batch/main.svelte new file mode 100644 index 0000000000..0a5346cc47 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-run-isolated-batch/main.svelte @@ -0,0 +1,23 @@ + + + + +{#if show} + +{/if} + + + + From 1e899cba354d2b25593afc1bbf04bf3555b5d26a Mon Sep 17 00:00:00 2001 From: Simon H <5968653+dummdidumm@users.noreply.github.com> Date: Mon, 18 May 2026 17:36:41 +0200 Subject: [PATCH 2/8] fix: execute uninitialized derived even if it's destroyed (#18228) Turns out there are a few unavoidable cases where we have to execute the derived even if we otherwise wouldn't, because of its lazy nature. Fixes #18139 --- .changeset/hungry-ads-beam.md | 5 +++ .../internal/client/reactivity/deriveds.js | 7 +++- .../Child.svelte | 10 ++++++ .../_config.js | 32 +++++++++++++++++++ .../main.svelte | 11 +++++++ 5 files changed, 64 insertions(+), 1 deletion(-) create mode 100644 .changeset/hungry-ads-beam.md create mode 100644 packages/svelte/tests/runtime-runes/samples/transition-derived-uninitialized/Child.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/transition-derived-uninitialized/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/transition-derived-uninitialized/main.svelte diff --git a/.changeset/hungry-ads-beam.md b/.changeset/hungry-ads-beam.md new file mode 100644 index 0000000000..260d177119 --- /dev/null +++ b/.changeset/hungry-ads-beam.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: execute uninitialized derived even if it's destroyed diff --git a/packages/svelte/src/internal/client/reactivity/deriveds.js b/packages/svelte/src/internal/client/reactivity/deriveds.js index 83e23dfab6..75be018552 100644 --- a/packages/svelte/src/internal/client/reactivity/deriveds.js +++ b/packages/svelte/src/internal/client/reactivity/deriveds.js @@ -338,7 +338,12 @@ export function execute_derived(derived) { var prev_active_effect = active_effect; var parent = derived.parent; - if (!is_destroying_effect && parent !== null && (parent.f & (DESTROYED | INERT)) !== 0) { + if ( + !is_destroying_effect && + parent !== null && + derived.v !== UNINITIALIZED && // if it was never evaluated before, it's guaranteed to fail downstream, so we try to execute instead + (parent.f & (DESTROYED | INERT)) !== 0 + ) { w.derived_inert(); return derived.v; diff --git a/packages/svelte/tests/runtime-runes/samples/transition-derived-uninitialized/Child.svelte b/packages/svelte/tests/runtime-runes/samples/transition-derived-uninitialized/Child.svelte new file mode 100644 index 0000000000..20bd606d9b --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/transition-derived-uninitialized/Child.svelte @@ -0,0 +1,10 @@ + + +
+ hello +
diff --git a/packages/svelte/tests/runtime-runes/samples/transition-derived-uninitialized/_config.js b/packages/svelte/tests/runtime-runes/samples/transition-derived-uninitialized/_config.js new file mode 100644 index 0000000000..734c84a0c4 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/transition-derived-uninitialized/_config.js @@ -0,0 +1,32 @@ +import { flushSync } from 'svelte'; +import { raf } from '../../../animation-helpers'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target }) { + const [fly_in, fly_out] = target.querySelectorAll('button'); + + fly_in.click(); + flushSync(); + raf.tick(25); + assert.htmlEqual( + target.innerHTML, + ` + + +
hello
+ ` + ); + + fly_out.click(); + flushSync(); + raf.tick(50); + assert.htmlEqual( + target.innerHTML, + ` + + + ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/transition-derived-uninitialized/main.svelte b/packages/svelte/tests/runtime-runes/samples/transition-derived-uninitialized/main.svelte new file mode 100644 index 0000000000..4fc3395af8 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/transition-derived-uninitialized/main.svelte @@ -0,0 +1,11 @@ + + + + + +{#if show} + +{/if} From dc1f037fa67bf2fbdeb1c5f18b2b1f7d023077e3 Mon Sep 17 00:00:00 2001 From: Simon H <5968653+dummdidumm@users.noreply.github.com> Date: Mon, 18 May 2026 17:37:01 +0200 Subject: [PATCH 3/8] fix: don't run teardown effects when deriveds are unfreezed (#18227) The logic was flawed - a teardown effect only has a teardown function but not `fn` property, but unfreeze thought that everything with a `teardown` needs to be unfreezed Helps with #18221 (though likely doesn't fix it completely, at least not the more general `batch.#roots`problems) --- .changeset/slimy-olives-cross.md | 5 +++++ .../internal/client/reactivity/deriveds.js | 6 +++--- .../async-state-eager-const/_config.js | 21 +++++++++++++++++++ .../async-state-eager-const/main.svelte | 21 +++++++++++++++++++ 4 files changed, 50 insertions(+), 3 deletions(-) create mode 100644 .changeset/slimy-olives-cross.md create mode 100644 packages/svelte/tests/runtime-runes/samples/async-state-eager-const/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-state-eager-const/main.svelte diff --git a/.changeset/slimy-olives-cross.md b/.changeset/slimy-olives-cross.md new file mode 100644 index 0000000000..3309e2deab --- /dev/null +++ b/.changeset/slimy-olives-cross.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: don't run teardown effects when deriveds are unfreezed diff --git a/packages/svelte/src/internal/client/reactivity/deriveds.js b/packages/svelte/src/internal/client/reactivity/deriveds.js index 75be018552..8d240bca31 100644 --- a/packages/svelte/src/internal/client/reactivity/deriveds.js +++ b/packages/svelte/src/internal/client/reactivity/deriveds.js @@ -452,8 +452,8 @@ export function freeze_derived_effects(derived) { // make it a noop so it doesn't get called again if the derived // is unfrozen. we don't set it to `null`, because the existence // of a teardown function is what determines whether the - // effect runs again during unfreezing - e.teardown = noop; + // effect runs again during unfreezing (but not for teardown-only effects) + if (e.fn !== null) e.teardown = noop; e.ac = null; remove_reactions(e, 0); @@ -471,7 +471,7 @@ export function unfreeze_derived_effects(derived) { for (const e of derived.effects) { // if the effect was previously frozen — indicated by the presence // of a teardown function — unfreeze it - if (e.teardown) { + if (e.teardown && e.fn !== null) { update_effect(e); } } diff --git a/packages/svelte/tests/runtime-runes/samples/async-state-eager-const/_config.js b/packages/svelte/tests/runtime-runes/samples/async-state-eager-const/_config.js new file mode 100644 index 0000000000..1511d443e5 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-state-eager-const/_config.js @@ -0,0 +1,21 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + compileOptions: { dev: true }, // for testing that teardown effect in eager $.get(loaded) doesn't lead to a crash (because it means REACTION_RAN is set, which means unfreeze_derived runs) + async test({ assert, target }) { + const [count, shift] = target.querySelectorAll('button'); + + shift.click(); + await tick(); + assert.htmlEqual(target.innerHTML, `

0

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

0 (...)

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

1

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

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

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

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

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

Hello, world!

123

Hello, world

`); + $$renderer.push(`

Hello, world!

123

Hello, world

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

Hello, {null}{name}!

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

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

\ No newline at end of file +

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

+
From a63bf862c03d5c63b73e412f2ca952cb83d444d8 Mon Sep 17 00:00:00 2001 From: Mukunda Rao Katta Date: Mon, 18 May 2026 12:42:20 -0700 Subject: [PATCH 8/8] docs: clarify context state getter pattern (#18226) Closes #18022 This updates the context docs to show the getter pattern for state that may be reassigned, such as primitive state. The new example passes a function through context so children read the current value instead of capturing the initial value. This is a docs-only clarification and does not change runtime behavior. Validation: - `git diff --check` - `pnpm exec prettier --check documentation/docs/06-runtime/02-context.md` Full `pnpm test`/`pnpm lint` were not run because this only changes documentation. --------- Co-authored-by: Rich Harris --- documentation/docs/06-runtime/02-context.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/documentation/docs/06-runtime/02-context.md b/documentation/docs/06-runtime/02-context.md index aee51c9a78..439272df8f 100644 --- a/documentation/docs/06-runtime/02-context.md +++ b/documentation/docs/06-runtime/02-context.md @@ -163,6 +163,8 @@ export const [getCounter, setCounter] = createContext(); Svelte will warn you if you get it wrong. +Similarly, to pass primitive values through context, use functions as described in [Passing state into functions]($state#Passing-state-into-functions). + ## Component testing When writing [component tests](testing#Unit-and-component-tests-with-Vitest-Component-testing), it can be useful to create a wrapper component that sets the context in order to check the behaviour of a component that uses it. As of version 5.49, you can do this sort of thing: