pull/18882/merge
Wu Shuwen 3 days ago committed by GitHub
commit 07b51edf45
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: correctly chain upstream plugin sourcemaps whose `sources` contain full paths (e.g. from `MagicString.generateMap({ source: id })`) by normalizing source entries to the file basename before remapping

@ -12,6 +12,24 @@ import {
import { decode_map } from './decode_sourcemap.js';
import { replace_in_code, slice_source } from './replace_in_code.js';
/**
* Normalize the `sources` array of a preprocessor source map so that any entry referring to
* the file being compiled (by basename match — e.g. a full or relative path) is replaced with
* `file_basename`. This mirrors the normalization done in `apply_preprocessor_sourcemap` for
* upstream plugin maps passed via `compileOptions.sourcemap`.
* @param {DecodedSourceMap | undefined} decoded_map
* @param {string} file_basename
*/
function normalize_preprocessor_sources(decoded_map, file_basename) {
if (!decoded_map?.sources) return;
for (let i = 0; i < decoded_map.sources.length; i++) {
const source = decoded_map.sources[i];
if (!source || get_basename(source) === file_basename) {
decoded_map.sources[i] = file_basename;
}
}
}
/**
* Represents intermediate states of the preprocessing.
* Implements the Source interface.
@ -115,6 +133,7 @@ function processed_content_to_code(processed, location, file_basename) {
decoded_map = decode_map(processed);
// decoded map may not have sources for empty maps like `{ mappings: '' }`
if (decoded_map?.sources) {
normalize_preprocessor_sources(decoded_map, file_basename);
// offset only segments pointing at original component source
const source_index = decoded_map.sources.indexOf(file_basename);
if (source_index !== -1) {
@ -311,14 +330,19 @@ async function process_markup(process, source) {
filename: source.filename
});
if (processed) {
/** @type {any} */
let map = undefined;
if (processed.map) {
// TODO: can we use decode_sourcemap?
map = typeof processed.map === 'string' ? JSON.parse(processed.map) : processed.map;
if (source.file_basename && map.sources) {
map = { ...map, sources: map.sources.slice() };
normalize_preprocessor_sources(map, source.file_basename);
}
}
return {
string: processed.code,
map: processed.map
? // TODO: can we use decode_sourcemap?
typeof processed.map === 'string'
? JSON.parse(processed.map)
: processed.map
: undefined,
map,
dependencies: processed.dependencies
};
} else {

@ -323,12 +323,24 @@ 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];
// Normalize source entries in the incoming map so they match the filename (basename) that
// svelte uses in its own map, otherwise `@jridgewell/remapping` can't chain the two maps.
// - A missing/empty `sources[i]` (e.g. from `new MagicString(code).generateMap()` without a
// `source` option) is treated as "this file", matching Vite's behavior in pluginContainer.
// - A full path or relative path whose basename equals `filename` (e.g. from
// `generateMap({ source: id })` where `id` is an absolute Vite module id like
// `/project/src/App.svelte`) also refers to this file, but a strict string comparison
// against the basename fails. Normalize it to `filename` so the chain matches.
if (preprocessor_map.sources) {
preprocessor_map.sources = preprocessor_map.sources.map((source) => {
if (!source) return filename;
if (source === filename) return source;
// If the source's basename matches `filename`, treat it as referring to this file.
// This handles full paths, relative paths with directories, etc. that upstream
// tools (Vite, MagicString with `{source: id}`) commonly produce.
if (get_basename(source) === filename) return filename;
return source;
});
}
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,

@ -0,0 +1,23 @@
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 calls `generateMap({ source: id })`
// where `id` is an absolute/fully-qualified module id (as Vite does). The source entry in
// the resulting map is a full path (e.g. `/project/src/App.svelte`) rather than a basename,
// which previously failed to match during map chaining (see #18778) and caused the upstream
// mappings to be silently dropped, producing wrong devtools/stack-trace positions.
const input = fs.readFileSync(new URL('./input.svelte', import.meta.url), 'utf-8');
const src = new MagicString(input);
// Perform a no-op transform but generate the map with a full absolute path as `source`
// (basename is still `input.svelte`), reproducing the scenario from the bug report where
// the reporter tried `m.generateMap({source: id})`, `m.generateMap({file: id})`, etc.
const simulated_vite_id = '/fake/project/path/src/input.svelte';
export default test({
compileOptions: {
sourcemap: src.generateMap({ source: simulated_vite_id, 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