Skip to content

Commit 728cceb

Browse files
committed
feat: Add support for index source maps
1 parent dffbdcd commit 728cceb

File tree

3 files changed

+137
-4
lines changed

3 files changed

+137
-4
lines changed

src/decode/decode.test.ts

Lines changed: 67 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,15 @@ import { describe, it } from "jsr:@std/testing/bdd";
66
import { ScopeInfoBuilder } from "../builder/builder.ts";
77
import { encode } from "../encode/encode.ts";
88
import {
9+
assert,
910
assertEquals,
1011
assertExists,
1112
assertStrictEquals,
1213
assertThrows,
1314
} from "jsr:@std/assert";
1415
import { encodeSigned, encodeUnsigned } from "../vlq.ts";
1516
import { decode, DecodeMode } from "./decode.ts";
16-
import type { SourceMapJson } from "../scopes.d.ts";
17+
import type { IndexSourceMapJson, SourceMapJson } from "../scopes.d.ts";
1718
import { GeneratedRangeFlags, OriginalScopeFlags, Tag } from "../codec.ts";
1819

1920
class ItemEncoder {
@@ -492,4 +493,69 @@ describe("decode", () => {
492493
],
493494
]);
494495
});
496+
497+
it("applies 'generatedOffset' option correctly for line 0", () => {
498+
const scopes = new ScopeInfoBuilder().startRange(0, 0).endRange(0, 10)
499+
.build();
500+
const map = encode(scopes);
501+
502+
const info = decode(map, { generatedOffset: { line: 0, column: 20 } });
503+
504+
assertEquals(info.ranges[0].start, { line: 0, column: 20 });
505+
assertEquals(info.ranges[0].end, { line: 0, column: 30 });
506+
});
507+
508+
it("applies 'generatedOffset' option correctly for non-zero line and column", () => {
509+
const scopes = new ScopeInfoBuilder().startRange(0, 10).endRange(0, 20)
510+
.build();
511+
const map = encode(scopes);
512+
513+
const info = decode(map, { generatedOffset: { line: 2, column: 5 } });
514+
515+
assertEquals(info.ranges[0].start, { line: 2, column: 15 });
516+
assertEquals(info.ranges[0].end, { line: 2, column: 25 });
517+
});
518+
519+
it("decodes index source maps", () => {
520+
const map1 = encode(
521+
new ScopeInfoBuilder().startRange(0, 0).endRange(0, 10)
522+
.build(),
523+
);
524+
const map2 = encode(
525+
new ScopeInfoBuilder().startRange(0, 0).endRange(1, 20)
526+
.build(),
527+
);
528+
const map: IndexSourceMapJson = {
529+
version: 3,
530+
sections: [
531+
{ offset: { line: 0, column: 0 }, map: map1 },
532+
{ offset: { line: 1, column: 42 }, map: map2 },
533+
],
534+
};
535+
536+
const info = decode(map);
537+
538+
assertEquals(info.ranges[0].start, { line: 0, column: 0 });
539+
assertEquals(info.ranges[0].end, { line: 0, column: 10 });
540+
assertEquals(info.ranges[1].start, { line: 1, column: 42 });
541+
assertEquals(info.ranges[1].end, { line: 2, column: 20 });
542+
});
543+
544+
it("ignores 'generatedOffset' option for index source maps", () => {
545+
const map1 = encode(
546+
new ScopeInfoBuilder().startRange(0, 0).endRange(0, 10)
547+
.build(),
548+
);
549+
const map: IndexSourceMapJson = {
550+
version: 3,
551+
sections: [
552+
{ offset: { line: 0, column: 0 }, map: map1 },
553+
],
554+
};
555+
556+
const info = decode(map, { generatedOffset: { line: 4, column: 42 } });
557+
558+
assertEquals(info.ranges[0].start, { line: 0, column: 0 });
559+
assertEquals(info.ranges[0].end, { line: 0, column: 10 });
560+
});
495561
});

src/decode/decode.ts

