fix(compiler-cli): produce smaller source maps for templates (#19578)
Assocating each template node with a the generated TypeScript generated overly verbose source maps. Changed to creating a source map entry per unique source span instead of each unique template ast node. Fixes: #19537 PR Close #19578
This commit is contained in:
parent
81167d9c4a
commit
f83989bb0d
|
@ -36,9 +36,10 @@ export class TypeScriptNodeEmitter {
|
||||||
ts.setEmitFlags(commentStmt, ts.EmitFlags.CustomPrologue);
|
ts.setEmitFlags(commentStmt, ts.EmitFlags.CustomPrologue);
|
||||||
preambleStmts.push(commentStmt);
|
preambleStmts.push(commentStmt);
|
||||||
}
|
}
|
||||||
const newSourceFile = ts.updateSourceFileNode(
|
const sourceStatments =
|
||||||
sourceFile,
|
[...preambleStmts, ...converter.getReexports(), ...converter.getImports(), ...statements];
|
||||||
[...preambleStmts, ...converter.getReexports(), ...converter.getImports(), ...statements]);
|
converter.updateSourceMap(sourceStatments);
|
||||||
|
const newSourceFile = ts.updateSourceFileNode(sourceFile, sourceStatments);
|
||||||
return [newSourceFile, converter.getNodeMap()];
|
return [newSourceFile, converter.getNodeMap()];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -63,7 +64,6 @@ function createLiteral(value: any) {
|
||||||
*/
|
*/
|
||||||
class _NodeEmitterVisitor implements StatementVisitor, ExpressionVisitor {
|
class _NodeEmitterVisitor implements StatementVisitor, ExpressionVisitor {
|
||||||
private _nodeMap = new Map<ts.Node, Node>();
|
private _nodeMap = new Map<ts.Node, Node>();
|
||||||
private _mapped = new Set<Node>();
|
|
||||||
private _importsWithPrefixes = new Map<string, string>();
|
private _importsWithPrefixes = new Map<string, string>();
|
||||||
private _reexports = new Map<string, {name: string, as: string}[]>();
|
private _reexports = new Map<string, {name: string, as: string}[]>();
|
||||||
private _templateSources = new Map<ParseSourceFile, ts.SourceMapSource>();
|
private _templateSources = new Map<ParseSourceFile, ts.SourceMapSource>();
|
||||||
|
@ -92,17 +92,49 @@ class _NodeEmitterVisitor implements StatementVisitor, ExpressionVisitor {
|
||||||
|
|
||||||
getNodeMap() { return this._nodeMap; }
|
getNodeMap() { return this._nodeMap; }
|
||||||
|
|
||||||
|
updateSourceMap(statements: ts.Statement[]) {
|
||||||
|
let lastRangeStartNode: ts.Node|undefined = undefined;
|
||||||
|
let lastRangeEndNode: ts.Node|undefined = undefined;
|
||||||
|
let lastRange: ts.SourceMapRange|undefined = undefined;
|
||||||
|
|
||||||
|
const recordLastSourceRange = () => {
|
||||||
|
if (lastRange && lastRangeStartNode && lastRangeEndNode) {
|
||||||
|
if (lastRangeStartNode == lastRangeEndNode) {
|
||||||
|
ts.setSourceMapRange(lastRangeEndNode, lastRange);
|
||||||
|
} else {
|
||||||
|
ts.setSourceMapRange(lastRangeStartNode, lastRange);
|
||||||
|
// Only emit the pos for the first node emitted in the range.
|
||||||
|
ts.setEmitFlags(lastRangeStartNode, ts.EmitFlags.NoTrailingSourceMap);
|
||||||
|
ts.setSourceMapRange(lastRangeEndNode, lastRange);
|
||||||
|
// Only emit emit end for the last node emitted in the range.
|
||||||
|
ts.setEmitFlags(lastRangeEndNode, ts.EmitFlags.NoLeadingSourceMap);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const visitNode = (tsNode: ts.Node) => {
|
||||||
|
const ngNode = this._nodeMap.get(tsNode);
|
||||||
|
if (ngNode) {
|
||||||
|
const range = this.sourceRangeOf(ngNode);
|
||||||
|
if (range) {
|
||||||
|
if (!lastRange || range.source != lastRange.source || range.pos != lastRange.pos ||
|
||||||
|
range.end != lastRange.end) {
|
||||||
|
recordLastSourceRange();
|
||||||
|
lastRangeStartNode = tsNode;
|
||||||
|
lastRange = range;
|
||||||
|
}
|
||||||
|
lastRangeEndNode = tsNode;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ts.forEachChild(tsNode, visitNode);
|
||||||
|
};
|
||||||
|
statements.forEach(visitNode);
|
||||||
|
recordLastSourceRange();
|
||||||
|
}
|
||||||
|
|
||||||
private record<T extends ts.Node>(ngNode: Node, tsNode: T|null): RecordedNode<T> {
|
private record<T extends ts.Node>(ngNode: Node, tsNode: T|null): RecordedNode<T> {
|
||||||
if (tsNode && !this._nodeMap.has(tsNode)) {
|
if (tsNode && !this._nodeMap.has(tsNode)) {
|
||||||
this._nodeMap.set(tsNode, ngNode);
|
this._nodeMap.set(tsNode, ngNode);
|
||||||
if (!this._mapped.has(ngNode)) {
|
|
||||||
this._mapped.add(ngNode);
|
|
||||||
const range = this.sourceRangeOf(ngNode);
|
|
||||||
if (range) {
|
|
||||||
ts.setSourceMapRange(tsNode, range);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ts.forEachChild(tsNode, child => this.record(ngNode, tsNode));
|
|
||||||
}
|
}
|
||||||
return tsNode as RecordedNode<T>;
|
return tsNode as RecordedNode<T>;
|
||||||
}
|
}
|
||||||
|
|
|
@ -419,6 +419,19 @@ describe('TypeScriptNodeEmitter', () => {
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function mappingItemsOf(text: string): MappingItem[] {
|
||||||
|
// find the source map:
|
||||||
|
const sourceMapMatch = /sourceMappingURL\=data\:application\/json;base64,(.*)$/.exec(text);
|
||||||
|
const sourceMapBase64 = sourceMapMatch ![1];
|
||||||
|
const sourceMapBuffer = Buffer.from(sourceMapBase64, 'base64');
|
||||||
|
const sourceMapText = sourceMapBuffer.toString('utf8');
|
||||||
|
const sourceMap: RawSourceMap = JSON.parse(sourceMapText);
|
||||||
|
const consumer = new SourceMapConsumer(sourceMap);
|
||||||
|
const mappings: MappingItem[] = [];
|
||||||
|
consumer.eachMapping(mapping => { mappings.push(mapping); });
|
||||||
|
return mappings;
|
||||||
|
}
|
||||||
|
|
||||||
it('should produce a source map that maps back to the source', () => {
|
it('should produce a source map that maps back to the source', () => {
|
||||||
const statement = someVar.set(o.literal(1)).toDeclStmt();
|
const statement = someVar.set(o.literal(1)).toDeclStmt();
|
||||||
const text = '<my-comp> a = 1 </my-comp>';
|
const text = '<my-comp> a = 1 </my-comp>';
|
||||||
|
@ -430,16 +443,8 @@ describe('TypeScriptNodeEmitter', () => {
|
||||||
statement.sourceSpan = new ParseSourceSpan(start, end);
|
statement.sourceSpan = new ParseSourceSpan(start, end);
|
||||||
|
|
||||||
const result = emitStmt(statement);
|
const result = emitStmt(statement);
|
||||||
|
const mappings = mappingItemsOf(result);
|
||||||
|
|
||||||
// find the source map:
|
|
||||||
const sourceMapMatch = /sourceMappingURL\=data\:application\/json;base64,(.*)$/.exec(result);
|
|
||||||
const sourceMapBase64 = sourceMapMatch ![1];
|
|
||||||
const sourceMapBuffer = Buffer.from(sourceMapBase64, 'base64');
|
|
||||||
const sourceMapText = sourceMapBuffer.toString('utf8');
|
|
||||||
const sourceMap: RawSourceMap = JSON.parse(sourceMapText);
|
|
||||||
const consumer = new SourceMapConsumer(sourceMap);
|
|
||||||
const mappings: MappingItem[] = [];
|
|
||||||
consumer.eachMapping(mapping => { mappings.push(mapping); });
|
|
||||||
expect(mappings).toEqual([
|
expect(mappings).toEqual([
|
||||||
{
|
{
|
||||||
source: sourceUrl,
|
source: sourceUrl,
|
||||||
|
@ -459,9 +464,45 @@ describe('TypeScriptNodeEmitter', () => {
|
||||||
}
|
}
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should produce a mapping per range instead of a mapping per node', () => {
|
||||||
|
const text = '<my-comp> a = 1 </my-comp>';
|
||||||
|
const sourceName = '/some/file.html';
|
||||||
|
const sourceUrl = '../some/file.html';
|
||||||
|
const file = new ParseSourceFile(text, sourceName);
|
||||||
|
const start = new ParseLocation(file, 0, 0, 0);
|
||||||
|
const end = new ParseLocation(file, text.length, 0, text.length);
|
||||||
|
const stmt = (loc: number) => {
|
||||||
|
const start = new ParseLocation(file, loc, 0, loc);
|
||||||
|
const end = new ParseLocation(file, loc + 1, 0, loc + 1);
|
||||||
|
const span = new ParseSourceSpan(start, end);
|
||||||
|
return someVar
|
||||||
|
.set(new o.BinaryOperatorExpr(
|
||||||
|
o.BinaryOperator.Plus, o.literal(loc, null, span), o.literal(loc, null, span), null,
|
||||||
|
span))
|
||||||
|
.toDeclStmt();
|
||||||
|
};
|
||||||
|
const stmts = [1, 2, 3, 4, 5, 6].map(stmt);
|
||||||
|
const result = emitStmt(stmts);
|
||||||
|
const mappings = mappingItemsOf(result);
|
||||||
|
|
||||||
|
// The span is used in three different nodes but should only be emitted at most twice
|
||||||
|
// (once for the start and once for the end of a span).
|
||||||
|
const maxDup = Math.max(
|
||||||
|
...Array.from(countsOfDuplicatesMap(mappings.map(m => m.originalColumn)).values()));
|
||||||
|
expect(maxDup <= 2).toBeTruthy('A redundant range was emitted');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function countsOfDuplicatesMap<T>(a: T[]): Map<T, number> {
|
||||||
|
const result = new Map<T, number>();
|
||||||
|
for (const item of a) {
|
||||||
|
result.set(item, (result.get(item) || 0) + 1);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
const FILES: Directory = {
|
const FILES: Directory = {
|
||||||
somePackage: {'someGenFile.ts': `export var a: number;`}
|
somePackage: {'someGenFile.ts': `export var a: number;`}
|
||||||
};
|
};
|
||||||
|
|
Loading…
Reference in New Issue