From a63d5f25308ff1965ae676e2aa5311417279e7d4 Mon Sep 17 00:00:00 2001 From: Bjorn Lu Date: Fri, 28 Jul 2023 09:52:05 +0800 Subject: [PATCH] feat: hires boundary (#255) --- README.md | 2 +- src/index.d.ts | 4 +++- src/utils/Mappings.js | 23 ++++++++++++++++++++++- test/MagicString.js | 35 +++++++++++++++++++++++++++++++++++ 4 files changed, 61 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index f1b39e2..8de32b3 100644 --- a/README.md +++ b/README.md @@ -107,7 +107,7 @@ Generates a [version 3 sourcemap](https://docs.google.com/document/d/1U1RGAehQwR * `file` - the filename where you plan to write the sourcemap * `source` - the filename of the file containing the original source * `includeContent` - whether to include the original content in the map's `sourcesContent` array -* `hires` - whether the mapping should be high-resolution. Hi-res mappings map every single character, meaning (for example) your devtools will always be able to pinpoint the exact location of function calls and so on. With lo-res mappings, devtools may only be able to identify the correct line - but they're quicker to generate and less bulky. If sourcemap locations have been specified with `s.addSourcemapLocation()`, they will be used here. +* `hires` - whether the mapping should be high-resolution. Hi-res mappings map every single character, meaning (for example) your devtools will always be able to pinpoint the exact location of function calls and so on. With lo-res mappings, devtools may only be able to identify the correct 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 sourcemap locations have been specified with `s.addSourcemapLocation()`, they will be used here. The returned sourcemap has two (non-enumerable) methods attached for convenience: diff --git a/src/index.d.ts b/src/index.d.ts index 8235604..20872aa 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -10,9 +10,11 @@ export interface SourceMapOptions { * be able to pinpoint the exact location of function calls and so on. * With lo-res mappings, devtools may only be able to identify the correct * 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 sourcemap locations have been specified with s.addSourceMapLocation(), they will be used here. */ - hires?: boolean; + hires?: boolean | 'boundary'; /** * The filename where you plan to write the sourcemap. */ diff --git a/src/utils/Mappings.js b/src/utils/Mappings.js index c517e10..849966f 100644 --- a/src/utils/Mappings.js +++ b/src/utils/Mappings.js @@ -1,3 +1,5 @@ +const wordRegex = /\w/; + export default class Mappings { constructor(hires) { this.hires = hires; @@ -26,10 +28,29 @@ export default class Mappings { addUneditedChunk(sourceIndex, chunk, original, loc, sourcemapLocations) { let originalCharIndex = chunk.start; let first = true; + // when iterating each char, check if it's in a word boundary + let charInHiresBoundary = false; while (originalCharIndex < chunk.end) { if (this.hires || first || sourcemapLocations.has(originalCharIndex)) { - this.rawSegments.push([this.generatedCodeColumn, sourceIndex, loc.line, loc.column]); + const segment = [this.generatedCodeColumn, sourceIndex, loc.line, loc.column]; + + if (this.hires === 'boundary') { + // in hires "boundary", group segments per word boundary than per char + if (wordRegex.test(original[originalCharIndex])) { + // for first char in the boundary found, start the boundary by pushing a segment + if (!charInHiresBoundary) { + this.rawSegments.push(segment); + charInHiresBoundary = true; + } + } else { + // for non-word char, end the boundary by pushing a segment + this.rawSegments.push(segment); + charInHiresBoundary = false; + } + } else { + this.rawSegments.push(segment); + } } if (original[originalCharIndex] === '\n') { diff --git a/test/MagicString.js b/test/MagicString.js index 07d1aff..1bf431e 100644 --- a/test/MagicString.js +++ b/test/MagicString.js @@ -434,6 +434,41 @@ describe('MagicString', () => { assert.deepEqual(map.sources, ['foo.js']); assert.deepEqual(map.x_google_ignoreList, [0]); }); + + it('generates segments per word boundary with hires "boundary"', () => { + 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: 'boundary' + }); + + assert.equal(map.mappings, 'AAAA,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,KAAG,CAAC,CAAC,CAAC'); + + const smc = new SourceMapConsumer(map); + let loc; + + loc = smc.originalPositionFor({ line: 1, column: 3 }); + assert.equal(loc.line, 1); + assert.equal(loc.column, 0); + + loc = smc.originalPositionFor({ line: 1, column: 11 }); + assert.equal(loc.line, 1); + assert.equal(loc.column, 9); + + loc = smc.originalPositionFor({ line: 1, column: 29 }); + assert.equal(loc.line, 1); + assert.equal(loc.column, 29); + + loc = smc.originalPositionFor({ line: 1, column: 35 }); + assert.equal(loc.line, 1); + assert.equal(loc.column, 33); + }); }); describe('getIndentString', () => {