fix(compiler-cli): do not duplicate repeated source-files in rendered source-maps (#40237)
When a source-map/source-file tree has nodes that refer to the same file, the flattened source-map rendering was those files multiple times, rather than consolidating them into a single source-map source. PR Close #40237
This commit is contained in:
parent
e7c3687936
commit
3158858059
|
@ -50,13 +50,19 @@ export class SourceFile {
|
||||||
* Render the raw source map generated from the flattened mappings.
|
* Render the raw source map generated from the flattened mappings.
|
||||||
*/
|
*/
|
||||||
renderFlattenedSourceMap(): RawSourceMap {
|
renderFlattenedSourceMap(): RawSourceMap {
|
||||||
const sources: SourceFile[] = [];
|
const sources = new IndexedMap<string, string>();
|
||||||
const names: string[] = [];
|
const names = new IndexedSet<string>();
|
||||||
|
|
||||||
const mappings: SourceMapMappings = [];
|
const mappings: SourceMapMappings = [];
|
||||||
|
const sourcePathDir = this.fs.dirname(this.sourcePath);
|
||||||
|
// Computing the relative path can be expensive, and we are likely to have the same path for
|
||||||
|
// many (if not all!) mappings.
|
||||||
|
const relativeSourcePathCache =
|
||||||
|
new Cache<string, string>(input => this.fs.relative(sourcePathDir, input));
|
||||||
|
|
||||||
for (const mapping of this.flattenedMappings) {
|
for (const mapping of this.flattenedMappings) {
|
||||||
const sourceIndex = findIndexOrAdd(sources, mapping.originalSource);
|
const sourceIndex = sources.set(
|
||||||
|
relativeSourcePathCache.get(mapping.originalSource.sourcePath),
|
||||||
|
mapping.originalSource.contents);
|
||||||
const mappingArray: SourceMapSegment = [
|
const mappingArray: SourceMapSegment = [
|
||||||
mapping.generatedSegment.column,
|
mapping.generatedSegment.column,
|
||||||
sourceIndex,
|
sourceIndex,
|
||||||
|
@ -64,7 +70,7 @@ export class SourceFile {
|
||||||
mapping.originalSegment.column,
|
mapping.originalSegment.column,
|
||||||
];
|
];
|
||||||
if (mapping.name !== undefined) {
|
if (mapping.name !== undefined) {
|
||||||
const nameIndex = findIndexOrAdd(names, mapping.name);
|
const nameIndex = names.add(mapping.name);
|
||||||
mappingArray.push(nameIndex);
|
mappingArray.push(nameIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -77,14 +83,13 @@ export class SourceFile {
|
||||||
mappings[line].push(mappingArray);
|
mappings[line].push(mappingArray);
|
||||||
}
|
}
|
||||||
|
|
||||||
const sourcePathDir = this.fs.dirname(this.sourcePath);
|
|
||||||
const sourceMap: RawSourceMap = {
|
const sourceMap: RawSourceMap = {
|
||||||
version: 3,
|
version: 3,
|
||||||
file: this.fs.relative(sourcePathDir, this.sourcePath),
|
file: this.fs.relative(sourcePathDir, this.sourcePath),
|
||||||
sources: sources.map(sf => this.fs.relative(sourcePathDir, sf.sourcePath)),
|
sources: sources.keys,
|
||||||
names,
|
names: names.values,
|
||||||
mappings: encode(mappings),
|
mappings: encode(mappings),
|
||||||
sourcesContent: sources.map(sf => sf.contents),
|
sourcesContent: sources.values,
|
||||||
};
|
};
|
||||||
return sourceMap;
|
return sourceMap;
|
||||||
}
|
}
|
||||||
|
@ -259,23 +264,6 @@ export interface Mapping {
|
||||||
readonly name?: string;
|
readonly name?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Find the index of `item` in the `items` array.
|
|
||||||
* If it is not found, then push `item` to the end of the array and return its new index.
|
|
||||||
*
|
|
||||||
* @param items the collection in which to look for `item`.
|
|
||||||
* @param item the item to look for.
|
|
||||||
* @returns the index of the `item` in the `items` array.
|
|
||||||
*/
|
|
||||||
function findIndexOrAdd<T>(items: T[], item: T): number {
|
|
||||||
const itemIndex = items.indexOf(item);
|
|
||||||
if (itemIndex > -1) {
|
|
||||||
return itemIndex;
|
|
||||||
} else {
|
|
||||||
items.push(item);
|
|
||||||
return items.length - 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -448,3 +436,96 @@ export function computeStartOfLinePositions(str: string) {
|
||||||
function computeLineLengths(str: string): number[] {
|
function computeLineLengths(str: string): number[] {
|
||||||
return (str.split(/\n/)).map(s => s.length);
|
return (str.split(/\n/)).map(s => s.length);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A collection of mappings between `keys` and `values` stored in the order in which the keys are
|
||||||
|
* first seen.
|
||||||
|
*
|
||||||
|
* The difference between this and a standard `Map` is that when you add a key-value pair the index
|
||||||
|
* of the `key` is returned.
|
||||||
|
*/
|
||||||
|
class IndexedMap<K, V> {
|
||||||
|
private map = new Map<K, number>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An array of keys added to this map.
|
||||||
|
*
|
||||||
|
* This array is guaranteed to be in the order of the first time the key was added to the map.
|
||||||
|
*/
|
||||||
|
readonly keys: K[] = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An array of values added to this map.
|
||||||
|
*
|
||||||
|
* This array is guaranteed to be in the order of the first time the associated key was added to
|
||||||
|
* the map.
|
||||||
|
*/
|
||||||
|
readonly values: V[] = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Associate the `value` with the `key` and return the index of the key in the collection.
|
||||||
|
*
|
||||||
|
* If the `key` already exists then the `value` is not set and the index of that `key` is
|
||||||
|
* returned; otherwise the `key` and `value` are stored and the index of the new `key` is
|
||||||
|
* returned.
|
||||||
|
*
|
||||||
|
* @param key the key to associated with the `value`.
|
||||||
|
* @param value the value to associated with the `key`.
|
||||||
|
* @returns the index of the `key` in the `keys` array.
|
||||||
|
*/
|
||||||
|
set(key: K, value: V): number {
|
||||||
|
if (this.map.has(key)) {
|
||||||
|
return this.map.get(key)!;
|
||||||
|
}
|
||||||
|
const index = this.values.push(value) - 1;
|
||||||
|
this.keys.push(key);
|
||||||
|
this.map.set(key, index);
|
||||||
|
return index;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A collection of `values` stored in the order in which they were added.
|
||||||
|
*
|
||||||
|
* The difference between this and a standard `Set` is that when you add a value the index of that
|
||||||
|
* item is returned.
|
||||||
|
*/
|
||||||
|
class IndexedSet<V> {
|
||||||
|
private map = new Map<V, number>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An array of values added to this set.
|
||||||
|
* This array is guaranteed to be in the order of the first time the value was added to the set.
|
||||||
|
*/
|
||||||
|
readonly values: V[] = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add the `value` to the `values` array, if it doesn't already exist; returning the index of the
|
||||||
|
* `value` in the `values` array.
|
||||||
|
*
|
||||||
|
* If the `value` already exists then the index of that `value` is returned, otherwise the new
|
||||||
|
* `value` is stored and the new index returned.
|
||||||
|
*
|
||||||
|
* @param value the value to add to the set.
|
||||||
|
* @returns the index of the `value` in the `values` array.
|
||||||
|
*/
|
||||||
|
add(value: V): number {
|
||||||
|
if (this.map.has(value)) {
|
||||||
|
return this.map.get(value)!;
|
||||||
|
}
|
||||||
|
const index = this.values.push(value) - 1;
|
||||||
|
this.map.set(value, index);
|
||||||
|
return index;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Cache<Input, Cached> {
|
||||||
|
private map = new Map<Input, Cached>();
|
||||||
|
constructor(private computeFn: (input: Input) => Cached) {}
|
||||||
|
get(input: Input): Cached {
|
||||||
|
if (!this.map.has(input)) {
|
||||||
|
this.map.set(input, this.computeFn(input));
|
||||||
|
}
|
||||||
|
return this.map.get(input)!;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -522,6 +522,42 @@ runInEachFileSystem(() => {
|
||||||
expect(aTocSourceMap.sourcesContent).toEqual(['abcdef']);
|
expect(aTocSourceMap.sourcesContent).toEqual(['abcdef']);
|
||||||
expect(aTocSourceMap.mappings).toEqual(aToBSourceMap.mappings);
|
expect(aTocSourceMap.mappings).toEqual(aToBSourceMap.mappings);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should consolidate source-files with the same relative path', () => {
|
||||||
|
const cSource1 = new SourceFile(_('/foo/src/lib/c.js'), 'bcd123e', null, false, [], fs);
|
||||||
|
const cSource2 = new SourceFile(_('/foo/src/lib/c.js'), 'bcd123e', null, false, [], fs);
|
||||||
|
|
||||||
|
const bToCSourceMap: RawSourceMap = {
|
||||||
|
mappings: encode([[[1, 0, 0, 0], [4, 0, 0, 3], [4, 0, 0, 6], [5, 0, 0, 7]]]),
|
||||||
|
names: [],
|
||||||
|
sources: ['c.js'],
|
||||||
|
version: 3
|
||||||
|
};
|
||||||
|
const bSource = new SourceFile(
|
||||||
|
_('/foo/src/lib/b.js'), 'abcdef', bToCSourceMap, false, [cSource1], fs);
|
||||||
|
|
||||||
|
const aToBCSourceMap: RawSourceMap = {
|
||||||
|
mappings:
|
||||||
|
encode([[[0, 0, 0, 0], [2, 0, 0, 3], [4, 0, 0, 2], [5, 0, 0, 5], [6, 1, 0, 3]]]),
|
||||||
|
names: [],
|
||||||
|
sources: ['lib/b.js', 'lib/c.js'],
|
||||||
|
version: 3
|
||||||
|
};
|
||||||
|
const aSource = new SourceFile(
|
||||||
|
_('/foo/src/a.js'), 'abdecf123', aToBCSourceMap, false, [bSource, cSource2], fs);
|
||||||
|
|
||||||
|
const aTocSourceMap = aSource.renderFlattenedSourceMap();
|
||||||
|
expect(aTocSourceMap.version).toEqual(3);
|
||||||
|
expect(aTocSourceMap.file).toEqual('a.js');
|
||||||
|
expect(aTocSourceMap.names).toEqual([]);
|
||||||
|
expect(aTocSourceMap.sourceRoot).toBeUndefined();
|
||||||
|
expect(aTocSourceMap.sources).toEqual(['lib/c.js']);
|
||||||
|
expect(aTocSourceMap.sourcesContent).toEqual(['bcd123e']);
|
||||||
|
expect(aTocSourceMap.mappings).toEqual(encode([[
|
||||||
|
[1, 0, 0, 0], [2, 0, 0, 2], [3, 0, 0, 3], [3, 0, 0, 6], [4, 0, 0, 1], [5, 0, 0, 7],
|
||||||
|
[6, 0, 0, 3]
|
||||||
|
]]));
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('getOriginalLocation()', () => {
|
describe('getOriginalLocation()', () => {
|
||||||
|
|
Loading…
Reference in New Issue