Skip to content

Commit 27c6638

Browse files
chuckjazvicb
authored andcommitted
fix(compiler-cli): set source file ranges in node emitter (#19348)
Enables source mapping from the template to the generated files.
1 parent 3f100eb commit 27c6638

File tree

3 files changed

+103
-1
lines changed

3 files changed

+103
-1
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
"@types/jasmine": "2.2.22-alpha",
4242
"@types/node": "6.0.88",
4343
"@types/selenium-webdriver": "3.0.7",
44+
"@types/source-map": "^0.5.1",
4445
"@types/systemjs": "0.19.32",
4546
"angular": "1.5.0",
4647
"angular-animate": "1.5.0",

packages/compiler-cli/src/transformers/node_emitter.ts

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9-
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';
9+
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';
1010
import * as ts from 'typescript';
1111

1212
export interface Node { sourceSpan: ParseSourceSpan|null; }
@@ -63,8 +63,10 @@ function createLiteral(value: any) {
6363
*/
6464
class _NodeEmitterVisitor implements StatementVisitor, ExpressionVisitor {
6565
private _nodeMap = new Map<ts.Node, Node>();
66+
private _mapped = new Set<Node>();
6667
private _importsWithPrefixes = new Map<string, string>();
6768
private _reexports = new Map<string, {name: string, as: string}[]>();
69+
private _templateSources = new Map<ParseSourceFile, ts.SourceMapSource>();
6870

6971
getReexports(): ts.Statement[] {
7072
return Array.from(this._reexports.entries())
@@ -93,11 +95,34 @@ class _NodeEmitterVisitor implements StatementVisitor, ExpressionVisitor {
9395
private record<T extends ts.Node>(ngNode: Node, tsNode: T|null): RecordedNode<T> {
9496
if (tsNode && !this._nodeMap.has(tsNode)) {
9597
this._nodeMap.set(tsNode, ngNode);
98+
if (!this._mapped.has(ngNode)) {
99+
this._mapped.add(ngNode);
100+
const range = this.sourceRangeOf(ngNode);
101+
if (range) {
102+
ts.setSourceMapRange(tsNode, range);
103+
}
104+
}
96105
ts.forEachChild(tsNode, child => this.record(ngNode, tsNode));
97106
}
98107
return tsNode as RecordedNode<T>;
99108
}
100109

110+
private sourceRangeOf(node: Node): ts.SourceMapRange|null {
111+
if (node.sourceSpan) {
112+
const span = node.sourceSpan;
113+
if (span.start.file == span.end.file) {
114+
const file = span.start.file;
115+
let source = this._templateSources.get(file);
116+
if (!source) {
117+
source = ts.createSourceMapSource(file.url, file.content, pos => pos);
118+
this._templateSources.set(file, source);
119+
}
120+
return {pos: span.start.offset, end: span.end.offset, source};
121+
}
122+
}
123+
return null;
124+
}
125+
101126
private getModifiers(stmt: Statement) {
102127
let modifiers: ts.Modifier[] = [];
103128
if (stmt.hasModifier(StmtModifier.Exported)) {

packages/compiler-cli/test/transformers/node_emitter_spec.ts

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9+
import {ParseLocation, ParseSourceFile, ParseSourceSpan} from '@angular/compiler';
910
import * as o from '@angular/compiler/src/output/output_ast';
11+
import {MappingItem, RawSourceMap, SourceMapConsumer} from 'source-map';
1012
import * as ts from 'typescript';
1113

1214
import {TypeScriptNodeEmitter} from '../../src/transformers/node_emitter';
@@ -384,6 +386,80 @@ describe('TypeScriptNodeEmitter', () => {
384386
it('should support a preamble', () => {
385387
expect(emitStmt(o.variable('a').toStmt(), '/* SomePreamble */')).toBe('/* SomePreamble */ a;');
386388
});
389+
390+
describe('source maps', () => {
391+
function emitStmt(stmt: o.Statement | o.Statement[], preamble?: string): string {
392+
const stmts = Array.isArray(stmt) ? stmt : [stmt];
393+
394+
const program = ts.createProgram(
395+
[someGenFileName], {
396+
module: ts.ModuleKind.CommonJS,
397+
target: ts.ScriptTarget.ES2017,
398+
sourceMap: true,
399+
inlineSourceMap: true,
400+
inlineSources: true,
401+
},
402+
host);
403+
const moduleSourceFile = program.getSourceFile(someGenFileName);
404+
const transformers: ts.CustomTransformers = {
405+
before: [context => {
406+
return sourceFile => {
407+
const [newSourceFile] = emitter.updateSourceFile(sourceFile, stmts, preamble);
408+
return newSourceFile;
409+
};
410+
}]
411+
};
412+
let result: string = '';
413+
const emitResult = program.emit(
414+
moduleSourceFile, (fileName, data, writeByteOrderMark, onError, sourceFiles) => {
415+
if (fileName.startsWith(someGenFilePath)) {
416+
result = data;
417+
}
418+
}, undefined, undefined, transformers);
419+
return result;
420+
}
421+
422+
it('should produce a source map that maps back to the source', () => {
423+
const statement = someVar.set(o.literal(1)).toDeclStmt();
424+
const text = '<my-comp> a = 1 </my-comp>';
425+
const sourceName = 'ng://some.file.html';
426+
const sourceUrl = 'file:///ng:/some.file.html';
427+
const file = new ParseSourceFile(text, sourceName);
428+
const start = new ParseLocation(file, 0, 0, 0);
429+
const end = new ParseLocation(file, text.length, 0, text.length);
430+
statement.sourceSpan = new ParseSourceSpan(start, end);
431+
432+
const result = emitStmt(statement);
433+
434+
// find the source map:
435+
const sourceMapMatch = /sourceMappingURL\=data\:application\/json;base64,(.*)$/.exec(result);
436+
const sourceMapBase64 = sourceMapMatch ![1];
437+
const sourceMapBuffer = Buffer.from(sourceMapBase64, 'base64');
438+
const sourceMapText = sourceMapBuffer.toString('utf8');
439+
const sourceMap: RawSourceMap = JSON.parse(sourceMapText);
440+
const consumer = new SourceMapConsumer(sourceMap);
441+
const mappings: MappingItem[] = [];
442+
consumer.eachMapping(mapping => { mappings.push(mapping); });
443+
expect(mappings).toEqual([
444+
{
445+
source: sourceUrl,
446+
generatedLine: 3,
447+
generatedColumn: 0,
448+
originalLine: 1,
449+
originalColumn: 0,
450+
name: null
451+
},
452+
{
453+
source: sourceUrl,
454+
generatedLine: 3,
455+
generatedColumn: 16,
456+
originalLine: 1,
457+
originalColumn: 26,
458+
name: null
459+
}
460+
]);
461+
});
462+
});
387463
});
388464

389465
const FILES: Directory = {

0 commit comments

Comments
 (0)