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