From 5031a53fa9bc0019f3b5657dd5b48eb674355668 Mon Sep 17 00:00:00 2001 From: Justin Ridgewell Date: Sun, 6 Feb 2022 20:30:53 -0500 Subject: [PATCH] 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. --- .../src/transformation/file/merge-map.ts | 79 +++++-------------- .../input.js | 5 ++ .../input.js.map | 9 +++ .../options.json | 4 + .../output.js | 2 + .../plugin.js | 52 ++++++++++++ .../source-map.json | 13 +++ .../plugin.js | 2 +- .../input.js | 5 ++ .../input.js.map | 9 +++ .../options.json | 4 + .../output.js | 1 + .../plugin.js | 38 +++++++++ .../source-map.json | 7 ++ 14 files changed, 169 insertions(+), 61 deletions(-) create mode 100644 packages/babel-core/test/fixtures/transformation/source-maps/input-source-map-multiple-output-sources-complete-replace/input.js create mode 100644 packages/babel-core/test/fixtures/transformation/source-maps/input-source-map-multiple-output-sources-complete-replace/input.js.map create mode 100644 packages/babel-core/test/fixtures/transformation/source-maps/input-source-map-multiple-output-sources-complete-replace/options.json create mode 100644 packages/babel-core/test/fixtures/transformation/source-maps/input-source-map-multiple-output-sources-complete-replace/output.js create mode 100644 packages/babel-core/test/fixtures/transformation/source-maps/input-source-map-multiple-output-sources-complete-replace/plugin.js create mode 100644 packages/babel-core/test/fixtures/transformation/source-maps/input-source-map-multiple-output-sources-complete-replace/source-map.json create mode 100644 packages/babel-core/test/fixtures/transformation/source-maps/input-source-map-sources-complete-replace/input.js create mode 100644 packages/babel-core/test/fixtures/transformation/source-maps/input-source-map-sources-complete-replace/input.js.map create mode 100644 packages/babel-core/test/fixtures/transformation/source-maps/input-source-map-sources-complete-replace/options.json create mode 100644 packages/babel-core/test/fixtures/transformation/source-maps/input-source-map-sources-complete-replace/output.js create mode 100644 packages/babel-core/test/fixtures/transformation/source-maps/input-source-map-sources-complete-replace/plugin.js create mode 100644 packages/babel-core/test/fixtures/transformation/source-maps/input-source-map-sources-complete-replace/source-map.json diff --git a/packages/babel-core/src/transformation/file/merge-map.ts b/packages/babel-core/src/transformation/file/merge-map.ts index 614c743f9e58..dc7227e23031 100644 --- a/packages/babel-core/src/transformation/file/merge-map.ts +++ b/packages/babel-core/src/transformation/file/merge-map.ts @@ -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 { diff --git a/packages/babel-core/test/fixtures/transformation/source-maps/input-source-map-multiple-output-sources-complete-replace/input.js b/packages/babel-core/test/fixtures/transformation/source-maps/input-source-map-multiple-output-sources-complete-replace/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-complete-replace/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-complete-replace/input.js.map b/packages/babel-core/test/fixtures/transformation/source-maps/input-source-map-multiple-output-sources-complete-replace/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-complete-replace/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-complete-replace/options.json b/packages/babel-core/test/fixtures/transformation/source-maps/input-source-map-multiple-output-sources-complete-replace/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-complete-replace/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-complete-replace/output.js b/packages/babel-core/test/fixtures/transformation/source-maps/input-source-map-multiple-output-sources-complete-replace/output.js new file mode 100644 index 000000000000..b0630eb937f4 --- /dev/null +++ b/packages/babel-core/test/fixtures/transformation/source-maps/input-source-map-multiple-output-sources-complete-replace/output.js @@ -0,0 +1,2 @@ +"bar"; +"baz"; diff --git a/packages/babel-core/test/fixtures/transformation/source-maps/input-source-map-multiple-output-sources-complete-replace/plugin.js b/packages/babel-core/test/fixtures/transformation/source-maps/input-source-map-multiple-output-sources-complete-replace/plugin.js new file mode 100644 index 000000000000..1d9b56675062 --- /dev/null +++ b/packages/babel-core/test/fixtures/transformation/source-maps/input-source-map-multiple-output-sources-complete-replace/plugin.js @@ -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': '', + '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); + }, + }, + }; +}; diff --git a/packages/babel-core/test/fixtures/transformation/source-maps/input-source-map-multiple-output-sources-complete-replace/source-map.json b/packages/babel-core/test/fixtures/transformation/source-maps/input-source-map-multiple-output-sources-complete-replace/source-map.json new file mode 100644 index 000000000000..3063977828b6 --- /dev/null +++ b/packages/babel-core/test/fixtures/transformation/source-maps/input-source-map-multiple-output-sources-complete-replace/source-map.json @@ -0,0 +1,13 @@ +{ + "mappings": "AAAC;ACAD,K", + "names": [], + "sources": [ + "bar.js", + "baz.js" + ], + "sourcesContent": [ + "", + "baz();" + ], + "version": 3 +} 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 index b9fd011c05bf..a5470426bf56 100644 --- 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 @@ -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': '', diff --git a/packages/babel-core/test/fixtures/transformation/source-maps/input-source-map-sources-complete-replace/input.js b/packages/babel-core/test/fixtures/transformation/source-maps/input-source-map-sources-complete-replace/input.js new file mode 100644 index 000000000000..298b7dfa0cb8 --- /dev/null +++ b/packages/babel-core/test/fixtures/transformation/source-maps/input-source-map-sources-complete-replace/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-sources-complete-replace/input.js.map b/packages/babel-core/test/fixtures/transformation/source-maps/input-source-map-sources-complete-replace/input.js.map new file mode 100644 index 000000000000..4f1791f12c23 --- /dev/null +++ b/packages/babel-core/test/fixtures/transformation/source-maps/input-source-map-sources-complete-replace/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-sources-complete-replace/options.json b/packages/babel-core/test/fixtures/transformation/source-maps/input-source-map-sources-complete-replace/options.json new file mode 100644 index 000000000000..6466aed13cd6 --- /dev/null +++ b/packages/babel-core/test/fixtures/transformation/source-maps/input-source-map-sources-complete-replace/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-sources-complete-replace/output.js b/packages/babel-core/test/fixtures/transformation/source-maps/input-source-map-sources-complete-replace/output.js new file mode 100644 index 000000000000..e4b5496b38df --- /dev/null +++ b/packages/babel-core/test/fixtures/transformation/source-maps/input-source-map-sources-complete-replace/output.js @@ -0,0 +1 @@ +"bar"; diff --git a/packages/babel-core/test/fixtures/transformation/source-maps/input-source-map-sources-complete-replace/plugin.js b/packages/babel-core/test/fixtures/transformation/source-maps/input-source-map-sources-complete-replace/plugin.js new file mode 100644 index 000000000000..a6d943636079 --- /dev/null +++ b/packages/babel-core/test/fixtures/transformation/source-maps/input-source-map-sources-complete-replace/plugin.js @@ -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': '', + }; + }, + + 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(); + }, + }, + }; +}; diff --git a/packages/babel-core/test/fixtures/transformation/source-maps/input-source-map-sources-complete-replace/source-map.json b/packages/babel-core/test/fixtures/transformation/source-maps/input-source-map-sources-complete-replace/source-map.json new file mode 100644 index 000000000000..3259064134d2 --- /dev/null +++ b/packages/babel-core/test/fixtures/transformation/source-maps/input-source-map-sources-complete-replace/source-map.json @@ -0,0 +1,7 @@ +{ + "mappings": "AAAC", + "names": [], + "sources": ["test.js"], + "sourcesContent": [""], + "version": 3 +}