From b9e03714ebe1dbad9b31351f4e2d5487c90c670a Mon Sep 17 00:00:00 2001 From: Paolo Ricciuti Date: Thu, 2 Jul 2026 08:30:23 +0200 Subject: [PATCH 001/110] fix: correctly transform declaration tags during SSR (#18492) Closes #18490 --- .changeset/rich-jokes-attack.md | 5 +++++ .../phases/3-transform/server/visitors/shared/utils.js | 6 ++++-- .../samples/declaration-tags-transform/_expected.html | 1 + .../samples/declaration-tags-transform/main.svelte | 4 ++++ 4 files changed, 14 insertions(+), 2 deletions(-) create mode 100644 .changeset/rich-jokes-attack.md create mode 100644 packages/svelte/tests/server-side-rendering/samples/declaration-tags-transform/_expected.html create mode 100644 packages/svelte/tests/server-side-rendering/samples/declaration-tags-transform/main.svelte diff --git a/.changeset/rich-jokes-attack.md b/.changeset/rich-jokes-attack.md new file mode 100644 index 0000000000..46e7d2d8c8 --- /dev/null +++ b/.changeset/rich-jokes-attack.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: correctly transform declaration tags 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 3da6fcf321..56157cee37 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 @@ -60,7 +60,9 @@ export function process_children(nodes, { visit, state }) { if (evaluated.is_known) { quasi.value.cooked += escape_html((evaluated.value ?? '') + ''); } else { - expressions.push(b.call('$.escape', /** @type {Expression} */ (visit(node.expression)))); + expressions.push( + b.call('$.escape', /** @type {Expression} */ (visit(node.expression, state))) + ); quasi = b.quasi('', i + 1 === sequence.length); quasis.push(quasi); @@ -80,7 +82,7 @@ export function process_children(nodes, { visit, state }) { if (node.type === 'ExpressionTag' && node.metadata.expression.is_async()) { flush(); - const expression = /** @type {Expression} */ (visit(node.expression)); + const expression = /** @type {Expression} */ (visit(node.expression, state)); let call = b.call( '$$renderer.push', diff --git a/packages/svelte/tests/server-side-rendering/samples/declaration-tags-transform/_expected.html b/packages/svelte/tests/server-side-rendering/samples/declaration-tags-transform/_expected.html new file mode 100644 index 0000000000..5665318328 --- /dev/null +++ b/packages/svelte/tests/server-side-rendering/samples/declaration-tags-transform/_expected.html @@ -0,0 +1 @@ +
number
\ No newline at end of file diff --git a/packages/svelte/tests/server-side-rendering/samples/declaration-tags-transform/main.svelte b/packages/svelte/tests/server-side-rendering/samples/declaration-tags-transform/main.svelte new file mode 100644 index 0000000000..d969f907a5 --- /dev/null +++ b/packages/svelte/tests/server-side-rendering/samples/declaration-tags-transform/main.svelte @@ -0,0 +1,4 @@ +
+ {let dt = $derived(Date.parse("2026-10-01T00:00:00Z"))} + {typeof dt} +
\ No newline at end of file From b1cadd1eae6a709fc5bdc596256b617986a71aaf Mon Sep 17 00:00:00 2001 From: Paolo Ricciuti Date: Thu, 2 Jul 2026 16:31:54 +0200 Subject: [PATCH 002/110] chore: drop dead code that make TSGO fail (#18496) --- .changeset/big-months-shout.md | 5 +++++ packages/svelte/src/internal/client/reactivity/props.js | 5 +---- 2 files changed, 6 insertions(+), 4 deletions(-) create mode 100644 .changeset/big-months-shout.md diff --git a/.changeset/big-months-shout.md b/.changeset/big-months-shout.md new file mode 100644 index 0000000000..3d8ca0045e --- /dev/null +++ b/.changeset/big-months-shout.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +chore: drop dead code that make TSGO fail diff --git a/packages/svelte/src/internal/client/reactivity/props.js b/packages/svelte/src/internal/client/reactivity/props.js index c2f3698809..274d780b1e 100644 --- a/packages/svelte/src/internal/client/reactivity/props.js +++ b/packages/svelte/src/internal/client/reactivity/props.js @@ -91,10 +91,7 @@ const rest_props_handler = { */ /*#__NO_SIDE_EFFECTS__*/ export function rest_props(props, exclude, name) { - return new Proxy( - DEV ? { props, exclude, name, other: {}, to_proxy: [] } : { props, exclude }, - rest_props_handler - ); + return new Proxy(DEV ? { props, exclude, name } : { props, exclude }, rest_props_handler); } /** From 8fb7ceeba5dd1d3b7fc3f587f4e2138bf1b978ed Mon Sep 17 00:00:00 2001 From: Paolo Ricciuti Date: Mon, 6 Jul 2026 13:50:56 +0200 Subject: [PATCH 003/110] fix: don't treat declaration tags as parts inside each blocks (#18507) Closes #18506 Declaration tags in the scope of an each block were considered "parts" and transformed to use their value instead of the signal itself. We can safely check on the `kind` of the binding since a `kind` of `state`, `state_raw` or `derived` in the each scope needs to be a declaration tag (and it's a stable reference so we can use them directly) --- .changeset/young-doodles-beam.md | 5 +++++ .../phases/3-transform/client/visitors/shared/utils.js | 6 +++++- .../samples/declaration-tags-each/_config.js | 10 ++++++++++ .../samples/declaration-tags-each/main.svelte | 6 ++++++ 4 files changed, 26 insertions(+), 1 deletion(-) create mode 100644 .changeset/young-doodles-beam.md create mode 100644 packages/svelte/tests/runtime-runes/samples/declaration-tags-each/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/declaration-tags-each/main.svelte diff --git a/.changeset/young-doodles-beam.md b/.changeset/young-doodles-beam.md new file mode 100644 index 0000000000..b68f55d241 --- /dev/null +++ b/.changeset/young-doodles-beam.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: don't treat declaration tags as parts inside each blocks diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js index 51c84dd01c..c305d53ea5 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js @@ -8,7 +8,7 @@ import { sanitize_template_string } from '../../../../../utils/sanitize_template import { regex_is_valid_identifier } from '../../../../patterns.js'; import is_reference from 'is-reference'; import { dev, is_ignored, locator, component_name } from '../../../../../state.js'; -import { build_getter } from '../../utils.js'; +import { build_getter, is_state_source } from '../../utils.js'; import { ExpressionMetadata } from '../../../../nodes.js'; /** @@ -272,6 +272,10 @@ export function build_bind_this(expression, value, { state, visit }) { const binding = state.scope.get(node.name); if (!binding) return; + // if it is a state or a derived it means is a declaration tag...in that case we don't want to pass the + // value but the signal itself or assignment will break + if (is_state_source(binding, state.analysis) || binding.kind === 'derived') return; + for (const [owner, scope] of state.scopes) { if (owner.type === 'EachBlock' && scope === binding.scope) { ids.push(node); diff --git a/packages/svelte/tests/runtime-runes/samples/declaration-tags-each/_config.js b/packages/svelte/tests/runtime-runes/samples/declaration-tags-each/_config.js new file mode 100644 index 0000000000..d5cdd103b5 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/declaration-tags-each/_config.js @@ -0,0 +1,10 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ target, assert }) { + const [btn1, btn2] = target.querySelectorAll('button'); + flushSync(() => btn2.click()); + assert.equal(btn1.textContent, '1'); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/declaration-tags-each/main.svelte b/packages/svelte/tests/runtime-runes/samples/declaration-tags-each/main.svelte new file mode 100644 index 0000000000..04d55913b4 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/declaration-tags-each/main.svelte @@ -0,0 +1,6 @@ +{#each Array(1)} + {let ref = $state()} + {let count = $state(0)} + + +{/each} \ No newline at end of file From 41642b70ee471dfe024dfface4c0c9939fa60d3b Mon Sep 17 00:00:00 2001 From: Paolo Ricciuti Date: Thu, 9 Jul 2026 15:06:08 +0200 Subject: [PATCH 004/110] fix: avoid declaration tag warning in event handlers (#18500) Closes #18493 Co-authored-by: justjavac --- .changeset/dull-oranges-fry.md | 5 +++++ .../compiler/phases/2-analyze/visitors/shared/function.js | 4 +++- .../declaration-tag-state-referenced-locally/input.svelte | 4 ++++ 3 files changed, 12 insertions(+), 1 deletion(-) create mode 100644 .changeset/dull-oranges-fry.md diff --git a/.changeset/dull-oranges-fry.md b/.changeset/dull-oranges-fry.md new file mode 100644 index 0000000000..0efcc6fa2d --- /dev/null +++ b/.changeset/dull-oranges-fry.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: avoid declaration tag warning in event handlers diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/function.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/function.js index 7bdb2243f2..6b3d227eca 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/function.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/function.js @@ -18,7 +18,9 @@ export function visit_function(node, context) { context.next({ ...context.state, - function_depth: context.state.function_depth + 1, + // we generally want to use scope.function_depth unless we specifically increased + // that in state.function_depth (e.g. a derived) + function_depth: Math.max(context.state.scope.function_depth, context.state.function_depth) + 1, expression: null }); } diff --git a/packages/svelte/tests/validator/samples/declaration-tag-state-referenced-locally/input.svelte b/packages/svelte/tests/validator/samples/declaration-tag-state-referenced-locally/input.svelte index 90cc208e72..f422be9622 100644 --- a/packages/svelte/tests/validator/samples/declaration-tag-state-referenced-locally/input.svelte +++ b/packages/svelte/tests/validator/samples/declaration-tag-state-referenced-locally/input.svelte @@ -7,3 +7,7 @@ {let e = $state(0), f = e} {a}{b}{c}{d}{e}{f} + + \ No newline at end of file From 5edd8b0602c81494ebdad7f1a5d8077f869c2097 Mon Sep 17 00:00:00 2001 From: Ivan <273312799+Socialpranker@users.noreply.github.com> Date: Thu, 9 Jul 2026 23:59:23 +0200 Subject: [PATCH 005/110] fix: chain preprocessor sourcemaps with an empty sources[0] (#18518) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Fixes #18491 — the compiler discards an upstream plugin's sourcemap when it's generated without a `source` option (e.g. `new MagicString(code).generateMap()`), producing wrong devtools/stack-trace positions. ### Root cause `compile()` already supports composing an incoming sourcemap into its output via `options.sourcemap` (`merge_with_preprocessor_map` → `apply_preprocessor_sourcemap` → `combine_sourcemaps` in `packages/svelte/src/compiler/utils/mapped_code.js`). This is the same mechanism `preprocess()` uses internally, and it's the documented contract for tools that transform a `.svelte` file before compiling it (see `CompileOptions.sourcemap`'s doc comment). `combine_sourcemaps` composes maps by matching `sourcefile === filename` (the basename of the file being compiled). A sourcemap produced by `new MagicString(code).generateMap()` **without** a `source` option — which is exactly what the reporter's Vite plugin does — has `sources: ['']`. That empty string never equals `filename`, so `remapping()` treats the node as a leaf (the "original" file) instead of a branch to keep chaining through, and the whole incoming map is silently dropped. The result: every position that flows through that segment resolves to `{ source: null, line: null, column: null }`, which is what produces the wrong/missing devtools mapping described in the issue. Vite itself already has to handle this exact ambiguity: in `pluginContainer.ts`'s `_getCombinedSourcemap`, an empty `sources[0]` from a MagicString-based transform is patched to refer to the file being transformed before Vite uses it internally. This PR applies the same normalization on svelte's side, so the contract holds regardless of whether the caller happens to pass a `source` option to `generateMap()`. ### Fix In `apply_preprocessor_sourcemap`, normalize an incoming map's `sources: ['']` (or `[null]`/`[undefined]`) to `[filename]` before calling `combine_sourcemaps`, so the chain-matching step can actually find it. --- .changeset/tame-donkeys-jump.md | 5 ++++ .../svelte/src/compiler/utils/mapped_code.js | 7 +++++ .../samples/sourcemap-empty-source/_config.js | 29 +++++++++++++++++++ .../sourcemap-empty-source/input.svelte | 6 ++++ 4 files changed, 47 insertions(+) create mode 100644 .changeset/tame-donkeys-jump.md create mode 100644 packages/svelte/tests/sourcemaps/samples/sourcemap-empty-source/_config.js create mode 100644 packages/svelte/tests/sourcemaps/samples/sourcemap-empty-source/input.svelte diff --git a/.changeset/tame-donkeys-jump.md b/.changeset/tame-donkeys-jump.md new file mode 100644 index 0000000000..aae38910db --- /dev/null +++ b/.changeset/tame-donkeys-jump.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: chain preprocessor sourcemaps with an empty `sources[0]` instead of dropping them diff --git a/packages/svelte/src/compiler/utils/mapped_code.js b/packages/svelte/src/compiler/utils/mapped_code.js index 7686ba59c6..635743c0c4 100644 --- a/packages/svelte/src/compiler/utils/mapped_code.js +++ b/packages/svelte/src/compiler/utils/mapped_code.js @@ -311,6 +311,13 @@ function apply_preprocessor_sourcemap(filename, svelte_map, preprocessor_map_inp typeof preprocessor_map_input === 'string' ? JSON.parse(preprocessor_map_input) : preprocessor_map_input; + // A preprocessor map with a missing/empty `sources[0]` (e.g. from a MagicString transform + // created without a `source` option) can't be matched against `filename` during combination, + // which silently drops the chain instead of erroring. Normalize it to `filename` first, the + // same way Vite treats an empty `sources[0]` as referring to the file being transformed. + if (preprocessor_map.sources?.length === 1 && !preprocessor_map.sources[0]) { + preprocessor_map.sources = [filename]; + } const result_map = combine_sourcemaps(filename, [svelte_map, preprocessor_map]); // Svelte expects a SourceMap which includes toUrl and toString. Instead of wrapping our output in a class, // we just tack on the extra properties. diff --git a/packages/svelte/tests/sourcemaps/samples/sourcemap-empty-source/_config.js b/packages/svelte/tests/sourcemaps/samples/sourcemap-empty-source/_config.js new file mode 100644 index 0000000000..43a2a4c16a --- /dev/null +++ b/packages/svelte/tests/sourcemaps/samples/sourcemap-empty-source/_config.js @@ -0,0 +1,29 @@ +import * as fs from 'node:fs'; +import MagicString from 'magic-string'; +import { test } from '../../test'; + +// Simulates a bundler plugin (e.g. a Vite plugin using `magic-string`) that transforms +// the Svelte source *before* it reaches `compile()`, and hands its own sourcemap to +// `compileOptions.sourcemap` — the documented way to let svelte chain an upstream map +// into its own output map. Crucially, the upstream map is generated *without* a `source` +// option, exactly like `new MagicString(code).generateMap()` — this yields a sourcemap +// whose `sources` is `['']`, which previously broke the chain entirely (see #18491) +// instead of being treated as "this file", causing every mapping through it to resolve +// to `{ source: null, line: null, column: null }`. +const input = fs.readFileSync(new URL('./input.svelte', import.meta.url), 'utf-8'); +const src = new MagicString(input); +src.overwrite( + src.original.indexOf('count * 2'), + src.original.indexOf('count * 2') + 'count * 2'.length, + 'count * 2', + { + storeName: false + } +); + +export default test({ + compileOptions: { + sourcemap: src.generateMap({ hires: true }) + }, + client: [{ str: 'let doubled' }] +}); diff --git a/packages/svelte/tests/sourcemaps/samples/sourcemap-empty-source/input.svelte b/packages/svelte/tests/sourcemaps/samples/sourcemap-empty-source/input.svelte new file mode 100644 index 0000000000..b4e0b50bdb --- /dev/null +++ b/packages/svelte/tests/sourcemaps/samples/sourcemap-empty-source/input.svelte @@ -0,0 +1,6 @@ + + + From c2b7642263ec1f9d89f5d027d68f272742c449a7 Mon Sep 17 00:00:00 2001 From: Simon H <5968653+dummdidumm@users.noreply.github.com> Date: Fri, 10 Jul 2026 15:24:35 +0200 Subject: [PATCH 006/110] chore: add benchmarks (#18523) Add two kairo variants to better measure of `#traverse` / block effects perf --- .../tests/kairo_broad_block.bench.js | 47 ++++++++++++++++++ .../tests/kairo_deep_block.bench.js | 48 +++++++++++++++++++ 2 files changed, 95 insertions(+) create mode 100644 benchmarking/benchmarks/reactivity/tests/kairo_broad_block.bench.js create mode 100644 benchmarking/benchmarks/reactivity/tests/kairo_deep_block.bench.js diff --git a/benchmarking/benchmarks/reactivity/tests/kairo_broad_block.bench.js b/benchmarking/benchmarks/reactivity/tests/kairo_broad_block.bench.js new file mode 100644 index 0000000000..e8451ec3fa --- /dev/null +++ b/benchmarking/benchmarks/reactivity/tests/kairo_broad_block.bench.js @@ -0,0 +1,47 @@ +import assert from 'node:assert'; +import * as $ from 'svelte/internal/client'; +import { block } from '../../../../packages/svelte/src/internal/client/reactivity/effects.js'; + +// Like `kairo_broad`, but each derived is also read by a block effect, as +// happens with e.g. `{#if derived}` in a component. Measures our #traverse perf better. +export default () => { + let head = $.state(0); + let last = head; + let counter = 0; + + const destroy = $.effect_root(() => { + for (let i = 0; i < 50; i++) { + let current = $.derived(() => { + return $.get(head) + i; + }); + let current2 = $.derived(() => { + return $.get(current) + 1; + }); + block(() => { + $.get(current2); + }); + $.render_effect(() => { + $.get(current2); + counter++; + }); + last = current2; + } + }); + + return { + destroy, + run() { + $.flush(() => { + $.set(head, 1); + }); + counter = 0; + for (let i = 0; i < 50; i++) { + $.flush(() => { + $.set(head, i); + }); + assert.equal($.get(last), i + 50); + } + assert.equal(counter, 50 * 50); + } + }; +}; diff --git a/benchmarking/benchmarks/reactivity/tests/kairo_deep_block.bench.js b/benchmarking/benchmarks/reactivity/tests/kairo_deep_block.bench.js new file mode 100644 index 0000000000..a8d9fa8c35 --- /dev/null +++ b/benchmarking/benchmarks/reactivity/tests/kairo_deep_block.bench.js @@ -0,0 +1,48 @@ +import assert from 'node:assert'; +import * as $ from 'svelte/internal/client'; +import { block } from '../../../../packages/svelte/src/internal/client/reactivity/effects.js'; + +let len = 50; +const iter = 50; + +// Like `kairo_deep`, but the derived chain is also read by a block effect, as +// happens with e.g. `{#if derived}` in a component. Measures our #traverse perf better. +export default () => { + let head = $.state(0); + let current = head; + for (let i = 0; i < len; i++) { + let c = current; + current = $.derived(() => { + return $.get(c) + 1; + }); + } + let counter = 0; + + const destroy = $.effect_root(() => { + block(() => { + $.get(current); + }); + + $.render_effect(() => { + $.get(current); + counter++; + }); + }); + + return { + destroy, + run() { + $.flush(() => { + $.set(head, 1); + }); + counter = 0; + for (let i = 0; i < iter; i++) { + $.flush(() => { + $.set(head, i); + }); + assert.equal($.get(current), len + i); + } + assert.equal(counter, iter); + } + }; +}; From bfbb026f2f7db6ced0d86ba0feb40587c0e8f598 Mon Sep 17 00:00:00 2001 From: JY Wey <34165386+JaiWey@users.noreply.github.com> Date: Sat, 11 Jul 2026 01:42:01 +1200 Subject: [PATCH 007/110] fix: skip unnecessary derived effect in earlier batch (#18525) fixes #18438 Currently the `mark` method inside `#merge` for `earlier_batch` will schedule effect for undirty derived. Add the condition for derived to prevent unnecessary effect be scheduled. ``` if ((flags & DERIVED) !== 0) { mark(/** @type {Derived} */ (reaction)); } ``` --- .changeset/curly-wasps-hide.md | 5 ++++ .../src/internal/client/reactivity/batch.js | 6 ++++ .../samples/async-batch-derived/_config.js | 28 ++++++++++++++++++ .../samples/async-batch-derived/main.svelte | 29 +++++++++++++++++++ 4 files changed, 68 insertions(+) create mode 100644 .changeset/curly-wasps-hide.md create mode 100644 packages/svelte/tests/runtime-runes/samples/async-batch-derived/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-batch-derived/main.svelte diff --git a/.changeset/curly-wasps-hide.md b/.changeset/curly-wasps-hide.md new file mode 100644 index 0000000000..7e55d77ba0 --- /dev/null +++ b/.changeset/curly-wasps-hide.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: skip unnecessary derived effect in earlier batch diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index 7d14b80519..5becae2dc9 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -530,6 +530,12 @@ export class Batch { const mark = (value) => { var reactions = value.reactions; if (reactions === null) return; + // skip if value is derived and is neither dirty nor maybe dirty. transitive + // deriveds (a derived depending on another derived) are only MAYBE_DIRTY, so + // we must continue traversing them to reach the effects that depend on them + if ((value.f & DERIVED) !== 0 && (value.f & (DIRTY | MAYBE_DIRTY)) === 0) { + return; + } for (const reaction of reactions) { var flags = reaction.f; diff --git a/packages/svelte/tests/runtime-runes/samples/async-batch-derived/_config.js b/packages/svelte/tests/runtime-runes/samples/async-batch-derived/_config.js new file mode 100644 index 0000000000..d5286d9d95 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-batch-derived/_config.js @@ -0,0 +1,28 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target }) { + const [increment, pop] = target.querySelectorAll('button'); + + increment.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + `

Loading...

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

Loading...

` + ); + pop.click(); + await tick(); + assert.htmlEqual(target.innerHTML, ` 2 2 1`); + + pop.click(); + await tick(); + assert.htmlEqual(target.innerHTML, ` 2 2 1`); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-batch-derived/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-batch-derived/main.svelte new file mode 100644 index 0000000000..af7f0f468c --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-batch-derived/main.svelte @@ -0,0 +1,29 @@ + + + + +{#snippet defaultPending()} +

Loading...

+{/snippet} + +{#if count > 0} + + {await push(count)} {count} {other} + +{/if} From 08a9e9e7e43453e2146357dd72d44cca1e41e29e Mon Sep 17 00:00:00 2001 From: Rabindra Kumar Meher Date: Fri, 10 Jul 2026 19:29:34 +0530 Subject: [PATCH 008/110] fix: prevent derived connection leak in untracked contexts (#18517) Fixes #18501. Problem is that is_updating_effect was also set to true for branch/root effects which are not reactive --------- Co-authored-by: Simon Holthausen --- packages/svelte/src/internal/client/runtime.js | 5 ++++- packages/svelte/tests/signals/test.ts | 18 ++++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index 227203523e..188d16a820 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -61,6 +61,9 @@ import { without_reactive_context } from './dom/elements/bindings/shared.js'; import { set_signal_status, update_derived_status } from './reactivity/status.js'; import * as w from './warnings.js'; +/** + * True if updating in an effect context that is reactive (i.e. not branch/root effects) + */ let is_updating_effect = false; export let is_destroying_effect = false; @@ -444,7 +447,7 @@ export function update_effect(effect) { var was_updating_effect = is_updating_effect; active_effect = effect; - is_updating_effect = true; + is_updating_effect = (flags & (BRANCH_EFFECT | ROOT_EFFECT)) === 0; // Branch/root effects are not reactive contexts if (DEV) { var previous_component_fn = dev_current_component_function; diff --git a/packages/svelte/tests/signals/test.ts b/packages/svelte/tests/signals/test.ts index 927ce2e665..07f79bd395 100644 --- a/packages/svelte/tests/signals/test.ts +++ b/packages/svelte/tests/signals/test.ts @@ -1503,4 +1503,22 @@ describe('signals', () => { assert.deepEqual(log, ['inner destroyed', 'inner destroyed']); }; }); + + test('derived read in an untracked context should not leak in deps reactions', () => { + return () => { + let s = state('hello'); + let a = derived(() => $.get(s)); + let b = derived(() => $.get(a)); + + let destroy = effect_root(() => { + $.get(b); + }); + + destroy(); + + // a was spuriously added to s.reactions via is_updating_effect + // even though the entire derived chain was read in an untracked context + assert.equal(s.reactions, null); + }; + }); }); From 8e4f26552c7dd40cc056a28357f6171542d2549d Mon Sep 17 00:00:00 2001 From: Simon H <5968653+dummdidumm@users.noreply.github.com> Date: Fri, 10 Jul 2026 16:01:20 +0200 Subject: [PATCH 009/110] chore: changeset (#18527) for #18517 --- .changeset/common-ways-deny.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/common-ways-deny.md diff --git a/.changeset/common-ways-deny.md b/.changeset/common-ways-deny.md new file mode 100644 index 0000000000..75e9375965 --- /dev/null +++ b/.changeset/common-ways-deny.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: don't (re)connect deriveds when read inside branch/root effects From b4d1583ae20f3869a88a731d9a265c546c099f66 Mon Sep 17 00:00:00 2001 From: Thribhuvan Date: Fri, 10 Jul 2026 19:31:48 +0530 Subject: [PATCH 010/110] fix: transform computed keys in keyed each block destructuring patterns (#18521) fixes #18519 --- .changeset/shiny-keys-dance.md | 5 ++++ .../3-transform/client/visitors/EachBlock.js | 5 +++- .../Child.svelte | 7 +++++ .../_config.js | 29 +++++++++++++++++++ .../main.svelte | 12 ++++++++ 5 files changed, 57 insertions(+), 1 deletion(-) create mode 100644 .changeset/shiny-keys-dance.md create mode 100644 packages/svelte/tests/runtime-runes/samples/each-keyed-computed-destructuring-key/Child.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/each-keyed-computed-destructuring-key/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/each-keyed-computed-destructuring-key/main.svelte diff --git a/.changeset/shiny-keys-dance.md b/.changeset/shiny-keys-dance.md new file mode 100644 index 0000000000..20cf754212 --- /dev/null +++ b/.changeset/shiny-keys-dance.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: transform computed keys in keyed `{#each}` destructuring patterns diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/EachBlock.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/EachBlock.js index a1371b516a..b33eddd461 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/EachBlock.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/EachBlock.js @@ -294,7 +294,10 @@ export function EachBlock(node, context) { let key_function = b.id('$.index'); if (node.metadata.keyed) { - const pattern = /** @type {Pattern} */ (node.context); // can only be keyed when a context is provided + // can only be keyed when a context is provided + const pattern = /** @type {Pattern} */ ( + context.visit(/** @type {Pattern} */ (node.context), key_state) + ); const expression = /** @type {Expression} */ ( context.visit(/** @type {Expression} */ (node.key), key_state) ); diff --git a/packages/svelte/tests/runtime-runes/samples/each-keyed-computed-destructuring-key/Child.svelte b/packages/svelte/tests/runtime-runes/samples/each-keyed-computed-destructuring-key/Child.svelte new file mode 100644 index 0000000000..35544d0a55 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/each-keyed-computed-destructuring-key/Child.svelte @@ -0,0 +1,7 @@ + + +{#each options as { [labelKey]: label, [valueKey]: value } (value)} +

{label}: {value}

+{/each} diff --git a/packages/svelte/tests/runtime-runes/samples/each-keyed-computed-destructuring-key/_config.js b/packages/svelte/tests/runtime-runes/samples/each-keyed-computed-destructuring-key/_config.js new file mode 100644 index 0000000000..8a8789d712 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/each-keyed-computed-destructuring-key/_config.js @@ -0,0 +1,29 @@ +import { flushSync } from 'svelte'; +import { ok, test } from '../../test'; + +// https://github.com/sveltejs/svelte/issues/18519 +export default test({ + html: ` + +

1: a1

+

2: a2

+

3: a3

+ `, + + test({ assert, target }) { + const btn = target.querySelector('button'); + ok(btn); + + flushSync(() => btn.click()); + + assert.htmlEqual( + target.innerHTML, + ` + +

3: a3

+

2: a2

+

1: a1

+ ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/each-keyed-computed-destructuring-key/main.svelte b/packages/svelte/tests/runtime-runes/samples/each-keyed-computed-destructuring-key/main.svelte new file mode 100644 index 0000000000..6885a6918e --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/each-keyed-computed-destructuring-key/main.svelte @@ -0,0 +1,12 @@ + + + + From af6f1d309b0fc7a9dcde7099b5f231078c9f5738 Mon Sep 17 00:00:00 2001 From: Simon H <5968653+dummdidumm@users.noreply.github.com> Date: Tue, 14 Jul 2026 00:06:53 +0200 Subject: [PATCH 011/110] chore: run every benchmark in its own process (#18542) While benchmarking it became apparent that when running the full `pnpm bench:compare` suite results could be wildly different compared to only comparing single benchmarks. The reason (most likely) is that the heap/GC/JIT state from one benchmark contaminates the others. Therefore this adjusts the runners such that each benchmark entry is run in its own forked process, not just each branch. This should give better, more stable results. Also a little fix to compare results by name; I made the mistake of adding a new benchmark to main which wasn't present on other branches yet, which made the results really confusing. --- benchmarking/compare/generate-report.js | 33 +++- benchmarking/compare/runner.js | 73 +++++++-- benchmarking/run.js | 203 +++++++++++++++--------- 3 files changed, 220 insertions(+), 89 deletions(-) diff --git a/benchmarking/compare/generate-report.js b/benchmarking/compare/generate-report.js index a61f58909b..a06aaf9fc2 100644 --- a/benchmarking/compare/generate-report.js +++ b/benchmarking/compare/generate-report.js @@ -33,11 +33,38 @@ export function generate_report(outdir) { write(''); - for (let i = 0; i < results[0].length; i += 1) { - write(`${results[0][i].benchmark}`); + // match results by benchmark name — branches may have different benchmark + // lists (e.g. a benchmark that only exists on one of the branches), so + // pairing by array index would misattribute results + const by_name = results.map((result) => new Map(result.map((r) => [r.benchmark, r]))); + + /** @type {string[]} */ + const names = []; + + for (const result of results) { + for (const { benchmark } of result) { + if (!names.includes(benchmark)) { + names.push(benchmark); + } + } + } + + for (const name of names) { + const entries = by_name.map((map) => map.get(name)); + const missing = entries + .map((entry, b) => (entry === undefined ? branches[b] : null)) + .filter((branch) => branch !== null); + + write(`${name}`); + + if (missing.length > 0) { + write(` skipped (missing on ${missing.join(', ')})`); + write(''); + continue; + } for (const metric of ['time', 'gc_time']) { - const times = results.map((result) => +result[i][metric]); + const times = entries.map((entry) => +entry[metric]); let min = Infinity; let max = -Infinity; let min_index = -1; diff --git a/benchmarking/compare/runner.js b/benchmarking/compare/runner.js index 31a8e6b44b..fa746f9869 100644 --- a/benchmarking/compare/runner.js +++ b/benchmarking/compare/runner.js @@ -1,18 +1,67 @@ +import { fork } from 'node:child_process'; +import { fileURLToPath } from 'node:url'; import { reactivity_benchmarks } from '../benchmarks/reactivity/index.js'; import { with_cpu_profile } from '../utils.js'; -const results = []; -const PROFILE_DIR = process.env.BENCH_PROFILE_DIR; +const PROFILE_DIR = process.env.BENCH_PROFILE_DIR ?? null; +const single = process.env.BENCH_SINGLE; -for (let i = 0; i < reactivity_benchmarks.length; i += 1) { - const benchmark = reactivity_benchmarks[i]; +if (single) { + // child mode — run a single benchmark and report the result to the parent + const benchmark = reactivity_benchmarks.find((b) => b.label === single); - process.stderr.write(`Running ${i + 1}/${reactivity_benchmarks.length} ${benchmark.label} `); - results.push({ - benchmark: benchmark.label, - ...(await with_cpu_profile(PROFILE_DIR, benchmark.label, () => benchmark.fn())) - }); - process.stderr.write('\x1b[2K\r'); -} + if (!benchmark) { + throw new Error(`Unknown benchmark ${single}`); + } + + const result = await with_cpu_profile(PROFILE_DIR, benchmark.label, () => benchmark.fn()); + + // exit via the callback so the message is guaranteed to be delivered + /** @type {NodeJS.Process} */ (process).send(result, () => process.exit(0)); +} else { + // parent mode — run every benchmark in its own child process, so that + // heap/GC/JIT state from one benchmark cannot contaminate the others + const filename = fileURLToPath(import.meta.url); + const results = []; + + for (let i = 0; i < reactivity_benchmarks.length; i += 1) { + const benchmark = reactivity_benchmarks[i]; + + process.stderr.write(`Running ${i + 1}/${reactivity_benchmarks.length} ${benchmark.label} `); + + const result = await new Promise((fulfil, reject) => { + const child = fork(filename, [], { + env: { + ...process.env, + BENCH_SINGLE: benchmark.label + } + }); + + /** @type {object | null} */ + let message_received = null; -process.send(results); + child.on('message', (message) => { + message_received = /** @type {object} */ (message); + }); + + child.on('error', reject); + + child.on('exit', (code) => { + if (message_received === null) { + reject(new Error(`benchmark ${benchmark.label} exited with code ${code}`)); + } else { + fulfil(message_received); + } + }); + }); + + results.push({ + benchmark: benchmark.label, + .../** @type {object} */ (result) + }); + + process.stderr.write('\x1b[2K\r'); + } + + /** @type {NodeJS.Process} */ (process).send(results); +} diff --git a/benchmarking/run.js b/benchmarking/run.js index 80e40a5ff1..e44e816dd0 100644 --- a/benchmarking/run.js +++ b/benchmarking/run.js @@ -1,94 +1,149 @@ +import { fork } from 'node:child_process'; +import { fileURLToPath } from 'node:url'; import * as $ from '../packages/svelte/src/internal/client/index.js'; import { reactivity_benchmarks } from './benchmarks/reactivity/index.js'; import { ssr_benchmarks } from './benchmarks/ssr/index.js'; import { with_cpu_profile } from './utils.js'; -// e.g. `pnpm bench kairo` to only run the kairo benchmarks -const filters = process.argv.slice(2); - const PROFILE_DIR = './benchmarking/.profiles'; -const suites = [ - { - benchmarks: reactivity_benchmarks.filter( - (b) => filters.length === 0 || filters.some((f) => b.label.includes(f)) - ), - name: 'reactivity benchmarks' - }, - { - benchmarks: ssr_benchmarks.filter( - (b) => filters.length === 0 || filters.some((f) => b.label.includes(f)) - ), - name: 'server-side rendering benchmarks' - } -].filter((suite) => suite.benchmarks.length > 0); - -if (suites.length === 0) { - console.log('No benchmarks matched provided filters'); - process.exit(1); -} - -const COLUMN_WIDTHS = [25, 9, 9]; -const TOTAL_WIDTH = COLUMN_WIDTHS.reduce((a, b) => a + b); - -const pad_right = (str, n) => str + ' '.repeat(n - str.length); -const pad_left = (str, n) => ' '.repeat(n - str.length) + str; +const single = process.env.BENCH_SINGLE; -let total_time = 0; -let total_gc_time = 0; +if (single) { + // child mode — run a single benchmark and report the result to the parent + const benchmark = [...reactivity_benchmarks, ...ssr_benchmarks].find((b) => b.label === single); -$.push({}, true); + if (!benchmark) { + throw new Error(`Unknown benchmark ${single}`); + } -try { - for (const { benchmarks, name } of suites) { - let suite_time = 0; - let suite_gc_time = 0; + $.push({}, true); + + const result = await with_cpu_profile(PROFILE_DIR, benchmark.label, () => benchmark.fn()); + + $.pop(); + + // exit via the callback so the message is guaranteed to be delivered + /** @type {NodeJS.Process} */ (process).send(result, () => process.exit(0)); +} else { + // parent mode — run every benchmark in its own child process, so that + // heap/GC/JIT state from one benchmark cannot contaminate the others + + // e.g. `pnpm bench kairo` to only run the kairo benchmarks + const filters = process.argv.slice(2); + + const suites = [ + { + benchmarks: reactivity_benchmarks.filter( + (b) => filters.length === 0 || filters.some((f) => b.label.includes(f)) + ), + name: 'reactivity benchmarks' + }, + { + benchmarks: ssr_benchmarks.filter( + (b) => filters.length === 0 || filters.some((f) => b.label.includes(f)) + ), + name: 'server-side rendering benchmarks' + } + ].filter((suite) => suite.benchmarks.length > 0); - console.log(`\nRunning ${name}...\n`); - console.log( - pad_right('Benchmark', COLUMN_WIDTHS[0]) + - pad_left('Time', COLUMN_WIDTHS[1]) + - pad_left('GC time', COLUMN_WIDTHS[2]) - ); - console.log('='.repeat(TOTAL_WIDTH)); + if (suites.length === 0) { + console.log('No benchmarks matched provided filters'); + process.exit(1); + } - for (const benchmark of benchmarks) { - const results = await with_cpu_profile(PROFILE_DIR, benchmark.label, () => benchmark.fn()); + const filename = fileURLToPath(import.meta.url); + + /** + * @param {string} label + * @returns {Promise<{ time: number, gc_time: number }>} + */ + const run_benchmark = (label) => { + return new Promise((fulfil, reject) => { + const child = fork(filename, [], { + env: { + ...process.env, + BENCH_SINGLE: label + } + }); + + /** @type {{ time: number, gc_time: number } | null} */ + let result = null; + + child.on('message', (message) => { + result = /** @type {{ time: number, gc_time: number }} */ (message); + }); + + child.on('error', reject); + + child.on('exit', (code) => { + if (result === null) { + reject(new Error(`benchmark ${label} exited with code ${code}`)); + } else { + fulfil(result); + } + }); + }); + }; + + const COLUMN_WIDTHS = [25, 9, 9]; + const TOTAL_WIDTH = COLUMN_WIDTHS.reduce((a, b) => a + b); + + /** @type {(str: string, n: number) => string} */ + const pad_right = (str, n) => str + ' '.repeat(n - str.length); + /** @type {(str: string, n: number) => string} */ + const pad_left = (str, n) => ' '.repeat(n - str.length) + str; + + let total_time = 0; + let total_gc_time = 0; + + try { + for (const { benchmarks, name } of suites) { + let suite_time = 0; + let suite_gc_time = 0; + + console.log(`\nRunning ${name}...\n`); + console.log( + pad_right('Benchmark', COLUMN_WIDTHS[0]) + + pad_left('Time', COLUMN_WIDTHS[1]) + + pad_left('GC time', COLUMN_WIDTHS[2]) + ); + console.log('='.repeat(TOTAL_WIDTH)); + + for (const benchmark of benchmarks) { + const results = await run_benchmark(benchmark.label); + console.log( + pad_right(benchmark.label, COLUMN_WIDTHS[0]) + + pad_left(results.time.toFixed(2), COLUMN_WIDTHS[1]) + + pad_left(results.gc_time.toFixed(2), COLUMN_WIDTHS[2]) + ); + total_time += results.time; + total_gc_time += results.gc_time; + suite_time += results.time; + suite_gc_time += results.gc_time; + } + + console.log('='.repeat(TOTAL_WIDTH)); console.log( - pad_right(benchmark.label, COLUMN_WIDTHS[0]) + - pad_left(results.time.toFixed(2), COLUMN_WIDTHS[1]) + - pad_left(results.gc_time.toFixed(2), COLUMN_WIDTHS[2]) + pad_right('suite', COLUMN_WIDTHS[0]) + + pad_left(suite_time.toFixed(2), COLUMN_WIDTHS[1]) + + pad_left(suite_gc_time.toFixed(2), COLUMN_WIDTHS[2]) ); - total_time += results.time; - total_gc_time += results.gc_time; - suite_time += results.time; - suite_gc_time += results.gc_time; + console.log('='.repeat(TOTAL_WIDTH)); } - console.log('='.repeat(TOTAL_WIDTH)); - console.log( - pad_right('suite', COLUMN_WIDTHS[0]) + - pad_left(suite_time.toFixed(2), COLUMN_WIDTHS[1]) + - pad_left(suite_gc_time.toFixed(2), COLUMN_WIDTHS[2]) - ); - console.log('='.repeat(TOTAL_WIDTH)); - } - - if (PROFILE_DIR !== null) { console.log(`\nCPU profiles written to ${PROFILE_DIR}`); + } catch (e) { + // eslint-disable-next-line no-console + console.error(e); + process.exit(1); } -} catch (e) { - // eslint-disable-next-line no-console - console.error(e); - process.exit(1); -} -$.pop(); + console.log(''); -console.log(''); - -console.log( - pad_right('total', COLUMN_WIDTHS[0]) + - pad_left(total_time.toFixed(2), COLUMN_WIDTHS[1]) + - pad_left(total_gc_time.toFixed(2), COLUMN_WIDTHS[2]) -); + console.log( + pad_right('total', COLUMN_WIDTHS[0]) + + pad_left(total_time.toFixed(2), COLUMN_WIDTHS[1]) + + pad_left(total_gc_time.toFixed(2), COLUMN_WIDTHS[2]) + ); +} From 4da9f74cd95e838acc56c4aa0d2e926d5f68972d Mon Sep 17 00:00:00 2001 From: Simon H <5968653+dummdidumm@users.noreply.github.com> Date: Tue, 14 Jul 2026 00:07:24 +0200 Subject: [PATCH 012/110] chore: bench compare report as HTML (#18543) Creates a `results.html` file which is much better to look at compared to the terminal output. --- .gitignore | 1 + benchmarking/compare/generate-report.js | 46 +- benchmarking/compare/index.js | 2 +- benchmarking/compare/results.template.html | 741 +++++++++++++++++++++ 4 files changed, 785 insertions(+), 5 deletions(-) create mode 100644 benchmarking/compare/results.template.html diff --git a/.gitignore b/.gitignore index 556cae6344..e4f9f9cf87 100644 --- a/.gitignore +++ b/.gitignore @@ -27,3 +27,4 @@ packages/svelte/scripts/_baseline/ benchmarking/.profiles benchmarking/compare/.results benchmarking/compare/.profiles +benchmarking/compare/results.html diff --git a/benchmarking/compare/generate-report.js b/benchmarking/compare/generate-report.js index a06aaf9fc2..70f7b30fbb 100644 --- a/benchmarking/compare/generate-report.js +++ b/benchmarking/compare/generate-report.js @@ -2,15 +2,27 @@ import fs from 'node:fs'; import path from 'node:path'; import { pathToFileURL } from 'node:url'; -export function generate_report(outdir) { +const REPORT_DATA_PLACEHOLDER = '%%REPORT_DATA%%'; +const report_template = fs.readFileSync( + new URL('./results.template.html', import.meta.url), + 'utf-8' +); + +if (!report_template.includes(REPORT_DATA_PLACEHOLDER)) { + throw new Error(`Missing ${REPORT_DATA_PLACEHOLDER} in results.template.html`); +} + +export function generate_report(outdir, branches) { const result_files = fs .readdirSync(outdir) - .filter((file) => file.endsWith('.json')) + .filter((file) => file.endsWith('.json') && (!branches || branches.includes(file.slice(0, -5)))) .sort((a, b) => a.localeCompare(b)); - const branches = result_files.map((file) => file.slice(0, -5)); + // always do this so that ordering lines up (branches argument might be passed in a different order than the result files are sorted + branches = result_files.map((file) => file.slice(0, -5)); + const results = result_files.map((file) => - JSON.parse(fs.readFileSync(`${outdir}/${file}`, 'utf-8')) + JSON.parse(fs.readFileSync(path.join(outdir, file), 'utf-8')) ); if (results.length === 0) { @@ -95,6 +107,32 @@ export function generate_report(outdir) { write(''); } + + const benchmarks = names.map((name) => ({ + name, + values: by_name.map((map) => { + const entry = map.get(name); + + if (entry === undefined) return null; + + return { + time: Number(entry.time), + gc_time: Number(entry.gc_time) + }; + }) + })); + const data = JSON.stringify({ + generated_at: new Date().toISOString(), + branches, + benchmarks + }) + .replaceAll('<', '\\u003c') + .replaceAll('\u2028', '\\u2028') + .replaceAll('\u2029', '\\u2029'); + const html_file = path.resolve(outdir, '../results.html'); + + fs.writeFileSync(html_file, report_template.replace(REPORT_DATA_PLACEHOLDER, data)); + console.log(`\nHTML report written to ${html_file}`); } function char(i) { diff --git a/benchmarking/compare/index.js b/benchmarking/compare/index.js index 9064ee7da9..2e76f46e1b 100644 --- a/benchmarking/compare/index.js +++ b/benchmarking/compare/index.js @@ -85,4 +85,4 @@ if (PROFILE_DIR !== null) { console.log(`\nCPU profiles written to ${PROFILE_DIR}`); } -generate_report(outdir); +generate_report(outdir, requested_branches); diff --git a/benchmarking/compare/results.template.html b/benchmarking/compare/results.template.html new file mode 100644 index 0000000000..02f208ac4f --- /dev/null +++ b/benchmarking/compare/results.template.html @@ -0,0 +1,741 @@ + + + + + + Benchmark comparison + + + +
+

Benchmark comparison

+

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

+ +
+ +
+
+
+

Branch standings

+

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

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

Results by benchmark

+

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

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

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

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

0

0

0

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

0

1

1

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

1

2

2

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

{await push(a)}

+

{d}

+

{d}

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

{await push(d)} {b}

From 199ffebca375b29b65fcac53fc1825b452175a53 Mon Sep 17 00:00:00 2001 From: prashantg-pixi Date: Tue, 14 Jul 2026 15:21:13 +0530 Subject: [PATCH 015/110] fix: clear previous_task reference after abort in Tween (#18541) avoids memory leaks --- .changeset/tween-previous-task-leak.md | 5 +++++ packages/svelte/src/motion/tweened.js | 1 + 2 files changed, 6 insertions(+) create mode 100644 .changeset/tween-previous-task-leak.md diff --git a/.changeset/tween-previous-task-leak.md b/.changeset/tween-previous-task-leak.md new file mode 100644 index 0000000000..6208e7c87a --- /dev/null +++ b/.changeset/tween-previous-task-leak.md @@ -0,0 +1,5 @@ +--- +"svelte": patch +--- + +fix: clear previous_task reference after abort in Tween to prevent memory leak on interrupted tweens diff --git a/packages/svelte/src/motion/tweened.js b/packages/svelte/src/motion/tweened.js index a24148d075..460db0df64 100644 --- a/packages/svelte/src/motion/tweened.js +++ b/packages/svelte/src/motion/tweened.js @@ -275,6 +275,7 @@ export class Tween { } previous_task?.abort(); + previous_task = null; } const elapsed = now - start; From a4fd67e361914df612ae432ff75283c2d332ae18 Mon Sep 17 00:00:00 2001 From: adiGuba Date: Tue, 14 Jul 2026 11:54:13 +0200 Subject: [PATCH 016/110] fix: $state.eager() is sometimes incorrect in SSR (#18530) Fix #18529 A simple fix : `context.visit()` was missing on the argument of `$state.eager()`, so the generated code can be incorrect in some case : --- .changeset/long-buttons-hunt.md | 5 +++++ .../phases/3-transform/server/visitors/CallExpression.js | 2 +- .../samples/state-eager/_expected.html | 2 ++ .../server-side-rendering/samples/state-eager/main.svelte | 7 +++++++ 4 files changed, 15 insertions(+), 1 deletion(-) create mode 100644 .changeset/long-buttons-hunt.md create mode 100644 packages/svelte/tests/server-side-rendering/samples/state-eager/_expected.html create mode 100644 packages/svelte/tests/server-side-rendering/samples/state-eager/main.svelte diff --git a/.changeset/long-buttons-hunt.md b/.changeset/long-buttons-hunt.md new file mode 100644 index 0000000000..68d0b60ff4 --- /dev/null +++ b/.changeset/long-buttons-hunt.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: ensure `$state.eager()` is correctly transormed for SSR output diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/CallExpression.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/CallExpression.js index 8525fb6366..ac78f55da9 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/CallExpression.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/CallExpression.js @@ -46,7 +46,7 @@ export function CallExpression(node, context) { } if (rune === '$state.eager') { - return node.arguments[0]; + return context.visit(node.arguments[0]); } if (rune === '$state.snapshot') { diff --git a/packages/svelte/tests/server-side-rendering/samples/state-eager/_expected.html b/packages/svelte/tests/server-side-rendering/samples/state-eager/_expected.html new file mode 100644 index 0000000000..9d3ea2f5b4 --- /dev/null +++ b/packages/svelte/tests/server-side-rendering/samples/state-eager/_expected.html @@ -0,0 +1,2 @@ +
value=0
+
eager=0
\ No newline at end of file diff --git a/packages/svelte/tests/server-side-rendering/samples/state-eager/main.svelte b/packages/svelte/tests/server-side-rendering/samples/state-eager/main.svelte new file mode 100644 index 0000000000..d144914f72 --- /dev/null +++ b/packages/svelte/tests/server-side-rendering/samples/state-eager/main.svelte @@ -0,0 +1,7 @@ + + +
value={value}
+
eager={$state.eager(value)}
From 602a873b6b82bba4e6edc91b039e2f6defbe3fc4 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 14 Jul 2026 13:59:06 +0200 Subject: [PATCH 017/110] Version Packages (#18497) This PR was opened by the [Changesets release](https://github.com/changesets/action) GitHub action. When you're ready to do a release, you can merge this and the packages will be published to npm automatically. If you're not ready to do a release yet, that's fine, whenever you add more changesets to main, this PR will be updated. # Releases ## svelte@5.56.5 ### Patch Changes - chore: drop dead code that make TSGO fail ([#18496](https://github.com/sveltejs/svelte/pull/18496)) - fix: don't (re)connect deriveds when read inside branch/root effects ([#18527](https://github.com/sveltejs/svelte/pull/18527)) - fix: skip unnecessary derived effect in earlier batch ([#18525](https://github.com/sveltejs/svelte/pull/18525)) - fix: avoid declaration tag warning in event handlers ([#18500](https://github.com/sveltejs/svelte/pull/18500)) - fix: abort deriveds own AbortSignal when it disconnects ([#18400](https://github.com/sveltejs/svelte/pull/18400)) - fix: ensure `$state.eager()` is correctly transormed for SSR output ([#18530](https://github.com/sveltejs/svelte/pull/18530)) - fix: correctly transform declaration tags during SSR ([#18492](https://github.com/sveltejs/svelte/pull/18492)) - fix: transform computed keys in keyed `{#each}` destructuring patterns ([#18521](https://github.com/sveltejs/svelte/pull/18521)) - fix: chain preprocessor sourcemaps with an empty `sources[0]` instead of dropping them ([#18518](https://github.com/sveltejs/svelte/pull/18518)) - fix: clear previous_task reference after abort in Tween to prevent memory leak on interrupted tweens ([#18541](https://github.com/sveltejs/svelte/pull/18541)) - fix: don't treat declaration tags as parts inside each blocks ([#18507](https://github.com/sveltejs/svelte/pull/18507)) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .changeset/big-months-shout.md | 5 ----- .changeset/common-ways-deny.md | 5 ----- .changeset/curly-wasps-hide.md | 5 ----- .changeset/dull-oranges-fry.md | 5 ----- .changeset/early-crabs-rest.md | 5 ----- .changeset/long-buttons-hunt.md | 5 ----- .changeset/rich-jokes-attack.md | 5 ----- .changeset/shiny-keys-dance.md | 5 ----- .changeset/tame-donkeys-jump.md | 5 ----- .changeset/tween-previous-task-leak.md | 5 ----- .changeset/young-doodles-beam.md | 5 ----- packages/svelte/CHANGELOG.md | 26 ++++++++++++++++++++++++++ packages/svelte/package.json | 2 +- packages/svelte/src/version.js | 2 +- 14 files changed, 28 insertions(+), 57 deletions(-) delete mode 100644 .changeset/big-months-shout.md delete mode 100644 .changeset/common-ways-deny.md delete mode 100644 .changeset/curly-wasps-hide.md delete mode 100644 .changeset/dull-oranges-fry.md delete mode 100644 .changeset/early-crabs-rest.md delete mode 100644 .changeset/long-buttons-hunt.md delete mode 100644 .changeset/rich-jokes-attack.md delete mode 100644 .changeset/shiny-keys-dance.md delete mode 100644 .changeset/tame-donkeys-jump.md delete mode 100644 .changeset/tween-previous-task-leak.md delete mode 100644 .changeset/young-doodles-beam.md diff --git a/.changeset/big-months-shout.md b/.changeset/big-months-shout.md deleted file mode 100644 index 3d8ca0045e..0000000000 --- a/.changeset/big-months-shout.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -chore: drop dead code that make TSGO fail diff --git a/.changeset/common-ways-deny.md b/.changeset/common-ways-deny.md deleted file mode 100644 index 75e9375965..0000000000 --- a/.changeset/common-ways-deny.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: don't (re)connect deriveds when read inside branch/root effects diff --git a/.changeset/curly-wasps-hide.md b/.changeset/curly-wasps-hide.md deleted file mode 100644 index 7e55d77ba0..0000000000 --- a/.changeset/curly-wasps-hide.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: skip unnecessary derived effect in earlier batch diff --git a/.changeset/dull-oranges-fry.md b/.changeset/dull-oranges-fry.md deleted file mode 100644 index 0efcc6fa2d..0000000000 --- a/.changeset/dull-oranges-fry.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: avoid declaration tag warning in event handlers diff --git a/.changeset/early-crabs-rest.md b/.changeset/early-crabs-rest.md deleted file mode 100644 index d3f290a5c2..0000000000 --- a/.changeset/early-crabs-rest.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: abort deriveds own AbortSignal when it disconnects diff --git a/.changeset/long-buttons-hunt.md b/.changeset/long-buttons-hunt.md deleted file mode 100644 index 68d0b60ff4..0000000000 --- a/.changeset/long-buttons-hunt.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: ensure `$state.eager()` is correctly transormed for SSR output diff --git a/.changeset/rich-jokes-attack.md b/.changeset/rich-jokes-attack.md deleted file mode 100644 index 46e7d2d8c8..0000000000 --- a/.changeset/rich-jokes-attack.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: correctly transform declaration tags during SSR diff --git a/.changeset/shiny-keys-dance.md b/.changeset/shiny-keys-dance.md deleted file mode 100644 index 20cf754212..0000000000 --- a/.changeset/shiny-keys-dance.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: transform computed keys in keyed `{#each}` destructuring patterns diff --git a/.changeset/tame-donkeys-jump.md b/.changeset/tame-donkeys-jump.md deleted file mode 100644 index aae38910db..0000000000 --- a/.changeset/tame-donkeys-jump.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: chain preprocessor sourcemaps with an empty `sources[0]` instead of dropping them diff --git a/.changeset/tween-previous-task-leak.md b/.changeset/tween-previous-task-leak.md deleted file mode 100644 index 6208e7c87a..0000000000 --- a/.changeset/tween-previous-task-leak.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"svelte": patch ---- - -fix: clear previous_task reference after abort in Tween to prevent memory leak on interrupted tweens diff --git a/.changeset/young-doodles-beam.md b/.changeset/young-doodles-beam.md deleted file mode 100644 index b68f55d241..0000000000 --- a/.changeset/young-doodles-beam.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: don't treat declaration tags as parts inside each blocks diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index 80b61facb0..d80da025c9 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,31 @@ # svelte +## 5.56.5 + +### Patch Changes + +- chore: drop dead code that make TSGO fail ([#18496](https://github.com/sveltejs/svelte/pull/18496)) + +- fix: don't (re)connect deriveds when read inside branch/root effects ([#18527](https://github.com/sveltejs/svelte/pull/18527)) + +- fix: skip unnecessary derived effect in earlier batch ([#18525](https://github.com/sveltejs/svelte/pull/18525)) + +- fix: avoid declaration tag warning in event handlers ([#18500](https://github.com/sveltejs/svelte/pull/18500)) + +- fix: abort deriveds own AbortSignal when it disconnects ([#18400](https://github.com/sveltejs/svelte/pull/18400)) + +- fix: ensure `$state.eager()` is correctly transormed for SSR output ([#18530](https://github.com/sveltejs/svelte/pull/18530)) + +- fix: correctly transform declaration tags during SSR ([#18492](https://github.com/sveltejs/svelte/pull/18492)) + +- fix: transform computed keys in keyed `{#each}` destructuring patterns ([#18521](https://github.com/sveltejs/svelte/pull/18521)) + +- fix: chain preprocessor sourcemaps with an empty `sources[0]` instead of dropping them ([#18518](https://github.com/sveltejs/svelte/pull/18518)) + +- fix: clear previous_task reference after abort in Tween to prevent memory leak on interrupted tweens ([#18541](https://github.com/sveltejs/svelte/pull/18541)) + +- fix: don't treat declaration tags as parts inside each blocks ([#18507](https://github.com/sveltejs/svelte/pull/18507)) + ## 5.56.4 ### Patch Changes diff --git a/packages/svelte/package.json b/packages/svelte/package.json index 34fa68c8a7..a26a3e4a76 100644 --- a/packages/svelte/package.json +++ b/packages/svelte/package.json @@ -2,7 +2,7 @@ "name": "svelte", "description": "Cybernetically enhanced web apps", "license": "MIT", - "version": "5.56.4", + "version": "5.56.5", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index ef468a2f79..80b38c225a 100644 --- a/packages/svelte/src/version.js +++ b/packages/svelte/src/version.js @@ -4,5 +4,5 @@ * The current version, as set in package.json. * @type {string} */ -export const VERSION = '5.56.4'; +export const VERSION = '5.56.5'; export const PUBLIC_VERSION = '5'; From 22e0adb75a07442015feb0465cee1d3a61729e90 Mon Sep 17 00:00:00 2001 From: Simon H <5968653+dummdidumm@users.noreply.github.com> Date: Thu, 16 Jul 2026 10:42:31 +0200 Subject: [PATCH 018/110] fix: rerun derived that had an abort controller on reconnection (#18551) Follow-up to #18400 - we need to mark a derived with an abort signal that is frozen as dirty so it is guaranteed to rerun when it reconnects/is re-requested. Else you could return a stale value, or worse, you returned a promise from the derived which you aborted, and it's now in the rejected state until you update one of its dependencies. --- .changeset/lucky-dolls-yell.md | 5 +++ .../svelte/src/internal/client/runtime.js | 2 ++ .../_config.js | 32 +++++++++++++++++++ .../main.svelte | 32 +++++++++++++++++++ 4 files changed, 71 insertions(+) create mode 100644 .changeset/lucky-dolls-yell.md create mode 100644 packages/svelte/tests/runtime-runes/samples/abort-signal-derived-rerun-on-reconnect/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/abort-signal-derived-rerun-on-reconnect/main.svelte diff --git a/.changeset/lucky-dolls-yell.md b/.changeset/lucky-dolls-yell.md new file mode 100644 index 0000000000..d158f376d1 --- /dev/null +++ b/.changeset/lucky-dolls-yell.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: rerun derived that had an abort controller on reconnection diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index 2ebce07916..4458595d35 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -413,6 +413,8 @@ function remove_reaction(signal, dependency) { without_reactive_context(() => { /** @type {AbortController} */ (derived.ac).abort(STALE_REACTION); derived.ac = null; + // ensure it reruns right away next time instead of potentially returning a rejected promise as its value + set_signal_status(derived, DIRTY); }); } diff --git a/packages/svelte/tests/runtime-runes/samples/abort-signal-derived-rerun-on-reconnect/_config.js b/packages/svelte/tests/runtime-runes/samples/abort-signal-derived-rerun-on-reconnect/_config.js new file mode 100644 index 0000000000..7caf765e3b --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/abort-signal-derived-rerun-on-reconnect/_config.js @@ -0,0 +1,32 @@ +import { test } from '../../test'; +import { tick } from 'svelte'; + +export default test({ + async test({ assert, target }) { + const [increment, toggle, resolve] = target.querySelectorAll('button'); + const [div] = target.querySelectorAll('div'); + + assert.htmlEqual(div.innerHTML, 'loading'); + resolve.click(); + await tick(); + assert.htmlEqual(div.innerHTML, '0'); + + increment.click(); + await tick(); + assert.htmlEqual(div.innerHTML, 'loading'); + + toggle.click(); + await tick(); + assert.htmlEqual(div.innerHTML, ''); + + toggle.click(); + await tick(); + assert.htmlEqual(div.innerHTML, 'loading'); + + resolve.click(); // this one's for clearing the obsolete/aborted one from the queue + await tick(); + resolve.click(); + await tick(); + assert.htmlEqual(div.innerHTML, '2'); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/abort-signal-derived-rerun-on-reconnect/main.svelte b/packages/svelte/tests/runtime-runes/samples/abort-signal-derived-rerun-on-reconnect/main.svelte new file mode 100644 index 0000000000..a047afdd44 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/abort-signal-derived-rerun-on-reconnect/main.svelte @@ -0,0 +1,32 @@ + + + + + + +
+ {#if show} + {#await double} + loading + {:then value} + {value} + {:catch} + error + {/await} + {/if} +
From d0dbe1a48013b9c405b518511f8330baf3a27bc9 Mon Sep 17 00:00:00 2001 From: Joe Schafer Date: Thu, 16 Jul 2026 04:35:37 -0700 Subject: [PATCH 019/110] 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 020/110] 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 021/110] 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 022/110] 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 023/110] 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 024/110] fix: preserve select selection with spread attributes (#18561) Fixes #18557 A ` + + + {#if show_extra} + + {/if} + From 3dde011d3a9e7b9145169da0b75dcd607a378c0e Mon Sep 17 00:00:00 2001 From: Nic Polumeyv Date: Thu, 23 Jul 2026 17:45:50 -0400 Subject: [PATCH 025/110] fix: call `onerror` and provide a working `reset` when hydrating a failed boundary (#18556) Fixes #18555 A boundary that failed during SSR hydrates via `#hydrate_failed_content`, which never calls `onerror` and passes the `failed` snippet a no-op `reset`. Both came in with #17672, whose docs say `onerror` "will be called upon hydration with the deserialized error object". Once hydrated as failed, the boundary can never leave that state. Downstream this is what keeps SvelteKit's `+error.svelte` mounted after navigating away from a server-rendered error page (sveltejs/kit#16345). This extracts the reset/onerror machinery from `#handle_error` into `#create_reset` and uses it in the hydration path too. `onerror` is invoked in a microtask because it may mutate state, which is disallowed while hydrating. `#handle_error` already invokes it asynchronously, so the timing matches the error path. The tests flip a `recovered` flag before calling `reset`, since the child would otherwise throw again. That is the intended retry pattern, and the same one kit uses when it resets route boundaries on navigation. Verified against kit end to end, hydrating a server-rendered error page and navigating away now tears it down with no kit changes needed. --- .changeset/hydrated-boundary-reset.md | 5 ++ .../internal/client/dom/blocks/boundary.js | 89 ++++++++++++------- .../error-boundary-hydrate-1/_config.js | 19 ++++ .../error-boundary-hydrate-1/child.svelte | 3 + .../error-boundary-hydrate-1/main.svelte | 29 ++++++ .../error-boundary-hydrate-2/_config.js | 16 ++++ .../error-boundary-hydrate-2/child.svelte | 3 + .../error-boundary-hydrate-2/main.svelte | 22 +++++ 8 files changed, 152 insertions(+), 34 deletions(-) create mode 100644 .changeset/hydrated-boundary-reset.md create mode 100644 packages/svelte/tests/runtime-runes/samples/error-boundary-hydrate-1/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/error-boundary-hydrate-1/child.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/error-boundary-hydrate-1/main.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/error-boundary-hydrate-2/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/error-boundary-hydrate-2/child.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/error-boundary-hydrate-2/main.svelte diff --git a/.changeset/hydrated-boundary-reset.md b/.changeset/hydrated-boundary-reset.md new file mode 100644 index 0000000000..799a57fd9b --- /dev/null +++ b/.changeset/hydrated-boundary-reset.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: call `onerror` and provide a working `reset` when hydrating a failed boundary diff --git a/packages/svelte/src/internal/client/dom/blocks/boundary.js b/packages/svelte/src/internal/client/dom/blocks/boundary.js index 0c0903ff52..4f655a3695 100644 --- a/packages/svelte/src/internal/client/dom/blocks/boundary.js +++ b/packages/svelte/src/internal/client/dom/blocks/boundary.js @@ -199,17 +199,68 @@ export class Boundary { */ #hydrate_failed_content(error) { const failed = this.#props.failed; + const { reset, invoke_onerror } = this.#create_reset(error); + + // `onerror` may mutate state, which is disallowed while hydrating + queue_micro_task(invoke_onerror); + if (!failed) return; this.#failed_effect = branch(() => { failed( this.#anchor, () => error, - () => () => {} + () => reset ); }); } + /** + * Creates the `reset` function for a failed boundary, along with a function + * that invokes `onerror` with it (if provided) + * @param {unknown} error + * @returns {{ reset: () => void, invoke_onerror: () => void }} + */ + #create_reset(error) { + var did_reset = false; + var calling_on_error = false; + + const reset = () => { + if (did_reset) { + w.svelte_boundary_reset_noop(); + return; + } + + did_reset = true; + + if (calling_on_error) { + e.svelte_boundary_reset_onerror(); + } + + if (this.#failed_effect !== null) { + pause_effect(this.#failed_effect, () => { + this.#failed_effect = null; + }); + } + + this.#run(() => { + this.#render(); + }); + }; + + const invoke_onerror = () => { + try { + calling_on_error = true; + this.#props.onerror?.(error, reset); + calling_on_error = false; + } catch (err) { + invoke_error_boundary(err, this.#effect && this.#effect.parent); + } + }; + + return { reset, invoke_onerror }; + } + #hydrate_pending_content() { const pending = this.#props.pending; if (!pending) return; @@ -429,43 +480,13 @@ export class Boundary { set_hydrate_node(skip_nodes()); } - var onerror = this.#props.onerror; let failed = this.#props.failed; - var did_reset = false; - var calling_on_error = false; - - const reset = () => { - if (did_reset) { - w.svelte_boundary_reset_noop(); - return; - } - - did_reset = true; - - if (calling_on_error) { - e.svelte_boundary_reset_onerror(); - } - - if (this.#failed_effect !== null) { - pause_effect(this.#failed_effect, () => { - this.#failed_effect = null; - }); - } - - this.#run(() => { - this.#render(); - }); - }; /** @param {unknown} transformed_error */ const handle_error_result = (transformed_error) => { - try { - calling_on_error = true; - onerror?.(transformed_error, reset); - calling_on_error = false; - } catch (error) { - invoke_error_boundary(error, this.#effect && this.#effect.parent); - } + const { reset, invoke_onerror } = this.#create_reset(transformed_error); + + invoke_onerror(); if (failed) { this.#failed_effect = this.#run(() => { diff --git a/packages/svelte/tests/runtime-runes/samples/error-boundary-hydrate-1/_config.js b/packages/svelte/tests/runtime-runes/samples/error-boundary-hydrate-1/_config.js new file mode 100644 index 0000000000..f0aef7e92f --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/error-boundary-hydrate-1/_config.js @@ -0,0 +1,19 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + mode: ['hydrate'], + ssrHtml: '

failed: error

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

recovered

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

recovered

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

failed: {error}

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

failed: error

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

recovered

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

recovered

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

failed: {error}

+ + {/snippet} +
From 23bc246a99c649c0c325509debb93e062948e240 Mon Sep 17 00:00:00 2001 From: Elliott Johnson Date: Fri, 24 Jul 2026 14:14:11 -0600 Subject: [PATCH 026/110] 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 027/110] 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 028/110] fix: skip controlled each fast path while another batch is pending (#18625) Fixes #18610 ### Problem In async mode, a controlled keyed `{#each}` throws `TypeError: Cannot read properties of undefined (reading 'e')` when its collection becomes empty while an earlier batch is still pending on the same block. The fast path in `pause_effects` cleared `state.items` and then called `destroy_effects`, which walks pending batch keys and reads `state.items.get(key).e`. Those EachItems are still needed for the pending batch (preserved offscreen), so clearing the map makes the dereference throw and aborts the commit mid-flight. ### Fix Only take the controlled-each fast path when `state.pending.size === 0`, so pending batches keep their items until they commit or discard. --- .changeset/async-each-controlled-pending.md | 5 ++ .../src/internal/client/dom/blocks/each.js | 8 ++- .../_config.js | 50 +++++++++++++++++ .../main.svelte | 54 +++++++++++++++++++ 4 files changed, 115 insertions(+), 2 deletions(-) create mode 100644 .changeset/async-each-controlled-pending.md create mode 100644 packages/svelte/tests/runtime-runes/samples/async-each-controlled-empty-pending/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-each-controlled-empty-pending/main.svelte diff --git a/.changeset/async-each-controlled-pending.md b/.changeset/async-each-controlled-pending.md new file mode 100644 index 0000000000..c37f914d13 --- /dev/null +++ b/.changeset/async-each-controlled-pending.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: skip controlled each fast path while another batch is pending diff --git a/packages/svelte/src/internal/client/dom/blocks/each.js b/packages/svelte/src/internal/client/dom/blocks/each.js index 2df1d6ffa1..9a2504f887 100644 --- a/packages/svelte/src/internal/client/dom/blocks/each.js +++ b/packages/svelte/src/internal/client/dom/blocks/each.js @@ -103,8 +103,12 @@ function pause_effects(state, to_destroy, controlled_anchor) { if (remaining === 0) { // If we're in a controlled each block (i.e. the block is the only child of an // element), and we are removing all items, _and_ there are no out transitions, - // we can use the fast path — emptying the element and replacing the anchor - var fast_path = transitions.length === 0 && controlled_anchor !== null; + // we can use the fast path — emptying the element and replacing the anchor. + // Skip the fast path when another batch is still pending on this each block: + // that batch's keys still reference EachItems in `state.items`, which + // `destroy_effects` needs to preserve offscreen (see #18610). + var fast_path = + transitions.length === 0 && controlled_anchor !== null && state.pending.size === 0; if (fast_path) { var anchor = /** @type {Element} */ (controlled_anchor); diff --git a/packages/svelte/tests/runtime-runes/samples/async-each-controlled-empty-pending/_config.js b/packages/svelte/tests/runtime-runes/samples/async-each-controlled-empty-pending/_config.js new file mode 100644 index 0000000000..29d374deec --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-each-controlled-empty-pending/_config.js @@ -0,0 +1,50 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +// Regression for #18610: emptying a controlled keyed {#each} while another +// batch is still pending must not take the fast path that clears state.items +// before destroy_effects walks pending keys. +export default test({ + mode: ['client'], + + async test({ assert, target }) { + await tick(); + + assert.htmlEqual( + target.innerHTML, + ` + + + +

A0/B0

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

A0/B1

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

{a}/{b}

+ + +
+ {#each items as item (item)} + {item} + {/each} +
From 3ed9db4ba78f61792b875b7651755a9c0a018037 Mon Sep 17 00:00:00 2001 From: Simon H <5968653+dummdidumm@users.noreply.github.com> Date: Wed, 12 Aug 2026 17:40:28 +0200 Subject: [PATCH 029/110] fix: don't duplicate comments in attributes (#18636) An attribute with a comment would be printed again atop the following attribute because we didn't take that case into account. --- .changeset/modern-otters-pick.md | 5 +++++ packages/svelte/src/compiler/print/index.js | 18 ++++++++++++------ .../comment-inside-attribute/input.svelte | 7 +++++++ .../comment-inside-attribute/output.svelte | 7 +++++++ 4 files changed, 31 insertions(+), 6 deletions(-) create mode 100644 .changeset/modern-otters-pick.md create mode 100644 packages/svelte/tests/print/samples/comment-inside-attribute/input.svelte create mode 100644 packages/svelte/tests/print/samples/comment-inside-attribute/output.svelte diff --git a/.changeset/modern-otters-pick.md b/.changeset/modern-otters-pick.md new file mode 100644 index 0000000000..de5803e19f --- /dev/null +++ b/.changeset/modern-otters-pick.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: don't duplicate comments in attributes diff --git a/packages/svelte/src/compiler/print/index.js b/packages/svelte/src/compiler/print/index.js index a5fbc96fc8..fe76f31ad3 100644 --- a/packages/svelte/src/compiler/print/index.js +++ b/packages/svelte/src/compiler/print/index.js @@ -82,6 +82,7 @@ function attributes(node, attributes, context, comments) { } const separator = context.new(); + let previous_attribute_end = node.start; const children = attributes.map((attribute) => { const child_context = context.new(); @@ -90,12 +91,16 @@ function attributes(node, attributes, context, comments) { const comment = comments[comment_index]; if (comment.start < attribute.start) { - if (comment.type === 'Line') { - child_context.write('//' + comment.value); - child_context.newline(); - } else { - child_context.write('/*' + comment.value + '*/'); // TODO match indentation? - child_context.append(separator); + // Inside a previous attribute's value can be comments which don't + // advance comment_index, therefore this additional check + if (comment.start >= previous_attribute_end) { + if (comment.type === 'Line') { + child_context.write('//' + comment.value); + child_context.newline(); + } else { + child_context.write('/*' + comment.value + '*/'); // TODO match indentation? + child_context.append(separator); + } } comment_index += 1; @@ -105,6 +110,7 @@ function attributes(node, attributes, context, comments) { } child_context.visit(attribute); + previous_attribute_end = attribute.end; length += child_context.measure() + 1; diff --git a/packages/svelte/tests/print/samples/comment-inside-attribute/input.svelte b/packages/svelte/tests/print/samples/comment-inside-attribute/input.svelte new file mode 100644 index 0000000000..4575abf75a --- /dev/null +++ b/packages/svelte/tests/print/samples/comment-inside-attribute/input.svelte @@ -0,0 +1,7 @@ +` +}); diff --git a/packages/svelte/tests/runtime-runes/samples/derived-destructured-from-derived/main.svelte b/packages/svelte/tests/runtime-runes/samples/derived-destructured-from-derived/main.svelte new file mode 100644 index 0000000000..b1882243f4 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/derived-destructured-from-derived/main.svelte @@ -0,0 +1,8 @@ + + + + From 8835003d41d23751d64e99492b7548ca554abd76 Mon Sep 17 00:00:00 2001 From: Marwan <145616984+marwan562@users.noreply.github.com> Date: Thu, 20 Aug 2026 00:13:06 +0300 Subject: [PATCH 043/110] 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 044/110] 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 045/110] 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 046/110] 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 047/110] 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 048/110] 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 049/110] 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 050/110] 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 051/110] 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 052/110] 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 053/110] 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 054/110] 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 055/110] 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 056/110] 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 057/110] 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 058/110] 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 059/110] 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 060/110] 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 061/110] 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 062/110] =?UTF-8?q?perf:=20O(n=C2=B2)=E2=86=92O(n)=20Map?= =?UTF-8?q?=20lookups=20for=20legacy=20reactive=20statements=20(#18602)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .changeset/legacy-reactive-map-lookups.md | 5 +++++ packages/svelte/src/compiler/phases/2-analyze/index.js | 3 ++- .../compiler/phases/3-transform/client/transform-client.js | 4 ++-- .../compiler/phases/3-transform/server/transform-server.js | 4 ++-- 4 files changed, 11 insertions(+), 5 deletions(-) create mode 100644 .changeset/legacy-reactive-map-lookups.md diff --git a/.changeset/legacy-reactive-map-lookups.md b/.changeset/legacy-reactive-map-lookups.md new file mode 100644 index 0000000000..ec6f4fbf9d --- /dev/null +++ b/.changeset/legacy-reactive-map-lookups.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +perf: O(n²)→O(n) Map lookups for legacy `$:` reactive statement ordering diff --git a/packages/svelte/src/compiler/phases/2-analyze/index.js b/packages/svelte/src/compiler/phases/2-analyze/index.js index f9cf2f1d89..1fd83e65b9 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/index.js +++ b/packages/svelte/src/compiler/phases/2-analyze/index.js @@ -1318,7 +1318,8 @@ function order_reactive_statements(unsorted_reactive_declarations) { * @returns */ const add_declaration = (node, declaration) => { - if ([...reactive_declarations.values()].includes(declaration)) return; + // Visited set: each ReactiveStatement is stored under exactly one LabeledStatement node + if (reactive_declarations.has(node)) return; for (const binding of declaration.dependencies) { if (declaration.assignments.has(binding)) continue; diff --git a/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js b/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js index 552fe89960..cbb5f2eac7 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js @@ -247,11 +247,11 @@ export function client_component(analysis, options) { } for (const [node] of analysis.reactive_statements) { - const statement = [...state.legacy_reactive_statements].find(([n]) => n === node); + const statement = state.legacy_reactive_statements.get(node); if (statement === undefined) { throw new Error('Could not find reactive statement'); } - instance.body.push(statement[1]); + instance.body.push(statement); } if (analysis.reactive_statements.size > 0) { diff --git a/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js b/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js index 24ca58bb41..0a533aec37 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js @@ -148,7 +148,7 @@ export function server_component(analysis, options) { const legacy_reactive_declarations = []; for (const [node] of analysis.reactive_statements) { - const statement = [...state.legacy_reactive_statements].find(([n]) => n === node); + const statement = state.legacy_reactive_statements.get(node); if (statement === undefined) { throw new Error('Could not find reactive statement'); } @@ -165,7 +165,7 @@ export function server_component(analysis, options) { } } - instance.body.push(statement[1]); + instance.body.push(statement); } if (legacy_reactive_declarations.length > 0) { From f4918d86f3f82cb03535836c329457b1a57e1fdb Mon Sep 17 00:00:00 2001 From: Jared Dunham Date: Thu, 20 Aug 2026 16:30:04 -0500 Subject: [PATCH 063/110] 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 064/110] 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 065/110] 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 066/110] 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 069/110] 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 070/110] 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 071/110] 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 072/110] 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 073/110] fix: prevent `selectedcontent` mutation from changing the selected option (#18495) Basically, in a situation like this ```svelte {}} /> ``` what happens is that the `oninput` is delegated and so it registers the global listener (which means it listen on every `oninput` not just the one from the input). When the select change, the `input` event is dispatched first, the listener runs, doesn't find an `__input` handler and returns. Now before the `change` event is emitted, the `MutationObserver` in `init_select` is triggered by the browser updating `selectedcontent` and invokes `select_option` with `select.__value`. However, since the `change` event has yet to fire, `select.__value` still has the old value, so we "reselect" that. When the change event runs, the selected option is effectively the old one and the whole thing breaks. I had to add the test in `runtime-browser` because JSDom doesn't support `selectedcontent` --- .changeset/tidy-pandas-refuse.md | 5 +++ .../client/dom/elements/bindings/select.js | 32 ++++++++++++++++--- .../_config.js | 22 +++++++++++++ .../main.svelte | 17 ++++++++++ 4 files changed, 71 insertions(+), 5 deletions(-) create mode 100644 .changeset/tidy-pandas-refuse.md create mode 100644 packages/svelte/tests/runtime-browser/samples/selectedcontent-oninput-interfere/_config.js create mode 100644 packages/svelte/tests/runtime-browser/samples/selectedcontent-oninput-interfere/main.svelte diff --git a/.changeset/tidy-pandas-refuse.md b/.changeset/tidy-pandas-refuse.md new file mode 100644 index 0000000000..7034a3e267 --- /dev/null +++ b/.changeset/tidy-pandas-refuse.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: prevent `selectedcontent` mutation from changing the selected option diff --git a/packages/svelte/src/internal/client/dom/elements/bindings/select.js b/packages/svelte/src/internal/client/dom/elements/bindings/select.js index e390e23ea0..a408613104 100644 --- a/packages/svelte/src/internal/client/dom/elements/bindings/select.js +++ b/packages/svelte/src/internal/client/dom/elements/bindings/select.js @@ -55,11 +55,14 @@ export function select_option(select, value, mounting = false) { * @param {HTMLSelectElement} select */ export function init_select(select) { - var observer = new MutationObserver(() => { - if ('__value' in select) { - // @ts-ignore - select_option(select, select.__value); - } + var observer = new MutationObserver((entries) => { + // Mutations related to `` can never affect the option list. + // Reacting to them could revert a user-initiated selection change, because the + // records are delivered as soon as any listener returns (e.g. a delegated `input` + // handler), which can happen before the `change` handler has updated `__value` + if (entries.every(is_selectedcontent_mutation) || !('__value' in select)) return; + // @ts-ignore + select_option(select, select.__value); // Deliberately don't update the potential binding value, // the model should be preserved unless explicitly changed }); @@ -164,3 +167,22 @@ function get_option_value(option) { return option.value; } } + +/** + * Returns `true` if the mutation stems from the browser mirroring the selected + * option's content into ``, or from us replacing the + * `` element with a clone of itself + * @param {MutationRecord} entry + */ +function is_selectedcontent_mutation(entry) { + if (/** @type {Element} */ (entry.target).closest('selectedcontent') !== null) { + return true; + } + + if (entry.type === 'childList') { + var nodes = [...entry.addedNodes, ...entry.removedNodes]; + return nodes.length > 0 && nodes.every((node) => node.nodeName === 'SELECTEDCONTENT'); + } + + return false; +} diff --git a/packages/svelte/tests/runtime-browser/samples/selectedcontent-oninput-interfere/_config.js b/packages/svelte/tests/runtime-browser/samples/selectedcontent-oninput-interfere/_config.js new file mode 100644 index 0000000000..d153eceeb7 --- /dev/null +++ b/packages/svelte/tests/runtime-browser/samples/selectedcontent-oninput-interfere/_config.js @@ -0,0 +1,22 @@ +import { flushSync } from 'svelte'; +import { ok, test } from '../../assert'; + +export default test({ + async test({ target, assert }) { + const select = target.querySelector('select'); + ok(select); + + select.value = 'B'; + select.dispatchEvent(new Event('input', { bubbles: true })); + + // because another element has a delegated `oninput` handler, a global `input` + // listener runs and, once it returns, a microtask checkpoint delivers the + // mutation records *before* the `change` event — we emulate that checkpoint here + await Promise.resolve(); + + select.dispatchEvent(new Event('change', { bubbles: true })); + flushSync(); + + assert.equal(select.value, 'B'); + } +}); diff --git a/packages/svelte/tests/runtime-browser/samples/selectedcontent-oninput-interfere/main.svelte b/packages/svelte/tests/runtime-browser/samples/selectedcontent-oninput-interfere/main.svelte new file mode 100644 index 0000000000..97634042ff --- /dev/null +++ b/packages/svelte/tests/runtime-browser/samples/selectedcontent-oninput-interfere/main.svelte @@ -0,0 +1,17 @@ + + + {}} /> + + + \ No newline at end of file From 8bf9ec87000fffd3805ebaac3586a0502cf92900 Mon Sep 17 00:00:00 2001 From: Barry <91018388+barry166@users.noreply.github.com> Date: Fri, 21 Aug 2026 21:45:41 +0800 Subject: [PATCH 074/110] 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 075/110] fix: route $derived teardown errors through invoke_error_boundary (#18486) Fixes #18485 Two parts to this: 1. explicitly invoke error boundary during teardown errors, else they go missing/bubble up outside the render tree 2. skip destroying/destroyed boundaries will searching a handler --------- Co-authored-by: Claude Sonnet 4.6 Co-authored-by: Simon Holthausen Co-authored-by: Simon H <5968653+dummdidumm@users.noreply.github.com> --- .changeset/boundary-teardown-error-routing.md | 5 +++ .../src/internal/client/error-handling.js | 12 +++++- .../internal/client/error-handling.test.ts | 39 +++++++++++++++++++ .../src/internal/client/reactivity/effects.js | 6 +++ .../samples/error-boundary-28/Trigger.svelte | 12 ++++++ .../samples/error-boundary-28/_config.js | 23 +++++++++++ .../samples/error-boundary-28/main.svelte | 24 ++++++++++++ 7 files changed, 119 insertions(+), 2 deletions(-) create mode 100644 .changeset/boundary-teardown-error-routing.md create mode 100644 packages/svelte/src/internal/client/error-handling.test.ts create mode 100644 packages/svelte/tests/runtime-runes/samples/error-boundary-28/Trigger.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/error-boundary-28/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/error-boundary-28/main.svelte diff --git a/.changeset/boundary-teardown-error-routing.md b/.changeset/boundary-teardown-error-routing.md new file mode 100644 index 0000000000..0b2249bf22 --- /dev/null +++ b/.changeset/boundary-teardown-error-routing.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: route $derived teardown errors through invoke_error_boundary diff --git a/packages/svelte/src/internal/client/error-handling.js b/packages/svelte/src/internal/client/error-handling.js index 7c404dd933..a46281d36c 100644 --- a/packages/svelte/src/internal/client/error-handling.js +++ b/packages/svelte/src/internal/client/error-handling.js @@ -3,7 +3,14 @@ import { DEV } from 'esm-env'; import { FILENAME } from '../../constants.js'; import { is_firefox } from './dom/operations.js'; -import { ERROR_VALUE, BOUNDARY_EFFECT, REACTION_RAN, EFFECT, DESTROYED } from './constants.js'; +import { + ERROR_VALUE, + BOUNDARY_EFFECT, + REACTION_RAN, + EFFECT, + DESTROYED, + DESTROYING +} from './constants.js'; import { define_property, get_descriptor } from '../shared/utils.js'; import { active_effect, active_reaction } from './runtime.js'; @@ -50,7 +57,8 @@ export function invoke_error_boundary(error, effect) { } while (effect !== null) { - if ((effect.f & BOUNDARY_EFFECT) !== 0) { + // Skip boundaries that are destroyed/destroying and cannot meaningfully handle the error. + if ((effect.f & BOUNDARY_EFFECT) !== 0 && (effect.f & (DESTROYED | DESTROYING)) === 0) { if ((effect.f & REACTION_RAN) === 0) { // we are still creating the boundary effect throw error; diff --git a/packages/svelte/src/internal/client/error-handling.test.ts b/packages/svelte/src/internal/client/error-handling.test.ts new file mode 100644 index 0000000000..2eb6093a2c --- /dev/null +++ b/packages/svelte/src/internal/client/error-handling.test.ts @@ -0,0 +1,39 @@ +import { assert, test } from 'vitest'; +import { BOUNDARY_EFFECT, DESTROYED, REACTION_RAN } from './constants'; +import { invoke_error_boundary } from './error-handling'; +import type { Effect } from './types'; + +test('ignores errors from a destroyed entry effect', () => { + const error = new Error('original'); + let handled = null; + const boundary = { + f: BOUNDARY_EFFECT | REACTION_RAN, + b: { error: (error: unknown) => (handled = error) }, + parent: null + } as unknown as Effect; + const effect = { f: DESTROYED, parent: boundary } as Effect; + + invoke_error_boundary(error, effect); + + assert.equal(handled, null); +}); + +test('skips destroyed boundary ancestors without masking the error', () => { + const error = new Error('original'); + let handled = null; + const live_boundary = { + f: BOUNDARY_EFFECT | REACTION_RAN, + b: { error: (error: unknown) => (handled = error) }, + parent: null + } as unknown as Effect; + const destroyed_boundary = { + f: BOUNDARY_EFFECT | DESTROYED | REACTION_RAN, + b: null, + parent: live_boundary + } as unknown as Effect; + const effect = { f: 0, parent: destroyed_boundary } as Effect; + + invoke_error_boundary(error, effect); + + assert.equal(handled, error); +}); diff --git a/packages/svelte/src/internal/client/reactivity/effects.js b/packages/svelte/src/internal/client/reactivity/effects.js index c5d195dfae..7e53138aae 100644 --- a/packages/svelte/src/internal/client/reactivity/effects.js +++ b/packages/svelte/src/internal/client/reactivity/effects.js @@ -36,6 +36,7 @@ import { MANAGED_EFFECT, DESTROYING } from '#client/constants'; +import { invoke_error_boundary } from '../error-handling.js'; import * as e from '../errors.js'; import { DEV } from 'esm-env'; import { define_property } from '../../shared/utils.js'; @@ -449,6 +450,11 @@ export function execute_effect_teardown(effect) { set_active_reaction(null); try { teardown.call(null); + } catch (error) { + // Route teardown errors through the boundary system so that a live + // ancestor can handle them. Boundaries that are + // themselves mid-teardown are skipped by invoke_error_boundary. + invoke_error_boundary(error, effect.parent); } finally { set_is_destroying_effect(previously_destroying_effect); set_active_reaction(previous_reaction); diff --git a/packages/svelte/tests/runtime-runes/samples/error-boundary-28/Trigger.svelte b/packages/svelte/tests/runtime-runes/samples/error-boundary-28/Trigger.svelte new file mode 100644 index 0000000000..46b4bd1c30 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/error-boundary-28/Trigger.svelte @@ -0,0 +1,12 @@ + + +trigger diff --git a/packages/svelte/tests/runtime-runes/samples/error-boundary-28/_config.js b/packages/svelte/tests/runtime-runes/samples/error-boundary-28/_config.js new file mode 100644 index 0000000000..4802089124 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/error-boundary-28/_config.js @@ -0,0 +1,23 @@ +import { flushSync, tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + // Regression test for https://github.com/sveltejs/svelte/issues/18485. + // A $derived that re-executes and throws during teardown should route the + // error to a live ancestor boundary. + mode: ['client'], + async test({ assert, target }) { + const [break_it, unmount] = target.querySelectorAll('button'); + + break_it.click(); + flushSync(); + + assert.doesNotThrow(() => { + unmount.click(); + flushSync(); + }); + + await tick(); + assert.htmlEqual(target.innerHTML, '

    caught

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

    caught

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

    hello

    + {/if} +
    From a6153f1d2a9168f8371149fbd775fa428c718a9b Mon Sep 17 00:00:00 2001 From: "svelte-triage-bot[bot]" <316883489+svelte-triage-bot[bot]@users.noreply.github.com> Date: Fri, 21 Aug 2026 22:16:30 +0200 Subject: [PATCH 079/110] 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 080/110] 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 081/110] fix: don't resurrect outroing elements when an ancestor block is paused and resumed (#18431) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes #15604 Given two nested blocks that both transition: ```svelte {#if fetching}

    loading

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

    loading

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

    loading

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

    loading

    +{:else} +
    + {#if shown} +
    red
    + {/if} +
    +{/if} From 2ac73663918645a20e4fe2294ff1073ca306d973 Mon Sep 17 00:00:00 2001 From: PD Shaheed Ali Khan Date: Sat, 22 Aug 2026 02:18:58 +0530 Subject: [PATCH 082/110] 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 083/110] 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 084/110] docs: Updated docs to include information on easing functions (#16070) closes #15992 This is a minor update to the easing documents. It adds an overview description for the module and short descriptions for each of the easing functions. --------- Co-authored-by: Simon H <5968653+dummdidumm@users.noreply.github.com> Co-authored-by: vercel[bot] <35613825+vercel[bot]@users.noreply.github.com> Co-authored-by: Simon Holthausen --- .../docs/98-reference/21-svelte-easing.md | 2 + packages/svelte/src/easing/index.js | 62 +++++++ packages/svelte/types/index.d.ts | 154 ++++++++++++++---- 3 files changed, 188 insertions(+), 30 deletions(-) diff --git a/documentation/docs/98-reference/21-svelte-easing.md b/documentation/docs/98-reference/21-svelte-easing.md index 3f47963cde..a2b49ff4a9 100644 --- a/documentation/docs/98-reference/21-svelte-easing.md +++ b/documentation/docs/98-reference/21-svelte-easing.md @@ -2,4 +2,6 @@ title: svelte/easing --- +This module provides a set of functions that allow you to manipulate time values in different ways. It’s particularly useful for animations when combined with the `motion` module. + > MODULE: svelte/easing diff --git a/packages/svelte/src/easing/index.js b/packages/svelte/src/easing/index.js index 08c86933ab..c2f7e63bc0 100644 --- a/packages/svelte/src/easing/index.js +++ b/packages/svelte/src/easing/index.js @@ -4,6 +4,8 @@ Distributed under MIT License https://github.com/mattdesl/eases/blob/master/LICE */ /** + * Returns value as is. + * * @param {number} t * @returns {number} */ @@ -12,6 +14,8 @@ export function linear(t) { } /** + * Rebound effect on start and end of value range. + * * @param {number} t * @returns {number} */ @@ -22,6 +26,8 @@ export function backInOut(t) { } /** + * Rebound effect on start. + * * @param {number} t * @returns {number} */ @@ -31,6 +37,8 @@ export function backIn(t) { } /** + * Rebound effect on end. + * * @param {number} t * @returns {number} */ @@ -40,6 +48,8 @@ export function backOut(t) { } /** + * Bounce effect on end. + * * @param {number} t * @returns {number} */ @@ -61,6 +71,8 @@ export function bounceOut(t) { } /** + * Bounce effect on start and end. + * * @param {number} t * @returns {number} */ @@ -69,6 +81,8 @@ export function bounceInOut(t) { } /** + * Bounce effect on start. + * * @param {number} t * @returns {number} */ @@ -77,6 +91,8 @@ export function bounceIn(t) { } /** + * Circular effect, accelerate on start, decelerate towards end. + * * @param {number} t * @returns {number} */ @@ -86,6 +102,8 @@ export function circInOut(t) { } /** + * Circular effect, accelerate on start. + * * @param {number} t * @returns {number} */ @@ -94,6 +112,8 @@ export function circIn(t) { } /** + * Circular effect, decelerate towards end. + * * @param {number} t * @returns {number} */ @@ -102,6 +122,8 @@ export function circOut(t) { } /** + * Cubic scaling, accelerate on start, decelerate towards end. + * * @param {number} t * @returns {number} */ @@ -110,6 +132,8 @@ export function cubicInOut(t) { } /** + * Cubic scaling, accelerate on start + * * @param {number} t * @returns {number} */ @@ -118,6 +142,8 @@ export function cubicIn(t) { } /** + * Cubic scaling, decelerate towards end. + * * @param {number} t * @returns {number} */ @@ -127,6 +153,8 @@ export function cubicOut(t) { } /** + * Elastic effect on start and end. + * * @param {number} t * @returns {number} */ @@ -140,6 +168,8 @@ export function elasticInOut(t) { } /** + * Elastic effect on start. + * * @param {number} t * @returns {number} */ @@ -148,6 +178,8 @@ export function elasticIn(t) { } /** + * Elastic effect on end. + * * @param {number} t * @returns {number} */ @@ -156,6 +188,8 @@ export function elasticOut(t) { } /** + * Exponential effect on start and end. + * * @param {number} t * @returns {number} */ @@ -168,6 +202,8 @@ export function expoInOut(t) { } /** + * Exponential effect on start. + * * @param {number} t * @returns {number} */ @@ -176,6 +212,8 @@ export function expoIn(t) { } /** + * Exponential effect on end. + * * @param {number} t * @returns {number} */ @@ -184,6 +222,8 @@ export function expoOut(t) { } /** + * Quadratic scaling, accelerate on start, decelerate towards end. + * * @param {number} t * @returns {number} */ @@ -195,6 +235,8 @@ export function quadInOut(t) { } /** + * Quadratic scaling, accelerate on start. + * * @param {number} t * @returns {number} */ @@ -203,6 +245,8 @@ export function quadIn(t) { } /** + * Quadratic scaling, decelerate towards end. + * * @param {number} t * @returns {number} */ @@ -211,6 +255,8 @@ export function quadOut(t) { } /** + * Quartic scaling, accelerate on start, decelerate towards end. + * * @param {number} t * @returns {number} */ @@ -219,6 +265,8 @@ export function quartInOut(t) { } /** + * Quartic scaling, accelerate on start. + * * @param {number} t * @returns {number} */ @@ -227,6 +275,8 @@ export function quartIn(t) { } /** + * Quartic scaling, decelerate towards end. + * * @param {number} t * @returns {number} */ @@ -235,6 +285,8 @@ export function quartOut(t) { } /** + * Quintic scaling, accelerate on start, decelerate towards end. + * * @param {number} t * @returns {number} */ @@ -244,6 +296,8 @@ export function quintInOut(t) { } /** + * Quintic scaling, accelerate on start. + * * @param {number} t * @returns {number} */ @@ -252,6 +306,8 @@ export function quintIn(t) { } /** + * Quintic scaling, decelerate towards end. + * * @param {number} t * @returns {number} */ @@ -260,6 +316,8 @@ export function quintOut(t) { } /** + * Sinusoidal effect, accelerate on start, decelerate towards end. + * * @param {number} t * @returns {number} */ @@ -268,6 +326,8 @@ export function sineInOut(t) { } /** + * Sinusoidal effect, accelerate on start. + * * @param {number} t * @returns {number} */ @@ -278,6 +338,8 @@ export function sineIn(t) { } /** + * Sinusoidal effect, decelerate towards end. + * * @param {number} t * @returns {number} */ diff --git a/packages/svelte/types/index.d.ts b/packages/svelte/types/index.d.ts index 96be34ef9e..dcc6dc5914 100644 --- a/packages/svelte/types/index.d.ts +++ b/packages/svelte/types/index.d.ts @@ -1875,66 +1875,160 @@ declare module 'svelte/compiler' { } declare module 'svelte/easing' { + /** + * Returns value as is. + * + * */ export function linear(t: number): number; - + /** + * Rebound effect on start and end of value range. + * + * */ export function backInOut(t: number): number; - + /** + * Rebound effect on start. + * + * */ export function backIn(t: number): number; - + /** + * Rebound effect on end. + * + * */ export function backOut(t: number): number; - + /** + * Bounce effect on end. + * + * */ export function bounceOut(t: number): number; - + /** + * Bounce effect on start and end. + * + * */ export function bounceInOut(t: number): number; - + /** + * Bounce effect on start. + * + * */ export function bounceIn(t: number): number; - + /** + * Circular effect, accelerate on start, decelerate towards end. + * + * */ export function circInOut(t: number): number; - + /** + * Circular effect, accelerate on start. + * + * */ export function circIn(t: number): number; - + /** + * Circular effect, decelerate towards end. + * + * */ export function circOut(t: number): number; - + /** + * Cubic scaling, accelerate on start, decelerate towards end. + * + * */ export function cubicInOut(t: number): number; - + /** + * Cubic scaling, accelerate on start + * + * */ export function cubicIn(t: number): number; - + /** + * Cubic scaling, decelerate towards end. + * + * */ export function cubicOut(t: number): number; - + /** + * Elastic effect on start and end. + * + * */ export function elasticInOut(t: number): number; - + /** + * Elastic effect on start. + * + * */ export function elasticIn(t: number): number; - + /** + * Elastic effect on end. + * + * */ export function elasticOut(t: number): number; - + /** + * Exponential effect on start and end. + * + * */ export function expoInOut(t: number): number; - + /** + * Exponential effect on start. + * + * */ export function expoIn(t: number): number; - + /** + * Exponential effect on end. + * + * */ export function expoOut(t: number): number; - + /** + * Quadratic scaling, accelerate on start, decelerate towards end. + * + * */ export function quadInOut(t: number): number; - + /** + * Quadratic scaling, accelerate on start. + * + * */ export function quadIn(t: number): number; - + /** + * Quadratic scaling, decelerate towards end. + * + * */ export function quadOut(t: number): number; - + /** + * Quartic scaling, accelerate on start, decelerate towards end. + * + * */ export function quartInOut(t: number): number; - + /** + * Quartic scaling, accelerate on start. + * + * */ export function quartIn(t: number): number; - + /** + * Quartic scaling, decelerate towards end. + * + * */ export function quartOut(t: number): number; - + /** + * Quintic scaling, accelerate on start, decelerate towards end. + * + * */ export function quintInOut(t: number): number; - + /** + * Quintic scaling, accelerate on start. + * + * */ export function quintIn(t: number): number; - + /** + * Quintic scaling, decelerate towards end. + * + * */ export function quintOut(t: number): number; - + /** + * Sinusoidal effect, accelerate on start, decelerate towards end. + * + * */ export function sineInOut(t: number): number; - + /** + * Sinusoidal effect, accelerate on start. + * + * */ export function sineIn(t: number): number; - + /** + * Sinusoidal effect, decelerate towards end. + * + * */ export function sineOut(t: number): number; export {}; From 4b61851ec80054c465b23603aafe2a7d69819438 Mon Sep 17 00:00:00 2001 From: "svelte-triage-bot[bot]" <316883489+svelte-triage-bot[bot]@users.noreply.github.com> Date: Mon, 24 Aug 2026 13:59:50 +0200 Subject: [PATCH 085/110] fix: clean up svelte:head anchors on unmount (#18697) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit include client-created anchors in the head effect’s DOM range remove anchors through the existing HEAD_EFFECT teardown add client and hydration regression coverage for repeated mount/unmount cycles Fixes #18695 --------- Co-authored-by: svelte-triage-bot Co-authored-by: Simon H <5968653+dummdidumm@users.noreply.github.com> --- .changeset/clean-head-anchors.md | 5 +++++ .../internal/client/dom/blocks/svelte-head.js | 8 ++++++++ .../svelte-head-anchor-cleanup/Head.svelte | 3 +++ .../svelte-head-anchor-cleanup/_config.js | 19 +++++++++++++++++++ .../svelte-head-anchor-cleanup/main.svelte | 11 +++++++++++ 5 files changed, 46 insertions(+) create mode 100644 .changeset/clean-head-anchors.md create mode 100644 packages/svelte/tests/runtime-runes/samples/svelte-head-anchor-cleanup/Head.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/svelte-head-anchor-cleanup/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/svelte-head-anchor-cleanup/main.svelte diff --git a/.changeset/clean-head-anchors.md b/.changeset/clean-head-anchors.md new file mode 100644 index 0000000000..d58283f502 --- /dev/null +++ b/.changeset/clean-head-anchors.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: remove `` anchors on unmount diff --git a/packages/svelte/src/internal/client/dom/blocks/svelte-head.js b/packages/svelte/src/internal/client/dom/blocks/svelte-head.js index 5721f7b056..d22d74b275 100644 --- a/packages/svelte/src/internal/client/dom/blocks/svelte-head.js +++ b/packages/svelte/src/internal/client/dom/blocks/svelte-head.js @@ -52,6 +52,14 @@ export function head(hash, render_fn) { block(() => { var e = branch(() => render_fn(anchor)); e.f |= HEAD_EFFECT; + + if (!hydrating) { + if (e.nodes === null) { + e.nodes = { start: anchor, end: anchor, a: null, t: null }; + } else { + e.nodes.end = anchor; + } + } }); } finally { if (was_hydrating) { diff --git a/packages/svelte/tests/runtime-runes/samples/svelte-head-anchor-cleanup/Head.svelte b/packages/svelte/tests/runtime-runes/samples/svelte-head-anchor-cleanup/Head.svelte new file mode 100644 index 0000000000..fa5ae4a349 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/svelte-head-anchor-cleanup/Head.svelte @@ -0,0 +1,3 @@ + + + diff --git a/packages/svelte/tests/runtime-runes/samples/svelte-head-anchor-cleanup/_config.js b/packages/svelte/tests/runtime-runes/samples/svelte-head-anchor-cleanup/_config.js new file mode 100644 index 0000000000..57ef735497 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/svelte-head-anchor-cleanup/_config.js @@ -0,0 +1,19 @@ +import { flushSync } from 'svelte'; +import { ok, test } from '../../test'; + +export default test({ + mode: ['client', 'hydrate'], + + test({ assert, target, window }) { + const initial = window.document.head.childNodes.length; + const button = target.querySelector('button'); + ok(button); + + for (let i = 0; i < 3; i++) { + flushSync(() => button.click()); + flushSync(() => button.click()); + } + + assert.equal(window.document.head.childNodes.length, initial); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/svelte-head-anchor-cleanup/main.svelte b/packages/svelte/tests/runtime-runes/samples/svelte-head-anchor-cleanup/main.svelte new file mode 100644 index 0000000000..f4cfefbadf --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/svelte-head-anchor-cleanup/main.svelte @@ -0,0 +1,11 @@ + + + + +{#if show} + +{/if} From 721244ad3ba75675414521d02af8581dcb5902c6 Mon Sep 17 00:00:00 2001 From: "svelte-triage-bot[bot]" <316883489+svelte-triage-bot[bot]@users.noreply.github.com> Date: Mon, 24 Aug 2026 14:14:18 +0200 Subject: [PATCH 086/110] fix: track SvelteDate snapshots in reactions (#18700) Read the Date value through `getTime()` before cloning it to get reactive updates from `SvelteDate`. Closes #18698 --------- Co-authored-by: svelte-triage-bot Co-authored-by: Simon H <5968653+dummdidumm@users.noreply.github.com> --- .changeset/calm-dates-track.md | 5 +++++ packages/svelte/src/internal/shared/clone.js | 2 ++ .../samples/state-snapshot-svelte-date/_config.js | 13 +++++++++++++ .../samples/state-snapshot-svelte-date/main.svelte | 9 +++++++++ 4 files changed, 29 insertions(+) create mode 100644 .changeset/calm-dates-track.md create mode 100644 packages/svelte/tests/runtime-runes/samples/state-snapshot-svelte-date/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/state-snapshot-svelte-date/main.svelte diff --git a/.changeset/calm-dates-track.md b/.changeset/calm-dates-track.md new file mode 100644 index 0000000000..e7275ae556 --- /dev/null +++ b/.changeset/calm-dates-track.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: track SvelteDate snapshots in reactions diff --git a/packages/svelte/src/internal/shared/clone.js b/packages/svelte/src/internal/shared/clone.js index 1a83adf8c5..ba0c731047 100644 --- a/packages/svelte/src/internal/shared/clone.js +++ b/packages/svelte/src/internal/shared/clone.js @@ -105,6 +105,8 @@ function clone(value, cloned, path, paths, original = null, no_tojson = false) { } if (value instanceof Date) { + // Ensure SvelteDate snapshots are tracked + value.getTime(); return /** @type {Snapshot} */ (structuredClone(value)); } diff --git a/packages/svelte/tests/runtime-runes/samples/state-snapshot-svelte-date/_config.js b/packages/svelte/tests/runtime-runes/samples/state-snapshot-svelte-date/_config.js new file mode 100644 index 0000000000..53d8e1d4ff --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/state-snapshot-svelte-date/_config.js @@ -0,0 +1,13 @@ +import { test } from '../../test'; +import { tick } from 'svelte'; + +export default test({ + async test({ assert, target }) { + assert.htmlEqual(target.innerHTML, '

    1970-01-01T00:00:00.000Z

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

    1970-01-02T00:00:00.000Z

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

    {snapshot.toISOString()}

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

    pending

    1

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

    pending

    +{/await} + +

    {foreign}

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

    4 1

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

    {total} {foreign}

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

    3 1

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

    {length} {foreign}

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

    failed

    1

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

    failed

    + {/snippet} + +

    {(await input).value}

    +
    + +

    {foreign}

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

    named fallback

    + fallback +

    named fallback

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

    named fallback

    + fallback +

    named fallback

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

    default fallback content

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

    foo fallback content

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

    Hello {name}!

    +fallback