Skip to content

Commit

Permalink
Support merging sourcemaps when transformed file is fully replaced (#…
Browse files Browse the repository at this point in the history
…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 remapping@2.1.0 to support source location override
  • Loading branch information
jridgewell committed Feb 8, 2022
1 parent a88bd30 commit 76dd491
Show file tree
Hide file tree
Showing 16 changed files with 183 additions and 78 deletions.
2 changes: 1 addition & 1 deletion packages/babel-core/package.json
Expand Up @@ -48,7 +48,7 @@
"./src/transformation/util/clone-deep.ts": "./src/transformation/util/clone-deep-browser.ts"
},
"dependencies": {
"@ampproject/remapping": "^2.0.0",
"@ampproject/remapping": "^2.1.0",
"@babel/code-frame": "workspace:^",
"@babel/generator": "workspace:^",
"@babel/helper-compilation-targets": "workspace:^",
Expand Down
76 changes: 13 additions & 63 deletions packages/babel-core/src/transformation/file/merge-map.ts
Expand Up @@ -6,77 +6,27 @@ export default function mergeSourceMap(
map: SourceMap,
source: string,
): SourceMap {
const outputSources = map.sources;

let result;
if (outputSources.length > 1) {
// When there are multiple output sources, we can't always be certain which
// source represents the file we just transformed.
const index = outputSources.indexOf(source);

// If we can't find the source, we fall back to the legacy behavior of
// outputting an empty sourcemap.
if (index === -1) {
result = emptyMap(inputMap);
} else {
result = mergeMultiSource(inputMap, map, index);
const result = remapping(rootless(map), (s, ctx) => {
if (s === source) {
// We empty the source location, which will prevent the sourcemap from
// becoming relative to the input's location. Eg, if we're transforming a
// file 'foo/bar.js', and it is a transformation of a `baz.js` file in the
// same directory, the expected output is just `baz.js`. Without this step,
// it would become `foo/baz.js`.
ctx.source = "";

return rootless(inputMap);
}
} else {
result = mergeSingleSource(inputMap, map);
}

return null;
});

if (typeof inputMap.sourceRoot === "string") {
result.sourceRoot = inputMap.sourceRoot;
}
return result;
}

// A single source transformation is the default, and easiest to handle.
function mergeSingleSource(inputMap: SourceMap, map: SourceMap): SourceMap {
return remapping([rootless(map), rootless(inputMap)], () => null);
}

// Transformation generated an output from multiple source files. When this
// happens, it's ambiguous which source was the transformed file, and which
// source is from the transformation process. We use remapping's multisource
// behavior, returning the input map when we encounter the transformed file.
function mergeMultiSource(inputMap: SourceMap, map: SourceMap, index: number) {
// We empty the source index, which will prevent the sourcemap from becoming
// relative the the input's location. Eg, if we're transforming a file
// 'foo/bar.js', and it is a transformation of a `baz.js` file in the same
// directory, the expected output is just `baz.js`. Without this step, it
// would become `foo/baz.js`.
map.sources[index] = "";

let count = 0;
return remapping(rootless(map), () => {
if (count++ === index) return rootless(inputMap);
return null;
});
}

// Legacy behavior of the old merger was to output a sourcemap without any
// mappings but with copied sourcesContent. This only happens if there are
// multiple output files and it's ambiguous which one is the transformed file.
function emptyMap(inputMap: SourceMap) {
const inputSources = inputMap.sources;

const sources = [];
const sourcesContent = inputMap.sourcesContent?.filter((content, i) => {
if (typeof content !== "string") return false;

sources.push(inputSources[i]);
return true;
});

return {
...inputMap,
sources,
sourcesContent,
mappings: "",
};
}

function rootless(map: SourceMap): SourceMap {
return {
...map,
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

@@ -0,0 +1,4 @@
{
"inputSourceMap": true,
"plugins": ["./plugin.js"]
}
@@ -0,0 +1,2 @@
"bar";
"baz";
@@ -0,0 +1,52 @@
module.exports = function (babel) {
const { types: t } = babel;

return {
visitor: {
Program(path) {
const { file } = this;
const { sourceFileName } = file.opts.generatorOpts;

// This injects the sourcesContent, though I don't imagine anyone's
// doing it.
file.code = {
[sourceFileName]: file.code,
'bar.js': '<bar />',
'baz.js': 'baz();',
};
},

CallExpression(path) {
const callee = path.node;
const { loc } = callee;

// This filename will cause a second source file to be generated in the
// output sourcemap.
loc.filename = "bar.js";
loc.start.column = 1;
loc.end.column = 4;

const node = t.stringLiteral('bar');
node.loc = loc;
path.replaceWith(node);
},

Function(path) {
const callee = path.node;
const { loc } = callee;

// This filename will cause a second source file to be generated in the
// output sourcemap.
loc.filename = "baz.js";
loc.start.column = 0;
loc.start.line = 1;
loc.end.column = 3;
loc.end.line = 1;

const node = t.stringLiteral('baz');
node.loc = loc;
path.replaceWith(node);
},
},
};
};
@@ -0,0 +1,13 @@
{
"mappings": "AAAC;ACAD,K",
"names": [],
"sources": [
"bar.js",
"baz.js"
],
"sourcesContent": [
"<bar />",
"baz();"
],
"version": 3
}
Expand Up @@ -20,7 +20,7 @@ module.exports = function (babel) {
path.replaceWith(node);

// This injects the sourcesContent, though I don't imagine anyone's
// doing it.
// doing it.
file.code = {
[sourceFileName]: file.code,
'test.js': '<bar />',
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

@@ -0,0 +1,4 @@
{
"inputSourceMap": true,
"plugins": ["./plugin.js"]
}
@@ -0,0 +1 @@
"bar";
@@ -0,0 +1,38 @@
module.exports = function (babel) {
const { types: t } = babel;

return {
visitor: {
Program(path) {
const { file } = this;
const { sourceFileName } = file.opts.generatorOpts;

// This injects the sourcesContent, though I don't imagine anyone's
// doing it.
file.code = {
[sourceFileName]: file.code,
'test.js': '<bar />',
};
},

CallExpression(path) {
const callee = path.node;
const { loc } = callee;

// This filename will cause a second source file to be generated in the
// output sourcemap.
loc.filename = "test.js";
loc.start.column = 1;
loc.end.column = 4;

const node = t.stringLiteral('bar');
node.loc = loc;
path.replaceWith(node);
},

Function(path) {
path.remove();
},
},
};
};
@@ -0,0 +1,7 @@
{
"mappings": "AAAC",
"names": [],
"sources": ["test.js"],
"sourcesContent": ["<bar />"],
"version": 3
}
32 changes: 19 additions & 13 deletions yarn.lock
Expand Up @@ -5,13 +5,12 @@ __metadata:
version: 5
cacheKey: 8

"@ampproject/remapping@npm:^2.0.0":
version: 2.0.2
resolution: "@ampproject/remapping@npm:2.0.2"
"@ampproject/remapping@npm:^2.0.0, @ampproject/remapping@npm:^2.1.0":
version: 2.1.0
resolution: "@ampproject/remapping@npm:2.1.0"
dependencies:
"@jridgewell/trace-mapping": ^0.2.2
sourcemap-codec: 1.4.8
checksum: 5759df3715f0291cbf97099a9bb7202201a1a267e232ee1505418c768b9ae7281cd550b1da563a12808a06529eb1298744a6cabde21ac354fc8450044c7f2213
"@jridgewell/trace-mapping": ^0.3.0
checksum: 10ff0d4a559f930082f1a4c1b68dc521d5b1a75e0b8ab4829e9eedf6621386893e4a008f0db6c716f64db5d8eed49c0abcfbf3bd6ff11d5a00312454a9351ed4
languageName: node
linkType: hard

Expand Down Expand Up @@ -323,7 +322,7 @@ __metadata:
version: 0.0.0-use.local
resolution: "@babel/core@workspace:packages/babel-core"
dependencies:
"@ampproject/remapping": ^2.0.0
"@ampproject/remapping": ^2.1.0
"@babel/code-frame": "workspace:^"
"@babel/generator": "workspace:^"
"@babel/helper-compilation-targets": "workspace:^"
Expand Down Expand Up @@ -4091,13 +4090,20 @@ __metadata:
languageName: node
linkType: hard

"@jridgewell/trace-mapping@npm:^0.2.2":
version: 0.2.5
resolution: "@jridgewell/trace-mapping@npm:0.2.5"
"@jridgewell/sourcemap-codec@npm:^1.4.10":
version: 1.4.10
resolution: "@jridgewell/sourcemap-codec@npm:1.4.10"
checksum: 247229218edbe165dcf0a5ae0c4b81bff1b5438818bb09221f756681fe158597fdf25c2a803f9260453b299c98c7e01ddebeb1555cda3157d987cd22c08605ef
languageName: node
linkType: hard

"@jridgewell/trace-mapping@npm:^0.3.0":
version: 0.3.2
resolution: "@jridgewell/trace-mapping@npm:0.3.2"
dependencies:
"@jridgewell/resolve-uri": ^3.0.3
sourcemap-codec: 1.4.8
checksum: 7ac0a2992f4a8d16c1cbe03bccbcfbd1e96bf5071b0a794dd97904a7588cc248b73e8091fabcb13dac8f40bc51297ee4ef98a6a870ca5a4dfb8e2dcbf6f33956
"@jridgewell/sourcemap-codec": ^1.4.10
checksum: b58be6b4133cbcb20bfd28c9ca843b8db9efa0bf1d7e0e9e26b2228dace94ad53161c996ab1d762d7c3955dfc398a7734e7b84a2493ae36b451f232234fbb257
languageName: node
linkType: hard

Expand Down Expand Up @@ -14168,7 +14174,7 @@ fsevents@^1.2.7:
languageName: node
linkType: hard

"sourcemap-codec@npm:1.4.8, sourcemap-codec@npm:^1.4.4":
"sourcemap-codec@npm:^1.4.4":
version: 1.4.8
resolution: "sourcemap-codec@npm:1.4.8"
checksum: b57981c05611afef31605732b598ccf65124a9fcb03b833532659ac4d29ac0f7bfacbc0d6c5a28a03e84c7510e7e556d758d0bb57786e214660016fb94279316
Expand Down

0 comments on commit 76dd491

Please sign in to comment.