Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Inherit child segments when parent contains only a line marker #120

Closed
wants to merge 12 commits into from
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ With remapping, none of your source code transformations need to be aware of the
they only need to generate an output sourcemap. This greatly simplifies building custom
transformations (think a find-and-replace).

Note that the remapped sourcemap depends on how precise the input sourcemaps are. The more segments
are recorded, the more accurate the remapped positions become.

## Installation

```sh
Expand Down
13 changes: 10 additions & 3 deletions src/build-source-map-tree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ function asArray<T>(value: T | T[]): T[] {
return [value];
}

function id(relativeRoot: string, index: number): string {
return `${relativeRoot}.${index}`;
}

/**
* Recursively builds a tree structure out of sourcemap files, with each node
* being either an `OriginalSource` "leaf" or a `SourceMapTree` composed of
Expand All @@ -41,15 +45,18 @@ function asArray<T>(value: T | T[]): T[] {
export default function buildSourceMapTree(
input: SourceMapInput | SourceMapInput[],
loader: SourceMapLoader,
relativeRoot?: string
relativeRoot: string
): SourceMapTree {
const maps = asArray(input).map(decodeSourceMap);
const map = maps.pop()!;

for (let i = 0; i < maps.length; i++) {
if (maps[i].sources.length !== 1) {
throw new Error(
`Transformation map ${i} must have exactly one source file.\n` +
`Transformation map ${id(
relativeRoot || 'input',
i
)} must have exactly one source file.\n` +
'Did you specify these with the most recent transformation maps first?'
);
}
Expand Down Expand Up @@ -79,7 +86,7 @@ export default function buildSourceMapTree(

// Else, it's a real sourcemap, and we need to recurse into it to load its
// source files.
return buildSourceMapTree(decodeSourceMap(sourceMap), loader, uri);
return buildSourceMapTree(sourceMap, loader, uri);
});

let tree = new SourceMapTree(map, children);
Expand Down
32 changes: 29 additions & 3 deletions src/original-source.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
* limitations under the License.
*/

import type { SourceMapSegmentObject } from './types';
import type { SourceMapSegment, SourceMapSegmentObject } from './types';

/**
* A "leaf" node in the sourcemap tree, representing an original, unmodified
Expand All @@ -29,11 +29,37 @@ export default class OriginalSource {
this.content = content;
}

/**
* Tracing a line happens when the parent map only recorded a line marker
* segment. Since we're in an `OriginalSource`, there is no additional
* information we can provide.
*/
traceLine(
line: number,
into: (s: SourceMapSegmentObject) => SourceMapSegment
): SourceMapSegment[] {
// Generate a line marker segment for this line, so that it is retained in
// the output.
return [into(this.traceSegment(0, line, 0, ''))];
}

/**
* Tracing a `SourceMapSegment` ends when we get to an `OriginalSource`,
* meaning this line/column location originated from this source file.
*/
traceSegment(line: number, column: number, name: string): SourceMapSegmentObject {
return { column, line, name, source: this };
traceSegment(
outputColumn: number,
line: number,
column: number,
name: string
): SourceMapSegmentObject {
return {
outputColumn,
line,
column,
name,
filename: this.filename,
content: this.content,
};
}
}
8 changes: 4 additions & 4 deletions src/remapping.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,12 @@ import SourceMap from './source-map';

import type { SourceMapInput, SourceMapLoader, Options } from './types';
export type {
SourceMapSegment,
RawSourceMap,
DecodedSourceMap,
Options,
RawSourceMap,
SourceMapInput,
SourceMapLoader,
Options,
SourceMapSegment,
} from './types';

/**
Expand All @@ -49,6 +49,6 @@ export default function remapping(
): SourceMap {
const opts =
typeof options === 'object' ? options : { excludeContent: !!options, decodedMappings: false };
const graph = buildSourceMapTree(input, loader);
const graph = buildSourceMapTree(input, loader, '');
return new SourceMap(graph.traceMappings(), opts);
}
200 changes: 130 additions & 70 deletions src/source-map-tree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,22 +21,24 @@ import FastStringArray from './fast-string-array';
import type OriginalSource from './original-source';
import type { DecodedSourceMap, SourceMapSegment, SourceMapSegmentObject } from './types';

type Sources = OriginalSource | SourceMapTree;
type Source = OriginalSource | SourceMapTree;

/**
* SourceMapTree represents a single sourcemap, with the ability to trace
* mappings into its child nodes (which may themselves be SourceMapTrees).
*/
export default class SourceMapTree {
map: DecodedSourceMap;
sources: Sources[];
private lastLine: number;
private lastColumn: number;
private lastIndex: number;

constructor(map: DecodedSourceMap, sources: Sources[]) {
declare map: DecodedSourceMap;
declare sources: Source[];
private declare lines: number;
private declare lastLine: number;
private declare lastColumn: number;
private declare lastIndex: number;

constructor(map: DecodedSourceMap, sources: Source[]) {
this.map = map;
this.sources = sources;
this.lines = map.mappings.length;
this.lastLine = 0;
this.lastColumn = 0;
this.lastIndex = 0;
Expand All @@ -48,71 +50,38 @@ export default class SourceMapTree {
* files.
*/
traceMappings(): DecodedSourceMap {
const mappings: SourceMapSegment[][] = [];
const names = new FastStringArray();
const sources = new FastStringArray();
const sourcesContent: (string | null)[] = [];
const { mappings: rootMappings, names: rootNames } = this.map;

let lastLineWithSegment = -1;
function intoSegments(segment: SourceMapSegmentObject): SourceMapSegment {
const { outputColumn, line, column, name, filename, content } = segment;
// Store the source location, and ensure we keep sourcesContent up to
// date with the sources array.
const sourceIndex = sources.put(filename);
sourcesContent[sourceIndex] = content;

// This looks like unnecessary duplication, but it noticeably increases
// performance. If we were to push the nameIndex onto length-4 array, v8
// would internally allocate 22 slots! That's 68 wasted bytes! Array
// literals have the same capacity as their length, saving memory.
if (name) return [outputColumn, sourceIndex, line, column, names.put(name)];
return [outputColumn, sourceIndex, line, column];
}

const { sources: rootSources } = this;
const { mappings: rootMappings, names: rootNames } = this.map;
const mappings: SourceMapSegment[][] = [];
let lastSegmentsLine = -1;
for (let i = 0; i < rootMappings.length; i++) {
const segments = rootMappings[i];
const tracedSegments: SourceMapSegment[] = [];
let lastTraced: SourceMapSegment | undefined = undefined;

for (let j = 0; j < segments.length; j++) {
const segment = segments[j];

// 1-length segments only move the current generated column, there's no
// source information to gather from it.
if (segment.length === 1) continue;
const source = this.sources[segment[1]];

const traced = source.traceSegment(
segment[2],
segment[3],
segment.length === 5 ? rootNames[segment[4]] : ''
);
if (!traced) continue;

// So we traced a segment down into its original source file. Now push a
// new segment pointing to this location.
const { column, line, name } = traced;
const { content, filename } = traced.source;

// Store the source location, and ensure we keep sourcesContent up to
// date with the sources array.
const sourceIndex = sources.put(filename);
sourcesContent[sourceIndex] = content;

if (
lastTraced &&
lastTraced[1] === sourceIndex &&
lastTraced[2] === line &&
lastTraced[3] === column
) {
// This is a duplicate mapping pointing at the exact same starting point in the source file.
// It doesn't provide any new information, and only bloats the sourcemap.
continue;
}

// This looks like unnecessary duplication, but it noticeably increases
// performance. If we were to push the nameIndex onto length-4 array, v8
// would internally allocate 22 slots! That's 68 wasted bytes! Array
// literals have the same capacity as their length, saving memory.
if (name) {
lastTraced = [segment[0], sourceIndex, line, column, names.put(name)];
} else {
lastTraced = [segment[0], sourceIndex, line, column];
}
tracedSegments.push(lastTraced);
lastLineWithSegment = i;
}

mappings.push(tracedSegments);
const traced = traceLine(segments, rootSources, rootNames, intoSegments);
if (traced.length > 0) lastSegmentsLine = i;
mappings.push(traced);
}
if (mappings.length > lastLineWithSegment + 1) {
mappings.length = lastLineWithSegment + 1;

for (let i = mappings.length - 1; i > lastSegmentsLine; i--) {
mappings.pop();
}

// TODO: Make all sources relative to the sourceRoot.
Expand All @@ -128,17 +97,32 @@ export default class SourceMapTree {
);
}

traceLine(
line: number,
into: (s: SourceMapSegmentObject) => SourceMapSegment
): SourceMapSegment[] {
if (line >= this.lines) return [];

const { mappings, names } = this.map;
const segments = mappings[line];
return traceLine(segments, this.sources, names, into);
}

/**
* traceSegment is only called on children SourceMapTrees. It recurses down
* into its own child SourceMapTrees, until we find the original source map.
*/
traceSegment(line: number, column: number, name: string): SourceMapSegmentObject | null {
const { mappings, names } = this.map;

traceSegment(
outputColumn: number,
line: number,
column: number,
name: string
): SourceMapSegmentObject | null {
// It's common for parent sourcemaps to have pointers to lines that have no
// mapping (like a "//# sourceMappingURL=") at the end of the child file.
if (line >= mappings.length) return null;
if (line >= this.lines) return null;

const { mappings, names } = this.map;
const segments = mappings[line];

if (segments.length === 0) return null;
Expand Down Expand Up @@ -177,6 +161,7 @@ export default class SourceMapTree {

// So now we can recurse down, until we hit the original source file.
return source.traceSegment(
outputColumn,
segment[2],
segment[3],
// A child map's recorded name for this segment takes precedence over the
Expand All @@ -186,6 +171,81 @@ export default class SourceMapTree {
}
}

function traceLine(
segments: SourceMapSegment[],
sources: Source[],
names: string[],
into: (s: SourceMapSegmentObject) => SourceMapSegment
): SourceMapSegment[] {
return (
traceUneditedLine(segments, sources, into) || traceEditedLine(segments, sources, names, into)
);
}

// An unedited line either contains only a line marker segment. Line markers are
// the default "lowres" segment generated by magic-string, and match [0, SOURCE,
// LINE, 0]. For these markers, we inherit the mappings from the referenced
// source directly, instead of trying to match the segments.
function traceUneditedLine(
segments: SourceMapSegment[],
sources: Source[],
into: (s: SourceMapSegmentObject) => SourceMapSegment
): null | SourceMapSegment[] {
if (segments.length !== 1) return null;

const segment = segments[0];
if (segment.length !== 4) return null;
if (segment[0] !== 0) return null;
// Source can be anything
// Line can by anything.
if (segment[3] !== 0) return null;

return sources[segment[1]].traceLine(segment[2], into);
}

function traceEditedLine(
segments: SourceMapSegment[],
sources: Source[],
names: string[],
into: (s: SourceMapSegmentObject) => SourceMapSegment
): SourceMapSegment[] {
const tracedSegments: SourceMapSegment[] = [];
let lastTraced: SourceMapSegmentObject | undefined = undefined;

for (let j = 0; j < segments.length; j++) {
const segment = segments[j];

// 1-length segments only move the current generated column, there's no
// source information to gather from it.
if (segment.length === 1) continue;
const source = sources[segment[1]];

const traced = source.traceSegment(
segment[0],
segment[2],
segment[3],
segment.length === 5 ? names[segment[4]] : ''
);
if (!traced) continue;

if (
lastTraced &&
lastTraced.filename === traced.filename &&
lastTraced.line === traced.line &&
lastTraced.column === traced.column
) {
// This is a duplicate mapping pointing at the exact same starting point in the source file.
// It doesn't provide any new information, and only bloats the sourcemap.
continue;
}

lastTraced = traced;
tracedSegments.push(into(traced));
}

return tracedSegments;
}

function segmentComparator(segment: SourceMapSegment, column: number): number {
return segment[0] - column;
}
2 changes: 1 addition & 1 deletion src/strip-filename.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
/**
* Removes the filename from a path.
*/
export default function stripFilename(path: string | undefined): string {
export default function stripFilename(path: string): string {
if (!path) return '';
const index = path.lastIndexOf('/');
return path.slice(0, index + 1);
Expand Down
Loading