Skip to content

Commit

Permalink
Merge multi-source output sourcemaps (#14246)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
jridgewell committed Feb 6, 2022
1 parent 89e26a0 commit 1deccb0
Show file tree
Hide file tree
Showing 8 changed files with 145 additions and 9 deletions.
16 changes: 8 additions & 8 deletions packages/babel-core/src/transformation/file/generate.ts
Expand Up @@ -14,18 +14,14 @@ export default function generateCode(
outputMap: SourceMap | null;
} {
const { opts, ast, code, inputMap } = file;
const { generatorOpts } = opts;

const results = [];
for (const plugins of pluginPasses) {
for (const plugin of plugins) {
const { generatorOverride } = plugin;
if (generatorOverride) {
const result = generatorOverride(
ast,
opts.generatorOpts,
code,
generate,
);
const result = generatorOverride(ast, generatorOpts, code, generate);

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

let result;
if (results.length === 0) {
result = generate(ast, opts.generatorOpts, code);
result = generate(ast, generatorOpts, code);
} else if (results.length === 1) {
result = results[0];

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

if (outputMap && inputMap) {
outputMap = mergeSourceMap(inputMap.toObject(), outputMap);
outputMap = mergeSourceMap(
inputMap.toObject(),
outputMap,
generatorOpts.sourceFileName,
);
}

if (opts.sourceMaps === "inline" || opts.sourceMaps === "both") {
Expand Down
66 changes: 65 additions & 1 deletion packages/babel-core/src/transformation/file/merge-map.ts
Expand Up @@ -4,15 +4,79 @@ import remapping from "@ampproject/remapping";
export default function mergeSourceMap(
inputMap: SourceMap,
map: SourceMap,
source: string,
): SourceMap {
const result = remapping([rootless(map), rootless(inputMap)], () => null);
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);
}

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,5 @@
"bar";

function foo(bar) {
throw new Error('Intentional.');
}
@@ -0,0 +1,32 @@
module.exports = function (babel) {
const { types: t } = babel;

return {
visitor: {
CallExpression(path) {
const { file } = this;
const { sourceFileName } = file.opts.generatorOpts;
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);

// This injects the sourcesContent, though I don't imagine anyone's
// doing it.
file.code = {
[sourceFileName]: file.code,
'test.js': '<bar />',
};
path.stop();
},
},
};
};
@@ -0,0 +1,17 @@
{
"mappings": "AAAC;;ACCD,SAASA,GAAT,CAAaC,GAAb,EAAwB;AACpB,QAAM,IAAIC,KAAJ,CAAU,cAAV,CAAN;AACH",
"names": [
"foo",
"bar",
"Error"
],
"sources": [
"test.js",
"input.tsx"
],
"sourcesContent": [
"<bar />",
"foo(1);\nfunction foo(bar: number): never {\n throw new Error('Intentional.');\n}"
],
"version": 3
}

0 comments on commit 1deccb0

Please sign in to comment.