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 +}