From fd446c1c29cd1035b8231be2c3f199239025037c Mon Sep 17 00:00:00 2001 From: Justin Ridgewell Date: Wed, 27 Apr 2022 23:20:23 -0400 Subject: [PATCH 1/7] Add maybeAddSegment/Mapping which skips adding useless segments --- src/gen-mapping.ts | 212 +++++++++++++++++++++++++++------------ test/gen-mapping.test.js | 173 +++++++++++++++++++++++++++++--- 2 files changed, 306 insertions(+), 79 deletions(-) diff --git a/src/gen-mapping.ts b/src/gen-mapping.ts index 7c5b7ce..6ae5540 100644 --- a/src/gen-mapping.ts +++ b/src/gen-mapping.ts @@ -81,6 +81,9 @@ export let addMapping: { ): void; }; +export let maybeAddSegment: typeof addSegment; +export let maybeAddMapping: typeof addMapping; + /** * Adds/removes the content of the source file to the source map. */ @@ -109,6 +112,29 @@ export let fromMap: (input: SourceMapInput) => GenMapping; */ export let allMappings: (map: GenMapping) => Mapping[]; +// This split declaration is only so that terser can elminiate the static initialization block. +let addSegmentInternal: ( + skipable: boolean, + map: GenMapping, + genLine: number, + genColumn: number, + source: S, + sourceLine: S extends string ? number : null | undefined, + sourceColumn: S extends string ? number : null | undefined, + name: S extends string ? string | null | undefined : null | undefined, +) => void; + +let addMappingInternal: ( + skipable: boolean, + map: GenMapping, + mapping: { + generated: Pos; + source: S; + original: S extends string ? Pos : null | undefined; + name: S extends string ? string | null | undefined : null | undefined; + }, +) => void; + /** * Provides the state to generate a sourcemap. */ @@ -127,45 +153,19 @@ export class GenMapping { static { addSegment = (map, genLine, genColumn, source, sourceLine, sourceColumn, name) => { - const { - _mappings: mappings, - _sources: sources, - _sourcesContent: sourcesContent, - _names: names, - } = map; - - const line = getLine(mappings, genLine); - if (!source) { - const seg: SourceMapSegment = [genColumn]; - const index = getColumnIndex(line, genColumn, seg); - return insert(line, index, seg); - } - - // Sigh, TypeScript can't figure out sourceLine and sourceColumn aren't nullish if source - // isn't nullish. - assert(sourceLine); - assert(sourceColumn); - const sourcesIndex = put(sources, source); - const seg: SourceMapSegment = name - ? [genColumn, sourcesIndex, sourceLine, sourceColumn, put(names, name)] - : [genColumn, sourcesIndex, sourceLine, sourceColumn]; + addSegmentInternal(false, map, genLine, genColumn, source, sourceLine, sourceColumn, name); + }; - const index = getColumnIndex(line, genColumn, seg); - if (sourcesIndex === sourcesContent.length) sourcesContent[sourcesIndex] = null; - insert(line, index, seg); + maybeAddSegment = (map, genLine, genColumn, source, sourceLine, sourceColumn, name) => { + addSegmentInternal(true, map, genLine, genColumn, source, sourceLine, sourceColumn, name); }; addMapping = (map, mapping) => { - const { generated, source, original, name } = mapping; - return (addSegment as any)( - map, - generated.line - 1, - generated.column, - source, - original == null ? undefined : original.line - 1, - original?.column, - name, - ); + return addMappingInternal(false, map, mapping as Parameters[2]); + }; + + maybeAddMapping = (map, mapping) => { + return addMappingInternal(true, map, mapping as Parameters[2]); }; setSourceContent = (map, source, content) => { @@ -182,6 +182,7 @@ export class GenMapping { _sourcesContent: sourcesContent, _names: names, } = map; + removeEmptyFinalLines(mappings); return { version: 3, @@ -241,6 +242,79 @@ export class GenMapping { return gen; }; + + // Internal helpers + addSegmentInternal = ( + skipable, + map, + genLine, + genColumn, + source, + sourceLine, + sourceColumn, + name, + ) => { + const { + _mappings: mappings, + _sources: sources, + _sourcesContent: sourcesContent, + _names: names, + } = map; + const line = getLine(mappings, genLine); + const index = getColumnIndex(line, genColumn); + + if (!source) { + if (skipable && skipSourceless(line, index)) return; + return insert(line, index, [genColumn]); + } + + // Sigh, TypeScript can't figure out sourceLine and sourceColumn aren't nullish if source + // isn't nullish. + assert(sourceLine); + assert(sourceColumn); + + const sourcesIndex = put(sources, source); + const namesIndex = name ? put(names, name) : -1; + if (sourcesIndex === sourcesContent.length) sourcesContent[sourcesIndex] = null; + + if (skipable && skipSource(line, index, sourcesIndex, sourceLine, sourceColumn)) return; + + insert( + line, + index, + name + ? [genColumn, sourcesIndex, sourceLine, sourceColumn, namesIndex] + : [genColumn, sourcesIndex, sourceLine, sourceColumn], + ); + }; + + addMappingInternal = (skipable, map, mapping) => { + const { generated, source, original, name } = mapping; + if (!source) { + return addSegmentInternal( + skipable, + map, + generated.line - 1, + generated.column, + null, + null, + null, + null, + ); + } + const s: string = source; + assert(original); + addSegmentInternal( + skipable, + map, + generated.line - 1, + generated.column, + s, + original.line - 1, + original.column, + name, + ); + }; } } @@ -255,43 +329,17 @@ function getLine(mappings: SourceMapSegment[][], index: number): SourceMapSegmen return mappings[index]; } -function getColumnIndex(line: SourceMapSegment[], column: number, seg: SourceMapSegment): number { +function getColumnIndex(line: SourceMapSegment[], column: number): number { let index = line.length; for (let i = index - 1; i >= 0; i--, index--) { const current = line[i]; const col = current[0]; if (col > column) continue; - if (col < column) break; - - const cmp = compare(current, seg); - if (cmp === 0) return index; - if (cmp < 0) break; + break; } return index; } -function compare(a: SourceMapSegment, b: SourceMapSegment): number { - let cmp = compareNum(a.length, b.length); - if (cmp !== 0) return cmp; - - // We've already checked genColumn - if (a.length === 1) return 0; - - cmp = compareNum(a[1], b[1]!); - if (cmp !== 0) return cmp; - cmp = compareNum(a[2], b[2]!); - if (cmp !== 0) return cmp; - cmp = compareNum(a[3], b[3]!); - if (cmp !== 0) return cmp; - - if (a.length === 4) return 0; - return compareNum(a[4], b[4]!); -} - -function compareNum(a: number, b: number): number { - return a - b; -} - function insert(array: T[], index: number, value: T) { for (let i = array.length; i > index; i--) { array[i] = array[i - 1]; @@ -299,6 +347,44 @@ function insert(array: T[], index: number, value: T) { array[index] = value; } +function removeEmptyFinalLines(mappings: SourceMapSegment[][]) { + const { length } = mappings; + let len = length; + for (let i = len - 1; i >= 0; len = i, i--) { + if (mappings[i].length > 0) break; + } + if (len < length) mappings.length = len; +} + function putAll(strarr: SetArray, array: string[]) { for (let i = 0; i < array.length; i++) put(strarr, array[i]); } + +function skipSourceless(line: SourceMapSegment[], index: number): boolean { + // The start of a line is already sourceless, so adding a sourceless segment to the beginning + // doesn't generate any useful information. + if (index === 0) return true; + + const prev = line[index - 1]; + + // If the previous segment is also sourceless, then adding another sourceless segment doesn't + // genrate any new information. Else, this segment will end the source/named segment and point to + // a sourceless position, which is useful. + return prev.length === 1; +} + +function skipSource( + line: SourceMapSegment[], + index: number, + sourcesIndex: number, + sourceLine: number, + sourceColumn: number, +): boolean { + // A source/named segment at the start of a line gives position at that genColumn + if (index === 0) return false; + + const prev = line[index - 1]; + // If the previous segment maps to the exact same source position, then this segment doesn't + // provide any new position information. + return sourcesIndex === prev[1] && sourceLine === prev[2] && sourceColumn === prev[3]; +} diff --git a/test/gen-mapping.test.js b/test/gen-mapping.test.js index 1e8177c..c705d54 100644 --- a/test/gen-mapping.test.js +++ b/test/gen-mapping.test.js @@ -7,6 +7,7 @@ const { toEncodedMap, allMappings, fromMap, + maybeAddSegment, } = require('..'); const assert = require('assert'); @@ -142,7 +143,11 @@ describe('GenMapping', () => { addSegment(map, 2, 0, 'input.js', 2, 0); addSegment(map, 0, 0, 'input.js', 0, 0); - assert.deepEqual(toDecodedMap(map).mappings, [[[0, 0, 0, 0]], [[0, 0, 1, 0]], [[0, 0, 2, 0]]]); + assert.deepEqual(toDecodedMap(map).mappings, [ + [[0, 0, 0, 0]], + [[0, 0, 1, 0]], + [[0, 0, 2, 0]], + ]); }); it('sorts generated column', () => { @@ -160,7 +165,7 @@ describe('GenMapping', () => { ]); }); - it('sorts source index', () => { + it('postfix sorts source index', () => { const map = new GenMapping(); addSegment(map, 1, 0, 'foo.js', 0, 0); @@ -169,14 +174,14 @@ describe('GenMapping', () => { assert.deepEqual(toDecodedMap(map).mappings, [ [ - [0, 0, 0, 0], [0, 1, 0, 0], + [0, 0, 0, 0], ], [[0, 0, 0, 0]], ]); }); - it('sorts source line', () => { + it('postfix sorts source line', () => { const map = new GenMapping(); addSegment(map, 0, 0, 'input.js', 1, 0); @@ -185,14 +190,14 @@ describe('GenMapping', () => { assert.deepEqual(toDecodedMap(map).mappings, [ [ - [0, 0, 0, 0], [0, 0, 1, 0], [0, 0, 2, 0], + [0, 0, 0, 0], ], ]); }); - it('sorts source column', () => { + it('postfix sorts source column', () => { const map = new GenMapping(); addSegment(map, 0, 0, 'input.js', 0, 1); @@ -201,14 +206,14 @@ describe('GenMapping', () => { assert.deepEqual(toDecodedMap(map).mappings, [ [ - [0, 0, 0, 0], [0, 0, 0, 1], [0, 0, 0, 2], + [0, 0, 0, 0], ], ]); }); - it('sorts name index', () => { + it('postfix sorts name index', () => { const map = new GenMapping(); addSegment(map, 1, 0, 'input.js', 0, 0, 'foo'); @@ -217,32 +222,32 @@ describe('GenMapping', () => { assert.deepEqual(toDecodedMap(map).mappings, [ [ - [0, 0, 0, 0, 0], [0, 0, 0, 0, 1], + [0, 0, 0, 0, 0], ], [[0, 0, 0, 0, 0]], ]); }); - it('sorts sourceless segment before source segment', () => { + it('postfix sorts sourceless segment after source segment', () => { const map = new GenMapping(); addSegment(map, 0, 0, 'input.js', 0, 0); addSegment(map, 0, 0); - assert.deepEqual(toDecodedMap(map).mappings, [[[0], [0, 0, 0, 0]]]); + assert.deepEqual(toDecodedMap(map).mappings, [[[0, 0, 0, 0], [0]]]); }); - it('sorts sourceless segment before named segment', () => { + it('postfix sorts sourceless segment after named segment', () => { const map = new GenMapping(); addSegment(map, 0, 0, 'input.js', 0, 0, 'foo'); addSegment(map, 0, 0); - assert.deepEqual(toDecodedMap(map).mappings, [[[0], [0, 0, 0, 0, 0]]]); + assert.deepEqual(toDecodedMap(map).mappings, [[[0, 0, 0, 0, 0], [0]]]); }); - it('sorts source segment before named segment', () => { + it('postfix sorts source segment after named segment', () => { const map = new GenMapping(); addSegment(map, 0, 0, 'input.js', 0, 0, 'foo'); @@ -250,8 +255,8 @@ describe('GenMapping', () => { assert.deepEqual(toDecodedMap(map).mappings, [ [ - [0, 0, 0, 0], [0, 0, 0, 0, 0], + [0, 0, 0, 0], ], ]); }); @@ -266,7 +271,7 @@ describe('GenMapping', () => { assert.deepEqual(toDecodedMap(map).mappings, [[[0], [0]], [[0]]]); }); - it('skips equivalent source segment', () => { + it('keeps equivalent source segment', () => { const map = new GenMapping(); addSegment(map, 0, 0, 'input.js', 0, 0); @@ -551,4 +556,140 @@ describe('GenMapping', () => { assert.deepEqual(toDecodedMap(map).mappings, [[[1, 0, 2, 3, 0]]]); }); }); + + describe('maybeAddMapping', () => { + describe('sourceless segment added afterwards', () => { + it('skips sourceless segment first on line', () => { + const map = new GenMapping(); + + maybeAddSegment(map, 0, 1, 'input.js', 0, 0); + maybeAddSegment(map, 1, 1); + + assert.deepEqual(toDecodedMap(map).mappings, [[[1, 0, 0, 0]]]); + }); + + it('skips sourceless segment sorted first in line', () => { + const map = new GenMapping(); + + maybeAddSegment(map, 0, 1, 'input.js', 0, 0); + maybeAddSegment(map, 0, 0); + + assert.deepEqual(toDecodedMap(map).mappings, [[[1, 0, 0, 0]]]); + }); + + it('skips equivalent sourceless segment', () => { + const map = new GenMapping(); + + maybeAddSegment(map, 0, 0, 'input.js', 0, 0); + maybeAddSegment(map, 0, 1); + maybeAddSegment(map, 0, 1); + + assert.deepEqual(toDecodedMap(map).mappings, [[[0, 0, 0, 0], [1]]]); + }); + + it('skips runs of sourceless segment', () => { + const map = new GenMapping(); + + maybeAddSegment(map, 0, 0, 'input.js', 0, 0); + maybeAddSegment(map, 0, 1); + maybeAddSegment(map, 0, 2); + + assert.deepEqual(toDecodedMap(map).mappings, [[[0, 0, 0, 0], [1]]]); + }); + + it('does not skip sourcless segment sorted before sourceless segment', () => { + const map = new GenMapping(); + + maybeAddSegment(map, 0, 0, 'input.js', 0, 0); + maybeAddSegment(map, 0, 2); + maybeAddSegment(map, 0, 1); + + assert.deepEqual(toDecodedMap(map).mappings, [[[0, 0, 0, 0], [1], [2]]]); + }); + + it('does not skip sourcless segment matching source segment', () => { + const map = new GenMapping(); + + maybeAddSegment(map, 0, 0, 'input.js', 0, 0); + maybeAddSegment(map, 0, 0); + + assert.deepEqual(toDecodedMap(map).mappings, [[[0, 0, 0, 0], [0]]]); + }); + }); + + describe('source segment added afterwards', () => { + it('skips equivalent source segment', () => { + const map = new GenMapping(); + + maybeAddSegment(map, 0, 0, 'input.js', 0, 0); + maybeAddSegment(map, 0, 0, 'input.js', 0, 0); + + assert.deepEqual(toDecodedMap(map).mappings, [[[0, 0, 0, 0]]]); + }); + + it('skips source segment after matching named segment', () => { + const map = new GenMapping(); + + maybeAddSegment(map, 0, 0, 'input.js', 0, 0, 'foo'); + maybeAddSegment(map, 0, 0, 'input.js', 0, 0); + + assert.deepEqual(toDecodedMap(map).mappings, [[[0, 0, 0, 0, 0]]]); + }); + + it('skips named segment after matching source segment', () => { + const map = new GenMapping(); + + maybeAddSegment(map, 0, 0, 'input.js', 0, 0); + maybeAddSegment(map, 0, 0, 'input.js', 0, 0, 'foo'); + + assert.deepEqual(toDecodedMap(map).mappings, [[[0, 0, 0, 0]]]); + }); + + it('skips runs of matching source segment', () => { + const map = new GenMapping(); + + maybeAddSegment(map, 0, 0, 'input.js', 0, 0); + maybeAddSegment(map, 0, 1, 'input.js', 0, 0); + + assert.deepEqual(toDecodedMap(map).mappings, [[[0, 0, 0, 0]]]); + }); + + it('keeps source segment pointing to different source file', () => { + const map = new GenMapping(); + + maybeAddSegment(map, 0, 0, 'input.js', 0, 0); + maybeAddSegment(map, 0, 0, 'foo.js', 0, 0); + + assert.deepEqual(toDecodedMap(map).mappings, [[[0, 0, 0, 0], [0, 1, 0, 0]]]); + }); + + it('keeps source segment pointing to different source line', () => { + const map = new GenMapping(); + + maybeAddSegment(map, 0, 0, 'input.js', 0, 0); + maybeAddSegment(map, 0, 0, 'input.js', 1, 0); + + assert.deepEqual(toDecodedMap(map).mappings, [[[0, 0, 0, 0], [0, 0, 1, 0]]]); + }); + + it('keeps source segment pointing to different source column', () => { + const map = new GenMapping(); + + maybeAddSegment(map, 0, 0, 'input.js', 0, 0); + maybeAddSegment(map, 0, 0, 'input.js', 0, 1); + + assert.deepEqual(toDecodedMap(map).mappings, [[[0, 0, 0, 0], [0, 0, 0, 1]]]); + }); + + it('keeps source segment after matching sourceless segment', () => { + const map = new GenMapping(); + + maybeAddSegment(map, 0, 0, 'input.js', 0, 0); + maybeAddSegment(map, 0, 1); + maybeAddSegment(map, 0, 1, 'input.js', 0, 0); + + assert.deepEqual(toDecodedMap(map).mappings, [[[0, 0, 0, 0], [1], [1, 0, 0, 0]]]); + }); + }); + }); }); From b80b8f68fdac3b78a59cf46e5d02ff2faaa005ed Mon Sep 17 00:00:00 2001 From: Justin Ridgewell Date: Thu, 28 Apr 2022 10:53:02 -0400 Subject: [PATCH 2/7] Clenaup --- src/gen-mapping.ts | 120 ++++++++++++++++++++++++++------------------- 1 file changed, 69 insertions(+), 51 deletions(-) diff --git a/src/gen-mapping.ts b/src/gen-mapping.ts index 6ae5540..a6e20e6 100644 --- a/src/gen-mapping.ts +++ b/src/gen-mapping.ts @@ -2,6 +2,14 @@ import { SetArray, put } from '@jridgewell/set-array'; import { encode } from '@jridgewell/sourcemap-codec'; import { TraceMap, decodedMappings } from '@jridgewell/trace-mapping'; +import { + COLUMN, + SOURCES_INDEX, + SOURCE_LINE, + SOURCE_COLUMN, + NAMES_INDEX, +} from './sourcemap-segment'; + import type { SourceMapInput } from '@jridgewell/trace-mapping'; import type { SourceMapSegment } from './sourcemap-segment'; import type { DecodedSourceMap, EncodedSourceMap, Pos, Mapping } from './types'; @@ -81,7 +89,18 @@ export let addMapping: { ): void; }; +/** + * Same as `addSegment`, but will only add the segment if it generates useful information in the + * resulting map. This only works correctly if segments are added **in order**, meaning you should + * not add a segment with a lower generated line/column than one that came before. + */ export let maybeAddSegment: typeof addSegment; + +/** + * Same as `addMapping`, but will only add the mapping if it generates useful information in the + * resulting map. This only works correctly if mappings are added **in order**, meaning you should + * not add a mapping with a lower generated line/column than one that came before. + */ export let maybeAddMapping: typeof addMapping; /** @@ -124,17 +143,6 @@ let addSegmentInternal: ( name: S extends string ? string | null | undefined : null | undefined, ) => void; -let addMappingInternal: ( - skipable: boolean, - map: GenMapping, - mapping: { - generated: Pos; - source: S; - original: S extends string ? Pos : null | undefined; - name: S extends string ? string | null | undefined : null | undefined; - }, -) => void; - /** * Provides the state to generate a sourcemap. */ @@ -186,7 +194,7 @@ export class GenMapping { return { version: 3, - file, + file: file || undefined, names: names.array, sourceRoot: sourceRoot || undefined, sources: sources.array, @@ -212,16 +220,16 @@ export class GenMapping { for (let j = 0; j < line.length; j++) { const seg = line[j]; - const generated = { line: i + 1, column: seg[0] }; + const generated = { line: i + 1, column: seg[COLUMN] }; let source: string | undefined = undefined; let original: Pos | undefined = undefined; let name: string | undefined = undefined; if (seg.length !== 1) { - source = sources.array[seg[1]]; - original = { line: seg[2] + 1, column: seg[3] }; + source = sources.array[seg[SOURCES_INDEX]]; + original = { line: seg[SOURCE_LINE] + 1, column: seg[SOURCE_COLUMN] }; - if (seg.length === 5) name = names.array[seg[4]]; + if (seg.length === 5) name = names.array[seg[NAMES_INDEX]]; } out.push({ generated, source, original, name } as Mapping); @@ -287,34 +295,6 @@ export class GenMapping { : [genColumn, sourcesIndex, sourceLine, sourceColumn], ); }; - - addMappingInternal = (skipable, map, mapping) => { - const { generated, source, original, name } = mapping; - if (!source) { - return addSegmentInternal( - skipable, - map, - generated.line - 1, - generated.column, - null, - null, - null, - null, - ); - } - const s: string = source; - assert(original); - addSegmentInternal( - skipable, - map, - generated.line - 1, - generated.column, - s, - original.line - 1, - original.column, - name, - ); - }; } } @@ -329,13 +309,11 @@ function getLine(mappings: SourceMapSegment[][], index: number): SourceMapSegmen return mappings[index]; } -function getColumnIndex(line: SourceMapSegment[], column: number): number { +function getColumnIndex(line: SourceMapSegment[], genColumn: number): number { let index = line.length; - for (let i = index - 1; i >= 0; i--, index--) { + for (let i = index - 1; i >= 0; index = i--) { const current = line[i]; - const col = current[0]; - if (col > column) continue; - break; + if (genColumn >= current[COLUMN]) break; } return index; } @@ -366,7 +344,6 @@ function skipSourceless(line: SourceMapSegment[], index: number): boolean { if (index === 0) return true; const prev = line[index - 1]; - // If the previous segment is also sourceless, then adding another sourceless segment doesn't // genrate any new information. Else, this segment will end the source/named segment and point to // a sourceless position, which is useful. @@ -386,5 +363,46 @@ function skipSource( const prev = line[index - 1]; // If the previous segment maps to the exact same source position, then this segment doesn't // provide any new position information. - return sourcesIndex === prev[1] && sourceLine === prev[2] && sourceColumn === prev[3]; + return ( + sourcesIndex === prev[SOURCES_INDEX] && + sourceLine === prev[SOURCE_LINE] && + sourceColumn === prev[SOURCE_COLUMN] + ); +} + +function addMappingInternal( + skipable: boolean, + map: GenMapping, + mapping: { + generated: Pos; + source: S; + original: S extends string ? Pos : null | undefined; + name: S extends string ? string | null | undefined : null | undefined; + }, +) { + const { generated, source, original, name } = mapping; + if (!source) { + return addSegmentInternal( + skipable, + map, + generated.line - 1, + generated.column, + null, + null, + null, + null, + ); + } + const s: string = source; + assert(original); + addSegmentInternal( + skipable, + map, + generated.line - 1, + generated.column, + s, + original.line - 1, + original.column, + name, + ); } From 4724f10332d1005bf6fa1e55144035bbf8cb687c Mon Sep 17 00:00:00 2001 From: Justin Ridgewell Date: Thu, 28 Apr 2022 10:53:15 -0400 Subject: [PATCH 3/7] test maybeAddMapping --- test/gen-mapping.test.js | 86 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 82 insertions(+), 4 deletions(-) diff --git a/test/gen-mapping.test.js b/test/gen-mapping.test.js index c705d54..0621331 100644 --- a/test/gen-mapping.test.js +++ b/test/gen-mapping.test.js @@ -8,6 +8,7 @@ const { allMappings, fromMap, maybeAddSegment, + maybeAddMapping, } = require('..'); const assert = require('assert'); @@ -557,7 +558,7 @@ describe('GenMapping', () => { }); }); - describe('maybeAddMapping', () => { + describe('maybeAddSegment', () => { describe('sourceless segment added afterwards', () => { it('skips sourceless segment first on line', () => { const map = new GenMapping(); @@ -660,7 +661,12 @@ describe('GenMapping', () => { maybeAddSegment(map, 0, 0, 'input.js', 0, 0); maybeAddSegment(map, 0, 0, 'foo.js', 0, 0); - assert.deepEqual(toDecodedMap(map).mappings, [[[0, 0, 0, 0], [0, 1, 0, 0]]]); + assert.deepEqual(toDecodedMap(map).mappings, [ + [ + [0, 0, 0, 0], + [0, 1, 0, 0], + ], + ]); }); it('keeps source segment pointing to different source line', () => { @@ -669,7 +675,12 @@ describe('GenMapping', () => { maybeAddSegment(map, 0, 0, 'input.js', 0, 0); maybeAddSegment(map, 0, 0, 'input.js', 1, 0); - assert.deepEqual(toDecodedMap(map).mappings, [[[0, 0, 0, 0], [0, 0, 1, 0]]]); + assert.deepEqual(toDecodedMap(map).mappings, [ + [ + [0, 0, 0, 0], + [0, 0, 1, 0], + ], + ]); }); it('keeps source segment pointing to different source column', () => { @@ -678,7 +689,12 @@ describe('GenMapping', () => { maybeAddSegment(map, 0, 0, 'input.js', 0, 0); maybeAddSegment(map, 0, 0, 'input.js', 0, 1); - assert.deepEqual(toDecodedMap(map).mappings, [[[0, 0, 0, 0], [0, 0, 0, 1]]]); + assert.deepEqual(toDecodedMap(map).mappings, [ + [ + [0, 0, 0, 0], + [0, 0, 0, 1], + ], + ]); }); it('keeps source segment after matching sourceless segment', () => { @@ -692,4 +708,66 @@ describe('GenMapping', () => { }); }); }); + + describe('maybeAddMapping', () => { + describe('sourceless segment added afterwards', () => { + it('skips sourceless segment first on line', () => { + const map = new GenMapping(); + + maybeAddMapping(map, { + generated: { line: 1, column: 1 }, + source: 'input.js', + original: { + line: 1, + column: 0, + }, + }); + maybeAddMapping(map, { generated: { line: 2, column: 1 } }); + + assert.deepEqual(toDecodedMap(map).mappings, [[[1, 0, 0, 0]]]); + }); + + it('skips equivalent sourceless segment', () => { + const map = new GenMapping(); + + maybeAddMapping(map, { + generated: { line: 1, column: 0 }, + source: 'input.js', + original: { + line: 1, + column: 0, + }, + }); + maybeAddMapping(map, { generated: { line: 1, column: 1 } }); + maybeAddMapping(map, { generated: { line: 1, column: 1 } }); + + assert.deepEqual(toDecodedMap(map).mappings, [[[0, 0, 0, 0], [1]]]); + }); + }); + + describe('source segment added afterwards', () => { + it('skips equivalent source segment', () => { + const map = new GenMapping(); + + maybeAddMapping(map, { + generated: { line: 1, column: 0 }, + source: 'input.js', + original: { + line: 1, + column: 0, + }, + }); + maybeAddMapping(map, { + generated: { line: 1, column: 0 }, + source: 'input.js', + original: { + line: 1, + column: 0, + }, + }); + + assert.deepEqual(toDecodedMap(map).mappings, [[[0, 0, 0, 0]]]); + }); + }); + }); }); From a260169046a265028a7348d7a3e2cc80fdd3d431 Mon Sep 17 00:00:00 2001 From: Justin Ridgewell Date: Thu, 28 Apr 2022 10:55:49 -0400 Subject: [PATCH 4/7] Tail call --- src/gen-mapping.ts | 26 ++++++++++++++++++++++---- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/src/gen-mapping.ts b/src/gen-mapping.ts index a6e20e6..7bb41f1 100644 --- a/src/gen-mapping.ts +++ b/src/gen-mapping.ts @@ -161,11 +161,29 @@ export class GenMapping { static { addSegment = (map, genLine, genColumn, source, sourceLine, sourceColumn, name) => { - addSegmentInternal(false, map, genLine, genColumn, source, sourceLine, sourceColumn, name); + return addSegmentInternal( + false, + map, + genLine, + genColumn, + source, + sourceLine, + sourceColumn, + name, + ); }; maybeAddSegment = (map, genLine, genColumn, source, sourceLine, sourceColumn, name) => { - addSegmentInternal(true, map, genLine, genColumn, source, sourceLine, sourceColumn, name); + return addSegmentInternal( + true, + map, + genLine, + genColumn, + source, + sourceLine, + sourceColumn, + name, + ); }; addMapping = (map, mapping) => { @@ -287,7 +305,7 @@ export class GenMapping { if (skipable && skipSource(line, index, sourcesIndex, sourceLine, sourceColumn)) return; - insert( + return insert( line, index, name @@ -395,7 +413,7 @@ function addMappingInternal( } const s: string = source; assert(original); - addSegmentInternal( + return addSegmentInternal( skipable, map, generated.line - 1, From 2abb151837d6904345558a4e4d071aca057d154d Mon Sep 17 00:00:00 2001 From: Justin Ridgewell Date: Thu, 28 Apr 2022 22:40:43 -0400 Subject: [PATCH 5/7] Keep segments which change name --- src/gen-mapping.ts | 7 +++++-- test/gen-mapping.test.js | 37 +++++++++++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+), 2 deletions(-) diff --git a/src/gen-mapping.ts b/src/gen-mapping.ts index 7bb41f1..fe418df 100644 --- a/src/gen-mapping.ts +++ b/src/gen-mapping.ts @@ -303,7 +303,8 @@ export class GenMapping { const namesIndex = name ? put(names, name) : -1; if (sourcesIndex === sourcesContent.length) sourcesContent[sourcesIndex] = null; - if (skipable && skipSource(line, index, sourcesIndex, sourceLine, sourceColumn)) return; + if (skipable && skipSource(line, index, sourcesIndex, sourceLine, sourceColumn, namesIndex)) + return; return insert( line, @@ -374,6 +375,7 @@ function skipSource( sourcesIndex: number, sourceLine: number, sourceColumn: number, + namesIndex: number, ): boolean { // A source/named segment at the start of a line gives position at that genColumn if (index === 0) return false; @@ -384,7 +386,8 @@ function skipSource( return ( sourcesIndex === prev[SOURCES_INDEX] && sourceLine === prev[SOURCE_LINE] && - sourceColumn === prev[SOURCE_COLUMN] + sourceColumn === prev[SOURCE_COLUMN] && + namesIndex === (prev.length === 5 ? prev[4] : -1) ); } diff --git a/test/gen-mapping.test.js b/test/gen-mapping.test.js index 0621331..a08bac3 100644 --- a/test/gen-mapping.test.js +++ b/test/gen-mapping.test.js @@ -655,6 +655,43 @@ describe('GenMapping', () => { assert.deepEqual(toDecodedMap(map).mappings, [[[0, 0, 0, 0]]]); }); + it('skips runs of matching named segment', () => { + const map = new GenMapping(); + + maybeAddSegment(map, 0, 0, 'input.js', 0, 0, 'foo'); + maybeAddSegment(map, 0, 1, 'input.js', 0, 0, 'foo'); + + assert.deepEqual(toDecodedMap(map).mappings, [[[0, 0, 0, 0, 0]]]); + }); + + it('keeps runs of matching source segment to named segment', () => { + const map = new GenMapping(); + + maybeAddSegment(map, 0, 0, 'input.js', 0, 0); + maybeAddSegment(map, 0, 1, 'input.js', 0, 0, 'foo'); + + assert.deepEqual(toDecodedMap(map).mappings, [ + [ + [0, 0, 0, 0], + [1, 0, 0, 0, 0], + ], + ]); + }); + + it('keeps runs of matching named segment to source segment', () => { + const map = new GenMapping(); + + maybeAddSegment(map, 0, 0, 'input.js', 0, 0, 'foo'); + maybeAddSegment(map, 0, 1, 'input.js', 0, 0); + + assert.deepEqual(toDecodedMap(map).mappings, [ + [ + [0, 0, 0, 0, 0], + [1, 0, 0, 0], + ], + ]); + }); + it('keeps source segment pointing to different source file', () => { const map = new GenMapping(); From 6b04ecd27579652465f38e3de78d74bf296d0c5d Mon Sep 17 00:00:00 2001 From: Justin Ridgewell Date: Fri, 29 Apr 2022 00:44:48 -0400 Subject: [PATCH 6/7] Cleanup --- src/gen-mapping.ts | 13 +++++++-- test/gen-mapping.test.js | 60 +++++++++++++++++++++++----------------- 2 files changed, 45 insertions(+), 28 deletions(-) diff --git a/src/gen-mapping.ts b/src/gen-mapping.ts index fe418df..44a87cc 100644 --- a/src/gen-mapping.ts +++ b/src/gen-mapping.ts @@ -21,6 +21,8 @@ export type Options = { sourceRoot?: string | null; }; +const NO_NAME = -1; + /** * A low-level API to associate a generated position with an original source position. Line and * column here are 0-based, unlike `addMapping`. @@ -300,11 +302,12 @@ export class GenMapping { assert(sourceColumn); const sourcesIndex = put(sources, source); - const namesIndex = name ? put(names, name) : -1; + const namesIndex = name ? put(names, name) : NO_NAME; if (sourcesIndex === sourcesContent.length) sourcesContent[sourcesIndex] = null; - if (skipable && skipSource(line, index, sourcesIndex, sourceLine, sourceColumn, namesIndex)) + if (skipable && skipSource(line, index, sourcesIndex, sourceLine, sourceColumn, namesIndex)) { return; + } return insert( line, @@ -381,13 +384,17 @@ function skipSource( if (index === 0) return false; const prev = line[index - 1]; + + // If the previous segment is sourceless, then we're transitioning to a source. + if (prev.length === 1) return false; + // If the previous segment maps to the exact same source position, then this segment doesn't // provide any new position information. return ( sourcesIndex === prev[SOURCES_INDEX] && sourceLine === prev[SOURCE_LINE] && sourceColumn === prev[SOURCE_COLUMN] && - namesIndex === (prev.length === 5 ? prev[4] : -1) + namesIndex === (prev.length === 5 ? prev[NAMES_INDEX] : NO_NAME) ); } diff --git a/test/gen-mapping.test.js b/test/gen-mapping.test.js index a08bac3..c461d9b 100644 --- a/test/gen-mapping.test.js +++ b/test/gen-mapping.test.js @@ -628,43 +628,49 @@ describe('GenMapping', () => { assert.deepEqual(toDecodedMap(map).mappings, [[[0, 0, 0, 0]]]); }); - it('skips source segment after matching named segment', () => { + it('keeps source segment after matching named segment', () => { const map = new GenMapping(); maybeAddSegment(map, 0, 0, 'input.js', 0, 0, 'foo'); maybeAddSegment(map, 0, 0, 'input.js', 0, 0); - assert.deepEqual(toDecodedMap(map).mappings, [[[0, 0, 0, 0, 0]]]); + assert.deepEqual(toDecodedMap(map).mappings, [ + [ + [0, 0, 0, 0, 0], + [0, 0, 0, 0], + ], + ]); }); - it('skips named segment after matching source segment', () => { + it('keeps runs of source segment after matching named segment', () => { const map = new GenMapping(); - maybeAddSegment(map, 0, 0, 'input.js', 0, 0); maybeAddSegment(map, 0, 0, 'input.js', 0, 0, 'foo'); - - assert.deepEqual(toDecodedMap(map).mappings, [[[0, 0, 0, 0]]]); - }); - - it('skips runs of matching source segment', () => { - const map = new GenMapping(); - - maybeAddSegment(map, 0, 0, 'input.js', 0, 0); maybeAddSegment(map, 0, 1, 'input.js', 0, 0); - assert.deepEqual(toDecodedMap(map).mappings, [[[0, 0, 0, 0]]]); + assert.deepEqual(toDecodedMap(map).mappings, [ + [ + [0, 0, 0, 0, 0], + [1, 0, 0, 0], + ], + ]); }); - it('skips runs of matching named segment', () => { + it('keeps named segment after matching source segment', () => { const map = new GenMapping(); + maybeAddSegment(map, 0, 0, 'input.js', 0, 0); maybeAddSegment(map, 0, 0, 'input.js', 0, 0, 'foo'); - maybeAddSegment(map, 0, 1, 'input.js', 0, 0, 'foo'); - assert.deepEqual(toDecodedMap(map).mappings, [[[0, 0, 0, 0, 0]]]); + assert.deepEqual(toDecodedMap(map).mappings, [ + [ + [0, 0, 0, 0], + [0, 0, 0, 0, 0], + ], + ]); }); - it('keeps runs of matching source segment to named segment', () => { + it('keeps runs of named segment after matching source segment', () => { const map = new GenMapping(); maybeAddSegment(map, 0, 0, 'input.js', 0, 0); @@ -678,18 +684,22 @@ describe('GenMapping', () => { ]); }); - it('keeps runs of matching named segment to source segment', () => { + it('skips runs of matching source segment', () => { const map = new GenMapping(); - maybeAddSegment(map, 0, 0, 'input.js', 0, 0, 'foo'); + maybeAddSegment(map, 0, 0, 'input.js', 0, 0); maybeAddSegment(map, 0, 1, 'input.js', 0, 0); - assert.deepEqual(toDecodedMap(map).mappings, [ - [ - [0, 0, 0, 0, 0], - [1, 0, 0, 0], - ], - ]); + assert.deepEqual(toDecodedMap(map).mappings, [[[0, 0, 0, 0]]]); + }); + + it('skips runs of matching named segment', () => { + const map = new GenMapping(); + + maybeAddSegment(map, 0, 0, 'input.js', 0, 0, 'foo'); + maybeAddSegment(map, 0, 1, 'input.js', 0, 0, 'foo'); + + assert.deepEqual(toDecodedMap(map).mappings, [[[0, 0, 0, 0, 0]]]); }); it('keeps source segment pointing to different source file', () => { From cad56e178515dbdd08567333aed8b0f83e4599ff Mon Sep 17 00:00:00 2001 From: Justin Ridgewell Date: Sat, 30 Apr 2022 00:20:49 -0400 Subject: [PATCH 7/7] Add readme docs --- README.md | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/README.md b/README.md index 8798c26..0250c5e 100644 --- a/README.md +++ b/README.md @@ -52,6 +52,47 @@ assert.deepEqual(encodedMap(map), { }); ``` +### Smaller Sourcemaps + +Not everything needs to be added to a sourcemap, and needless markings can cause signficantly +larger file sizes. `gen-mapping` exposes `maybeAddSegment`/`maybeAddMapping` APIs that will +intelligently determine if this marking adds useful information. If not, the marking will be +skipped. + +```typescript +import { GenMapping, encodedMap, maybeAddMapping } from '@jridgewell/gen-mapping'; + +const map = new GenMapping(); + +// Adding a sourceless marking at the beginning of a line isn't useful. +maybeAddMapping(map, { + generated: { line: 1, column: 0 }, +}); + +// Adding a new source marking is useful. +maybeAddMapping(map, { + generated: { line: 1, column: 0 }, + source: 'input.js', + original: { line: 1, column: 0 }, +}); + +// But adding another marking pointing to the exact same original location isn't, even if the +// generated column changed. +maybeAddMapping(map, { + generated: { line: 1, column: 9 }, + source: 'input.js', + original: { line: 1, column: 0 }, +}); + +assert.deepEqual(encodedMap(map), { + version: 3, + names: [], + sources: ['input.js'], + sourcesContent: [null], + mappings: 'AAAA', +}); +``` + ## Benchmarks ```