fix: chain preprocessor sourcemaps with an empty sources[0] (#18518)

## 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.
pull/18523/head
Ivan 6 days ago committed by GitHub
parent 41642b70ee
commit 5edd8b0602
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: chain preprocessor sourcemaps with an empty `sources[0]` instead of dropping them

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

@ -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' }]
});

@ -0,0 +1,6 @@
<script>
let count = 0;
let doubled = count * 2;
</script>
<button>clicks: {count}</button>
Loading…
Cancel
Save