Skip to content

Commit e54ae8b

Browse files
committed
Support range mappings for hires mode
Adds an "experimental-range" option for the hires mode that provides a similar resolution as "true" but uses range mappings to reduce the number of mappings. Requires support for range mappings proposal in source map consumers: https://github.com/tc39/ecma426/blob/main/proposals/range-mappings.md
1 parent 410fd4d commit e54ae8b

5 files changed

Lines changed: 168 additions & 2 deletions

File tree

src/MagicString.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,7 @@ export default class MagicString {
181181
names,
182182
mappings: mappings.raw,
183183
x_google_ignoreList: this.ignoreList ? [sourceIndex] : undefined,
184+
rangeMappings: mappings.rawRangeMappings,
184185
};
185186
}
186187

src/SourceMap.js

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { encode } from '@jridgewell/sourcemap-codec';
1+
import { encode, encodeRangeMappings } from '@jridgewell/sourcemap-codec';
22

33
function getBtoa() {
44
if (typeof globalThis !== 'undefined' && typeof globalThis.btoa === 'function') {
@@ -28,6 +28,18 @@ export default class SourceMap {
2828
if (typeof properties.debugId !== 'undefined') {
2929
this.debugId = properties.debugId;
3030
}
31+
if (typeof properties.rangeMappings !== 'undefined') {
32+
let shouldOutputRangeMapping = false;
33+
for (const line of properties.rangeMappings) {
34+
if (line.length !== 0) {
35+
shouldOutputRangeMapping = true;
36+
break;
37+
}
38+
}
39+
if (shouldOutputRangeMapping) {
40+
this.rangeMappings = encodeRangeMappings(properties.rangeMappings);
41+
}
42+
}
3143
}
3244

3345
toString() {

src/index.d.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,11 @@ export interface SourceMapOptions {
1212
* line - but they're quicker to generate and less bulky.
1313
* You can also set `"boundary"` to generate a semi-hi-res mappings segmented per word boundary
1414
* instead of per character, suitable for string semantics that are separated by words.
15+
* If you set `"experimental-range"` to generate hires mappings that use range mappings, a
16+
* source map extension that can map all positions in a range. This feature is experimental.
1517
* If sourcemap locations have been specified with s.addSourceMapLocation(), they will be used here.
1618
*/
17-
hires?: boolean | 'boundary';
19+
hires?: boolean | 'boundary' | 'experimental-range';
1820
/**
1921
* The filename where you plan to write the sourcemap.
2022
*/

src/utils/Mappings.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ export default class Mappings {
77
this.generatedCodeColumn = 0;
88
this.raw = [];
99
this.rawSegments = this.raw[this.generatedCodeLine] = [];
10+
this.rawRangeMappings = [];
11+
this.rawRangeMappingsIndices = this.rawRangeMappings[this.generatedCodeLine] = [];
1012
this.pending = null;
1113
}
1214

@@ -26,6 +28,7 @@ export default class Mappings {
2628

2729
this.generatedCodeLine += 1;
2830
this.raw[this.generatedCodeLine] = this.rawSegments = [];
31+
this.rawRangeMappings[this.generatedCodeLine] = [];
2932
this.generatedCodeColumn = 0;
3033

3134
previousContentLineEnd = contentLineEnd;
@@ -54,11 +57,15 @@ export default class Mappings {
5457
let charInHiresBoundary = false;
5558

5659
while (originalCharIndex < chunk.end) {
60+
if (this.hires === "experimental-range" && originalCharIndex + 1 >= chunk.end) {
61+
this.rawSegments.push([this.generatedCodeColumn, sourceIndex, loc.line, loc.column]);
62+
}
5763
if (original[originalCharIndex] === '\n') {
5864
loc.line += 1;
5965
loc.column = 0;
6066
this.generatedCodeLine += 1;
6167
this.raw[this.generatedCodeLine] = this.rawSegments = [];
68+
this.rawRangeMappings[this.generatedCodeLine] = this.rawRangeMappingsIndices = [];
6269
this.generatedCodeColumn = 0;
6370
first = true;
6471
charInHiresBoundary = false;
@@ -79,6 +86,11 @@ export default class Mappings {
7986
this.rawSegments.push(segment);
8087
charInHiresBoundary = false;
8188
}
89+
} else if (this.hires === "experimental-range") {
90+
if (originalCharIndex === chunk.start) {
91+
this.rawRangeMappingsIndices.push(this.rawSegments.length);
92+
this.rawSegments.push(segment);
93+
}
8294
} else {
8395
this.rawSegments.push(segment);
8496
}
@@ -104,6 +116,7 @@ export default class Mappings {
104116
for (let i = 0; i < lines.length - 1; i++) {
105117
this.generatedCodeLine++;
106118
this.raw[this.generatedCodeLine] = this.rawSegments = [];
119+
this.rawRangeMappings[this.generatedCodeLine] = this.rawRangeMappingsIndices = [];
107120
}
108121
this.generatedCodeColumn = 0;
109122
}

test/MagicString.test.js

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -547,6 +547,144 @@ describe('MagicString', () => {
547547
assert.equal(loc.column, 12);
548548
});
549549

550+
it('generates segments per chunk with hires "experimental-range"', () => {
551+
const s = new MagicString('function foo(){ console.log("bar") }');
552+
553+
// rename bar to hello
554+
s.overwrite(29, 32, 'hello');
555+
556+
const map = s.generateMap({
557+
file: 'output.js',
558+
source: 'input.js',
559+
includeContent: true,
560+
hires: 'experimental-range',
561+
});
562+
563+
assert.equal(
564+
map.mappings,
565+
'AAAA,4BAA4B,CAAC,KAAG,GAAG',
566+
);
567+
assert.equal(
568+
map.rangeMappings,
569+
'BD',
570+
);
571+
572+
const smc = new SourceMapConsumer(map);
573+
let loc;
574+
575+
// FIXME: the consumer library doesn't support range mappings yet
576+
//loc = smc.originalPositionFor({ line: 1, column: 15 });
577+
//assert.equal(loc.line, 1);
578+
//assert.equal(loc.column, 15);
579+
580+
loc = smc.originalPositionFor({ line: 1, column: 28 });
581+
assert.equal(loc.line, 1);
582+
assert.equal(loc.column, 28);
583+
584+
loc = smc.originalPositionFor({ line: 1, column: 29 });
585+
assert.equal(loc.line, 1);
586+
assert.equal(loc.column, 29);
587+
588+
loc = smc.originalPositionFor({ line: 1, column: 34 });
589+
assert.equal(loc.line, 1);
590+
assert.equal(loc.column, 32);
591+
592+
// FIXME: see above
593+
//loc = smc.originalPositionFor({ line: 1, column: 35 });
594+
//assert.equal(loc.line, 1);
595+
//assert.equal(loc.column, 33);
596+
});
597+
598+
it('generates segments per chunk with hires "experimental-range" (multiple ranges on a line)', () => {
599+
const s = new MagicString('function foo(){ console.log("bar") }');
600+
601+
// rename foo to baz, bar to hello
602+
s.overwrite(9, 12, 'baz');
603+
s.overwrite(29, 32, 'hello');
604+
605+
const map = s.generateMap({
606+
file: 'output.js',
607+
source: 'input.js',
608+
includeContent: true,
609+
hires: 'experimental-range',
610+
});
611+
612+
assert.equal(
613+
map.mappings,
614+
'AAAA,QAAQ,CAAC,GAAG,gBAAgB,CAAC,KAAG,GAAG',
615+
);
616+
assert.equal(
617+
map.rangeMappings,
618+
'BDD',
619+
);
620+
621+
const smc = new SourceMapConsumer(map);
622+
let loc;
623+
624+
// FIXME: the consumer library doesn't support range mappings yet
625+
//loc = smc.originalPositionFor({ line: 1, column: 15 });
626+
//assert.equal(loc.line, 1);
627+
//assert.equal(loc.column, 15);
628+
629+
loc = smc.originalPositionFor({ line: 1, column: 28 });
630+
assert.equal(loc.line, 1);
631+
assert.equal(loc.column, 28);
632+
633+
loc = smc.originalPositionFor({ line: 1, column: 29 });
634+
assert.equal(loc.line, 1);
635+
assert.equal(loc.column, 29);
636+
637+
loc = smc.originalPositionFor({ line: 1, column: 34 });
638+
assert.equal(loc.line, 1);
639+
assert.equal(loc.column, 32);
640+
641+
// FIXME: see above
642+
//loc = smc.originalPositionFor({ line: 1, column: 35 });
643+
//assert.equal(loc.line, 1);
644+
//assert.equal(loc.column, 33);
645+
});
646+
647+
it('generates segments per chunk with hires "experimental-range" in the next line', () => {
648+
const s = new MagicString('// foo\nconsole.log("bar")');
649+
650+
// rename bar to hello
651+
s.overwrite(20, 23, 'hello');
652+
653+
const map = s.generateMap({
654+
file: 'output.js',
655+
source: 'input.js',
656+
includeContent: true,
657+
hires: 'experimental-range',
658+
});
659+
660+
assert.equal(map.mappings, 'AAAA;YACY,CAAC,KAAG,CAAC');
661+
assert.equal(map.rangeMappings, 'B;D');
662+
663+
const smc = new SourceMapConsumer(map);
664+
let loc;
665+
666+
// FIXME: the consumer library doesn't support range mappings yet
667+
//loc = smc.originalPositionFor({ line: 1, column: 2 });
668+
//assert.equal(loc.line, 1);
669+
//assert.equal(loc.column, 2);
670+
671+
//loc = smc.originalPositionFor({ line: 2, column: 2 });
672+
//assert.equal(loc.line, 2);
673+
//assert.equal(loc.column, 2);
674+
675+
loc = smc.originalPositionFor({ line: 2, column: 12 });
676+
assert.equal(loc.line, 2);
677+
assert.equal(loc.column, 12);
678+
679+
loc = smc.originalPositionFor({ line: 2, column: 18 });
680+
assert.equal(loc.line, 2);
681+
assert.equal(loc.column, 16);
682+
683+
loc = smc.originalPositionFor({ line: 2, column: 19 });
684+
assert.equal(loc.line, 2);
685+
assert.equal(loc.column, 17);
686+
});
687+
550688
it('generates a correct source map with update using a content containing a new line', () => {
551689
const s = new MagicString('foobar');
552690
s.update(3, 4, '\nbb');

0 commit comments

Comments
 (0)