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] 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 @@
+
+
+