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
A follow-up to babel#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.
  • Loading branch information
jridgewell committed Feb 7, 2022
1 parent 1deccb0 commit 5031a53
Show file tree
Hide file tree
Showing 14 changed files with 169 additions and 61 deletions.
79 changes: 19 additions & 60 deletions packages/babel-core/src/transformation/file/merge-map.ts
Expand Up @@ -8,73 +8,32 @@ export default function mergeSourceMap(
): 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);
}
} else {
result = mergeSingleSource(inputMap, map);
const index = outputSources.indexOf(source);

if (index > -1) {
// We empty the source index, 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`.
map.sources[index] = "";
}

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), () => {
const result = remapping(rootless(map), () => {
// Return the inputMap only when we hit the correct source index, so that
// remapping will trace through it into the original locations. Remapping
// calls the loader cb for each source in depth-first order, and since
// we're not returning any other sourcemaps, the source is guaranteed to be
// the index'th call.
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: "",
};
if (typeof inputMap.sourceRoot === "string") {
result.sourceRoot = inputMap.sourceRoot;
}
return result;
}

function rootless(map: SourceMap): SourceMap {
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
}

0 comments on commit 5031a53

Please sign in to comment.