From 8fb7ceeba5dd1d3b7fc3f587f4e2138bf1b978ed Mon Sep 17 00:00:00 2001 From: Paolo Ricciuti Date: Mon, 6 Jul 2026 13:50:56 +0200 Subject: [PATCH 1/3] 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 2/3] 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 3/3] 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 @@ + + +