From 76dd49197af69a312283145cae2403ff2bd8961b Mon Sep 17 00:00:00 2001 From: Justin Ridgewell Date: Tue, 8 Feb 2022 14:57:50 -0500 Subject: [PATCH] Support merging sourcemaps when transformed file is fully replaced (#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 --- packages/babel-core/package.json | 2 +- .../src/transformation/file/merge-map.ts | 76 ++++--------------- .../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 ++ yarn.lock | 32 ++++---- 16 files changed, 183 insertions(+), 78 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/package.json b/packages/babel-core/package.json index ea945a5ab950..c47dd20ceef8 100644 --- a/packages/babel-core/package.json +++ b/packages/babel-core/package.json @@ -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:^", diff --git a/packages/babel-core/src/transformation/file/merge-map.ts b/packages/babel-core/src/transformation/file/merge-map.ts index 614c743f9e58..1742db173f67 100644 --- a/packages/babel-core/src/transformation/file/merge-map.ts +++ b/packages/babel-core/src/transformation/file/merge-map.ts @@ -6,24 +6,20 @@ 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; @@ -31,52 +27,6 @@ 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-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 +} diff --git a/yarn.lock b/yarn.lock index bd558cd6df86..462cd873a908 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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 @@ -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:^" @@ -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 @@ -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