From 205983ac575673e7995b8edb1931ea88cad859ef Mon Sep 17 00:00:00 2001 From: Justin Ridgewell Date: Sun, 6 Feb 2022 01:12:11 -0500 Subject: [PATCH] Merge multi-source output sourcemaps 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 https://github.com/ampproject/remapping/issues/159. --- .../src/transformation/file/generate.ts | 16 ++--- .../src/transformation/file/merge-map.ts | 66 ++++++++++++++++++- .../input.js | 5 ++ .../input.js.map | 9 +++ .../options.json | 4 ++ .../output.js | 5 ++ .../plugin.js | 32 +++++++++ .../source-map.json | 17 +++++ 8 files changed, 145 insertions(+), 9 deletions(-) create mode 100644 packages/babel-core/test/fixtures/transformation/source-maps/input-source-map-multiple-output-sources/input.js create mode 100644 packages/babel-core/test/fixtures/transformation/source-maps/input-source-map-multiple-output-sources/input.js.map create mode 100644 packages/babel-core/test/fixtures/transformation/source-maps/input-source-map-multiple-output-sources/options.json create mode 100644 packages/babel-core/test/fixtures/transformation/source-maps/input-source-map-multiple-output-sources/output.js create mode 100644 packages/babel-core/test/fixtures/transformation/source-maps/input-source-map-multiple-output-sources/plugin.js create mode 100644 packages/babel-core/test/fixtures/transformation/source-maps/input-source-map-multiple-output-sources/source-map.json diff --git a/packages/babel-core/src/transformation/file/generate.ts b/packages/babel-core/src/transformation/file/generate.ts index 7248d2509961..1b60aa9b1a2f 100644 --- a/packages/babel-core/src/transformation/file/generate.ts +++ b/packages/babel-core/src/transformation/file/generate.ts @@ -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); } @@ -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]; @@ -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") { diff --git a/packages/babel-core/src/transformation/file/merge-map.ts b/packages/babel-core/src/transformation/file/merge-map.ts index 7abcd957064f..614c743f9e58 100644 --- a/packages/babel-core/src/transformation/file/merge-map.ts +++ b/packages/babel-core/src/transformation/file/merge-map.ts @@ -4,8 +4,26 @@ 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; @@ -13,6 +31,52 @@ export default function mergeSourceMap( 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, diff --git a/packages/babel-core/test/fixtures/transformation/source-maps/input-source-map-multiple-output-sources/input.js b/packages/babel-core/test/fixtures/transformation/source-maps/input-source-map-multiple-output-sources/input.js new file mode 100644 index 000000000000..298b7dfa0cb8 --- /dev/null +++ b/packages/babel-core/test/fixtures/transformation/source-maps/input-source-map-multiple-output-sources/input.js @@ -0,0 +1,5 @@ +foo(1); +function foo(bar) { + throw new Error('Intentional.'); +} +//# sourceMappingURL=input.js.map diff --git a/packages/babel-core/test/fixtures/transformation/source-maps/input-source-map-multiple-output-sources/input.js.map b/packages/babel-core/test/fixtures/transformation/source-maps/input-source-map-multiple-output-sources/input.js.map new file mode 100644 index 000000000000..4f1791f12c23 --- /dev/null +++ b/packages/babel-core/test/fixtures/transformation/source-maps/input-source-map-multiple-output-sources/input.js.map @@ -0,0 +1,9 @@ +{ + "version": 3, + "sources": ["input.tsx"], + "names": [], + "mappings": "AAAA,GAAG,CAAC,CAAC,CAAC,CAAC;AACP,SAAS,GAAG,CAAC,GAAW;IACpB,MAAM,IAAI,KAAK,CAAC,cAAc,CAAC,CAAC;AACpC,CAAC", + "sourcesContent": [ + "foo(1);\nfunction foo(bar: number): never {\n throw new Error('Intentional.');\n}" + ] +} diff --git a/packages/babel-core/test/fixtures/transformation/source-maps/input-source-map-multiple-output-sources/options.json b/packages/babel-core/test/fixtures/transformation/source-maps/input-source-map-multiple-output-sources/options.json new file mode 100644 index 000000000000..6466aed13cd6 --- /dev/null +++ b/packages/babel-core/test/fixtures/transformation/source-maps/input-source-map-multiple-output-sources/options.json @@ -0,0 +1,4 @@ +{ + "inputSourceMap": true, + "plugins": ["./plugin.js"] +} diff --git a/packages/babel-core/test/fixtures/transformation/source-maps/input-source-map-multiple-output-sources/output.js b/packages/babel-core/test/fixtures/transformation/source-maps/input-source-map-multiple-output-sources/output.js new file mode 100644 index 000000000000..4d0b018046c9 --- /dev/null +++ b/packages/babel-core/test/fixtures/transformation/source-maps/input-source-map-multiple-output-sources/output.js @@ -0,0 +1,5 @@ +"bar"; + +function foo(bar) { + throw new Error('Intentional.'); +} diff --git a/packages/babel-core/test/fixtures/transformation/source-maps/input-source-map-multiple-output-sources/plugin.js b/packages/babel-core/test/fixtures/transformation/source-maps/input-source-map-multiple-output-sources/plugin.js new file mode 100644 index 000000000000..b9fd011c05bf --- /dev/null +++ b/packages/babel-core/test/fixtures/transformation/source-maps/input-source-map-multiple-output-sources/plugin.js @@ -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': '', + }; + path.stop(); + }, + }, + }; +}; diff --git a/packages/babel-core/test/fixtures/transformation/source-maps/input-source-map-multiple-output-sources/source-map.json b/packages/babel-core/test/fixtures/transformation/source-maps/input-source-map-multiple-output-sources/source-map.json new file mode 100644 index 000000000000..c896cd9f05a6 --- /dev/null +++ b/packages/babel-core/test/fixtures/transformation/source-maps/input-source-map-multiple-output-sources/source-map.json @@ -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": [ + "", + "foo(1);\nfunction foo(bar: number): never {\n throw new Error('Intentional.');\n}" + ], + "version": 3 +}