Skip to content

Commit 1deccb0

Browse files
authored
Merge multi-source output sourcemaps (#14246)
Surprisingly, Babel allows a transformer to mark the source file of a node to allow it to be sourced from any file. When this happens, the output sourcemap will contain multiple `sources`. I didn't realize this when I created #14209, and this `remapping` will throw an error if the output map has multiple sources. This can be fixed by using `remapping`'s graph building API (don't pass an array). This allows us to return an input map for _any_ source file, and we just need some special handling to figure out which source is our transformed file. This actually adds a new feature, allowing us to remap these multi-source outputs. Previously, the merging would silently fail and generate a blank (no `mappings`) sourcemap. That's not great. The new behavior will properly merge the maps, provided we can figure out which source is the transformed file (which should always work, I can't think of a case it wouldn't). Fixes ampproject/remapping#159.
1 parent 89e26a0 commit 1deccb0

8 files changed

Lines changed: 145 additions & 9 deletions

File tree

packages/babel-core/src/transformation/file/generate.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,18 +14,14 @@ export default function generateCode(
1414
outputMap: SourceMap | null;
1515
} {
1616
const { opts, ast, code, inputMap } = file;
17+
const { generatorOpts } = opts;
1718

1819
const results = [];
1920
for (const plugins of pluginPasses) {
2021
for (const plugin of plugins) {
2122
const { generatorOverride } = plugin;
2223
if (generatorOverride) {
23-
const result = generatorOverride(
24-
ast,
25-
opts.generatorOpts,
26-
code,
27-
generate,
28-
);
24+
const result = generatorOverride(ast, generatorOpts, code, generate);
2925

3026
if (result !== undefined) results.push(result);
3127
}
@@ -34,7 +30,7 @@ export default function generateCode(
3430

3531
let result;
3632
if (results.length === 0) {
37-
result = generate(ast, opts.generatorOpts, code);
33+
result = generate(ast, generatorOpts, code);
3834
} else if (results.length === 1) {
3935
result = results[0];
4036

@@ -53,7 +49,11 @@ export default function generateCode(
5349
let { code: outputCode, map: outputMap } = result;
5450

5551
if (outputMap && inputMap) {
56-
outputMap = mergeSourceMap(inputMap.toObject(), outputMap);
52+
outputMap = mergeSourceMap(
53+
inputMap.toObject(),
54+
outputMap,
55+
generatorOpts.sourceFileName,
56+
);
5757
}
5858

5959
if (opts.sourceMaps === "inline" || opts.sourceMaps === "both") {

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

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,79 @@ import remapping from "@ampproject/remapping";
44
export default function mergeSourceMap(
55
inputMap: SourceMap,
66
map: SourceMap,
7+
source: string,
78
): SourceMap {
8-
const result = remapping([rootless(map), rootless(inputMap)], () => null);
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);
23+
}
24+
} else {
25+
result = mergeSingleSource(inputMap, map);
26+
}
927

1028
if (typeof inputMap.sourceRoot === "string") {
1129
result.sourceRoot = inputMap.sourceRoot;
1230
}
1331
return result;
1432
}
1533

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+
1680
function rootless(map: SourceMap): SourceMap {
1781
return {
1882
...map,

packages/babel-core/test/fixtures/transformation/source-maps/input-source-map-multiple-output-sources/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/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.
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"inputSourceMap": true,
3+
"plugins": ["./plugin.js"]
4+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
"bar";
2+
3+
function foo(bar) {
4+
throw new Error('Intentional.');
5+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
module.exports = function (babel) {
2+
const { types: t } = babel;
3+
4+
return {
5+
visitor: {
6+
CallExpression(path) {
7+
const { file } = this;
8+
const { sourceFileName } = file.opts.generatorOpts;
9+
const callee = path.node;
10+
const { loc } = callee;
11+
12+
// This filename will cause a second source file to be generated in the
13+
// output sourcemap.
14+
loc.filename = "test.js";
15+
loc.start.column = 1;
16+
loc.end.column = 4;
17+
18+
const node = t.stringLiteral('bar');
19+
node.loc = loc;
20+
path.replaceWith(node);
21+
22+
// This injects the sourcesContent, though I don't imagine anyone's
23+
// doing it.
24+
file.code = {
25+
[sourceFileName]: file.code,
26+
'test.js': '<bar />',
27+
};
28+
path.stop();
29+
},
30+
},
31+
};
32+
};
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
{
2+
"mappings": "AAAC;;ACCD,SAASA,GAAT,CAAaC,GAAb,EAAwB;AACpB,QAAM,IAAIC,KAAJ,CAAU,cAAV,CAAN;AACH",
3+
"names": [
4+
"foo",
5+
"bar",
6+
"Error"
7+
],
8+
"sources": [
9+
"test.js",
10+
"input.tsx"
11+
],
12+
"sourcesContent": [
13+
"<bar />",
14+
"foo(1);\nfunction foo(bar: number): never {\n throw new Error('Intentional.');\n}"
15+
],
16+
"version": 3
17+
}

0 commit comments

Comments
 (0)