diff --git a/package.json b/package.json index cfc0ad0bf7..d57e41e49c 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,7 @@ "@types/jasmine": "2.2.22-alpha", "@types/node": "6.0.88", "@types/selenium-webdriver": "3.0.7", + "@types/source-map": "^0.5.1", "@types/systemjs": "0.19.32", "angular": "1.5.0", "angular-animate": "1.5.0", diff --git a/packages/compiler-cli/src/transformers/node_emitter.ts b/packages/compiler-cli/src/transformers/node_emitter.ts index 6b32c40cc2..c55b678d0c 100644 --- a/packages/compiler-cli/src/transformers/node_emitter.ts +++ b/packages/compiler-cli/src/transformers/node_emitter.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import {AssertNotNull, BinaryOperator, BinaryOperatorExpr, BuiltinMethod, BuiltinVar, CastExpr, ClassStmt, CommaExpr, CommentStmt, CompileIdentifierMetadata, ConditionalExpr, DeclareFunctionStmt, DeclareVarStmt, ExpressionStatement, ExpressionVisitor, ExternalExpr, ExternalReference, FunctionExpr, IfStmt, InstantiateExpr, InvokeFunctionExpr, InvokeMethodExpr, LiteralArrayExpr, LiteralExpr, LiteralMapExpr, NotExpr, ParseSourceSpan, ReadKeyExpr, ReadPropExpr, ReadVarExpr, ReturnStatement, Statement, StatementVisitor, StaticSymbol, StmtModifier, ThrowStmt, TryCatchStmt, WriteKeyExpr, WritePropExpr, WriteVarExpr} from '@angular/compiler'; +import {AssertNotNull, BinaryOperator, BinaryOperatorExpr, BuiltinMethod, BuiltinVar, CastExpr, ClassStmt, CommaExpr, CommentStmt, CompileIdentifierMetadata, ConditionalExpr, DeclareFunctionStmt, DeclareVarStmt, ExpressionStatement, ExpressionVisitor, ExternalExpr, ExternalReference, FunctionExpr, IfStmt, InstantiateExpr, InvokeFunctionExpr, InvokeMethodExpr, LiteralArrayExpr, LiteralExpr, LiteralMapExpr, NotExpr, ParseSourceFile, ParseSourceSpan, ReadKeyExpr, ReadPropExpr, ReadVarExpr, ReturnStatement, Statement, StatementVisitor, StaticSymbol, StmtModifier, ThrowStmt, TryCatchStmt, WriteKeyExpr, WritePropExpr, WriteVarExpr} from '@angular/compiler'; import * as ts from 'typescript'; export interface Node { sourceSpan: ParseSourceSpan|null; } @@ -63,8 +63,10 @@ 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(); getReexports(): ts.Statement[] { return Array.from(this._reexports.entries()) @@ -93,11 +95,34 @@ class _NodeEmitterVisitor implements StatementVisitor, ExpressionVisitor { 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; } + private sourceRangeOf(node: Node): ts.SourceMapRange|null { + if (node.sourceSpan) { + const span = node.sourceSpan; + if (span.start.file == span.end.file) { + const file = span.start.file; + let source = this._templateSources.get(file); + if (!source) { + source = ts.createSourceMapSource(file.url, file.content, pos => pos); + this._templateSources.set(file, source); + } + return {pos: span.start.offset, end: span.end.offset, source}; + } + } + return null; + } + private getModifiers(stmt: Statement) { let modifiers: ts.Modifier[] = []; if (stmt.hasModifier(StmtModifier.Exported)) { diff --git a/packages/compiler-cli/test/transformers/node_emitter_spec.ts b/packages/compiler-cli/test/transformers/node_emitter_spec.ts index 3082e750ed..637ca8be7a 100644 --- a/packages/compiler-cli/test/transformers/node_emitter_spec.ts +++ b/packages/compiler-cli/test/transformers/node_emitter_spec.ts @@ -6,7 +6,9 @@ * found in the LICENSE file at https://angular.io/license */ +import {ParseLocation, ParseSourceFile, ParseSourceSpan} from '@angular/compiler'; import * as o from '@angular/compiler/src/output/output_ast'; +import {MappingItem, RawSourceMap, SourceMapConsumer} from 'source-map'; import * as ts from 'typescript'; import {TypeScriptNodeEmitter} from '../../src/transformers/node_emitter'; @@ -384,6 +386,80 @@ describe('TypeScriptNodeEmitter', () => { it('should support a preamble', () => { expect(emitStmt(o.variable('a').toStmt(), '/* SomePreamble */')).toBe('/* SomePreamble */ a;'); }); + + describe('source maps', () => { + function emitStmt(stmt: o.Statement | o.Statement[], preamble?: string): string { + const stmts = Array.isArray(stmt) ? stmt : [stmt]; + + const program = ts.createProgram( + [someGenFileName], { + module: ts.ModuleKind.CommonJS, + target: ts.ScriptTarget.ES2017, + sourceMap: true, + inlineSourceMap: true, + inlineSources: true, + }, + host); + const moduleSourceFile = program.getSourceFile(someGenFileName); + const transformers: ts.CustomTransformers = { + before: [context => { + return sourceFile => { + const [newSourceFile] = emitter.updateSourceFile(sourceFile, stmts, preamble); + return newSourceFile; + }; + }] + }; + let result: string = ''; + const emitResult = program.emit( + moduleSourceFile, (fileName, data, writeByteOrderMark, onError, sourceFiles) => { + if (fileName.startsWith(someGenFilePath)) { + result = data; + } + }, undefined, undefined, transformers); + return result; + } + + it('should produce a source map that maps back to the source', () => { + const statement = someVar.set(o.literal(1)).toDeclStmt(); + const text = ' a = 1 '; + const sourceName = 'ng://some.file.html'; + const sourceUrl = 'file:///ng:/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); + statement.sourceSpan = new ParseSourceSpan(start, end); + + const result = emitStmt(statement); + + // 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, + generatedLine: 3, + generatedColumn: 0, + originalLine: 1, + originalColumn: 0, + name: null + }, + { + source: sourceUrl, + generatedLine: 3, + generatedColumn: 16, + originalLine: 1, + originalColumn: 26, + name: null + } + ]); + }); + }); }); const FILES: Directory = {