Lines changed: 57 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,11 @@ import {
1111
} from "../codec.ts";
1212
import type {
1313
GeneratedRange,
14+
IndexSourceMapJson,
1415
OriginalScope,
16+
Position,
1517
ScopeInfo,
18+
SourceMap,
1619
SourceMapJson,
1720
SubRangeBinding,
1821
} from "../scopes.d.ts";
@@ -37,15 +40,64 @@ export const enum DecodeMode {
3740
LAX = 2,
3841
}
3942

43+
export interface DecodeOptions {
44+
mode: DecodeMode;
45+
46+
/**
47+
* Offsets `start` and `end` of all generated ranges by the specified amount.
48+
* Intended to be used when decoding sections of index source maps one-by-one.
49+
*
50+
* Has no effect when passing a {@link IndexSourceMapJson} directly to {@link decode}.
51+
*/
52+
generatedOffset: Position;
53+
}
54+
55+
export const DEFAULT_DECODE_OPTIONS: DecodeOptions = {
56+
mode: DecodeMode.LAX,
57+
generatedOffset: { line: 0, column: 0 },
58+
};
59+
4060
export function decode(
61+
sourceMap: SourceMap,
62+
options: Partial<DecodeOptions> = DEFAULT_DECODE_OPTIONS,
63+
): ScopeInfo {
64+
const opts = { ...DEFAULT_DECODE_OPTIONS, ...options };
65+
if ("sections" in sourceMap) {
66+
return decodeIndexMap(sourceMap, {
67+
...opts,
68+
generatedOffset: { line: 0, column: 0 },
69+
});
70+
}
71+
return decodeMap(sourceMap, opts);
72+
}
73+
74+
function decodeMap(
4175
sourceMap: SourceMapJson,
42-
options?: { mode: DecodeMode },
76+
options: DecodeOptions,
4377
): ScopeInfo {
4478
if (!sourceMap.scopes || !sourceMap.names) return { scopes: [], ranges: [] };
4579

4680
return new Decoder(sourceMap.scopes, sourceMap.names, options).decode();
4781
}
4882

83+
function decodeIndexMap(
84+
sourceMap: IndexSourceMapJson,
85+
options: DecodeOptions,
86+
): ScopeInfo {
87+
const scopeInfo: ScopeInfo = { scopes: [], ranges: [] };
88+
89+
for (const section of sourceMap.sections) {
90+
const { scopes, ranges } = decode(section.map, {
91+
...options,
92+
generatedOffset: section.offset,
93+
});
94+
for (const scope of scopes) scopeInfo.scopes.push(scope);
95+
for (const range of ranges) scopeInfo.ranges.push(range);
96+
}
97+
98+
return scopeInfo;
99+
}
100+
49101
const DEFAULT_SCOPE_STATE = {
50102
line: 0,
51103
column: 0,
@@ -77,10 +129,12 @@ class Decoder {
77129
#flatOriginalScopes: OriginalScope[] = [];
78130
#subRangeBindingsForRange = new Map<number, [number, number, number][]>();
79131

80-
constructor(scopes: string, names: string[], options?: { mode: DecodeMode }) {
132+
constructor(scopes: string, names: string[], options: DecodeOptions) {
81133
this.#encodedScopes = scopes;
82134
this.#names = names;
83-
this.#mode = options?.mode ?? DecodeMode.LAX;
135+
this.#mode = options.mode;
136+
this.#rangeState.line = options.generatedOffset.line;
137+
this.#rangeState.column = options.generatedOffset.column;
84138
}
85139

86140
decode(): ScopeInfo {

src/scopes.d.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,19 @@ export interface OriginalPosition extends Position {
159159
sourceIndex: number;
160160
}
161161

162+
/**
163+
* A standard source map, or index source map as per https://tc39.es/ecma426.
164+
*/
165+
export type SourceMap = SourceMapJson | IndexSourceMapJson;
166+
167+
/**
168+
* A standard index source map json object as per https://tc39.es/ecma426.
169+
*/
170+
export interface IndexSourceMapJson {
171+
version: 3;
172+
sections: Array<{ offset: Position; map: SourceMapJson }>;
173+
}
174+
162175
/**
163176
* A standard source map json object as per https://tc39.es/ecma426.
164177
*/

0 commit comments

Comments
 (0)