diff --git a/src/gen-mapping.ts b/src/gen-mapping.ts index 601c745..946b57c 100644 --- a/src/gen-mapping.ts +++ b/src/gen-mapping.ts @@ -23,124 +23,276 @@ export type Options = { const NO_NAME = -1; +/** + * Provides the state to generate a sourcemap. + */ +export class GenMapping { + private declare _names; + private declare _sources; + private declare _sourcesContent: (string | null)[]; + private declare _mappings: SourceMapSegment[][]; + declare file: string | null | undefined; + declare sourceRoot: string | null | undefined; + + constructor({ file, sourceRoot }: Options = {}) { + this._names = new SetArray(); + this._sources = new SetArray(); + this._sourcesContent = []; + this._mappings = []; + this.file = file; + this.sourceRoot = sourceRoot; + } +} + +interface PublicMap { + _names: GenMapping['_names']; + _sources: GenMapping['_sources']; + _sourcesContent: GenMapping['_sourcesContent']; + _mappings: GenMapping['_mappings']; +} + +/** + * Typescript doesn't allow friend access to private fields, so this just casts the map into a type + * with public access modifiers. + */ +function cast(map: unknown): PublicMap { + return map as any; +} + /** * A low-level API to associate a generated position with an original source position. Line and * column here are 0-based, unlike `addMapping`. */ -export let addSegment: { - ( - map: GenMapping, - genLine: number, - genColumn: number, - source?: null, - sourceLine?: null, - sourceColumn?: null, - name?: null, - content?: null, - ): void; - ( - map: GenMapping, - genLine: number, - genColumn: number, - source: string, - sourceLine: number, - sourceColumn: number, - name?: null, - content?: string | null, - ): void; - ( - map: GenMapping, - genLine: number, - genColumn: number, - source: string, - sourceLine: number, - sourceColumn: number, - name: string, - content?: string | null, - ): void; -}; +export function addSegment( + map: GenMapping, + genLine: number, + genColumn: number, + source?: null, + sourceLine?: null, + sourceColumn?: null, + name?: null, + content?: null, +): void; +export function addSegment( + map: GenMapping, + genLine: number, + genColumn: number, + source: string, + sourceLine: number, + sourceColumn: number, + name?: null, + content?: string | null, +): void; +export function addSegment( + map: GenMapping, + genLine: number, + genColumn: number, + source: string, + sourceLine: number, + sourceColumn: number, + name: string, + content?: string | null, +): void; +export function addSegment( + map: GenMapping, + genLine: number, + genColumn: number, + source?: string | null, + sourceLine?: number | null, + sourceColumn?: number | null, + name?: string | null, + content?: string | null, +): void { + return addSegmentInternal( + false, + map, + genLine, + genColumn, + source, + sourceLine, + sourceColumn, + name, + content, + ); +} /** * A high-level API to associate a generated position with an original source position. Line is * 1-based, but column is 0-based, due to legacy behavior in `source-map` library. */ -export let addMapping: { - ( - map: GenMapping, - mapping: { - generated: Pos; - source?: null; - original?: null; - name?: null; - content?: null; - }, - ): void; - ( - map: GenMapping, - mapping: { - generated: Pos; - source: string; - original: Pos; - name?: null; - content?: string | null; - }, - ): void; - ( - map: GenMapping, - mapping: { - generated: Pos; - source: string; - original: Pos; - name: string; - content?: string | null; - }, - ): void; -}; +export function addMapping( + map: GenMapping, + mapping: { + generated: Pos; + source?: null; + original?: null; + name?: null; + content?: null; + }, +): void; +export function addMapping( + map: GenMapping, + mapping: { + generated: Pos; + source: string; + original: Pos; + name?: null; + content?: string | null; + }, +): void; +export function addMapping( + map: GenMapping, + mapping: { + generated: Pos; + source: string; + original: Pos; + name: string; + content?: string | null; + }, +): void; +export function addMapping( + map: GenMapping, + mapping: { + generated: Pos; + source?: string | null; + original?: Pos | null; + name?: string | null; + content?: string | null; + }, +): void { + return addMappingInternal(false, map, mapping as Parameters[2]); +} /** * 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; +export const maybeAddSegment: typeof addSegment = ( + map, + genLine, + genColumn, + source, + sourceLine, + sourceColumn, + name, + content, +) => { + return addSegmentInternal( + true, + map, + genLine, + genColumn, + source, + sourceLine, + sourceColumn, + name, + content, + ); +}; /** * 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; +export const maybeAddMapping: typeof addMapping = (map, mapping) => { + return addMappingInternal(true, map, mapping as Parameters[2]); +}; /** * Adds/removes the content of the source file to the source map. */ -export let setSourceContent: (map: GenMapping, source: string, content: string | null) => void; +export function setSourceContent(map: GenMapping, source: string, content: string | null): void { + const { _sources: sources, _sourcesContent: sourcesContent } = cast(map); + sourcesContent[put(sources, source)] = content; +} /** * Returns a sourcemap object (with decoded mappings) suitable for passing to a library that expects * a sourcemap, or to JSON.stringify. */ -export let toDecodedMap: (map: GenMapping) => DecodedSourceMap; +export function toDecodedMap(map: GenMapping): DecodedSourceMap { + const { + _mappings: mappings, + _sources: sources, + _sourcesContent: sourcesContent, + _names: names, + } = cast(map); + removeEmptyFinalLines(mappings); + + return { + version: 3, + file: map.file || undefined, + names: names.array, + sourceRoot: map.sourceRoot || undefined, + sources: sources.array, + sourcesContent, + mappings, + }; +} /** * Returns a sourcemap object (with encoded mappings) suitable for passing to a library that expects * a sourcemap, or to JSON.stringify. */ -export let toEncodedMap: (map: GenMapping) => EncodedSourceMap; +export function toEncodedMap(map: GenMapping): EncodedSourceMap { + const decoded = toDecodedMap(map); + return { + ...decoded, + mappings: encode(decoded.mappings as SourceMapSegment[][]), + }; +} /** * Constructs a new GenMapping, using the already present mappings of the input. */ -export let fromMap: (input: SourceMapInput) => GenMapping; +export function fromMap(input: SourceMapInput): GenMapping { + const map = new TraceMap(input); + const gen = new GenMapping({ file: map.file, sourceRoot: map.sourceRoot }); + + putAll(cast(gen)._names, map.names); + putAll(cast(gen)._sources, map.sources as string[]); + cast(gen)._sourcesContent = map.sourcesContent || map.sources.map(() => null); + cast(gen)._mappings = decodedMappings(map) as GenMapping['_mappings']; + + return gen; +} /** * Returns an array of high-level mapping objects for every recorded segment, which could then be * passed to the `source-map` library. */ -export let allMappings: (map: GenMapping) => Mapping[]; +export function allMappings(map: GenMapping): Mapping[] { + const out: Mapping[] = []; + const { _mappings: mappings, _sources: sources, _names: names } = cast(map); + + for (let i = 0; i < mappings.length; i++) { + const line = mappings[i]; + for (let j = 0; j < line.length; j++) { + const seg = line[j]; + + 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[SOURCES_INDEX]]; + original = { line: seg[SOURCE_LINE] + 1, column: seg[SOURCE_COLUMN] }; + + if (seg.length === 5) name = names.array[seg[NAMES_INDEX]]; + } + + out.push({ generated, source, original, name } as Mapping); + } + } + + return out; +} // This split declaration is only so that terser can elminiate the static initialization block. -let addSegmentInternal: ( +function addSegmentInternal( skipable: boolean, map: GenMapping, genLine: number, @@ -150,193 +302,41 @@ let addSegmentInternal: ( sourceColumn: S extends string ? number : null | undefined, name: S extends string ? string | null | undefined : null | undefined, content: S extends string ? string | null | undefined : null | undefined, -) => void; - -/** - * Provides the state to generate a sourcemap. - */ -export class GenMapping { - private _names = new SetArray(); - private _sources = new SetArray(); - private _sourcesContent: (string | null)[] = []; - private _mappings: SourceMapSegment[][] = []; - declare file: string | null | undefined; - declare sourceRoot: string | null | undefined; +): void { + const { + _mappings: mappings, + _sources: sources, + _sourcesContent: sourcesContent, + _names: names, + } = cast(map); + const line = getLine(mappings, genLine); + const index = getColumnIndex(line, genColumn); - constructor({ file, sourceRoot }: Options = {}) { - this.file = file; - this.sourceRoot = sourceRoot; + if (!source) { + if (skipable && skipSourceless(line, index)) return; + return insert(line, index, [genColumn]); } - static { - addSegment = (map, genLine, genColumn, source, sourceLine, sourceColumn, name, content) => { - return addSegmentInternal( - false, - map, - genLine, - genColumn, - source, - sourceLine, - sourceColumn, - name, - content, - ); - }; - - maybeAddSegment = ( - map, - genLine, - genColumn, - source, - sourceLine, - sourceColumn, - name, - content, - ) => { - return addSegmentInternal( - true, - map, - genLine, - genColumn, - source, - sourceLine, - sourceColumn, - name, - content, - ); - }; - - addMapping = (map, mapping) => { - return addMappingInternal(false, map, mapping as Parameters[2]); - }; - - maybeAddMapping = (map, mapping) => { - return addMappingInternal(true, map, mapping as Parameters[2]); - }; - - setSourceContent = (map, source, content) => { - const { _sources: sources, _sourcesContent: sourcesContent } = map; - sourcesContent[put(sources, source)] = content; - }; - - toDecodedMap = (map) => { - const { - file, - sourceRoot, - _mappings: mappings, - _sources: sources, - _sourcesContent: sourcesContent, - _names: names, - } = map; - removeEmptyFinalLines(mappings); - - return { - version: 3, - file: file || undefined, - names: names.array, - sourceRoot: sourceRoot || undefined, - sources: sources.array, - sourcesContent, - mappings, - }; - }; - - toEncodedMap = (map) => { - const decoded = toDecodedMap(map); - return { - ...decoded, - mappings: encode(decoded.mappings as SourceMapSegment[][]), - }; - }; - - allMappings = (map) => { - const out: Mapping[] = []; - const { _mappings: mappings, _sources: sources, _names: names } = map; - - for (let i = 0; i < mappings.length; i++) { - const line = mappings[i]; - for (let j = 0; j < line.length; j++) { - const seg = line[j]; - - 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[SOURCES_INDEX]]; - original = { line: seg[SOURCE_LINE] + 1, column: seg[SOURCE_COLUMN] }; - - if (seg.length === 5) name = names.array[seg[NAMES_INDEX]]; - } - - out.push({ generated, source, original, name } as Mapping); - } - } + // Sigh, TypeScript can't figure out sourceLine and sourceColumn aren't nullish if source + // isn't nullish. + assert(sourceLine); + assert(sourceColumn); - return out; - }; + const sourcesIndex = put(sources, source); + const namesIndex = name ? put(names, name) : NO_NAME; + if (sourcesIndex === sourcesContent.length) sourcesContent[sourcesIndex] = content ?? null; - fromMap = (input) => { - const map = new TraceMap(input); - const gen = new GenMapping({ file: map.file, sourceRoot: map.sourceRoot }); - - putAll(gen._names, map.names); - putAll(gen._sources, map.sources as string[]); - gen._sourcesContent = map.sourcesContent || map.sources.map(() => null); - gen._mappings = decodedMappings(map) as GenMapping['_mappings']; - - return gen; - }; - - // Internal helpers - addSegmentInternal = ( - skipable, - map, - genLine, - genColumn, - source, - sourceLine, - sourceColumn, - name, - content, - ) => { - 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) : NO_NAME; - if (sourcesIndex === sourcesContent.length) sourcesContent[sourcesIndex] = content ?? null; - - if (skipable && skipSource(line, index, sourcesIndex, sourceLine, sourceColumn, namesIndex)) { - return; - } - - return insert( - line, - index, - name - ? [genColumn, sourcesIndex, sourceLine, sourceColumn, namesIndex] - : [genColumn, sourcesIndex, sourceLine, sourceColumn], - ); - }; + if (skipable && skipSource(line, index, sourcesIndex, sourceLine, sourceColumn, namesIndex)) { + return; } + + return insert( + line, + index, + name + ? [genColumn, sourcesIndex, sourceLine, sourceColumn, namesIndex] + : [genColumn, sourcesIndex, sourceLine, sourceColumn], + ); } function assert(_val: unknown): asserts _val is T { @@ -442,14 +442,13 @@ function addMappingInternal( null, ); } - const s: string = source; assert(original); return addSegmentInternal( skipable, map, generated.line - 1, generated.column, - s, + source satisfies string, original.line - 1, original.column, name,