diff --git a/packages/compiler-cli/src/transformers/node_emitter.ts b/packages/compiler-cli/src/transformers/node_emitter.ts index c55b678d0c..83b9fdd279 100644 --- a/packages/compiler-cli/src/transformers/node_emitter.ts +++ b/packages/compiler-cli/src/transformers/node_emitter.ts @@ -36,9 +36,10 @@ export class TypeScriptNodeEmitter { ts.setEmitFlags(commentStmt, ts.EmitFlags.CustomPrologue); preambleStmts.push(commentStmt); } - const newSourceFile = ts.updateSourceFileNode( - sourceFile, - [...preambleStmts, ...converter.getReexports(), ...converter.getImports(), ...statements]); + const sourceStatments = + [...preambleStmts, ...converter.getReexports(), ...converter.getImports(), ...statements]; + converter.updateSourceMap(sourceStatments); + const newSourceFile = ts.updateSourceFileNode(sourceFile, sourceStatments); return [newSourceFile, converter.getNodeMap()]; } } @@ -63,7 +64,6 @@ function createLiteral(value: any) { */ class _NodeEmitterVisitor implements StatementVisitor, ExpressionVisitor { private _nodeMap = new Map(); - private _mapped = new Set(); private _importsWithPrefixes = new Map(); private _reexports = new Map(); private _templateSources = new Map(); @@ -92,17 +92,49 @@ class _NodeEmitterVisitor implements StatementVisitor, ExpressionVisitor { 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(ngNode: Node, tsNode: T|null): RecordedNode { if (tsNode && !this._nodeMap.has(tsNode)) { 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; } diff --git a/packages/compiler-cli/test/transformers/node_emitter_spec.ts b/packages/compiler-cli/test/transformers/node_emitter_spec.ts index 0ac9a0a53b..d589402cfb 100644 --- a/packages/compiler-cli/test/transformers/node_emitter_spec.ts +++ b/packages/compiler-cli/test/transformers/node_emitter_spec.ts @@ -419,6 +419,19 @@ describe('TypeScriptNodeEmitter', () => { 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', () => { const statement = someVar.set(o.literal(1)).toDeclStmt(); const text = ' a = 1 '; @@ -430,16 +443,8 @@ describe('TypeScriptNodeEmitter', () => { statement.sourceSpan = new ParseSourceSpan(start, end); 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([ { source: sourceUrl, @@ -459,9 +464,45 @@ describe('TypeScriptNodeEmitter', () => { } ]); }); + + it('should produce a mapping per range instead of a mapping per node', () => { + const text = ' a = 1 '; + 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(a: T[]): Map { + const result = new Map(); + for (const item of a) { + result.set(item, (result.get(item) || 0) + 1); + } + return result; +} + const FILES: Directory = { somePackage: {'someGenFile.ts': `export var a: number;`} };