fix(compiler-cli): set source file ranges in node emitter (#19348)
Enables source mapping from the template to the generated files.
This commit is contained in:
parent
3f100eb23a
commit
27c6638913
|
@ -41,6 +41,7 @@
|
||||||
"@types/jasmine": "2.2.22-alpha",
|
"@types/jasmine": "2.2.22-alpha",
|
||||||
"@types/node": "6.0.88",
|
"@types/node": "6.0.88",
|
||||||
"@types/selenium-webdriver": "3.0.7",
|
"@types/selenium-webdriver": "3.0.7",
|
||||||
|
"@types/source-map": "^0.5.1",
|
||||||
"@types/systemjs": "0.19.32",
|
"@types/systemjs": "0.19.32",
|
||||||
"angular": "1.5.0",
|
"angular": "1.5.0",
|
||||||
"angular-animate": "1.5.0",
|
"angular-animate": "1.5.0",
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
* found in the LICENSE file at https://angular.io/license
|
* 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';
|
import * as ts from 'typescript';
|
||||||
|
|
||||||
export interface Node { sourceSpan: ParseSourceSpan|null; }
|
export interface Node { sourceSpan: ParseSourceSpan|null; }
|
||||||
|
@ -63,8 +63,10 @@ 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>();
|
||||||
|
|
||||||
getReexports(): ts.Statement[] {
|
getReexports(): ts.Statement[] {
|
||||||
return Array.from(this._reexports.entries())
|
return Array.from(this._reexports.entries())
|
||||||
|
@ -93,11 +95,34 @@ class _NodeEmitterVisitor implements StatementVisitor, ExpressionVisitor {
|
||||||
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));
|
ts.forEachChild(tsNode, child => this.record(ngNode, tsNode));
|
||||||
}
|
}
|
||||||
return tsNode as RecordedNode<T>;
|
return tsNode as RecordedNode<T>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) {
|
private getModifiers(stmt: Statement) {
|
||||||
let modifiers: ts.Modifier[] = [];
|
let modifiers: ts.Modifier[] = [];
|
||||||
if (stmt.hasModifier(StmtModifier.Exported)) {
|
if (stmt.hasModifier(StmtModifier.Exported)) {
|
||||||
|
|
|
@ -6,7 +6,9 @@
|
||||||
* found in the LICENSE file at https://angular.io/license
|
* 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 * as o from '@angular/compiler/src/output/output_ast';
|
||||||
|
import {MappingItem, RawSourceMap, SourceMapConsumer} from 'source-map';
|
||||||
import * as ts from 'typescript';
|
import * as ts from 'typescript';
|
||||||
|
|
||||||
import {TypeScriptNodeEmitter} from '../../src/transformers/node_emitter';
|
import {TypeScriptNodeEmitter} from '../../src/transformers/node_emitter';
|
||||||
|
@ -384,6 +386,80 @@ describe('TypeScriptNodeEmitter', () => {
|
||||||
it('should support a preamble', () => {
|
it('should support a preamble', () => {
|
||||||
expect(emitStmt(o.variable('a').toStmt(), '/* SomePreamble */')).toBe('/* SomePreamble */ a;');
|
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 = '<my-comp> a = 1 </my-comp>';
|
||||||
|
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 = {
|
const FILES: Directory = {
|
||||||
|
|
Loading…
Reference in New Issue