diff --git a/src/build-source-map-tree.ts b/src/build-source-map-tree.ts index 616e088..7265d2f 100644 --- a/src/build-source-map-tree.ts +++ b/src/build-source-map-tree.ts @@ -1,8 +1,8 @@ import { TraceMap } from '@jridgewell/trace-mapping'; -import OriginalSource from './original-source'; -import { SourceMapTree } from './source-map-tree'; +import { OriginalSource, MapSource } from './source-map-tree'; +import type { Sources } from './source-map-tree'; import type { SourceMapInput, SourceMapLoader, LoaderContext } from './types'; function asArray(value: T | T[]): T[] { @@ -24,7 +24,7 @@ function asArray(value: T | T[]): T[] { export default function buildSourceMapTree( input: SourceMapInput | SourceMapInput[], loader: SourceMapLoader -): SourceMapTree { +): Sources { const maps = asArray(input).map((m) => new TraceMap(m, '')); const map = maps.pop()!; @@ -39,7 +39,7 @@ export default function buildSourceMapTree( let tree = build(map, loader, '', 0); for (let i = maps.length - 1; i >= 0; i--) { - tree = new SourceMapTree(maps[i], [tree]); + tree = MapSource(maps[i], [tree]); } return tree; } @@ -49,44 +49,42 @@ function build( loader: SourceMapLoader, importer: string, importerDepth: number -): SourceMapTree { +): Sources { const { resolvedSources, sourcesContent } = map; const depth = importerDepth + 1; - const children = resolvedSources.map( - (sourceFile: string | null, i: number): SourceMapTree | OriginalSource => { - // The loading context gives the loader more information about why this file is being loaded - // (eg, from which importer). It also allows the loader to override the location of the loaded - // sourcemap/original source, or to override the content in the sourcesContent field if it's - // an unmodified source file. - const ctx: LoaderContext = { - importer, - depth, - source: sourceFile || '', - content: undefined, - }; + const children = resolvedSources.map((sourceFile: string | null, i: number): Sources => { + // The loading context gives the loader more information about why this file is being loaded + // (eg, from which importer). It also allows the loader to override the location of the loaded + // sourcemap/original source, or to override the content in the sourcesContent field if it's + // an unmodified source file. + const ctx: LoaderContext = { + importer, + depth, + source: sourceFile || '', + content: undefined, + }; - // Use the provided loader callback to retrieve the file's sourcemap. - // TODO: We should eventually support async loading of sourcemap files. - const sourceMap = loader(ctx.source, ctx); + // Use the provided loader callback to retrieve the file's sourcemap. + // TODO: We should eventually support async loading of sourcemap files. + const sourceMap = loader(ctx.source, ctx); - const { source, content } = ctx; + const { source, content } = ctx; - // If there is no sourcemap, then it is an unmodified source file. - if (!sourceMap) { - // The contents of this unmodified source file can be overridden via the loader context, - // allowing it to be explicitly null or a string. If it remains undefined, we fall back to - // the importing sourcemap's `sourcesContent` field. - const sourceContent = - content !== undefined ? content : sourcesContent ? sourcesContent[i] : null; - return new OriginalSource(source, sourceContent); - } - - // Else, it's a real sourcemap, and we need to recurse into it to load its - // source files. - return build(new TraceMap(sourceMap, source), loader, source, depth); + // If there is no sourcemap, then it is an unmodified source file. + if (!sourceMap) { + // The contents of this unmodified source file can be overridden via the loader context, + // allowing it to be explicitly null or a string. If it remains undefined, we fall back to + // the importing sourcemap's `sourcesContent` field. + const sourceContent = + content !== undefined ? content : sourcesContent ? sourcesContent[i] : null; + return OriginalSource(source, sourceContent); } - ); - return new SourceMapTree(map, children); + // Else, it's a real sourcemap, and we need to recurse into it to load its + // source files. + return build(new TraceMap(sourceMap, source), loader, source, depth); + }); + + return MapSource(map, children); } diff --git a/src/original-source.ts b/src/original-source.ts deleted file mode 100644 index a96a076..0000000 --- a/src/original-source.ts +++ /dev/null @@ -1,23 +0,0 @@ -import type { SourceMapSegmentObject } from './types'; - -/** - * A "leaf" node in the sourcemap tree, representing an original, unmodified - * source file. Recursive segment tracing ends at the `OriginalSource`. - */ -export default class OriginalSource { - declare content: string | null; - declare source: string; - - constructor(source: string, content: string | null) { - this.source = source; - this.content = content; - } - - /** - * Tracing a `SourceMapSegment` ends when we get to an `OriginalSource`, - * meaning this line/column location originated from this source file. - */ - originalPositionFor(line: number, column: number, name: string): SourceMapSegmentObject { - return { column, line, name, source: this.source, content: this.content }; - } -} diff --git a/src/source-map-tree.ts b/src/source-map-tree.ts index b84745b..c815ade 100644 --- a/src/source-map-tree.ts +++ b/src/source-map-tree.ts @@ -2,20 +2,65 @@ import { FastStringArray, put } from './fast-string-array'; import { presortedDecodedMap, traceSegment, decodedMappings } from '@jridgewell/trace-mapping'; import type { TraceMap } from '@jridgewell/trace-mapping'; -import type OriginalSource from './original-source'; import type { SourceMapSegment, SourceMapSegmentObject } from './types'; -type Sources = OriginalSource | SourceMapTree; - const INVALID_MAPPING = undefined; const SOURCELESS_MAPPING = null; +const EMPTY_SOURCES: Sources[] = []; + type MappingSource = SourceMapSegmentObject | typeof INVALID_MAPPING | typeof SOURCELESS_MAPPING; +type OriginalSource = { + map: TraceMap; + sources: Sources[]; + source: string; + content: string | null; +}; + +type MapSource = { + map: TraceMap; + sources: Sources[]; + source: string; + content: string | null; +}; + +export type Sources = OriginalSource | MapSource; + +function Source( + map: TraceMap | null, + sources: Sources[], + source: string, + content: string | null +): M extends null ? OriginalSource : MapSource { + return { + map, + sources, + source, + content, + } as any; +} + +/** + * MapSource represents a single sourcemap, with the ability to trace mappings into its child nodes + * (which may themselves be SourceMapTrees). + */ +export function MapSource(map: TraceMap, sources: Sources[]): MapSource { + return Source(map, sources, '', null); +} + +/** + * A "leaf" node in the sourcemap tree, representing an original, unmodified source file. Recursive + * segment tracing ends at the `OriginalSource`. + */ +export function OriginalSource(source: string, content: string | null): OriginalSource { + return Source(null, EMPTY_SOURCES, source, content); +} + /** * traceMappings is only called on the root level SourceMapTree, and begins the process of * resolving each mapping in terms of the original source files. */ -export function traceMappings(tree: SourceMapTree): TraceMap { +export function traceMappings(tree: Sources): TraceMap { const mappings: SourceMapSegment[][] = []; const names = FastStringArray(); const sources = FastStringArray(); @@ -41,7 +86,8 @@ export function traceMappings(tree: SourceMapTree): TraceMap { // to gather from it. if (segment.length !== 1) { const source = rootSources[segment[1]]; - traced = source.originalPositionFor( + traced = originalPositionFor( + source, segment[2], segment[3], segment.length === 5 ? rootNames[segment[4]] : '' @@ -117,36 +163,31 @@ export function traceMappings(tree: SourceMapTree): TraceMap { } /** - * SourceMapTree represents a single sourcemap, with the ability to trace - * mappings into its child nodes (which may themselves be SourceMapTrees). + * originalPositionFor is only called on children SourceMapTrees. It recurses down into its own + * child SourceMapTrees, until we find the original source map. */ -export class SourceMapTree { - declare map: TraceMap; - declare sources: Sources[]; - - constructor(map: TraceMap, sources: Sources[]) { - this.map = map; - this.sources = sources; +export function originalPositionFor( + source: Sources, + line: number, + column: number, + name: string +): MappingSource { + if (!source.map) { + return { column, line, name, source: source.source, content: source.content }; } - /** - * originalPositionFor is only called on children SourceMapTrees. It recurses down - * into its own child SourceMapTrees, until we find the original source map. - */ - originalPositionFor(line: number, column: number, name: string): MappingSource { - const segment = traceSegment(this.map, line, column); - - // If we couldn't find a segment, then this doesn't exist in the sourcemap. - if (segment == null) return INVALID_MAPPING; - // 1-length segments only move the current generated column, there's no source information - // to gather from it. - if (segment.length === 1) return SOURCELESS_MAPPING; - - const source = this.sources[segment[1]]; - return source.originalPositionFor( - segment[2], - segment[3], - segment.length === 5 ? this.map.names[segment[4]] : name - ); - } + const segment = traceSegment(source.map, line, column); + + // If we couldn't find a segment, then this doesn't exist in the sourcemap. + if (segment == null) return INVALID_MAPPING; + // 1-length segments only move the current generated column, there's no source information + // to gather from it. + if (segment.length === 1) return SOURCELESS_MAPPING; + + return originalPositionFor( + source.sources[segment[1]], + segment[2], + segment[3], + segment.length === 5 ? source.map.names[segment[4]] : name + ); } diff --git a/test/unit/original-source.ts b/test/unit/original-source.ts deleted file mode 100644 index 98696de..0000000 --- a/test/unit/original-source.ts +++ /dev/null @@ -1,61 +0,0 @@ -import OriginalSource from '../../src/original-source'; - -describe('OriginalSource', () => { - let source: OriginalSource; - - beforeEach(() => { - source = new OriginalSource('file.js', '1 + 1'); - }); - - describe('originalPositionFor()', () => { - test('returns the same line number', () => { - const line = Math.random(); - const column = Math.random(); - const name = String(Math.random()); - - const trace = source.originalPositionFor(line, column, name); - - expect(trace.line).toBe(line); - }); - - test('returns the same column number', () => { - const line = Math.random(); - const column = Math.random(); - const name = String(Math.random()); - - const trace = source.originalPositionFor(line, column, name); - - expect(trace.column).toBe(column); - }); - - test('returns the same name', () => { - const line = Math.random(); - const column = Math.random(); - const name = String(Math.random()); - - const trace = source.originalPositionFor(line, column, name); - - expect(trace.name).toBe(name); - }); - - test("returns the original source's source", () => { - const line = Math.random(); - const column = Math.random(); - const name = String(Math.random()); - - const trace = source.originalPositionFor(line, column, name); - - expect(trace.source).toBe(source.source); - }); - - test("returns the original source's content", () => { - const line = Math.random(); - const column = Math.random(); - const name = String(Math.random()); - - const trace = source.originalPositionFor(line, column, name); - - expect(trace.content).toBe(source.content); - }); - }); -}); diff --git a/test/unit/source-map-tree.ts b/test/unit/source-map-tree.ts index f3f1343..e92f386 100644 --- a/test/unit/source-map-tree.ts +++ b/test/unit/source-map-tree.ts @@ -1,10 +1,14 @@ import { decodedMappings, TraceMap } from '@jridgewell/trace-mapping'; -import OriginalSource from '../../src/original-source'; -import { SourceMapTree, traceMappings } from '../../src/source-map-tree'; +import { + OriginalSource, + MapSource, + originalPositionFor, + traceMappings, +} from '../../src/source-map-tree'; import type { DecodedSourceMap } from '../../src/types'; -describe('SourceMapTree', () => { +describe('MapSource', () => { describe('traceMappings()', () => { const sourceRoot = 'foo'; const baseMap: DecodedSourceMap = { @@ -14,7 +18,7 @@ describe('SourceMapTree', () => { sources: ['child.js'], version: 3, }; - const child = new SourceMapTree( + const child = MapSource( new TraceMap({ mappings: [ [ @@ -29,7 +33,7 @@ describe('SourceMapTree', () => { sources: ['original.js'], version: 3, }), - [new OriginalSource(`${sourceRoot}/original.js`, '')] + [OriginalSource(`${sourceRoot}/original.js`, '')] ); test('records segment if segment is 1-length', () => { @@ -38,7 +42,7 @@ describe('SourceMapTree', () => { mappings: [[[0, 0, 0, 4], [5]]], }; - const tree = new SourceMapTree(new TraceMap(map), [child]); + const tree = MapSource(new TraceMap(map), [child]); const traced = traceMappings(tree); expect(decodedMappings(traced)).toEqual([[[0, 0, 1, 1], [5]]]); }); @@ -54,7 +58,7 @@ describe('SourceMapTree', () => { ], }; - const tree = new SourceMapTree(new TraceMap(map), [child]); + const tree = MapSource(new TraceMap(map), [child]); const traced = traceMappings(tree); expect(decodedMappings(traced)).toEqual([[[0, 0, 1, 1], [5]]]); }); @@ -68,7 +72,7 @@ describe('SourceMapTree', () => { mappings: [[[0, sourceIndex, line, column]]], }; - const tree = new SourceMapTree(new TraceMap(map), [child]); + const tree = MapSource(new TraceMap(map), [child]); const traced = traceMappings(tree); expect(decodedMappings(traced)).toEqual([]); }); @@ -85,7 +89,7 @@ describe('SourceMapTree', () => { names: [name], }; - const tree = new SourceMapTree(new TraceMap(map), [child]); + const tree = MapSource(new TraceMap(map), [child]); const traced = traceMappings(tree); expect(decodedMappings(traced)).toEqual([[[0, 0, 0, 0, 0]]]); expect(traced).toMatchObject({ @@ -102,7 +106,7 @@ describe('SourceMapTree', () => { mappings: [[[0, sourceIndex, line, column]]], }; - const tree = new SourceMapTree(new TraceMap(map), [child]); + const tree = MapSource(new TraceMap(map), [child]); const traced = traceMappings(tree); expect(decodedMappings(traced)).toEqual([[[0, 0, 1, 1]]]); }); @@ -116,7 +120,7 @@ describe('SourceMapTree', () => { mappings: [[[0, sourceIndex, line, column]]], }; - const tree = new SourceMapTree(new TraceMap(map), [child]); + const tree = MapSource(new TraceMap(map), [child]); const traced = traceMappings(tree); expect(decodedMappings(traced)).toEqual([[[0, 0, 0, 0, 0]]]); expect(traced).toMatchObject({ @@ -136,7 +140,7 @@ describe('SourceMapTree', () => { ...extras, }; - const tree = new SourceMapTree(new TraceMap(map), [child]); + const tree = MapSource(new TraceMap(map), [child]); const traced = traceMappings(tree); expect(traced).toMatchObject(extras); }); @@ -147,7 +151,7 @@ describe('SourceMapTree', () => { mappings: [[[0, 0, 0, 0]]], }; - const tree = new SourceMapTree(new TraceMap(map), [child]); + const tree = MapSource(new TraceMap(map), [child]); const traced = traceMappings(tree); expect(traced).toMatchObject({ // TODO: support sourceRoot @@ -163,7 +167,7 @@ describe('SourceMapTree', () => { sourceRoot, }; - const tree = new SourceMapTree(new TraceMap(map), [child]); + const tree = MapSource(new TraceMap(map), [child]); const traced = traceMappings(tree); expect(decodedMappings(traced)).toEqual([[[0, 0, 0, 0]]]); }); @@ -175,7 +179,7 @@ describe('SourceMapTree', () => { sourceRoot, }; - const tree = new SourceMapTree(new TraceMap(map), [child]); + const tree = MapSource(new TraceMap(map), [child]); const traced = traceMappings(tree); expect(decodedMappings(traced)).toEqual([]); }); @@ -192,7 +196,7 @@ describe('SourceMapTree', () => { ], }; - const tree = new SourceMapTree(new TraceMap(map), [child]); + const tree = MapSource(new TraceMap(map), [child]); const traced = traceMappings(tree); expect(decodedMappings(traced)).toEqual([[[0, 0, 0, 0]]]); }); @@ -203,7 +207,7 @@ describe('SourceMapTree', () => { mappings: [[[0, 0, 0, 0]], [[0, 0, 0, 0]]], }; - const tree = new SourceMapTree(new TraceMap(map), [child]); + const tree = MapSource(new TraceMap(map), [child]); const traced = traceMappings(tree); expect(decodedMappings(traced)).toEqual([[[0, 0, 0, 0]], [[0, 0, 0, 0]]]); }); @@ -232,17 +236,17 @@ describe('SourceMapTree', () => { sources: ['child.js'], version: 3, }; - const tree = new SourceMapTree(new TraceMap(map), [new OriginalSource('child.js', '')]); + const tree = MapSource(new TraceMap(map), [OriginalSource('child.js', '')]); test('traces LineSegments to the segment with matching generated column', () => { - const trace = tree.originalPositionFor(0, 4, ''); + const trace = originalPositionFor(tree, 0, 4, ''); expect(trace).toMatchObject({ line: 1, column: 1 }); }); test('traces all generated cols on a line back to their source when source had characters removed', () => { const expectedCols = [0, 0, 0, 0, 0, 6, 6, 6, 6]; for (let genCol = 0; genCol < expectedCols.length; genCol++) { - const trace = tree.originalPositionFor(4, genCol, ''); + const trace = originalPositionFor(tree, 4, genCol, ''); expect(trace).toMatchObject({ line: 4, column: expectedCols[genCol] }); } }); @@ -250,7 +254,7 @@ describe('SourceMapTree', () => { test('traces all generated cols on a line back to their source when source had characters added', () => { const expectedCols = [0, 0, 0, 0, 0, null, 5, 5, 5, 5, 5]; for (let genCol = 0; genCol < expectedCols.length; genCol++) { - const trace = tree.originalPositionFor(5, genCol, ''); + const trace = originalPositionFor(tree, 5, genCol, ''); if (expectedCols[genCol] == null) { expect(trace).toBe(null); } else { @@ -260,74 +264,74 @@ describe('SourceMapTree', () => { }); test('returns undefined if line is longer than mapping lines', () => { - const trace = tree.originalPositionFor(10, 0, ''); + const trace = originalPositionFor(tree, 10, 0, ''); expect(trace).toBe(undefined); }); test('returns undefined if no matching segment column', () => { //line 1 col 0 of generated doesn't exist in the original source - const trace = tree.originalPositionFor(1, 0, ''); + const trace = originalPositionFor(tree, 1, 0, ''); expect(trace).toBe(undefined); }); test('returns null if segment is 1-length', () => { - const trace = tree.originalPositionFor(2, 0, ''); + const trace = originalPositionFor(tree, 2, 0, ''); expect(trace).toBe(null); }); test('passes in outer name to trace', () => { - const trace = tree.originalPositionFor(0, 0, 'foo'); + const trace = originalPositionFor(tree, 0, 0, 'foo'); expect(trace).toMatchObject({ name: 'foo' }); }); test('overrides name if segment is 5-length', () => { - const trace = tree.originalPositionFor(3, 0, 'foo'); + const trace = originalPositionFor(tree, 3, 0, 'foo'); expect(trace).toMatchObject({ name: 'name' }); }); describe('tracing same line multiple times', () => { describe('later column', () => { test('returns matching segment after match', () => { - expect(tree.originalPositionFor(0, 1, '')).not.toBe(undefined); - const trace = tree.originalPositionFor(0, 4, ''); + expect(originalPositionFor(tree, 0, 1, '')).not.toBe(undefined); + const trace = originalPositionFor(tree, 0, 4, ''); expect(trace).toMatchObject({ line: 1, column: 1 }); }); test('returns matching segment after undefined match', () => { - expect(tree.originalPositionFor(1, 0, '')).toBe(undefined); - const trace = tree.originalPositionFor(1, 2, ''); + expect(originalPositionFor(tree, 1, 0, '')).toBe(undefined); + const trace = originalPositionFor(tree, 1, 2, ''); expect(trace).toMatchObject({ line: 0, column: 0 }); }); test('returns undefined segment segment after undefined match', () => { - expect(tree.originalPositionFor(1, 0, '')).toBe(undefined); - const trace = tree.originalPositionFor(1, 1, ''); + expect(originalPositionFor(tree, 1, 0, '')).toBe(undefined); + const trace = originalPositionFor(tree, 1, 1, ''); expect(trace).toBe(undefined); }); test('returns matching segment after almost match', () => { - expect(tree.originalPositionFor(4, 2, '')).not.toBe(undefined); - const trace = tree.originalPositionFor(4, 5, ''); + expect(originalPositionFor(tree, 4, 2, '')).not.toBe(undefined); + const trace = originalPositionFor(tree, 4, 5, ''); expect(trace).toMatchObject({ line: 4, column: 6 }); }); }); describe('earlier column', () => { test('returns matching segment after match', () => { - expect(tree.originalPositionFor(0, 4, '')).not.toBe(undefined); - const trace = tree.originalPositionFor(0, 1, ''); + expect(originalPositionFor(tree, 0, 4, '')).not.toBe(undefined); + const trace = originalPositionFor(tree, 0, 1, ''); expect(trace).toMatchObject({ line: 0, column: 0 }); }); test('returns undefined segment segment after undefined match', () => { - expect(tree.originalPositionFor(1, 1, '')).toBe(undefined); - const trace = tree.originalPositionFor(1, 0, ''); + expect(originalPositionFor(tree, 1, 1, '')).toBe(undefined); + const trace = originalPositionFor(tree, 1, 0, ''); expect(trace).toBe(undefined); }); test('returns matching segment after almost match', () => { - expect(tree.originalPositionFor(4, 2, '')).not.toBe(undefined); - const trace = tree.originalPositionFor(4, 0, ''); + expect(originalPositionFor(tree, 4, 2, '')).not.toBe(undefined); + const trace = originalPositionFor(tree, 4, 0, ''); expect(trace).toMatchObject({ line: 4, column: 0 }); }); });