Skip to content

Commit 76dd491

Browse files
authored
Support merging sourcemaps when transformed file is fully replaced (#14247)
* Support merging sourcemaps when transformed file is fully replaced A follow-up to #14246. I finally thought of a case where the sourceFileName's content is fully replaced, leading to only the injected file existing in the output sourcemap. This can happen where only one output source is created, or multiple. In the single output source case, we'd incorrectly associate the mappings through the inputMap, even though none of the content actually comes from there. In the multiple source case, we'd silently fail and output empty mappings. Both cases are now fixed, with the correct remapping being done through in all possible output cases now. * Update to [email protected] to support source location override
1 parent a88bd30 commit 76dd491

16 files changed

Lines changed: 183 additions & 78 deletions

File tree

packages/babel-core/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@
4848
"./src/transformation/util/clone-deep.ts": "./src/transformation/util/clone-deep-browser.ts"
4949
},
5050
"dependencies": {
51-
"@ampproject/remapping": "^2.0.0",
51+
"@ampproject/remapping": "^2.1.0",
5252
"@babel/code-frame": "workspace:^",
5353
"@babel/generator": "workspace:^",
5454
"@babel/helper-compilation-targets": "workspace:^",

packages/babel-core/src/transformation/file/merge-map.ts

Lines changed: 13 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -6,77 +6,27 @@ export default function mergeSourceMap(
66
map: SourceMap,
77
source: string,
88
): SourceMap {
9-
const outputSources = map.sources;
10-
11-
let result;
12-
if (outputSources.length > 1) {
13-
// When there are multiple output sources, we can't always be certain which
14-
// source represents the file we just transformed.
15-
const index = outputSources.indexOf(source);
16-
17-
// If we can't find the source, we fall back to the legacy behavior of
18-
// outputting an empty sourcemap.
19-
if (index === -1) {
20-
result = emptyMap(inputMap);
21-
} else {
22-
result = mergeMultiSource(inputMap, map, index);
9+
const result = remapping(rootless(map), (s, ctx) => {
10+
if (s === source) {
11+
// We empty the source location, which will prevent the sourcemap from
12+
// becoming relative to the input's location. Eg, if we're transforming a
13+
// file 'foo/bar.js', and it is a transformation of a `baz.js` file in the
14+
// same directory, the expected output is just `baz.js`. Without this step,
15+
// it would become `foo/baz.js`.
16+
ctx.source = "";
17+
18+
return rootless(inputMap);
2319
}
24-
} else {
25-
result = mergeSingleSource(inputMap, map);
26-
}
20+
21+
return null;
22+
});
2723

2824
if (typeof inputMap.sourceRoot === "string") {
2925
result.sourceRoot = inputMap.sourceRoot;
3026
}
3127
return result;
3228
}
3329

34-
// A single source transformation is the default, and easiest to handle.
35-
function mergeSingleSource(inputMap: SourceMap, map: SourceMap): SourceMap {
36-
return remapping([rootless(map), rootless(inputMap)], () => null);
37-
}
38-
39-
// Transformation generated an output from multiple source files. When this
40-
// happens, it's ambiguous which source was the transformed file, and which
41-
// source is from the transformation process. We use remapping's multisource
42-
// behavior, returning the input map when we encounter the transformed file.
43-
function mergeMultiSource(inputMap: SourceMap, map: SourceMap, index: number) {
44-
// We empty the source index, which will prevent the sourcemap from becoming
45-
// relative the the input's location. Eg, if we're transforming a file
46-
// 'foo/bar.js', and it is a transformation of a `baz.js` file in the same
47-
// directory, the expected output is just `baz.js`. Without this step, it
48-
// would become `foo/baz.js`.
49-
map.sources[index] = "";
50-
51-
let count = 0;
52-
return remapping(rootless(map), () => {
53-
if (count++ === index) return rootless(inputMap);
54-
return null;
55-
});
56-
}
57-
58-
// Legacy behavior of the old merger was to output a sourcemap without any
59-
// mappings but with copied sourcesContent. This only happens if there are
60-
// multiple output files and it's ambiguous which one is the transformed file.
61-
function emptyMap(inputMap: SourceMap) {
62-
const inputSources = inputMap.sources;
63-
64-
const sources = [];
65-
const sourcesContent = inputMap.sourcesContent?.filter((content, i) => {
66-
if (typeof content !== "string") return false;
67-
68-
sources.push(inputSources[i]);
69-
return true;
70-
});
71-
72-
return {
73-
...inputMap,
74-
sources,
75-
sourcesContent,
76-
mappings: "",
77-
};
78-
}
79-
8030
function rootless(map: SourceMap): SourceMap {
8131
return {
8232
...map,

packages/babel-core/test/fixtures/transformation/source-maps/input-source-map-multiple-output-sources-complete-replace/input.js

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/babel-core/test/fixtures/transformation/source-maps/input-source-map-multiple-output-sources-complete-replace/input.js.map

Lines changed: 9 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"inputSourceMap": true,
3+
"plugins": ["./plugin.js"]
4+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
"bar";
2+
"baz";
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
module.exports = function (babel) {
2+
const { types: t } = babel;
3+
4+
return {
5+
visitor: {
6+
Program(path) {
7+
const { file } = this;
8+
const { sourceFileName } = file.opts.generatorOpts;
9+
10+
// This injects the sourcesContent, though I don't imagine anyone's
11+
// doing it.
12+
file.code = {
13+
[sourceFileName]: file.code,
14+
'bar.js': '<bar />',
15+
'baz.js': 'baz();',
16+
};
17+
},
18+
19+
CallExpression(path) {
20+
const callee = path.node;
21+
const { loc } = callee;
22+
23+
// This filename will cause a second source file to be generated in the
24+
// output sourcemap.
25+
loc.filename = "bar.js";
26+
loc.start.column = 1;
27+
loc.end.column = 4;
28+
29+
const node = t.stringLiteral('bar');
30+
node.loc = loc;
31+
path.replaceWith(node);
32+
},
33+
34+
Function(path) {
35+
const callee = path.node;
36+
const { loc } = callee;
37+
38+
// This filename will cause a second source file to be generated in the
39+
// output sourcemap.
40+
loc.filename = "baz.js";
41+
loc.start.column = 0;
42+
loc.start.line = 1;
43+
loc.end.column = 3;
44+
loc.end.line = 1;
45+
46+
const node = t.stringLiteral('baz');
47+
node.loc = loc;
48+
path.replaceWith(node);
49+
},
50+
},
51+
};
52+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"mappings": "AAAC;ACAD,K",
3+
"names": [],
4+
"sources": [
5+
"bar.js",
6+
"baz.js"
7+
],
8+
"sourcesContent": [
9+
"<bar />",
10+
"baz();"
11+
],
12+
"version": 3
13+
}

packages/babel-core/test/fixtures/transformation/source-maps/input-source-map-multiple-output-sources/plugin.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ module.exports = function (babel) {
2020
path.replaceWith(node);
2121

2222
// This injects the sourcesContent, though I don't imagine anyone's
23-
// doing it.
23+
// doing it.
2424
file.code = {
2525
[sourceFileName]: file.code,
2626
'test.js': '<bar />',

packages/babel-core/test/fixtures/transformation/source-maps/input-source-map-sources-complete-replace/input.js

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)