diff --git a/src/MagicString.js b/src/MagicString.js index f9ac740..3b8987c 100644 --- a/src/MagicString.js +++ b/src/MagicString.js @@ -181,6 +181,7 @@ export default class MagicString { names, mappings: mappings.raw, x_google_ignoreList: this.ignoreList ? [sourceIndex] : undefined, + rangeMappings: mappings.rawRangeMappings, }; } diff --git a/src/SourceMap.js b/src/SourceMap.js index 3783db8..91ee8a0 100644 --- a/src/SourceMap.js +++ b/src/SourceMap.js @@ -1,4 +1,4 @@ -import { encode } from '@jridgewell/sourcemap-codec'; +import { encode, encodeRangeMappings } from '@jridgewell/sourcemap-codec'; function getBtoa() { if (typeof globalThis !== 'undefined' && typeof globalThis.btoa === 'function') { @@ -28,6 +28,18 @@ export default class SourceMap { if (typeof properties.debugId !== 'undefined') { this.debugId = properties.debugId; } + if (typeof properties.rangeMappings !== 'undefined') { + let shouldOutputRangeMapping = false; + for (const line of properties.rangeMappings) { + if (line.length !== 0) { + shouldOutputRangeMapping = true; + break; + } + } + if (shouldOutputRangeMapping) { + this.rangeMappings = encodeRangeMappings(properties.rangeMappings); + } + } } toString() { diff --git a/src/index.d.ts b/src/index.d.ts index 76cc537..50d3df4 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -12,9 +12,11 @@ export interface SourceMapOptions { * line - but they're quicker to generate and less bulky. * You can also set `"boundary"` to generate a semi-hi-res mappings segmented per word boundary * instead of per character, suitable for string semantics that are separated by words. + * If you set `"experimental-range"` to generate hires mappings that use range mappings, a + * source map extension that can map all positions in a range. This feature is experimental. * If sourcemap locations have been specified with s.addSourceMapLocation(), they will be used here. */ - hires?: boolean | 'boundary'; + hires?: boolean | 'boundary' | 'experimental-range'; /** * The filename where you plan to write the sourcemap. */ diff --git a/src/utils/Mappings.js b/src/utils/Mappings.js index 868e614..82f5b5b 100644 --- a/src/utils/Mappings.js +++ b/src/utils/Mappings.js @@ -7,6 +7,8 @@ export default class Mappings { this.generatedCodeColumn = 0; this.raw = []; this.rawSegments = this.raw[this.generatedCodeLine] = []; + this.rawRangeMappings = []; + this.rawRangeMappingsIndices = this.rawRangeMappings[this.generatedCodeLine] = []; this.pending = null; } @@ -26,6 +28,7 @@ export default class Mappings { this.generatedCodeLine += 1; this.raw[this.generatedCodeLine] = this.rawSegments = []; + this.rawRangeMappings[this.generatedCodeLine] = []; this.generatedCodeColumn = 0; previousContentLineEnd = contentLineEnd; @@ -54,11 +57,15 @@ export default class Mappings { let charInHiresBoundary = false; while (originalCharIndex < chunk.end) { + if (this.hires === "experimental-range" && originalCharIndex + 1 >= chunk.end) { + this.rawSegments.push([this.generatedCodeColumn, sourceIndex, loc.line, loc.column]); + } if (original[originalCharIndex] === '\n') { loc.line += 1; loc.column = 0; this.generatedCodeLine += 1; this.raw[this.generatedCodeLine] = this.rawSegments = []; + this.rawRangeMappings[this.generatedCodeLine] = this.rawRangeMappingsIndices = []; this.generatedCodeColumn = 0; first = true; charInHiresBoundary = false; @@ -79,6 +86,11 @@ export default class Mappings { this.rawSegments.push(segment); charInHiresBoundary = false; } + } else if (this.hires === "experimental-range") { + if (originalCharIndex === chunk.start) { + this.rawRangeMappingsIndices.push(this.rawSegments.length); + this.rawSegments.push(segment); + } } else { this.rawSegments.push(segment); } @@ -104,6 +116,7 @@ export default class Mappings { for (let i = 0; i < lines.length - 1; i++) { this.generatedCodeLine++; this.raw[this.generatedCodeLine] = this.rawSegments = []; + this.rawRangeMappings[this.generatedCodeLine] = this.rawRangeMappingsIndices = []; } this.generatedCodeColumn = 0; } diff --git a/test/MagicString.test.js b/test/MagicString.test.js index cd0d6e6..98e0210 100644 --- a/test/MagicString.test.js +++ b/test/MagicString.test.js @@ -547,6 +547,144 @@ describe('MagicString', () => { assert.equal(loc.column, 12); }); + it('generates segments per chunk with hires "experimental-range"', () => { + const s = new MagicString('function foo(){ console.log("bar") }'); + + // rename bar to hello + s.overwrite(29, 32, 'hello'); + + const map = s.generateMap({ + file: 'output.js', + source: 'input.js', + includeContent: true, + hires: 'experimental-range', + }); + + assert.equal( + map.mappings, + 'AAAA,4BAA4B,CAAC,KAAG,GAAG', + ); + assert.equal( + map.rangeMappings, + 'BD', + ); + + const smc = new SourceMapConsumer(map); + let loc; + + // FIXME: the consumer library doesn't support range mappings yet + //loc = smc.originalPositionFor({ line: 1, column: 15 }); + //assert.equal(loc.line, 1); + //assert.equal(loc.column, 15); + + loc = smc.originalPositionFor({ line: 1, column: 28 }); + assert.equal(loc.line, 1); + assert.equal(loc.column, 28); + + loc = smc.originalPositionFor({ line: 1, column: 29 }); + assert.equal(loc.line, 1); + assert.equal(loc.column, 29); + + loc = smc.originalPositionFor({ line: 1, column: 34 }); + assert.equal(loc.line, 1); + assert.equal(loc.column, 32); + + // FIXME: see above + //loc = smc.originalPositionFor({ line: 1, column: 35 }); + //assert.equal(loc.line, 1); + //assert.equal(loc.column, 33); + }); + + it('generates segments per chunk with hires "experimental-range" (multiple ranges on a line)', () => { + const s = new MagicString('function foo(){ console.log("bar") }'); + + // rename foo to baz, bar to hello + s.overwrite(9, 12, 'baz'); + s.overwrite(29, 32, 'hello'); + + const map = s.generateMap({ + file: 'output.js', + source: 'input.js', + includeContent: true, + hires: 'experimental-range', + }); + + assert.equal( + map.mappings, + 'AAAA,QAAQ,CAAC,GAAG,gBAAgB,CAAC,KAAG,GAAG', + ); + assert.equal( + map.rangeMappings, + 'BDD', + ); + + const smc = new SourceMapConsumer(map); + let loc; + + // FIXME: the consumer library doesn't support range mappings yet + //loc = smc.originalPositionFor({ line: 1, column: 15 }); + //assert.equal(loc.line, 1); + //assert.equal(loc.column, 15); + + loc = smc.originalPositionFor({ line: 1, column: 28 }); + assert.equal(loc.line, 1); + assert.equal(loc.column, 28); + + loc = smc.originalPositionFor({ line: 1, column: 29 }); + assert.equal(loc.line, 1); + assert.equal(loc.column, 29); + + loc = smc.originalPositionFor({ line: 1, column: 34 }); + assert.equal(loc.line, 1); + assert.equal(loc.column, 32); + + // FIXME: see above + //loc = smc.originalPositionFor({ line: 1, column: 35 }); + //assert.equal(loc.line, 1); + //assert.equal(loc.column, 33); + }); + + it('generates segments per chunk with hires "experimental-range" in the next line', () => { + const s = new MagicString('// foo\nconsole.log("bar")'); + + // rename bar to hello + s.overwrite(20, 23, 'hello'); + + const map = s.generateMap({ + file: 'output.js', + source: 'input.js', + includeContent: true, + hires: 'experimental-range', + }); + + assert.equal(map.mappings, 'AAAA;YACY,CAAC,KAAG,CAAC'); + assert.equal(map.rangeMappings, 'B;D'); + + const smc = new SourceMapConsumer(map); + let loc; + + // FIXME: the consumer library doesn't support range mappings yet + //loc = smc.originalPositionFor({ line: 1, column: 2 }); + //assert.equal(loc.line, 1); + //assert.equal(loc.column, 2); + + //loc = smc.originalPositionFor({ line: 2, column: 2 }); + //assert.equal(loc.line, 2); + //assert.equal(loc.column, 2); + + loc = smc.originalPositionFor({ line: 2, column: 12 }); + assert.equal(loc.line, 2); + assert.equal(loc.column, 12); + + loc = smc.originalPositionFor({ line: 2, column: 18 }); + assert.equal(loc.line, 2); + assert.equal(loc.column, 16); + + loc = smc.originalPositionFor({ line: 2, column: 19 }); + assert.equal(loc.line, 2); + assert.equal(loc.column, 17); + }); + it('generates a correct source map with update using a content containing a new line', () => { const s = new MagicString('foobar'); s.update(3, 4, '\nbb');