diff --git a/CHANGELOG.md b/CHANGELOG.md index cb4638bb22e..6f341da8c41 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,17 @@ # rollup changelog +## 2.69.1 + +_2022-03-04_ + +### Bug Fixes + +- Approximate source position instead of ignoring it when using a low-resolution source map in a transform hook (#4334) + +### Pull Requests + +- [#4334](https://github.com/rollup/rollup/pull/4334): fix(sourcemap): fall back to low-resolution line mapping (@aleclarson and @lukastaegert) + ## 2.69.0 _2022-03-02_ diff --git a/package-lock.json b/package-lock.json index 3324a89bcc5..c97e0b22952 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "rollup", - "version": "2.69.0", + "version": "2.69.1", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index d0fd4c46d39..f03dfb493dc 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "rollup", - "version": "2.69.0", + "version": "2.69.1", "description": "Next-generation ES module bundler", "main": "dist/rollup.js", "module": "dist/es/rollup.js", diff --git a/src/utils/collapseSourcemaps.ts b/src/utils/collapseSourcemaps.ts index 2f9ac46025d..e4c87734cd1 100644 --- a/src/utils/collapseSourcemaps.ts +++ b/src/utils/collapseSourcemaps.ts @@ -47,6 +47,7 @@ class Link { traceMappings() { const sources: string[] = []; + const sourceIndexMap = new Map(); const sourcesContent: string[] = []; const names: string[] = []; const nameIndexMap = new Map(); @@ -57,7 +58,7 @@ class Link { const tracedLine: SourceMapSegment[] = []; for (const segment of line) { - if (segment.length == 1) continue; + if (segment.length === 1) continue; const source = this.sources[segment[1]]; if (!source) continue; @@ -68,36 +69,34 @@ class Link { ); if (traced) { - // newer sources are more likely to be used, so search backwards. - let sourceIndex = sources.lastIndexOf(traced.source.filename); - if (sourceIndex === -1) { + const { + column, + line, + name, + source: { content, filename } + } = traced; + let sourceIndex = sourceIndexMap.get(filename); + if (sourceIndex === undefined) { sourceIndex = sources.length; - sources.push(traced.source.filename); - sourcesContent[sourceIndex] = traced.source.content; + sources.push(filename); + sourceIndexMap.set(filename, sourceIndex); + sourcesContent[sourceIndex] = content; } else if (sourcesContent[sourceIndex] == null) { - sourcesContent[sourceIndex] = traced.source.content; - } else if ( - traced.source.content != null && - sourcesContent[sourceIndex] !== traced.source.content - ) { + sourcesContent[sourceIndex] = content; + } else if (content != null && sourcesContent[sourceIndex] !== content) { return error({ - message: `Multiple conflicting contents for sourcemap source ${traced.source.filename}` + message: `Multiple conflicting contents for sourcemap source ${filename}` }); } - const tracedSegment: SourceMapSegment = [ - segment[0], - sourceIndex, - traced.line, - traced.column - ]; + const tracedSegment: SourceMapSegment = [segment[0], sourceIndex, line, column]; - if (traced.name) { - let nameIndex = nameIndexMap.get(traced.name); + if (name) { + let nameIndex = nameIndexMap.get(name); if (nameIndex === undefined) { nameIndex = names.length; - names.push(traced.name); - nameIndexMap.set(traced.name, nameIndex); + names.push(name); + nameIndexMap.set(name, nameIndex); } (tracedSegment as SourceMapSegment)[4] = nameIndex; @@ -118,13 +117,17 @@ class Link { if (!segments) return null; // binary search through segments for the given column - let i = 0; - let j = segments.length - 1; + let searchStart = 0; + let searchEnd = segments.length - 1; - while (i <= j) { - const m = (i + j) >> 1; + while (searchStart <= searchEnd) { + const m = (searchStart + searchEnd) >> 1; const segment = segments[m]; - if (segment[0] === column) { + + // If a sourcemap does not have sufficient resolution to contain a + // necessary mapping, e.g. because it only contains line information, we + // use the best approximation we could find + if (segment[0] === column || searchStart === searchEnd) { if (segment.length == 1) return null; const source = this.sources[segment[1]]; if (!source) return null; @@ -136,9 +139,9 @@ class Link { ); } if (segment[0] > column) { - j = m - 1; + searchEnd = m - 1; } else { - i = m + 1; + searchStart = m + 1; } } diff --git a/src/utils/getOriginalLocation.ts b/src/utils/getOriginalLocation.ts index f5f8e7544c8..4303a1c3003 100644 --- a/src/utils/getOriginalLocation.ts +++ b/src/utils/getOriginalLocation.ts @@ -2,35 +2,30 @@ import type { DecodedSourceMapOrMissing, ExistingDecodedSourceMap } from '../rol export function getOriginalLocation( sourcemapChain: readonly DecodedSourceMapOrMissing[], - location: { column: number; line: number; name?: string; source?: string } + location: { column: number; line: number } ): { column: number; line: number } { const filteredSourcemapChain = sourcemapChain.filter( (sourcemap): sourcemap is ExistingDecodedSourceMap => !!sourcemap.mappings ); - - while (filteredSourcemapChain.length > 0) { + traceSourcemap: while (filteredSourcemapChain.length > 0) { const sourcemap = filteredSourcemapChain.pop()!; const line = sourcemap.mappings[location.line - 1]; - let locationFound = false; - - if (line !== undefined) { - for (const segment of line) { - if (segment[0] >= location.column) { - if (segment.length === 1) break; + if (line) { + const filteredLine = line.filter( + (segment): segment is [number, number, number, number] => segment.length > 1 + ); + const lastSegment = filteredLine[filteredLine.length - 1]; + for (const segment of filteredLine) { + if (segment[0] >= location.column || segment === lastSegment) { location = { column: segment[3], - line: segment[2] + 1, - name: segment.length === 5 ? sourcemap.names[segment[4]] : undefined, - source: sourcemap.sources[segment[1]] + line: segment[2] + 1 }; - locationFound = true; - break; + continue traceSourcemap; } } } - if (!locationFound) { - throw new Error("Can't resolve original location of error."); - } + throw new Error("Can't resolve original location of error."); } return location; } diff --git a/test/function/samples/cannot-resolve-sourcemap-warning/_config.js b/test/function/samples/cannot-resolve-sourcemap-warning/_config.js index a803b5b72e8..3ec5a853772 100644 --- a/test/function/samples/cannot-resolve-sourcemap-warning/_config.js +++ b/test/function/samples/cannot-resolve-sourcemap-warning/_config.js @@ -7,7 +7,7 @@ module.exports = { plugins: { name: 'test-plugin', transform() { - return { code: 'export default this', map: { mappings: 'X' } }; + return { code: 'export default this', map: { mappings: '' } }; } } }, diff --git a/test/function/samples/warning-low-resolution-location/_config.js b/test/function/samples/warning-low-resolution-location/_config.js new file mode 100644 index 00000000000..a8e18a9d87c --- /dev/null +++ b/test/function/samples/warning-low-resolution-location/_config.js @@ -0,0 +1,37 @@ +const path = require('path'); +const { encode } = require('sourcemap-codec'); +const ID_MAIN = path.join(__dirname, 'main.js'); + +module.exports = { + description: 'handles when a low resolution sourcemap is used to report an error', + options: { + plugins: { + name: 'test-plugin', + transform() { + // each entry of each line consist of + // [generatedColumn, sourceIndex, sourceLine, sourceColumn]; + // this mapping only maps the first line to itself + const decodedMap = [[[0], [0, 0, 0, 0], [1]]]; + return { code: 'export default this', map: { mappings: encode(decodedMap), sources: [] } }; + } + } + }, + warnings: [ + { + code: 'THIS_IS_UNDEFINED', + frame: ` +1: console.log('original source'); + ^`, + id: ID_MAIN, + loc: { + column: 0, + file: ID_MAIN, + line: 1 + }, + message: + "The 'this' keyword is equivalent to 'undefined' at the top level of an ES module, and has been rewritten", + pos: 15, + url: 'https://rollupjs.org/guide/en/#error-this-is-undefined' + } + ] +}; diff --git a/test/function/samples/warning-low-resolution-location/main.js b/test/function/samples/warning-low-resolution-location/main.js new file mode 100644 index 00000000000..2f40c1f1894 --- /dev/null +++ b/test/function/samples/warning-low-resolution-location/main.js @@ -0,0 +1 @@ +console.log('original source'); diff --git a/test/sourcemaps/samples/transform-low-resolution/_config.js b/test/sourcemaps/samples/transform-low-resolution/_config.js new file mode 100644 index 00000000000..ad66c2b15d7 --- /dev/null +++ b/test/sourcemaps/samples/transform-low-resolution/_config.js @@ -0,0 +1,47 @@ +const assert = require('assert'); +const MagicString = require('magic-string'); +const { SourceMapConsumer } = require('source-map'); +const { encode } = require('sourcemap-codec'); +const getLocation = require('../../getLocation'); + +module.exports = { + description: 'handles combining low-resolution and high-resolution source-maps when transforming', + options: { + output: { name: 'bundle' }, + plugins: [ + { + transform(code) { + // each entry of each line consist of + // [generatedColumn, sourceIndex, sourceLine, sourceColumn]; + // this mapping only maps the second line to the first with no column + // details + const decodedMap = [[], [[0, 0, 0, 0]]]; + return { + code: `console.log('added');\n${code}`, + map: { mappings: encode(decodedMap) } + }; + } + }, + { + transform(code) { + const s = new MagicString(code); + s.prepend("console.log('second');\n"); + + return { + code: s.toString(), + map: s.generateMap({ hires: true }) + }; + } + } + ] + }, + async test(code, map) { + const smc = await new SourceMapConsumer(map); + + const generatedLoc = getLocation(code, code.indexOf("'baz'")); + const originalLoc = smc.originalPositionFor(generatedLoc); + + assert.strictEqual(originalLoc.line, 1); + assert.strictEqual(originalLoc.column, 0); + } +}; diff --git a/test/sourcemaps/samples/transform-low-resolution/main.js b/test/sourcemaps/samples/transform-low-resolution/main.js new file mode 100644 index 00000000000..e4161efb281 --- /dev/null +++ b/test/sourcemaps/samples/transform-low-resolution/main.js @@ -0,0 +1 @@ +export let foo = 'bar'; foo += 'baz';