diff --git a/packages/compiler/src/aot/compiler.ts b/packages/compiler/src/aot/compiler.ts index d88dbbdf84..67ebc6d939 100644 --- a/packages/compiler/src/aot/compiler.ts +++ b/packages/compiler/src/aot/compiler.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import {CompileDirectiveMetadata, CompileIdentifierMetadata, CompileNgModuleMetadata, CompileProviderMetadata, componentFactoryName, createHostComponentMeta, flatten, identifierName} from '../compile_metadata'; +import {CompileDirectiveMetadata, CompileIdentifierMetadata, CompileNgModuleMetadata, CompileProviderMetadata, componentFactoryName, createHostComponentMeta, flatten, identifierName, templateSourceUrl} from '../compile_metadata'; import {CompilerConfig} from '../config'; import {Identifiers, createIdentifier, createIdentifierToken} from '../identifiers'; import {CompileMetadataResolver} from '../metadata_resolver'; @@ -189,7 +189,7 @@ export class AotCompiler { const {template: parsedTemplate, pipes: usedPipes} = this._templateParser.parse( compMeta, compMeta.template.template, directives, pipes, ngModule.schemas, - identifierName(compMeta.type)); + templateSourceUrl(ngModule.type, compMeta, compMeta.template)); const stylesExpr = componentStyles ? o.variable(componentStyles.stylesVar) : o.literalArr([]); const viewResult = this._viewCompiler.compileComponent(compMeta, parsedTemplate, stylesExpr, usedPipes); diff --git a/packages/compiler/src/compile_metadata.ts b/packages/compiler/src/compile_metadata.ts index 72046a041e..289b619e87 100644 --- a/packages/compiler/src/compile_metadata.ts +++ b/packages/compiler/src/compile_metadata.ts @@ -7,6 +7,7 @@ */ import {ChangeDetectionStrategy, ComponentFactory, RendererType2, SchemaMetadata, Type, ViewEncapsulation, ɵLifecycleHooks, ɵreflector, ɵstringify as stringify} from '@angular/core'; + import {StaticSymbol} from './aot/static_symbol'; import {CssSelector} from './selector'; import {splitAtColon} from './util'; @@ -257,6 +258,7 @@ export class CompileTemplateMetadata { encapsulation: ViewEncapsulation; template: string; templateUrl: string; + isInline: boolean; styles: string[]; styleUrls: string[]; externalStylesheets: CompileStylesheetMetadata[]; @@ -265,7 +267,7 @@ export class CompileTemplateMetadata { interpolation: [string, string]; constructor( {encapsulation, template, templateUrl, styles, styleUrls, externalStylesheets, animations, - ngContentSelectors, interpolation}: { + ngContentSelectors, interpolation, isInline}: { encapsulation?: ViewEncapsulation, template?: string, templateUrl?: string, @@ -275,6 +277,7 @@ export class CompileTemplateMetadata { ngContentSelectors?: string[], animations?: any[], interpolation?: [string, string], + isInline?: boolean } = {}) { this.encapsulation = encapsulation; this.template = template; @@ -288,6 +291,7 @@ export class CompileTemplateMetadata { throw new Error(`'interpolation' should have a start and an end symbol.`); } this.interpolation = interpolation; + this.isInline = isInline; } toSummary(): CompileTemplateSummary { @@ -524,7 +528,8 @@ export function createHostComponentMeta( styles: [], styleUrls: [], ngContentSelectors: [], - animations: [] + animations: [], + isInline: true, }), changeDetection: ChangeDetectionStrategy.Default, inputs: [], @@ -752,3 +757,41 @@ export function flatten(list: Array): T[] { return (flat).concat(flatItem); }, []); } + +/** + * Note: Using `location.origin` as prefix helps displaying them as a hierarchy in chrome. + * It also helps long-stack-trace zone when rewriting stack traces to not break + * source maps (as now all scripts have the same origin). + */ +function ngJitFolder() { + return 'ng://'; +} + +export function templateSourceUrl( + ngModuleType: CompileIdentifierMetadata, compMeta: {type: CompileIdentifierMetadata}, + templateMeta: {isInline: boolean, templateUrl: string}) { + if (templateMeta.isInline) { + if (compMeta.type.reference instanceof StaticSymbol) { + return compMeta.type.reference.filePath; + } else { + return `${ngJitFolder()}/${identifierName(ngModuleType)}/${identifierName(compMeta.type)}.html`; + } + } else { + return templateMeta.templateUrl; + } +} + +export function sharedStylesheetJitUrl(meta: CompileStylesheetMetadata, id: number) { + const pathParts = meta.moduleUrl.split(/\/\\/g); + const baseName = pathParts[pathParts.length - 1]; + return `${ngJitFolder()}/css/${id}${baseName}.ngstyle.js`; +} + +export function ngModuleJitUrl(moduleMeta: CompileNgModuleMetadata): string { + return `${ngJitFolder()}/${identifierName(moduleMeta.type)}/module.ngfactory.js`; +} + +export function templateJitUrl( + ngModuleType: CompileIdentifierMetadata, compMeta: CompileDirectiveMetadata): string { + return `${ngJitFolder()}/${identifierName(ngModuleType)}/${identifierName(compMeta.type)}.ngfactory.js`; +} diff --git a/packages/compiler/src/compiler.ts b/packages/compiler/src/compiler.ts index 8001bfd1d7..6ddab6402d 100644 --- a/packages/compiler/src/compiler.ts +++ b/packages/compiler/src/compiler.ts @@ -28,6 +28,7 @@ export {CompilerConfig} from './config'; export * from './compile_metadata'; export * from './aot/compiler_factory'; export * from './aot/compiler'; +export * from './aot/generated_file'; export * from './aot/compiler_options'; export * from './aot/compiler_host'; export * from './aot/static_reflector'; diff --git a/packages/compiler/src/directive_normalizer.ts b/packages/compiler/src/directive_normalizer.ts index 1ae15e24e9..2b7ec4b175 100644 --- a/packages/compiler/src/directive_normalizer.ts +++ b/packages/compiler/src/directive_normalizer.ts @@ -7,7 +7,7 @@ */ import {ViewEncapsulation, ɵstringify as stringify} from '@angular/core'; -import {CompileAnimationEntryMetadata, CompileDirectiveMetadata, CompileStylesheetMetadata, CompileTemplateMetadata} from './compile_metadata'; +import {CompileAnimationEntryMetadata, CompileDirectiveMetadata, CompileStylesheetMetadata, CompileTemplateMetadata, templateSourceUrl} from './compile_metadata'; import {CompilerConfig} from './config'; import {CompilerInjectable} from './injectable'; import * as html from './ml_parser/ast'; @@ -20,6 +20,7 @@ import {UrlResolver} from './url_resolver'; import {SyncAsyncResult, syntaxError} from './util'; export interface PrenormalizedTemplateMetadata { + ngModuleType: any; componentType: any; moduleUrl: string; template?: string; @@ -104,20 +105,25 @@ export class DirectiveNormalizer { } normalizeLoadedTemplate( - prenomData: PrenormalizedTemplateMetadata, template: string, + prenormData: PrenormalizedTemplateMetadata, template: string, templateAbsUrl: string): CompileTemplateMetadata { - const interpolationConfig = InterpolationConfig.fromArray(prenomData.interpolation); + const isInline = !!prenormData.template; + const interpolationConfig = InterpolationConfig.fromArray(prenormData.interpolation); const rootNodesAndErrors = this._htmlParser.parse( - template, stringify(prenomData.componentType), true, interpolationConfig); + template, + templateSourceUrl( + {reference: prenormData.ngModuleType}, {type: {reference: prenormData.componentType}}, + {isInline, templateUrl: templateAbsUrl}), + true, interpolationConfig); if (rootNodesAndErrors.errors.length > 0) { const errorString = rootNodesAndErrors.errors.join('\n'); throw syntaxError(`Template parse errors:\n${errorString}`); } const templateMetadataStyles = this.normalizeStylesheet(new CompileStylesheetMetadata({ - styles: prenomData.styles, - styleUrls: prenomData.styleUrls, - moduleUrl: prenomData.moduleUrl + styles: prenormData.styles, + styleUrls: prenormData.styleUrls, + moduleUrl: prenormData.moduleUrl })); const visitor = new TemplatePreparseVisitor(); @@ -125,7 +131,7 @@ export class DirectiveNormalizer { const templateStyles = this.normalizeStylesheet(new CompileStylesheetMetadata( {styles: visitor.styles, styleUrls: visitor.styleUrls, moduleUrl: templateAbsUrl})); - let encapsulation = prenomData.encapsulation; + let encapsulation = prenormData.encapsulation; if (encapsulation == null) { encapsulation = this._config.defaultEncapsulation; } @@ -143,8 +149,8 @@ export class DirectiveNormalizer { template, templateUrl: templateAbsUrl, styles, styleUrls, ngContentSelectors: visitor.ngContentSelectors, - animations: prenomData.animations, - interpolation: prenomData.interpolation, + animations: prenormData.animations, + interpolation: prenormData.interpolation, isInline }); } @@ -160,7 +166,8 @@ export class DirectiveNormalizer { externalStylesheets: externalStylesheets, ngContentSelectors: templateMeta.ngContentSelectors, animations: templateMeta.animations, - interpolation: templateMeta.interpolation + interpolation: templateMeta.interpolation, + isInline: templateMeta.isInline, })); } diff --git a/packages/compiler/src/i18n/digest.ts b/packages/compiler/src/i18n/digest.ts index e294cc88c9..f3f5e9d02f 100644 --- a/packages/compiler/src/i18n/digest.ts +++ b/packages/compiler/src/i18n/digest.ts @@ -210,7 +210,9 @@ enum Endian { Big, } -function utf8Encode(str: string): string { +// TODO(vicb): move this to some shared place, as we also need it +// for SourceMaps. +export function utf8Encode(str: string): string { let encoded: string = ''; for (let index = 0; index < str.length; index++) { diff --git a/packages/compiler/src/jit/compiler.ts b/packages/compiler/src/jit/compiler.ts index 5c6e558a2f..04e50f0be2 100644 --- a/packages/compiler/src/jit/compiler.ts +++ b/packages/compiler/src/jit/compiler.ts @@ -8,7 +8,7 @@ import {Compiler, ComponentFactory, Inject, Injector, ModuleWithComponentFactories, NgModuleFactory, Type, ɵgetComponentViewDefinitionFactory as getComponentViewDefinitionFactory, ɵstringify as stringify} from '@angular/core'; -import {CompileDirectiveMetadata, CompileIdentifierMetadata, CompileNgModuleMetadata, ProviderMeta, ProxyClass, createHostComponentMeta, identifierName} from '../compile_metadata'; +import {CompileDirectiveMetadata, CompileIdentifierMetadata, CompileNgModuleMetadata, CompileStylesheetMetadata, ProviderMeta, ProxyClass, createHostComponentMeta, identifierName, ngModuleJitUrl, sharedStylesheetJitUrl, templateJitUrl, templateSourceUrl} from '../compile_metadata'; import {CompilerConfig} from '../config'; import {CompilerInjectable} from '../injectable'; import {CompileMetadataResolver} from '../metadata_resolver'; @@ -38,6 +38,7 @@ export class JitCompiler implements Compiler { private _compiledHostTemplateCache = new Map, CompiledTemplate>(); private _compiledDirectiveWrapperCache = new Map, Type>(); private _compiledNgModuleCache = new Map, NgModuleFactory>(); + private _sharedStylesheetCount = 0; constructor( private _injector: Injector, private _metadataResolver: CompileMetadataResolver, @@ -128,7 +129,7 @@ export class JitCompiler implements Compiler { interpretStatements(compileResult.statements, [compileResult.ngModuleFactoryVar])[0]; } else { ngModuleFactory = jitStatements( - `/${identifierName(moduleMeta.type)}/module.ngfactory.js`, compileResult.statements, + ngModuleJitUrl(moduleMeta), compileResult.statements, [compileResult.ngModuleFactoryVar])[0]; } this._compiledNgModuleCache.set(moduleMeta.type.reference, ngModuleFactory); @@ -251,7 +252,7 @@ export class JitCompiler implements Compiler { pipe => this._metadataResolver.getPipeSummary(pipe.reference)); const {template: parsedTemplate, pipes: usedPipes} = this._templateParser.parse( compMeta, compMeta.template.template, directives, pipes, template.ngModule.schemas, - identifierName(compMeta.type)); + templateSourceUrl(template.ngModule.type, template.compMeta, template.compMeta.template)); const compileResult = this._viewCompiler.compileComponent( compMeta, parsedTemplate, ir.variable(stylesCompileResult.componentStylesheet.stylesVar), usedPipes); @@ -263,10 +264,9 @@ export class JitCompiler implements Compiler { [viewClass, rendererType] = interpretStatements( statements, [compileResult.viewClassVar, compileResult.rendererTypeVar]); } else { - const sourceUrl = - `/${identifierName(template.ngModule.type)}/${identifierName(template.compType)}/${template.isHost?'host':'component'}.ngfactory.js`; [viewClass, rendererType] = jitStatements( - sourceUrl, statements, [compileResult.viewClassVar, compileResult.rendererTypeVar]); + templateJitUrl(template.ngModule.type, template.compMeta), statements, + [compileResult.viewClassVar, compileResult.rendererTypeVar]); } template.compiled(viewClass, rendererType); } @@ -289,7 +289,8 @@ export class JitCompiler implements Compiler { return interpretStatements(result.statements, [result.stylesVar])[0]; } else { return jitStatements( - `/${result.meta.moduleUrl}.ngstyle.js`, result.statements, [result.stylesVar])[0]; + sharedStylesheetJitUrl(result.meta, this._sharedStylesheetCount++), result.statements, + [result.stylesVar])[0]; } } } diff --git a/packages/compiler/src/metadata_resolver.ts b/packages/compiler/src/metadata_resolver.ts index fd3ce3b7e8..16661fb64c 100644 --- a/packages/compiler/src/metadata_resolver.ts +++ b/packages/compiler/src/metadata_resolver.ts @@ -155,7 +155,8 @@ export class CompileMetadataResolver { return typeSummary && typeSummary.summaryKind === kind ? typeSummary : null; } - private _loadDirectiveMetadata(directiveType: any, isSync: boolean): Promise { + private _loadDirectiveMetadata(ngModuleType: any, directiveType: any, isSync: boolean): + Promise { if (this._directiveCache.has(directiveType)) { return; } @@ -191,6 +192,7 @@ export class CompileMetadataResolver { if (metadata.isComponent) { const templateMeta = this._directiveNormalizer.normalizeTemplate({ + ngModuleType, componentType: directiveType, moduleUrl: componentModuleUrl(this._reflector, directiveType, annotation), encapsulation: metadata.template.encapsulation, @@ -249,7 +251,8 @@ export class CompileMetadataResolver { styles: dirMeta.styles, styleUrls: dirMeta.styleUrls, animations: animations, - interpolation: dirMeta.interpolation + interpolation: dirMeta.interpolation, + isInline: !!dirMeta.template }); } @@ -378,7 +381,7 @@ export class CompileMetadataResolver { const loading: Promise[] = []; if (ngModule) { ngModule.declaredDirectives.forEach((id) => { - const promise = this._loadDirectiveMetadata(id.reference, isSync); + const promise = this._loadDirectiveMetadata(moduleType, id.reference, isSync); if (promise) { loading.push(promise); } diff --git a/packages/compiler/src/ng_module_compiler.ts b/packages/compiler/src/ng_module_compiler.ts index ed5b1f7e2f..beff9b3fe0 100644 --- a/packages/compiler/src/ng_module_compiler.ts +++ b/packages/compiler/src/ng_module_compiler.ts @@ -14,7 +14,7 @@ import {CompilerInjectable} from './injectable'; import {ClassBuilder, createClassStmt} from './output/class_builder'; import * as o from './output/output_ast'; import {convertValueToOutputAst} from './output/value_util'; -import {ParseLocation, ParseSourceFile, ParseSourceSpan} from './parse_util'; +import {ParseLocation, ParseSourceFile, ParseSourceSpan, typeSourceSpan} from './parse_util'; import {NgModuleProviderAnalyzer} from './provider_analyzer'; import {ProviderAst} from './template_parser/template_ast'; @@ -37,14 +37,7 @@ export class NgModuleCompileResult { export class NgModuleCompiler { compile(ngModuleMeta: CompileNgModuleMetadata, extraProviders: CompileProviderMetadata[]): NgModuleCompileResult { - const moduleUrl = identifierModuleUrl(ngModuleMeta.type); - const sourceFileName = moduleUrl != null ? - `in NgModule ${identifierName(ngModuleMeta.type)} in ${moduleUrl}` : - `in NgModule ${identifierName(ngModuleMeta.type)}`; - const sourceFile = new ParseSourceFile('', sourceFileName); - const sourceSpan = new ParseSourceSpan( - new ParseLocation(sourceFile, null, null, null), - new ParseLocation(sourceFile, null, null, null)); + const sourceSpan = typeSourceSpan('NgModule', ngModuleMeta.type); const deps: ComponentFactoryDependency[] = []; const bootstrapComponentFactories: CompileIdentifierMetadata[] = []; const entryComponentFactories = diff --git a/packages/compiler/src/output/abstract_emitter.ts b/packages/compiler/src/output/abstract_emitter.ts index 0d574221ca..a201cd1227 100644 --- a/packages/compiler/src/output/abstract_emitter.ts +++ b/packages/compiler/src/output/abstract_emitter.ts @@ -420,7 +420,12 @@ export abstract class AbstractEmitterVisitor implements o.StatementVisitor, o.Ex ctx.print(ast, `}`, useNewLine); return null; } - + visitCommaExpr(ast: o.CommaExpr, ctx: EmitterVisitorContext): any { + ctx.print(ast, '('); + this.visitAllExpressions(ast.parts, ctx, ','); + ctx.print(ast, ')'); + return null; + } visitAllExpressions( expressions: o.Expression[], ctx: EmitterVisitorContext, separator: string, newLine: boolean = false): void { diff --git a/packages/compiler/src/output/js_emitter.ts b/packages/compiler/src/output/js_emitter.ts index 2b331b2dfc..dac480288e 100644 --- a/packages/compiler/src/output/js_emitter.ts +++ b/packages/compiler/src/output/js_emitter.ts @@ -34,11 +34,12 @@ export class JavaScriptEmitter implements OutputEmitter { srcParts.push(ctx.toSource()); const prefixLines = converter.importsWithPrefixes.size; - const sm = ctx.toSourceMapGenerator(null, prefixLines).toJsComment(); + const sm = ctx.toSourceMapGenerator(genFilePath, prefixLines).toJsComment(); if (sm) { srcParts.push(sm); } - + // always add a newline at the end, as some tools have bugs without it. + srcParts.push(''); return srcParts.join('\n'); } } diff --git a/packages/compiler/src/output/output_ast.ts b/packages/compiler/src/output/output_ast.ts index 95a3108d4c..69929ff74f 100644 --- a/packages/compiler/src/output/output_ast.ts +++ b/packages/compiler/src/output/output_ast.ts @@ -466,6 +466,15 @@ export class LiteralMapExpr extends Expression { } } +export class CommaExpr extends Expression { + constructor(public parts: Expression[], sourceSpan?: ParseSourceSpan) { + super(parts[parts.length - 1].type, sourceSpan); + } + visitExpression(visitor: ExpressionVisitor, context: any): any { + return visitor.visitCommaExpr(this, context); + } +} + export interface ExpressionVisitor { visitReadVarExpr(ast: ReadVarExpr, context: any): any; visitWriteVarExpr(expr: WriteVarExpr, context: any): any; @@ -485,6 +494,7 @@ export interface ExpressionVisitor { visitReadKeyExpr(ast: ReadKeyExpr, context: any): any; visitLiteralArrayExpr(ast: LiteralArrayExpr, context: any): any; visitLiteralMapExpr(ast: LiteralMapExpr, context: any): any; + visitCommaExpr(ast: CommaExpr, context: any): any; } export const THIS_EXPR = new ReadVarExpr(BuiltinVar.This); @@ -653,88 +663,121 @@ export interface StatementVisitor { visitCommentStmt(stmt: CommentStmt, context: any): any; } -export class ExpressionTransformer implements StatementVisitor, ExpressionVisitor { - visitReadVarExpr(ast: ReadVarExpr, context: any): any { return ast; } +export class AstTransformer implements StatementVisitor, ExpressionVisitor { + transformExpr(expr: Expression, context: any): Expression { return expr; } + + transformStmt(stmt: Statement, context: any): Statement { return stmt; } + + visitReadVarExpr(ast: ReadVarExpr, context: any): any { return this.transformExpr(ast, context); } visitWriteVarExpr(expr: WriteVarExpr, context: any): any { - return new WriteVarExpr( - expr.name, expr.value.visitExpression(this, context), expr.type, expr.sourceSpan); + return this.transformExpr( + new WriteVarExpr( + expr.name, expr.value.visitExpression(this, context), expr.type, expr.sourceSpan), + context); } visitWriteKeyExpr(expr: WriteKeyExpr, context: any): any { - return new WriteKeyExpr( - expr.receiver.visitExpression(this, context), expr.index.visitExpression(this, context), - expr.value.visitExpression(this, context), expr.type, expr.sourceSpan); + return this.transformExpr( + new WriteKeyExpr( + expr.receiver.visitExpression(this, context), expr.index.visitExpression(this, context), + expr.value.visitExpression(this, context), expr.type, expr.sourceSpan), + context); } visitWritePropExpr(expr: WritePropExpr, context: any): any { - return new WritePropExpr( - expr.receiver.visitExpression(this, context), expr.name, - expr.value.visitExpression(this, context), expr.type, expr.sourceSpan); + return this.transformExpr( + new WritePropExpr( + expr.receiver.visitExpression(this, context), expr.name, + expr.value.visitExpression(this, context), expr.type, expr.sourceSpan), + context); } visitInvokeMethodExpr(ast: InvokeMethodExpr, context: any): any { const method = ast.builtin || ast.name; - return new InvokeMethodExpr( - ast.receiver.visitExpression(this, context), method, - this.visitAllExpressions(ast.args, context), ast.type, ast.sourceSpan); + return this.transformExpr( + new InvokeMethodExpr( + ast.receiver.visitExpression(this, context), method, + this.visitAllExpressions(ast.args, context), ast.type, ast.sourceSpan), + context); } visitInvokeFunctionExpr(ast: InvokeFunctionExpr, context: any): any { - return new InvokeFunctionExpr( - ast.fn.visitExpression(this, context), this.visitAllExpressions(ast.args, context), - ast.type, ast.sourceSpan); + return this.transformExpr( + new InvokeFunctionExpr( + ast.fn.visitExpression(this, context), this.visitAllExpressions(ast.args, context), + ast.type, ast.sourceSpan), + context); } visitInstantiateExpr(ast: InstantiateExpr, context: any): any { - return new InstantiateExpr( - ast.classExpr.visitExpression(this, context), this.visitAllExpressions(ast.args, context), - ast.type, ast.sourceSpan); + return this.transformExpr( + new InstantiateExpr( + ast.classExpr.visitExpression(this, context), + this.visitAllExpressions(ast.args, context), ast.type, ast.sourceSpan), + context); } - visitLiteralExpr(ast: LiteralExpr, context: any): any { return ast; } + visitLiteralExpr(ast: LiteralExpr, context: any): any { return this.transformExpr(ast, context); } - visitExternalExpr(ast: ExternalExpr, context: any): any { return ast; } + visitExternalExpr(ast: ExternalExpr, context: any): any { + return this.transformExpr(ast, context); + } visitConditionalExpr(ast: ConditionalExpr, context: any): any { - return new ConditionalExpr( - ast.condition.visitExpression(this, context), ast.trueCase.visitExpression(this, context), - ast.falseCase.visitExpression(this, context), ast.type, ast.sourceSpan); + return this.transformExpr( + new ConditionalExpr( + ast.condition.visitExpression(this, context), + ast.trueCase.visitExpression(this, context), + ast.falseCase.visitExpression(this, context), ast.type, ast.sourceSpan), + context); } visitNotExpr(ast: NotExpr, context: any): any { - return new NotExpr(ast.condition.visitExpression(this, context), ast.sourceSpan); + return this.transformExpr( + new NotExpr(ast.condition.visitExpression(this, context), ast.sourceSpan), context); } visitCastExpr(ast: CastExpr, context: any): any { - return new CastExpr(ast.value.visitExpression(this, context), context, ast.sourceSpan); + return this.transformExpr( + new CastExpr(ast.value.visitExpression(this, context), ast.type, ast.sourceSpan), context); } visitFunctionExpr(ast: FunctionExpr, context: any): any { - // Don't descend into nested functions - return ast; + return this.transformExpr( + new FunctionExpr( + ast.params, this.visitAllStatements(ast.statements, context), ast.type, ast.sourceSpan), + context); } visitBinaryOperatorExpr(ast: BinaryOperatorExpr, context: any): any { - return new BinaryOperatorExpr( - ast.operator, ast.lhs.visitExpression(this, context), - ast.rhs.visitExpression(this, context), ast.type, ast.sourceSpan); + return this.transformExpr( + new BinaryOperatorExpr( + ast.operator, ast.lhs.visitExpression(this, context), + ast.rhs.visitExpression(this, context), ast.type, ast.sourceSpan), + context); } visitReadPropExpr(ast: ReadPropExpr, context: any): any { - return new ReadPropExpr( - ast.receiver.visitExpression(this, context), ast.name, ast.type, ast.sourceSpan); + return this.transformExpr( + new ReadPropExpr( + ast.receiver.visitExpression(this, context), ast.name, ast.type, ast.sourceSpan), + context); } visitReadKeyExpr(ast: ReadKeyExpr, context: any): any { - return new ReadKeyExpr( - ast.receiver.visitExpression(this, context), ast.index.visitExpression(this, context), - ast.type, ast.sourceSpan); + return this.transformExpr( + new ReadKeyExpr( + ast.receiver.visitExpression(this, context), ast.index.visitExpression(this, context), + ast.type, ast.sourceSpan), + context); } visitLiteralArrayExpr(ast: LiteralArrayExpr, context: any): any { - return new LiteralArrayExpr( - this.visitAllExpressions(ast.entries, context), ast.type, ast.sourceSpan); + return this.transformExpr( + new LiteralArrayExpr( + this.visitAllExpressions(ast.entries, context), ast.type, ast.sourceSpan), + context); } visitLiteralMapExpr(ast: LiteralMapExpr, context: any): any { @@ -742,53 +785,88 @@ export class ExpressionTransformer implements StatementVisitor, ExpressionVisito (entry): LiteralMapEntry => new LiteralMapEntry( entry.key, entry.value.visitExpression(this, context), entry.quoted, )); const mapType = new MapType(ast.valueType); - return new LiteralMapExpr(entries, mapType, ast.sourceSpan); + return this.transformExpr(new LiteralMapExpr(entries, mapType, ast.sourceSpan), context); + } + visitCommaExpr(ast: CommaExpr, context: any): any { + return this.transformExpr( + new CommaExpr(this.visitAllExpressions(ast.parts, context), ast.sourceSpan), context); } visitAllExpressions(exprs: Expression[], context: any): Expression[] { return exprs.map(expr => expr.visitExpression(this, context)); } visitDeclareVarStmt(stmt: DeclareVarStmt, context: any): any { - return new DeclareVarStmt( - stmt.name, stmt.value.visitExpression(this, context), stmt.type, stmt.modifiers, - stmt.sourceSpan); + return this.transformStmt( + new DeclareVarStmt( + stmt.name, stmt.value.visitExpression(this, context), stmt.type, stmt.modifiers, + stmt.sourceSpan), + context); } visitDeclareFunctionStmt(stmt: DeclareFunctionStmt, context: any): any { - // Don't descend into nested functions - return stmt; + return this.transformStmt( + new DeclareFunctionStmt( + stmt.name, stmt.params, this.visitAllStatements(stmt.statements, context), stmt.type, + stmt.modifiers, stmt.sourceSpan), + context); } visitExpressionStmt(stmt: ExpressionStatement, context: any): any { - return new ExpressionStatement(stmt.expr.visitExpression(this, context), stmt.sourceSpan); + return this.transformStmt( + new ExpressionStatement(stmt.expr.visitExpression(this, context), stmt.sourceSpan), + context); } visitReturnStmt(stmt: ReturnStatement, context: any): any { - return new ReturnStatement(stmt.value.visitExpression(this, context), stmt.sourceSpan); + return this.transformStmt( + new ReturnStatement(stmt.value.visitExpression(this, context), stmt.sourceSpan), context); } visitDeclareClassStmt(stmt: ClassStmt, context: any): any { - // Don't descend into nested functions - return stmt; + const parent = stmt.parent.visitExpression(this, context); + const getters = stmt.getters.map( + getter => new ClassGetter( + getter.name, this.visitAllStatements(getter.body, context), getter.type, + getter.modifiers)); + const ctorMethod = stmt.constructorMethod && + new ClassMethod(stmt.constructorMethod.name, stmt.constructorMethod.params, + this.visitAllStatements(stmt.constructorMethod.body, context), + stmt.constructorMethod.type, stmt.constructorMethod.modifiers); + const methods = stmt.methods.map( + method => new ClassMethod( + method.name, method.params, this.visitAllStatements(method.body, context), method.type, + method.modifiers)); + return this.transformStmt( + new ClassStmt( + stmt.name, parent, stmt.fields, getters, ctorMethod, methods, stmt.modifiers, + stmt.sourceSpan), + context); } visitIfStmt(stmt: IfStmt, context: any): any { - return new IfStmt( - stmt.condition.visitExpression(this, context), - this.visitAllStatements(stmt.trueCase, context), - this.visitAllStatements(stmt.falseCase, context), stmt.sourceSpan); + return this.transformStmt( + new IfStmt( + stmt.condition.visitExpression(this, context), + this.visitAllStatements(stmt.trueCase, context), + this.visitAllStatements(stmt.falseCase, context), stmt.sourceSpan), + context); } visitTryCatchStmt(stmt: TryCatchStmt, context: any): any { - return new TryCatchStmt( - this.visitAllStatements(stmt.bodyStmts, context), - this.visitAllStatements(stmt.catchStmts, context), stmt.sourceSpan); + return this.transformStmt( + new TryCatchStmt( + this.visitAllStatements(stmt.bodyStmts, context), + this.visitAllStatements(stmt.catchStmts, context), stmt.sourceSpan), + context); } visitThrowStmt(stmt: ThrowStmt, context: any): any { - return new ThrowStmt(stmt.error.visitExpression(this, context), stmt.sourceSpan); + return this.transformStmt( + new ThrowStmt(stmt.error.visitExpression(this, context), stmt.sourceSpan), context); } - visitCommentStmt(stmt: CommentStmt, context: any): any { return stmt; } + visitCommentStmt(stmt: CommentStmt, context: any): any { + return this.transformStmt(stmt, context); + } visitAllStatements(stmts: Statement[], context: any): Statement[] { return stmts.map(stmt => stmt.visitStatement(this, context)); @@ -796,7 +874,7 @@ export class ExpressionTransformer implements StatementVisitor, ExpressionVisito } -export class RecursiveExpressionVisitor implements StatementVisitor, ExpressionVisitor { +export class RecursiveAstVisitor implements StatementVisitor, ExpressionVisitor { visitReadVarExpr(ast: ReadVarExpr, context: any): any { return ast; } visitWriteVarExpr(expr: WriteVarExpr, context: any): any { expr.value.visitExpression(this, context); @@ -844,7 +922,10 @@ export class RecursiveExpressionVisitor implements StatementVisitor, ExpressionV ast.value.visitExpression(this, context); return ast; } - visitFunctionExpr(ast: FunctionExpr, context: any): any { return ast; } + visitFunctionExpr(ast: FunctionExpr, context: any): any { + this.visitAllStatements(ast.statements, context); + return ast; + } visitBinaryOperatorExpr(ast: BinaryOperatorExpr, context: any): any { ast.lhs.visitExpression(this, context); ast.rhs.visitExpression(this, context); @@ -867,6 +948,9 @@ export class RecursiveExpressionVisitor implements StatementVisitor, ExpressionV ast.entries.forEach((entry) => entry.value.visitExpression(this, context)); return ast; } + visitCommaExpr(ast: CommaExpr, context: any): any { + this.visitAllExpressions(ast.parts, context); + } visitAllExpressions(exprs: Expression[], context: any): void { exprs.forEach(expr => expr.visitExpression(this, context)); } @@ -876,7 +960,7 @@ export class RecursiveExpressionVisitor implements StatementVisitor, ExpressionV return stmt; } visitDeclareFunctionStmt(stmt: DeclareFunctionStmt, context: any): any { - // Don't descend into nested functions + this.visitAllStatements(stmt.statements, context); return stmt; } visitExpressionStmt(stmt: ExpressionStatement, context: any): any { @@ -888,7 +972,12 @@ export class RecursiveExpressionVisitor implements StatementVisitor, ExpressionV return stmt; } visitDeclareClassStmt(stmt: ClassStmt, context: any): any { - // Don't descend into nested functions + stmt.parent.visitExpression(this, context); + stmt.getters.forEach(getter => this.visitAllStatements(getter.body, context)); + if (stmt.constructorMethod) { + this.visitAllStatements(stmt.constructorMethod.body, context); + } + stmt.methods.forEach(method => this.visitAllStatements(method.body, context)); return stmt; } visitIfStmt(stmt: IfStmt, context: any): any { @@ -912,30 +1001,48 @@ export class RecursiveExpressionVisitor implements StatementVisitor, ExpressionV } } -export function replaceVarInExpression( - varName: string, newValue: Expression, expression: Expression): Expression { - const transformer = new _ReplaceVariableTransformer(varName, newValue); - return expression.visitExpression(transformer, null); -} - -class _ReplaceVariableTransformer extends ExpressionTransformer { - constructor(private _varName: string, private _newValue: Expression) { super(); } - visitReadVarExpr(ast: ReadVarExpr, context: any): any { - return ast.name == this._varName ? this._newValue : ast; +export function applySourceSpanToStatementIfNeeded( + stmt: Statement, sourceSpan: ParseSourceSpan): Statement { + if (!sourceSpan) { + return stmt; } + const transformer = new _ApplySourceSpanTransformer(sourceSpan); + return stmt.visitStatement(transformer, null); } -export function findReadVarNames(stmts: Statement[]): Set { - const finder = new _VariableFinder(); - finder.visitAllStatements(stmts, null); - return finder.varNames; +export function applySourceSpanToExpressionIfNeeded( + expr: Expression, sourceSpan: ParseSourceSpan): Expression { + if (!sourceSpan) { + return expr; + } + const transformer = new _ApplySourceSpanTransformer(sourceSpan); + return expr.visitExpression(transformer, null); } -class _VariableFinder extends RecursiveExpressionVisitor { - varNames = new Set(); - visitReadVarExpr(ast: ReadVarExpr, context: any): any { - this.varNames.add(ast.name); - return null; +class _ApplySourceSpanTransformer extends AstTransformer { + constructor(private sourceSpan: ParseSourceSpan) { super(); } + private _clone(obj: any): any { + const clone = Object.create(obj.constructor.prototype); + for (let prop in obj) { + clone[prop] = obj[prop]; + } + return clone; + } + + transformExpr(expr: Expression, context: any): Expression { + if (!expr.sourceSpan) { + expr = this._clone(expr); + expr.sourceSpan = this.sourceSpan; + } + return expr; + } + + transformStmt(stmt: Statement, context: any): Statement { + if (!stmt.sourceSpan) { + stmt = this._clone(stmt); + stmt.sourceSpan = this.sourceSpan; + } + return stmt; } } diff --git a/packages/compiler/src/output/output_interpreter.ts b/packages/compiler/src/output/output_interpreter.ts index a30d4b0515..20099a23e6 100644 --- a/packages/compiler/src/output/output_interpreter.ts +++ b/packages/compiler/src/output/output_interpreter.ts @@ -304,7 +304,10 @@ class StatementInterpreter implements o.StatementVisitor, o.ExpressionVisitor { (entry) => (result as any)[entry.key] = entry.value.visitExpression(this, ctx)); return result; } - + visitCommaExpr(ast: o.CommaExpr, context: any): any { + const values = this.visitAllExpressions(ast.parts, context); + return values[values.length - 1]; + } visitAllExpressions(expressions: o.Expression[], ctx: _ExecutionContext): any { return expressions.map((expr) => expr.visitExpression(this, ctx)); } diff --git a/packages/compiler/src/output/output_jit.ts b/packages/compiler/src/output/output_jit.ts index cb22b44ec5..7ba5d1cd2f 100644 --- a/packages/compiler/src/output/output_jit.ts +++ b/packages/compiler/src/output/output_jit.ts @@ -6,6 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ +import {isDevMode} from '@angular/core'; import {identifierName} from '../compile_metadata'; import {EmitterVisitorContext} from './abstract_emitter'; @@ -14,14 +15,23 @@ import * as o from './output_ast'; function evalExpression( sourceUrl: string, ctx: EmitterVisitorContext, vars: {[key: string]: any}): any { - const fnBody = - `${ctx.toSource()}\n//# sourceURL=${sourceUrl}\n${ctx.toSourceMapGenerator().toJsComment()}`; + let fnBody = `${ctx.toSource()}\n//# sourceURL=${sourceUrl}`; const fnArgNames: string[] = []; const fnArgValues: any[] = []; for (const argName in vars) { fnArgNames.push(argName); fnArgValues.push(vars[argName]); } + if (isDevMode()) { + // using `new Function(...)` generates a header, 1 line of no arguments, 2 lines otherwise + // E.g. ``` + // function anonymous(a,b,c + // /**/) { ... }``` + // We don't want to hard code this fact, so we auto detect it via an empty function first. + const emptyFn = new Function(...fnArgNames.concat('return null;')).toString(); + const headerLines = emptyFn.slice(0, emptyFn.indexOf('return null;')).split('\n').length - 1; + fnBody += `\n${ctx.toSourceMapGenerator(sourceUrl, headerLines).toJsComment()}`; + } return new Function(...fnArgNames.concat(fnBody))(...fnArgValues); } diff --git a/packages/compiler/src/output/source_map.ts b/packages/compiler/src/output/source_map.ts index 5a9139bdd8..20892ea58a 100644 --- a/packages/compiler/src/output/source_map.ts +++ b/packages/compiler/src/output/source_map.ts @@ -6,6 +6,8 @@ * found in the LICENSE file at https://angular.io/license */ +import {utf8Encode} from '../i18n/digest'; + // https://docs.google.com/document/d/1U1RGAehQwRypUTovF1KRlpiOFze0b-_2gc6fAH0KY0k/edit const VERSION = 3; @@ -143,7 +145,7 @@ export class SourceMapGenerator { export function toBase64String(value: string): string { let b64 = ''; - + value = utf8Encode(value); for (let i = 0; i < value.length;) { const i1 = value.charCodeAt(i++); const i2 = value.charCodeAt(i++); diff --git a/packages/compiler/src/output/ts_emitter.ts b/packages/compiler/src/output/ts_emitter.ts index 2806e076d4..bd22cebdbd 100644 --- a/packages/compiler/src/output/ts_emitter.ts +++ b/packages/compiler/src/output/ts_emitter.ts @@ -70,10 +70,12 @@ export class TypeScriptEmitter implements OutputEmitter { srcParts.push(ctx.toSource()); const prefixLines = converter.reexports.size + converter.importsWithPrefixes.size; - const sm = ctx.toSourceMapGenerator(null, prefixLines).toJsComment(); + const sm = ctx.toSourceMapGenerator(genFilePath, prefixLines).toJsComment(); if (sm) { srcParts.push(sm); } + // always add a newline at the end, as some tools have bugs without it. + srcParts.push(''); return srcParts.join('\n'); } diff --git a/packages/compiler/src/parse_util.ts b/packages/compiler/src/parse_util.ts index 1f2eb2b8a3..aa16fd460e 100644 --- a/packages/compiler/src/parse_util.ts +++ b/packages/compiler/src/parse_util.ts @@ -6,6 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ import * as chars from './chars'; +import {CompileIdentifierMetadata, identifierModuleUrl, identifierName} from './compile_metadata'; export class ParseLocation { constructor( @@ -124,3 +125,13 @@ export class ParseError { return `${this.msg}${contextStr}: ${this.span.start}${details}`; } } + +export function typeSourceSpan(kind: string, type: CompileIdentifierMetadata): ParseSourceSpan { + const moduleUrl = identifierModuleUrl(type); + const sourceFileName = moduleUrl != null ? `in ${kind} ${identifierName(type)} in ${moduleUrl}` : + `in ${kind} ${identifierName(type)}`; + const sourceFile = new ParseSourceFile('', sourceFileName); + return new ParseSourceSpan( + new ParseLocation(sourceFile, null, null, null), + new ParseLocation(sourceFile, null, null, null)); +} diff --git a/packages/compiler/src/provider_analyzer.ts b/packages/compiler/src/provider_analyzer.ts index f1772c821a..133a263e70 100644 --- a/packages/compiler/src/provider_analyzer.ts +++ b/packages/compiler/src/provider_analyzer.ts @@ -32,7 +32,7 @@ export class ProviderViewContext { viewProviders: Map; errors: ProviderError[] = []; - constructor(public component: CompileDirectiveMetadata, public sourceSpan: ParseSourceSpan) { + constructor(public component: CompileDirectiveMetadata) { this.viewQueries = _getViewQueries(component); this.viewProviders = new Map(); component.viewProviders.forEach((provider) => { diff --git a/packages/compiler/src/template_parser/template_parser.ts b/packages/compiler/src/template_parser/template_parser.ts index d4712d4187..79d4700cf7 100644 --- a/packages/compiler/src/template_parser/template_parser.ts +++ b/packages/compiler/src/template_parser/template_parser.ts @@ -132,8 +132,7 @@ export class TemplateParser { if (htmlAstWithErrors.rootNodes.length > 0) { const uniqDirectives = removeSummaryDuplicates(directives); const uniqPipes = removeSummaryDuplicates(pipes); - const providerViewContext = - new ProviderViewContext(component, htmlAstWithErrors.rootNodes[0].sourceSpan); + const providerViewContext = new ProviderViewContext(component); let interpolationConfig: InterpolationConfig; if (component.template && component.template.interpolation) { interpolationConfig = { diff --git a/packages/compiler/src/view_compiler/view_compiler.ts b/packages/compiler/src/view_compiler/view_compiler.ts index 8f1ee5c775..5b5987a9ba 100644 --- a/packages/compiler/src/view_compiler/view_compiler.ts +++ b/packages/compiler/src/view_compiler/view_compiler.ts @@ -16,6 +16,7 @@ import {Identifiers, createIdentifier, createIdentifierToken, resolveIdentifier} import {CompilerInjectable} from '../injectable'; import * as o from '../output/output_ast'; import {convertValueToOutputAst} from '../output/value_util'; +import {ParseSourceSpan} from '../parse_util'; import {ElementSchemaRegistry} from '../schema/element_schema_registry'; import {AttrAst, BoundDirectivePropertyAst, BoundElementPropertyAst, BoundEventAst, BoundTextAst, DirectiveAst, ElementAst, EmbeddedTemplateAst, NgContentAst, PropertyBindingType, ProviderAst, ProviderAstType, QueryMatch, ReferenceAst, TemplateAst, TemplateAstVisitor, TextAst, VariableAst, templateVisitAll} from '../template_parser/template_ast'; @@ -82,10 +83,12 @@ interface ViewBuilderFactory { } interface UpdateExpression { - nodeIndex: number; - expressions: {context: o.Expression, value: AST}[]; + context: o.Expression; + sourceSpan: ParseSourceSpan; + value: AST; } +const LOG_VAR = o.variable('log'); const VIEW_VAR = o.variable('view'); const CHECK_VAR = o.variable('check'); const COMP_VAR = o.variable('comp'); @@ -93,16 +96,19 @@ const NODE_INDEX_VAR = o.variable('nodeIndex'); const EVENT_NAME_VAR = o.variable('eventName'); const ALLOW_DEFAULT_VAR = o.variable(`allowDefault`); -class ViewBuilder implements TemplateAstVisitor, LocalResolver, BuiltinConverterFactory { +class ViewBuilder implements TemplateAstVisitor, LocalResolver { private compType: o.Type; - private nodeDefs: (() => o.Expression)[] = []; + private nodes: (() => { + sourceSpan: ParseSourceSpan, + nodeDef: o.Expression, + updateDirectives?: UpdateExpression[], + updateRenderer?: UpdateExpression[] + })[] = []; private purePipeNodeIndices: {[pipeName: string]: number} = Object.create(null); // Need Object.create so that we don't have builtin values... private refNodeIndices: {[refName: string]: number} = Object.create(null); private variables: VariableAst[] = []; private children: ViewBuilder[] = []; - private updateDirectivesExpressions: UpdateExpression[] = []; - private updateRendererExpressions: UpdateExpression[] = []; constructor( private parent: ViewBuilder, private component: CompileDirectiveMetadata, @@ -125,7 +131,7 @@ class ViewBuilder implements TemplateAstVisitor, LocalResolver, BuiltinConverter if (!this.parent) { this.usedPipes.forEach((pipe) => { if (pipe.pure) { - this.purePipeNodeIndices[pipe.name] = this._createPipe(pipe); + this.purePipeNodeIndices[pipe.name] = this._createPipe(null, pipe); } }); } @@ -142,10 +148,14 @@ class ViewBuilder implements TemplateAstVisitor, LocalResolver, BuiltinConverter } else { flags |= NodeFlags.DynamicQuery; } - this.nodeDefs.push(() => o.importExpr(createIdentifier(Identifiers.queryDef)).callFn([ - o.literal(flags), o.literal(queryId), - new o.LiteralMapExpr([new o.LiteralMapEntry(query.propertyName, o.literal(bindingType))]) - ])); + this.nodes.push(() => ({ + sourceSpan: null, + nodeDef: o.importExpr(createIdentifier(Identifiers.queryDef)).callFn([ + o.literal(flags), o.literal(queryId), + new o.LiteralMapExpr( + [new o.LiteralMapEntry(query.propertyName, o.literal(bindingType))]) + ]) + })); }); } templateVisitAll(this, astNodes); @@ -153,17 +163,23 @@ class ViewBuilder implements TemplateAstVisitor, LocalResolver, BuiltinConverter (this.parent && needsAdditionalRootNode(astNodes[astNodes.length - 1]))) { // if the view is empty, or an embedded view has a view container as last root nde, // create an additional root node. - this.nodeDefs.push(() => o.importExpr(createIdentifier(Identifiers.anchorDef)).callFn([ - o.literal(NodeFlags.None), o.NULL_EXPR, o.NULL_EXPR, o.literal(0) - ])); + this.nodes.push(() => ({ + sourceSpan: null, + nodeDef: o.importExpr(createIdentifier(Identifiers.anchorDef)).callFn([ + o.literal(NodeFlags.None), o.NULL_EXPR, o.NULL_EXPR, o.literal(0) + ]) + })); } } build(targetStatements: o.Statement[] = []): o.Statement[] { this.children.forEach((child) => child.build(targetStatements)); - const updateDirectivesFn = this._createUpdateFn(this.updateDirectivesExpressions); - const updateRendererFn = this._createUpdateFn(this.updateRendererExpressions); + const {updateRendererStmts, updateDirectivesStmts, nodeDefExprs} = + this._createNodeExpressions(); + + const updateRendererFn = this._createUpdateFn(updateRendererStmts); + const updateDirectivesFn = this._createUpdateFn(updateDirectivesStmts); let viewFlags = ViewFlags.None; @@ -171,10 +187,10 @@ class ViewBuilder implements TemplateAstVisitor, LocalResolver, BuiltinConverter viewFlags |= ViewFlags.OnPush; } const viewFactory = new o.DeclareFunctionStmt( - this.viewName, [], + this.viewName, [new o.FnParam(LOG_VAR.name)], [new o.ReturnStatement(o.importExpr(createIdentifier(Identifiers.viewDef)).callFn([ o.literal(viewFlags), - o.literalArr(this.nodeDefs.map(nd => nd())), + o.literalArr(nodeDefExprs), updateDirectivesFn, updateRendererFn, ]))], @@ -184,20 +200,7 @@ class ViewBuilder implements TemplateAstVisitor, LocalResolver, BuiltinConverter return targetStatements; } - private _createUpdateFn(expressions: UpdateExpression[]): o.Expression { - const updateStmts: o.Statement[] = []; - let updateBindingCount = 0; - expressions.forEach(({expressions, nodeIndex}) => { - const exprs = expressions.map(({context, value}) => { - const bindingId = `${updateBindingCount++}`; - const nameResolver = context === COMP_VAR ? this : null; - const {stmts, currValExpr} = - convertPropertyBinding(nameResolver, context, value, bindingId); - updateStmts.push(...stmts); - return currValExpr; - }); - updateStmts.push(callCheckStmt(nodeIndex, exprs).toStmt()); - }); + private _createUpdateFn(updateStmts: o.Statement[]): o.Expression { let updateFn: o.Expression; if (updateStmts.length > 0) { const preStmts: o.Statement[] = []; @@ -218,40 +221,50 @@ class ViewBuilder implements TemplateAstVisitor, LocalResolver, BuiltinConverter visitNgContent(ast: NgContentAst, context: any): any { // ngContentDef(ngContentIndex: number, index: number): NodeDef; - this.nodeDefs.push(() => o.importExpr(createIdentifier(Identifiers.ngContentDef)).callFn([ - o.literal(ast.ngContentIndex), o.literal(ast.index) - ])); + this.nodes.push(() => ({ + sourceSpan: ast.sourceSpan, + nodeDef: o.importExpr(createIdentifier(Identifiers.ngContentDef)).callFn([ + o.literal(ast.ngContentIndex), o.literal(ast.index) + ]) + })); } visitText(ast: TextAst, context: any): any { // textDef(ngContentIndex: number, constants: string[]): NodeDef; - this.nodeDefs.push(() => o.importExpr(createIdentifier(Identifiers.textDef)).callFn([ - o.literal(ast.ngContentIndex), o.literalArr([o.literal(ast.value)]) - ])); + this.nodes.push(() => ({ + sourceSpan: ast.sourceSpan, + nodeDef: o.importExpr(createIdentifier(Identifiers.textDef)).callFn([ + o.literal(ast.ngContentIndex), o.literalArr([o.literal(ast.value)]) + ]) + })); } visitBoundText(ast: BoundTextAst, context: any): any { - const nodeIndex = this.nodeDefs.length; + const nodeIndex = this.nodes.length; // reserve the space in the nodeDefs array - this.nodeDefs.push(null); + this.nodes.push(null); const astWithSource = ast.value; const inter = astWithSource.ast; - this._addUpdateExpressions( - nodeIndex, inter.expressions.map((expr) => { return {context: COMP_VAR, value: expr}; }), - this.updateRendererExpressions); + const updateRendererExpressions = inter.expressions.map( + (expr) => this._preprocessUpdateExpression( + {sourceSpan: ast.sourceSpan, context: COMP_VAR, value: expr})); // textDef(ngContentIndex: number, constants: string[]): NodeDef; - this.nodeDefs[nodeIndex] = () => o.importExpr(createIdentifier(Identifiers.textDef)).callFn([ - o.literal(ast.ngContentIndex), o.literalArr(inter.strings.map(s => o.literal(s))) - ]); + this.nodes[nodeIndex] = () => ({ + sourceSpan: ast.sourceSpan, + nodeDef: o.importExpr(createIdentifier(Identifiers.textDef)).callFn([ + o.literal(ast.ngContentIndex), o.literalArr(inter.strings.map(s => o.literal(s))) + ]), + updateRenderer: updateRendererExpressions + }); } visitEmbeddedTemplate(ast: EmbeddedTemplateAst, context: any): any { - const nodeIndex = this.nodeDefs.length; + const nodeIndex = this.nodes.length; // reserve the space in the nodeDefs array - this.nodeDefs.push(null); + this.nodes.push(null); const {flags, queryMatchesExpr, hostEvents} = this._visitElementOrTemplate(nodeIndex, ast); @@ -259,27 +272,29 @@ class ViewBuilder implements TemplateAstVisitor, LocalResolver, BuiltinConverter this.children.push(childVisitor); childVisitor.visitAll(ast.variables, ast.children); - const childCount = this.nodeDefs.length - nodeIndex - 1; + const childCount = this.nodes.length - nodeIndex - 1; // anchorDef( // flags: NodeFlags, matchedQueries: [string, QueryValueType][], ngContentIndex: number, // childCount: number, handleEventFn?: ElementHandleEventFn, templateFactory?: // ViewDefinitionFactory): NodeDef; - const nodeDef = () => o.importExpr(createIdentifier(Identifiers.anchorDef)).callFn([ - o.literal(flags), - queryMatchesExpr, - o.literal(ast.ngContentIndex), - o.literal(childCount), - this._createElementHandleEventFn(nodeIndex, hostEvents), - o.variable(childVisitor.viewName), - ]); - this.nodeDefs[nodeIndex] = nodeDef; + this.nodes[nodeIndex] = () => ({ + sourceSpan: ast.sourceSpan, + nodeDef: o.importExpr(createIdentifier(Identifiers.anchorDef)).callFn([ + o.literal(flags), + queryMatchesExpr, + o.literal(ast.ngContentIndex), + o.literal(childCount), + this._createElementHandleEventFn(nodeIndex, hostEvents), + o.variable(childVisitor.viewName), + ]) + }); } visitElement(ast: ElementAst, context: any): any { - const nodeIndex = this.nodeDefs.length; + const nodeIndex = this.nodes.length; // reserve the space in the nodeDefs array so we can add children - this.nodeDefs.push(null); + this.nodes.push(null); let elName = ast.name; if (ast.name === NG_CONTAINER_TAG) { @@ -291,18 +306,25 @@ class ViewBuilder implements TemplateAstVisitor, LocalResolver, BuiltinConverter this._visitElementOrTemplate(nodeIndex, ast); let inputDefs: o.Expression[] = []; + let updateRendererExpressions: UpdateExpression[] = []; let outputDefs: o.Expression[] = []; if (elName) { const hostBindings = ast.inputs .map((inputAst) => ({ context: COMP_VAR as o.Expression, - value: inputAst.value, - bindingDef: elementBindingDef(inputAst, null), + inputAst, + dirAst: null, })) .concat(dirHostBindings); if (hostBindings.length) { - this._addUpdateExpressions(nodeIndex, hostBindings, this.updateRendererExpressions); - inputDefs = hostBindings.map(entry => entry.bindingDef); + updateRendererExpressions = + hostBindings.map((hostBinding) => this._preprocessUpdateExpression({ + context: hostBinding.context, + sourceSpan: hostBinding.inputAst.sourceSpan, + value: hostBinding.inputAst.value + })); + inputDefs = hostBindings.map( + hostBinding => elementBindingDef(hostBinding.inputAst, hostBinding.dirAst)); } outputDefs = usedEvents.map( ([target, eventName]) => o.literalArr([o.literal(target), o.literal(eventName)])); @@ -310,7 +332,7 @@ class ViewBuilder implements TemplateAstVisitor, LocalResolver, BuiltinConverter templateVisitAll(this, ast.children); - const childCount = this.nodeDefs.length - nodeIndex - 1; + const childCount = this.nodes.length - nodeIndex - 1; const compAst = ast.directives.find(dirAst => dirAst.directive.isComponent); let compRendererType = o.NULL_EXPR; @@ -331,15 +353,23 @@ class ViewBuilder implements TemplateAstVisitor, LocalResolver, BuiltinConverter // outputs?: ([OutputType.ElementOutput | OutputType.DirectiveHostOutput, string, string])[], // handleEvent?: ElementHandleEventFn, // componentView?: () => ViewDefinition, componentRendererType?: RendererType2): NodeDef; - const nodeDef = () => o.importExpr(createIdentifier(Identifiers.elementDef)).callFn([ - o.literal(flags), queryMatchesExpr, o.literal(ast.ngContentIndex), o.literal(childCount), - o.literal(elName), elName ? fixedAttrsDef(ast) : o.NULL_EXPR, - inputDefs.length ? o.literalArr(inputDefs) : o.NULL_EXPR, - outputDefs.length ? o.literalArr(outputDefs) : o.NULL_EXPR, - this._createElementHandleEventFn(nodeIndex, hostEvents), compView, compRendererType - ]); - - this.nodeDefs[nodeIndex] = nodeDef; + this.nodes[nodeIndex] = () => ({ + sourceSpan: ast.sourceSpan, + nodeDef: o.importExpr(createIdentifier(Identifiers.elementDef)).callFn([ + o.literal(flags), + queryMatchesExpr, + o.literal(ast.ngContentIndex), + o.literal(childCount), + o.literal(elName), + elName ? fixedAttrsDef(ast) : o.NULL_EXPR, + inputDefs.length ? o.literalArr(inputDefs) : o.NULL_EXPR, + outputDefs.length ? o.literalArr(outputDefs) : o.NULL_EXPR, + this._createElementHandleEventFn(nodeIndex, hostEvents), + compView, + compRendererType, + ]), + updateRenderer: updateRendererExpressions + }); } private _visitElementOrTemplate(nodeIndex: number, ast: { @@ -353,7 +383,8 @@ class ViewBuilder implements TemplateAstVisitor, LocalResolver, BuiltinConverter flags: NodeFlags, usedEvents: [string, string][], queryMatchesExpr: o.Expression, - hostBindings: {value: AST, context: o.Expression, bindingDef: o.Expression}[], + hostBindings: + {context: o.Expression, inputAst: BoundElementPropertyAst, dirAst: DirectiveAst}[], hostEvents: {context: o.Expression, eventAst: BoundEventAst, dirAst: DirectiveAst}[], } { let flags = NodeFlags.None; @@ -371,7 +402,8 @@ class ViewBuilder implements TemplateAstVisitor, LocalResolver, BuiltinConverter usedEvents.set(elementEventFullName(target, name), [target, name]); }); }); - const hostBindings: {value: AST, context: o.Expression, bindingDef: o.Expression}[] = []; + const hostBindings: + {context: o.Expression, inputAst: BoundElementPropertyAst, dirAst: DirectiveAst}[] = []; const hostEvents: {context: o.Expression, eventAst: BoundEventAst, dirAst: DirectiveAst}[] = []; const componentFactoryResolverProvider = createComponentFactoryResolver(ast.directives); if (componentFactoryResolverProvider) { @@ -441,12 +473,13 @@ class ViewBuilder implements TemplateAstVisitor, LocalResolver, BuiltinConverter providerAst: ProviderAst, dirAst: DirectiveAst, directiveIndex: number, elementNodeIndex: number, refs: ReferenceAst[], queryMatches: QueryMatch[], usedEvents: Map, queryIds: StaticAndDynamicQueryIds): { - hostBindings: {value: AST, context: o.Expression, bindingDef: o.Expression}[], + hostBindings: + {context: o.Expression, inputAst: BoundElementPropertyAst, dirAst: DirectiveAst}[], hostEvents: {context: o.Expression, eventAst: BoundEventAst, dirAst: DirectiveAst}[] } { - const nodeIndex = this.nodeDefs.length; + const nodeIndex = this.nodes.length; // reserve the space in the nodeDefs array so we can add children - this.nodeDefs.push(null); + this.nodes.push(null); dirAst.directive.queries.forEach((query, queryIndex) => { let flags = NodeFlags.TypeContentQuery; @@ -459,17 +492,21 @@ class ViewBuilder implements TemplateAstVisitor, LocalResolver, BuiltinConverter flags |= NodeFlags.DynamicQuery; } const bindingType = query.first ? QueryBindingType.First : QueryBindingType.All; - this.nodeDefs.push(() => o.importExpr(createIdentifier(Identifiers.queryDef)).callFn([ - o.literal(flags), o.literal(queryId), - new o.LiteralMapExpr([new o.LiteralMapEntry(query.propertyName, o.literal(bindingType))]) - ])); + this.nodes.push(() => ({ + sourceSpan: dirAst.sourceSpan, + nodeDef: o.importExpr(createIdentifier(Identifiers.queryDef)).callFn([ + o.literal(flags), o.literal(queryId), + new o.LiteralMapExpr( + [new o.LiteralMapEntry(query.propertyName, o.literal(bindingType))]) + ]), + })); }); // Note: the operation below might also create new nodeDefs, // but we don't want them to be a child of a directive, // as they might be a provider/pipe on their own. // I.e. we only allow queries as children of directives nodes. - const childCount = this.nodeDefs.length - nodeIndex - 1; + const childCount = this.nodes.length - nodeIndex - 1; let {flags, queryMatchExprs, providerExpr, depsExpr} = this._visitProviderOrDirective(providerAst, queryMatches); @@ -501,22 +538,21 @@ class ViewBuilder implements TemplateAstVisitor, LocalResolver, BuiltinConverter outputDefs.push(new o.LiteralMapEntry(propName, o.literal(eventName), false)); } }); + let updateDirectiveExpressions: UpdateExpression[] = []; if (dirAst.inputs.length || (flags & (NodeFlags.DoCheck | NodeFlags.OnInit)) > 0) { - this._addUpdateExpressions( - nodeIndex, - dirAst.inputs.map((input) => { return {context: COMP_VAR, value: input.value}; }), - this.updateDirectivesExpressions); + updateDirectiveExpressions = dirAst.inputs.map( + (input) => this._preprocessUpdateExpression( + {sourceSpan: input.sourceSpan, context: COMP_VAR, value: input.value})); } const dirContextExpr = o.importExpr(createIdentifier(Identifiers.nodeValue)).callFn([ VIEW_VAR, o.literal(nodeIndex) ]); - const hostBindings = - dirAst.hostProperties.map((hostBindingAst) => ({ - value: (hostBindingAst.value).ast, - context: dirContextExpr, - bindingDef: elementBindingDef(hostBindingAst, dirAst), - })); + const hostBindings = dirAst.hostProperties.map((inputAst) => ({ + context: dirContextExpr, + dirAst, + inputAst, + })); const hostEvents = dirAst.hostEvents.map((hostEventAst) => ({ context: dirContextExpr, eventAst: hostEventAst, dirAst, @@ -528,21 +564,24 @@ class ViewBuilder implements TemplateAstVisitor, LocalResolver, BuiltinConverter // any, // deps: ([DepFlags, any] | any)[], props?: {[name: string]: [number, string]}, // outputs?: {[name: string]: string}, component?: () => ViewDefinition): NodeDef; - const nodeDef = () => o.importExpr(createIdentifier(Identifiers.directiveDef)).callFn([ - o.literal(flags), queryMatchExprs.length ? o.literalArr(queryMatchExprs) : o.NULL_EXPR, - o.literal(childCount), providerExpr, depsExpr, - inputDefs.length ? new o.LiteralMapExpr(inputDefs) : o.NULL_EXPR, - outputDefs.length ? new o.LiteralMapExpr(outputDefs) : o.NULL_EXPR - ]); - this.nodeDefs[nodeIndex] = nodeDef; + this.nodes[nodeIndex] = () => ({ + sourceSpan: dirAst.sourceSpan, + nodeDef: o.importExpr(createIdentifier(Identifiers.directiveDef)).callFn([ + o.literal(flags), queryMatchExprs.length ? o.literalArr(queryMatchExprs) : o.NULL_EXPR, + o.literal(childCount), providerExpr, depsExpr, + inputDefs.length ? new o.LiteralMapExpr(inputDefs) : o.NULL_EXPR, + outputDefs.length ? new o.LiteralMapExpr(outputDefs) : o.NULL_EXPR + ]), + updateDirectives: updateDirectiveExpressions, + }); return {hostBindings, hostEvents}; } private _visitProvider(providerAst: ProviderAst, queryMatches: QueryMatch[]): void { - const nodeIndex = this.nodeDefs.length; + const nodeIndex = this.nodes.length; // reserve the space in the nodeDefs array so we can add children - this.nodeDefs.push(null); + this.nodes.push(null); const {flags, queryMatchExprs, providerExpr, depsExpr} = this._visitProviderOrDirective(providerAst, queryMatches); @@ -550,11 +589,13 @@ class ViewBuilder implements TemplateAstVisitor, LocalResolver, BuiltinConverter // providerDef( // flags: NodeFlags, matchedQueries: [string, QueryValueType][], token:any, // value: any, deps: ([DepFlags, any] | any)[]): NodeDef; - const nodeDef = () => o.importExpr(createIdentifier(Identifiers.providerDef)).callFn([ - o.literal(flags), queryMatchExprs.length ? o.literalArr(queryMatchExprs) : o.NULL_EXPR, - tokenExpr(providerAst.token), providerExpr, depsExpr - ]); - this.nodeDefs[nodeIndex] = nodeDef; + this.nodes[nodeIndex] = () => ({ + sourceSpan: providerAst.sourceSpan, + nodeDef: o.importExpr(createIdentifier(Identifiers.providerDef)).callFn([ + o.literal(flags), queryMatchExprs.length ? o.literalArr(queryMatchExprs) : o.NULL_EXPR, + tokenExpr(providerAst.token), providerExpr, depsExpr + ]) + }); } private _visitProviderOrDirective(providerAst: ProviderAst, queryMatches: QueryMatch[]): { @@ -615,43 +656,50 @@ class ViewBuilder implements TemplateAstVisitor, LocalResolver, BuiltinConverter return null; } - createLiteralArrayConverter(argCount: number): BuiltinConverter { + createLiteralArrayConverter(sourceSpan: ParseSourceSpan, argCount: number): BuiltinConverter { if (argCount === 0) { const valueExpr = o.importExpr(createIdentifier(Identifiers.EMPTY_ARRAY)); return () => valueExpr; } - const nodeIndex = this.nodeDefs.length; + const nodeIndex = this.nodes.length; // pureArrayDef(argCount: number): NodeDef; - const nodeDef = () => - o.importExpr(createIdentifier(Identifiers.pureArrayDef)).callFn([o.literal(argCount)]); - this.nodeDefs.push(nodeDef); + this.nodes.push( + () => ({ + sourceSpan, + nodeDef: + o.importExpr(createIdentifier(Identifiers.pureArrayDef)).callFn([o.literal(argCount)]) + })); return (args: o.Expression[]) => callCheckStmt(nodeIndex, args); } - createLiteralMapConverter(keys: string[]): BuiltinConverter { + createLiteralMapConverter(sourceSpan: ParseSourceSpan, keys: string[]): BuiltinConverter { if (keys.length === 0) { const valueExpr = o.importExpr(createIdentifier(Identifiers.EMPTY_MAP)); return () => valueExpr; } - const nodeIndex = this.nodeDefs.length; + const nodeIndex = this.nodes.length; // function pureObjectDef(propertyNames: string[]): NodeDef - const nodeDef = () => - o.importExpr(createIdentifier(Identifiers.pureObjectDef)).callFn([o.literalArr( - keys.map(key => o.literal(key)))]); - this.nodeDefs.push(nodeDef); + this.nodes.push(() => ({ + sourceSpan, + nodeDef: o.importExpr(createIdentifier(Identifiers.pureObjectDef)) + .callFn([o.literalArr(keys.map(key => o.literal(key)))]) + })); return (args: o.Expression[]) => callCheckStmt(nodeIndex, args); } - createPipeConverter(name: string, argCount: number): BuiltinConverter { - const pipe = this._findPipe(name); + createPipeConverter(sourceSpan: ParseSourceSpan, name: string, argCount: number): + BuiltinConverter { + const pipe = this.usedPipes.find((pipeSummary) => pipeSummary.name === name); if (pipe.pure) { - const nodeIndex = this.nodeDefs.length; + const nodeIndex = this.nodes.length; // function purePipeDef(argCount: number): NodeDef; - const nodeDef = () => - o.importExpr(createIdentifier(Identifiers.purePipeDef)).callFn([o.literal(argCount)]); - this.nodeDefs.push(nodeDef); + this.nodes.push(() => ({ + sourceSpan, + nodeDef: o.importExpr(createIdentifier(Identifiers.purePipeDef)) + .callFn([o.literal(argCount)]) + })); // find underlying pipe in the component view let compViewExpr: o.Expression = VIEW_VAR; @@ -669,7 +717,7 @@ class ViewBuilder implements TemplateAstVisitor, LocalResolver, BuiltinConverter return (args: o.Expression[]) => callUnwrapValue(callCheckStmt(nodeIndex, [pipeValueExpr].concat(args))); } else { - const nodeIndex = this._createPipe(pipe); + const nodeIndex = this._createPipe(sourceSpan, pipe); const nodeValueExpr = o.importExpr(createIdentifier(Identifiers.nodeValue)).callFn([ VIEW_VAR, o.literal(nodeIndex) ]); @@ -678,12 +726,8 @@ class ViewBuilder implements TemplateAstVisitor, LocalResolver, BuiltinConverter } } - private _findPipe(name: string): CompilePipeSummary { - return this.usedPipes.find((pipeSummary) => pipeSummary.name === name); - } - - private _createPipe(pipe: CompilePipeSummary): number { - const nodeIndex = this.nodeDefs.length; + private _createPipe(sourceSpan: ParseSourceSpan, pipe: CompilePipeSummary): number { + const nodeIndex = this.nodes.length; let flags = NodeFlags.None; pipe.type.lifecycleHooks.forEach((lifecycleHook) => { // for pipes, we only support ngOnDestroy @@ -695,24 +739,76 @@ class ViewBuilder implements TemplateAstVisitor, LocalResolver, BuiltinConverter const depExprs = pipe.type.diDeps.map(depDef); // function pipeDef( // flags: NodeFlags, ctor: any, deps: ([DepFlags, any] | any)[]): NodeDef - const nodeDef = () => o.importExpr(createIdentifier(Identifiers.pipeDef)).callFn([ - o.literal(flags), o.importExpr(pipe.type), o.literalArr(depExprs) - ]); - this.nodeDefs.push(nodeDef); + this.nodes.push(() => ({ + sourceSpan, + nodeDef: o.importExpr(createIdentifier(Identifiers.pipeDef)).callFn([ + o.literal(flags), o.importExpr(pipe.type), o.literalArr(depExprs) + ]) + })); return nodeIndex; } // Attention: This might create new nodeDefs (for pipes and literal arrays and literal maps)! - private _addUpdateExpressions( - nodeIndex: number, expressions: {context: o.Expression, value: AST}[], - target: UpdateExpression[]) { - const transformedExpressions = expressions.map(({context, value}) => { - if (value instanceof ASTWithSource) { - value = value.ast; + private _preprocessUpdateExpression(expression: UpdateExpression): UpdateExpression { + return { + sourceSpan: expression.sourceSpan, + context: expression.context, + value: convertPropertyBindingBuiltins( + { + createLiteralArrayConverter: (argCount: number) => this.createLiteralArrayConverter( + expression.sourceSpan, argCount), + createLiteralMapConverter: + (keys: string[]) => this.createLiteralMapConverter(expression.sourceSpan, keys), + createPipeConverter: (name: string, argCount: number) => + this.createPipeConverter(expression.sourceSpan, name, argCount) + }, + expression.value) + }; + } + + private _createNodeExpressions(): { + updateRendererStmts: o.Statement[], + updateDirectivesStmts: o.Statement[], + nodeDefExprs: o.Expression[] + } { + const self = this; + let updateBindingCount = 0; + const updateRendererStmts: o.Statement[] = []; + const updateDirectivesStmts: o.Statement[] = []; + const nodeDefExprs = this.nodes.map((factory, nodeIndex) => { + const {nodeDef, updateDirectives, updateRenderer, sourceSpan} = factory(); + if (updateRenderer) { + updateRendererStmts.push(...createUpdateStatements(nodeIndex, sourceSpan, updateRenderer)); } - return {context, value: convertPropertyBindingBuiltins(this, value)}; + if (updateDirectives) { + updateDirectivesStmts.push( + ...createUpdateStatements(nodeIndex, sourceSpan, updateDirectives)); + } + // We use a comma expression to call the log function before + // the nodeDef function, but still use the result of the nodeDef function + // as the value. + const logWithNodeDef = new o.CommaExpr([LOG_VAR.callFn([]).callFn([]), nodeDef]); + return o.applySourceSpanToExpressionIfNeeded(logWithNodeDef, sourceSpan); }); - target.push({nodeIndex, expressions: transformedExpressions}); + return {updateRendererStmts, updateDirectivesStmts, nodeDefExprs}; + + function createUpdateStatements( + nodeIndex: number, sourceSpan: ParseSourceSpan, + expressions: UpdateExpression[]): o.Statement[] { + const updateStmts: o.Statement[] = []; + const exprs = expressions.map(({sourceSpan, context, value}) => { + const bindingId = `${updateBindingCount++}`; + const nameResolver = context === COMP_VAR ? self : null; + const {stmts, currValExpr} = + convertPropertyBinding(nameResolver, context, value, bindingId); + updateStmts.push( + ...stmts.map(stmt => o.applySourceSpanToStatementIfNeeded(stmt, sourceSpan))); + return o.applySourceSpanToExpressionIfNeeded(currValExpr, sourceSpan); + }); + updateStmts.push(o.applySourceSpanToStatementIfNeeded( + callCheckStmt(nodeIndex, exprs).toStmt(), sourceSpan)); + return updateStmts; + } } private _createElementHandleEventFn( @@ -723,18 +819,17 @@ class ViewBuilder implements TemplateAstVisitor, LocalResolver, BuiltinConverter handlers.forEach(({context, eventAst, dirAst}) => { const bindingId = `${handleEventBindingCount++}`; const nameResolver = context === COMP_VAR ? this : null; - const expression = - eventAst.handler instanceof ASTWithSource ? eventAst.handler.ast : eventAst.handler; const {stmts, allowDefault} = - convertActionBinding(nameResolver, context, expression, bindingId); + convertActionBinding(nameResolver, context, eventAst.handler, bindingId); const trueStmts = stmts; if (allowDefault) { trueStmts.push(ALLOW_DEFAULT_VAR.set(allowDefault.and(ALLOW_DEFAULT_VAR)).toStmt()); } const {target: eventTarget, name: eventName} = elementEventNameAndTarget(eventAst, dirAst); const fullEventName = elementEventFullName(eventTarget, eventName); - handleEventStmts.push( - new o.IfStmt(o.literal(fullEventName).identical(EVENT_NAME_VAR), trueStmts)); + handleEventStmts.push(o.applySourceSpanToStatementIfNeeded( + new o.IfStmt(o.literal(fullEventName).identical(EVENT_NAME_VAR), trueStmts), + eventAst.sourceSpan)); }); let handleEventFn: o.Expression; if (handleEventStmts.length > 0) { diff --git a/packages/compiler/test/aot/compiler_spec.ts b/packages/compiler/test/aot/compiler_spec.ts index dc267e4d48..cea4f7c6cf 100644 --- a/packages/compiler/test/aot/compiler_spec.ts +++ b/packages/compiler/test/aot/compiler_spec.ts @@ -6,12 +6,15 @@ * found in the LICENSE file at https://angular.io/license */ -import {AotCompiler, AotCompilerHost, AotCompilerOptions, createAotCompiler} from '@angular/compiler'; +import {AotCompiler, AotCompilerHost, AotCompilerOptions, GeneratedFile, createAotCompiler} from '@angular/compiler'; import {RenderComponentType, ɵReflectionCapabilities as ReflectionCapabilities, ɵreflector as reflector} from '@angular/core'; -import {async} from '@angular/core/testing'; +import {async, fakeAsync, tick} from '@angular/core/testing'; import {MetadataBundler, MetadataCollector, ModuleMetadata, privateEntriesToIndex} from '@angular/tsc-wrapped'; import * as ts from 'typescript'; -import {EmittingCompilerHost, MockAotCompilerHost, MockCompilerHost, MockData, MockMetadataBundlerHost, settings} from './test_util'; + +import {extractSourceMap, originalPositionFor} from '../output/source_map_util'; + +import {EmittingCompilerHost, MockAotCompilerHost, MockCompilerHost, MockData, MockDirectory, MockMetadataBundlerHost, settings} from './test_util'; const DTS = /\.d\.ts$/; @@ -39,6 +42,9 @@ describe('compiler (unbundled Angular)', () => { angularFiles = emittingHost.written; }); + // Restore reflector since AoT compiler will update it with a new static reflector + afterEach(() => { reflector.updateCapabilities(new ReflectionCapabilities()); }); + describe('Quickstart', () => { let host: MockCompilerHost; let aotHost: MockAotCompilerHost; @@ -48,9 +54,6 @@ describe('compiler (unbundled Angular)', () => { aotHost = new MockAotCompilerHost(host); }); - // Restore reflector since AoT compiler will update it with a new static reflector - afterEach(() => { reflector.updateCapabilities(new ReflectionCapabilities()); }); - it('should compile', async(() => compile(host, aotHost, expectNoDiagnostics).then(generatedFiles => { expect(generatedFiles.find(f => /app\.component\.ngfactory\.ts/.test(f.genFileUrl))) @@ -67,6 +70,155 @@ describe('compiler (unbundled Angular)', () => { .toBeDefined(); }))); }); + + describe('aot source mapping', () => { + const componentPath = '/app/app.component.ts'; + + let rootDir: MockDirectory; + let appDir: MockDirectory; + + beforeEach(() => { + appDir = { + 'app.module.ts': ` + import { NgModule } from '@angular/core'; + + import { AppComponent } from './app.component'; + + @NgModule({ + declarations: [ AppComponent ], + bootstrap: [ AppComponent ] + }) + export class AppModule { } + ` + }; + rootDir = {'app': appDir}; + }); + + function compileApp(): GeneratedFile { + const host = new MockCompilerHost(['/app/app.module.ts'], rootDir, angularFiles); + const aotHost = new MockAotCompilerHost(host); + let result: GeneratedFile[]; + let error: Error; + compile(host, aotHost, expectNoDiagnostics, expectNoDiagnostics) + .then((files) => result = files, (err) => error = err); + tick(); + if (error) { + throw error; + } + return result.find(genFile => genFile.srcFileUrl === componentPath); + ; + } + + function findLineAndColumn(file: string, token: string): {line: number, column: number} { + const index = file.indexOf(token); + if (index === -1) { + return {line: null, column: null}; + } + const linesUntilToken = file.slice(0, index).split('\n'); + const line = linesUntilToken.length; + const column = linesUntilToken[linesUntilToken.length - 1].length; + return {line, column}; + } + + function createComponentSource(componentDecorator: string) { + return ` + import { NgModule, Component } from '@angular/core'; + + @Component({ + ${componentDecorator} + }) + export class AppComponent { + someMethod() {} + } + `; + } + + describe('inline templates', () => { + const templateUrl = componentPath; + + function templateDecorator(template: string) { return `template: \`${template}\`,`; } + + declareTests({templateUrl, templateDecorator}); + }); + + describe('external templates', () => { + const templateUrl = '/app/app.component.html'; + + function templateDecorator(template: string) { + appDir['app.component.html'] = template; + return `templateUrl: 'app.component.html',`; + } + + declareTests({templateUrl, templateDecorator}); + }); + + function declareTests( + {templateUrl, templateDecorator}: + {templateUrl: string, templateDecorator: (template: string) => string}) { + it('should use the right source url in html parse errors', fakeAsync(() => { + appDir['app.component.ts'] = + createComponentSource(templateDecorator('
\n ')); + + expect(() => compileApp()) + .toThrowError(new RegExp(`Template parse errors[\\s\\S]*${templateUrl}@1:2`)); + })); + + it('should use the right source url in template parse errors', fakeAsync(() => { + appDir['app.component.ts'] = createComponentSource( + templateDecorator('
\n
')); + + expect(() => compileApp()) + .toThrowError(new RegExp(`Template parse errors[\\s\\S]*${templateUrl}@1:7`)); + })); + + it('should create a sourceMap for the template', fakeAsync(() => { + const template = 'Hello World!'; + + appDir['app.component.ts'] = createComponentSource(templateDecorator(template)); + + const genFile = compileApp(); + const sourceMap = extractSourceMap(genFile.source); + expect(sourceMap.file).toEqual(genFile.genFileUrl); + // the generated file contains the host view and the component view. + // we are only interested in the component view. + const sourceIndex = sourceMap.sources.indexOf(templateUrl); + expect(sourceMap.sourcesContent[sourceIndex]).toEqual(template); + })); + + it('should map elements correctly to the source', fakeAsync(() => { + const template = '
\n
'; + + appDir['app.component.ts'] = createComponentSource(templateDecorator(template)); + + const genFile = compileApp(); + const sourceMap = extractSourceMap(genFile.source); + expect(originalPositionFor(sourceMap, findLineAndColumn(genFile.source, `'span'`))) + .toEqual({line: 2, column: 3, source: templateUrl}); + })); + + it('should map bindings correctly to the source', fakeAsync(() => { + const template = `
\n
`; + + appDir['app.component.ts'] = createComponentSource(templateDecorator(template)); + + const genFile = compileApp(); + const sourceMap = extractSourceMap(genFile.source); + expect(originalPositionFor(sourceMap, findLineAndColumn(genFile.source, `someMethod()`))) + .toEqual({line: 2, column: 9, source: templateUrl}); + })); + + it('should map events correctly to the source', fakeAsync(() => { + const template = `
\n
`; + + appDir['app.component.ts'] = createComponentSource(templateDecorator(template)); + + const genFile = compileApp(); + const sourceMap = extractSourceMap(genFile.source); + expect(originalPositionFor(sourceMap, findLineAndColumn(genFile.source, `someMethod()`))) + .toEqual({line: 2, column: 9, source: templateUrl}); + })); + } + }); }); describe('compiler (bundled Angular)', () => { diff --git a/packages/compiler/test/directive_normalizer_spec.ts b/packages/compiler/test/directive_normalizer_spec.ts index 25c0aa9f8f..9c3c9c5308 100644 --- a/packages/compiler/test/directive_normalizer_spec.ts +++ b/packages/compiler/test/directive_normalizer_spec.ts @@ -28,6 +28,7 @@ export function main() { it('should throw if no template was specified', inject([DirectiveNormalizer], (normalizer: DirectiveNormalizer) => { expect(() => normalizer.normalizeTemplate({ + ngModuleType: null, componentType: SomeComp, moduleUrl: SOME_MODULE_URL, })).toThrowError('No template specified for component SomeComp'); @@ -35,6 +36,7 @@ export function main() { it('should throw if template is not a string', inject([DirectiveNormalizer], (normalizer: DirectiveNormalizer) => { expect(() => normalizer.normalizeTemplate({ + ngModuleType: null, componentType: SomeComp, moduleUrl: SOME_MODULE_URL, template: {} @@ -43,6 +45,7 @@ export function main() { it('should throw if templateUrl is not a string', inject([DirectiveNormalizer], (normalizer: DirectiveNormalizer) => { expect(() => normalizer.normalizeTemplate({ + ngModuleType: null, componentType: SomeComp, moduleUrl: SOME_MODULE_URL, templateUrl: {} @@ -54,6 +57,7 @@ export function main() { it('should store the template', inject([DirectiveNormalizer], (normalizer: DirectiveNormalizer) => { const template = normalizer.normalizeTemplateSync({ + ngModuleType: null, componentType: SomeComp, moduleUrl: SOME_MODULE_URL, encapsulation: null, @@ -64,11 +68,13 @@ export function main() { }); expect(template.template).toEqual('a'); expect(template.templateUrl).toEqual('package:some/module/a.js'); + expect(template.isInline).toBe(true); })); it('should resolve styles on the annotation against the moduleUrl', inject([DirectiveNormalizer], (normalizer: DirectiveNormalizer) => { const template = normalizer.normalizeTemplateSync({ + ngModuleType: null, componentType: SomeComp, moduleUrl: SOME_MODULE_URL, encapsulation: null, @@ -83,6 +89,7 @@ export function main() { it('should resolve styles in the template against the moduleUrl', inject([DirectiveNormalizer], (normalizer: DirectiveNormalizer) => { const template = normalizer.normalizeTemplateSync({ + ngModuleType: null, componentType: SomeComp, moduleUrl: SOME_MODULE_URL, encapsulation: null, @@ -97,6 +104,7 @@ export function main() { it('should use ViewEncapsulation.Emulated by default', inject([DirectiveNormalizer], (normalizer: DirectiveNormalizer) => { const template = normalizer.normalizeTemplateSync({ + ngModuleType: null, componentType: SomeComp, moduleUrl: SOME_MODULE_URL, encapsulation: null, @@ -114,6 +122,7 @@ export function main() { (config: CompilerConfig, normalizer: DirectiveNormalizer) => { config.defaultEncapsulation = ViewEncapsulation.None; const template = normalizer.normalizeTemplateSync({ + ngModuleType: null, componentType: SomeComp, moduleUrl: SOME_MODULE_URL, encapsulation: null, @@ -136,6 +145,7 @@ export function main() { resourceLoader.expect('package:some/module/sometplurl.html', 'a'); normalizer .normalizeTemplateAsync({ + ngModuleType: null, componentType: SomeComp, moduleUrl: SOME_MODULE_URL, encapsulation: null, @@ -147,6 +157,7 @@ export function main() { .then((template: CompileTemplateMetadata) => { expect(template.template).toEqual('a'); expect(template.templateUrl).toEqual('package:some/module/sometplurl.html'); + expect(template.isInline).toBe(false); async.done(); }); resourceLoader.flush(); @@ -160,6 +171,7 @@ export function main() { resourceLoader.expect('package:some/module/tpl/sometplurl.html', ''); normalizer .normalizeTemplateAsync({ + ngModuleType: null, componentType: SomeComp, moduleUrl: SOME_MODULE_URL, encapsulation: null, @@ -184,6 +196,7 @@ export function main() { 'package:some/module/tpl/sometplurl.html', ''); normalizer .normalizeTemplateAsync({ + ngModuleType: null, componentType: SomeComp, moduleUrl: SOME_MODULE_URL, encapsulation: null, @@ -271,6 +284,7 @@ export function main() { resourceLoader: MockResourceLoader) => { resourceLoader.expect('package:some/module/cmp.html', 'a'); const prenormMeta = { + ngModuleType: null as any, componentType: SomeComp, moduleUrl: SOME_MODULE_URL, templateUrl: 'cmp.html', @@ -297,6 +311,7 @@ export function main() { const viewEncapsulation = ViewEncapsulation.Native; const template = normalizer.normalizeLoadedTemplate( { + ngModuleType: null, componentType: SomeComp, moduleUrl: SOME_MODULE_URL, encapsulation: viewEncapsulation, @@ -311,6 +326,7 @@ export function main() { inject([DirectiveNormalizer], (normalizer: DirectiveNormalizer) => { const template = normalizer.normalizeLoadedTemplate( { + ngModuleType: null, componentType: SomeComp, moduleUrl: SOME_MODULE_URL, encapsulation: null, @@ -325,6 +341,7 @@ export function main() { inject([DirectiveNormalizer], (normalizer: DirectiveNormalizer) => { const template = normalizer.normalizeLoadedTemplate( { + ngModuleType: null, componentType: SomeComp, moduleUrl: SOME_MODULE_URL, encapsulation: null, @@ -339,6 +356,7 @@ export function main() { inject([DirectiveNormalizer], (normalizer: DirectiveNormalizer) => { const template = normalizer.normalizeLoadedTemplate( { + ngModuleType: null, componentType: SomeComp, moduleUrl: SOME_MODULE_URL, encapsulation: null, @@ -354,6 +372,7 @@ export function main() { inject([DirectiveNormalizer], (normalizer: DirectiveNormalizer) => { const template = normalizer.normalizeLoadedTemplate( { + ngModuleType: null, componentType: SomeComp, moduleUrl: SOME_MODULE_URL, encapsulation: null, @@ -368,6 +387,7 @@ export function main() { inject([DirectiveNormalizer], (normalizer: DirectiveNormalizer) => { const template = normalizer.normalizeLoadedTemplate( { + ngModuleType: null, componentType: SomeComp, moduleUrl: SOME_MODULE_URL, encapsulation: null, @@ -382,6 +402,7 @@ export function main() { inject([DirectiveNormalizer], (normalizer: DirectiveNormalizer) => { const template = normalizer.normalizeLoadedTemplate( { + ngModuleType: null, componentType: SomeComp, moduleUrl: SOME_MODULE_URL, encapsulation: null, @@ -396,6 +417,7 @@ export function main() { inject([DirectiveNormalizer], (normalizer: DirectiveNormalizer) => { const template = normalizer.normalizeLoadedTemplate( { + ngModuleType: null, componentType: SomeComp, moduleUrl: SOME_MODULE_URL, encapsulation: null, @@ -410,6 +432,7 @@ export function main() { inject([DirectiveNormalizer], (normalizer: DirectiveNormalizer) => { const template = normalizer.normalizeLoadedTemplate( { + ngModuleType: null, componentType: SomeComp, moduleUrl: SOME_MODULE_URL, encapsulation: null, @@ -424,6 +447,7 @@ export function main() { inject([DirectiveNormalizer], (normalizer: DirectiveNormalizer) => { const template = normalizer.normalizeLoadedTemplate( { + ngModuleType: null, componentType: SomeComp, moduleUrl: SOME_MODULE_URL, encapsulation: null, @@ -438,6 +462,7 @@ export function main() { inject([DirectiveNormalizer], (normalizer: DirectiveNormalizer) => { const template = normalizer.normalizeLoadedTemplate( { + ngModuleType: null, componentType: SomeComp, moduleUrl: SOME_MODULE_URL, encapsulation: null, @@ -453,6 +478,7 @@ export function main() { inject([DirectiveNormalizer], (normalizer: DirectiveNormalizer) => { const template = normalizer.normalizeLoadedTemplate( { + ngModuleType: null, componentType: SomeComp, moduleUrl: SOME_MODULE_URL, encapsulation: null, @@ -467,6 +493,7 @@ export function main() { inject([DirectiveNormalizer], (normalizer: DirectiveNormalizer) => { const template = normalizer.normalizeLoadedTemplate( { + ngModuleType: null, componentType: SomeComp, moduleUrl: SOME_MODULE_URL, encapsulation: null, @@ -482,6 +509,7 @@ export function main() { inject([DirectiveNormalizer], (normalizer: DirectiveNormalizer) => { const template = normalizer.normalizeLoadedTemplate( { + ngModuleType: null, componentType: SomeComp, moduleUrl: SOME_HTTP_MODULE_URL, encapsulation: null, @@ -497,6 +525,7 @@ export function main() { inject([DirectiveNormalizer], (normalizer: DirectiveNormalizer) => { const template = normalizer.normalizeLoadedTemplate( { + ngModuleType: null, componentType: SomeComp, moduleUrl: SOME_MODULE_URL, encapsulation: ViewEncapsulation.Emulated, @@ -511,6 +540,7 @@ export function main() { inject([DirectiveNormalizer], (normalizer: DirectiveNormalizer) => { const template = normalizer.normalizeLoadedTemplate( { + ngModuleType: null, componentType: SomeComp, moduleUrl: SOME_MODULE_URL, encapsulation: null, @@ -526,6 +556,7 @@ export function main() { inject([DirectiveNormalizer], (normalizer: DirectiveNormalizer) => { const template = normalizer.normalizeLoadedTemplate( { + ngModuleType: null, componentType: SomeComp, moduleUrl: SOME_MODULE_URL, encapsulation: null, diff --git a/packages/compiler/test/output/abstract_emitter_node_only_spec.ts b/packages/compiler/test/output/abstract_emitter_node_only_spec.ts index 8120a75bdb..103a593614 100644 --- a/packages/compiler/test/output/abstract_emitter_node_only_spec.ts +++ b/packages/compiler/test/output/abstract_emitter_node_only_spec.ts @@ -9,10 +9,7 @@ import {ParseLocation, ParseSourceFile, ParseSourceSpan} from '@angular/compiler'; import {EmitterVisitorContext} from '@angular/compiler/src/output/abstract_emitter'; import {SourceMap} from '@angular/compiler/src/output/source_map'; - -const SourceMapConsumer = require('source-map').SourceMapConsumer; -const b64 = require('base64-js'); - +import {extractSourceMap, originalPositionFor} from './source_map_util'; export function main() { describe('AbstractEmitter', () => { @@ -47,12 +44,10 @@ export function main() { ctx.print(createSourceSpan(fileA, 0), 'fileA-0'); const sm = ctx.toSourceMapGenerator(null, 10).toJSON(); - const smc = new SourceMapConsumer(sm); - expect(smc.originalPositionFor({line: 11, column: 0})).toEqual({ + expect(originalPositionFor(sm, {line: 11, column: 0})).toEqual({ line: 1, column: 0, source: 'a.js', - name: null, }); }); @@ -109,9 +104,8 @@ function expectMap( ctx: EmitterVisitorContext, genLine: number, genCol: number, source: string = null, srcLine: number = null, srcCol: number = null) { const sm = ctx.toSourceMapGenerator().toJSON(); - const smc = new SourceMapConsumer(sm); const genPosition = {line: genLine + 1, column: genCol}; - const origPosition = smc.originalPositionFor(genPosition); + const origPosition = originalPositionFor(sm, genPosition); expect(origPosition.source).toEqual(source); expect(origPosition.line).toEqual(srcLine === null ? null : srcLine + 1); expect(origPosition.column).toEqual(srcCol); @@ -134,15 +128,3 @@ function createSourceSpan(file: ParseSourceFile, idx: number) { const sourceSpan = new ParseSourceSpan(start, end); return {sourceSpan}; } - -export function extractSourceMap(source: string): SourceMap { - let idx = source.lastIndexOf('\n//#'); - if (idx == -1) return null; - const smComment = source.slice(idx).trim(); - const smB64 = smComment.split('sourceMappingURL=data:application/json;base64,')[1]; - return smB64 ? JSON.parse(decodeB64String(smB64)) : null; -} - -function decodeB64String(s: string): string { - return b64.toByteArray(s).reduce((s: string, c: number) => s + String.fromCharCode(c), ''); -} \ No newline at end of file diff --git a/packages/compiler/test/output/abstract_emitter_spec.ts b/packages/compiler/test/output/abstract_emitter_spec.ts index ea9546e535..7f091a3d3a 100644 --- a/packages/compiler/test/output/abstract_emitter_spec.ts +++ b/packages/compiler/test/output/abstract_emitter_spec.ts @@ -33,7 +33,10 @@ export function main() { }); } -export function stripSourceMap(source: string): string { +export function stripSourceMapAndNewLine(source: string): string { + if (source.endsWith('\n')) { + source = source.substring(0, source.length - 1); + } const smi = source.lastIndexOf('\n//#'); if (smi == -1) return source; return source.slice(0, smi); diff --git a/packages/compiler/test/output/js_emitter_node_only_spec.ts b/packages/compiler/test/output/js_emitter_node_only_spec.ts index 43d7984010..61c529e92a 100644 --- a/packages/compiler/test/output/js_emitter_node_only_spec.ts +++ b/packages/compiler/test/output/js_emitter_node_only_spec.ts @@ -14,9 +14,7 @@ import {ImportResolver} from '@angular/compiler/src/output/path_util'; import {SourceMap} from '@angular/compiler/src/output/source_map'; import {ParseLocation, ParseSourceFile, ParseSourceSpan} from '@angular/compiler/src/parse_util'; -import {extractSourceMap} from './abstract_emitter_node_only_spec'; - -const SourceMapConsumer = require('source-map').SourceMapConsumer; +import {extractSourceMap, originalPositionFor} from './source_map_util'; const someModuleUrl = 'somePackage/somePath'; @@ -54,12 +52,11 @@ export function main() { const sourceSpan = new ParseSourceSpan(startLocation, endLocation); const someVar = o.variable('someVar', null, sourceSpan); const sm = emitSourceMap(someVar.toStmt()); - const smc = new SourceMapConsumer(sm); expect(sm.sources).toEqual(['in.js']); expect(sm.sourcesContent).toEqual([';;;var']); - expect(smc.originalPositionFor({line: 1, column: 0})) - .toEqual({line: 1, column: 3, source: 'in.js', name: null}); + expect(originalPositionFor(sm, {line: 1, column: 0})) + .toEqual({line: 1, column: 3, source: 'in.js'}); }); }); }); diff --git a/packages/compiler/test/output/js_emitter_spec.ts b/packages/compiler/test/output/js_emitter_spec.ts index fc85cec67d..6af6f22491 100644 --- a/packages/compiler/test/output/js_emitter_spec.ts +++ b/packages/compiler/test/output/js_emitter_spec.ts @@ -12,7 +12,7 @@ import {JavaScriptEmitter} from '@angular/compiler/src/output/js_emitter'; import * as o from '@angular/compiler/src/output/output_ast'; import {ImportResolver} from '@angular/compiler/src/output/path_util'; -import {stripSourceMap} from './abstract_emitter_spec'; +import {stripSourceMapAndNewLine} from './abstract_emitter_spec'; const someModuleUrl = 'somePackage/somePath'; const anotherModuleUrl = 'somePackage/someOtherPath'; @@ -50,7 +50,7 @@ export function main() { function emitStmt(stmt: o.Statement, exportedVars: string[] = null): string { const source = emitter.emitStatements(someModuleUrl, [stmt], exportedVars || []); - return stripSourceMap(source); + return stripSourceMapAndNewLine(source); } it('should declare variables', () => { diff --git a/packages/compiler/test/output/source_map_util.ts b/packages/compiler/test/output/source_map_util.ts new file mode 100644 index 0000000000..c65fffdd84 --- /dev/null +++ b/packages/compiler/test/output/source_map_util.ts @@ -0,0 +1,38 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {SourceMap} from '@angular/compiler/src/output/source_map'; +const b64 = require('base64-js'); +const SourceMapConsumer = require('source-map').SourceMapConsumer; + +export interface SourceLocation { + line: number; + column: number; + source: string; +} + +export function originalPositionFor( + sourceMap: SourceMap, genPosition: {line: number, column: number}): SourceLocation { + const smc = new SourceMapConsumer(sourceMap); + // Note: We don't return the original object as it also contains a `name` property + // which is always null and we don't want to include that in our assertions... + const {line, column, source} = smc.originalPositionFor(genPosition); + return {line, column, source}; +} + +export function extractSourceMap(source: string): SourceMap { + let idx = source.lastIndexOf('\n//#'); + if (idx == -1) return null; + const smComment = source.slice(idx).trim(); + const smB64 = smComment.split('sourceMappingURL=data:application/json;base64,')[1]; + return smB64 ? JSON.parse(decodeB64String(smB64)) : null; +} + +function decodeB64String(s: string): string { + return b64.toByteArray(s).reduce((s: string, c: number) => s + String.fromCharCode(c), ''); +} \ No newline at end of file diff --git a/packages/compiler/test/output/ts_emitter_node_only_spec.ts b/packages/compiler/test/output/ts_emitter_node_only_spec.ts index 391c948447..050bfe1c49 100644 --- a/packages/compiler/test/output/ts_emitter_node_only_spec.ts +++ b/packages/compiler/test/output/ts_emitter_node_only_spec.ts @@ -14,9 +14,7 @@ import {SourceMap} from '@angular/compiler/src/output/source_map'; import {TypeScriptEmitter} from '@angular/compiler/src/output/ts_emitter'; import {ParseSourceSpan} from '@angular/compiler/src/parse_util'; -import {extractSourceMap} from './abstract_emitter_node_only_spec'; - -const SourceMapConsumer = require('source-map').SourceMapConsumer; +import {extractSourceMap, originalPositionFor} from './source_map_util'; const someModuleUrl = 'somePackage/somePath'; @@ -59,12 +57,11 @@ export function main() { const sourceSpan = new ParseSourceSpan(startLocation, endLocation); const someVar = o.variable('someVar', null, sourceSpan); const sm = emitSourceMap(someVar.toStmt()); - const smc = new SourceMapConsumer(sm); expect(sm.sources).toEqual(['in.js']); expect(sm.sourcesContent).toEqual([';;;var']); - expect(smc.originalPositionFor({line: 1, column: 0})) - .toEqual({line: 1, column: 3, source: 'in.js', name: null}); + expect(originalPositionFor(sm, {line: 1, column: 0})) + .toEqual({line: 1, column: 3, source: 'in.js'}); }); }); }); diff --git a/packages/compiler/test/output/ts_emitter_spec.ts b/packages/compiler/test/output/ts_emitter_spec.ts index 4c0018ff63..70b5b33887 100644 --- a/packages/compiler/test/output/ts_emitter_spec.ts +++ b/packages/compiler/test/output/ts_emitter_spec.ts @@ -12,7 +12,7 @@ import * as o from '@angular/compiler/src/output/output_ast'; import {ImportResolver} from '@angular/compiler/src/output/path_util'; import {TypeScriptEmitter} from '@angular/compiler/src/output/ts_emitter'; -import {stripSourceMap} from './abstract_emitter_spec'; +import {stripSourceMapAndNewLine} from './abstract_emitter_spec'; const someModuleUrl = 'somePackage/somePath'; const anotherModuleUrl = 'somePackage/someOtherPath'; @@ -52,7 +52,7 @@ export function main() { function emitStmt(stmt: o.Statement | o.Statement[], exportedVars: string[] = null): string { const stmts = Array.isArray(stmt) ? stmt : [stmt]; const source = emitter.emitStatements(someModuleUrl, stmts, exportedVars || []); - return stripSourceMap(source); + return stripSourceMapAndNewLine(source); } it('should declare variables', () => { diff --git a/packages/compiler/testing/src/resource_loader_mock.ts b/packages/compiler/testing/src/resource_loader_mock.ts index ba9504e006..9585933013 100644 --- a/packages/compiler/testing/src/resource_loader_mock.ts +++ b/packages/compiler/testing/src/resource_loader_mock.ts @@ -23,6 +23,8 @@ export class MockResourceLoader extends ResourceLoader { return request.getPromise(); } + hasPendingRequests() { return !!this._requests.length; } + /** * Add an expectation for the given URL. Incoming requests will be checked against * the next expectation (in FIFO order). The `verifyNoOutstandingExpectations` method diff --git a/packages/core/src/debug/debug_node.ts b/packages/core/src/debug/debug_node.ts index e8fe7eeeee..219066bd7e 100644 --- a/packages/core/src/debug/debug_node.ts +++ b/packages/core/src/debug/debug_node.ts @@ -7,7 +7,7 @@ */ import {Injector} from '../di'; -import {RenderDebugInfo} from '../render/api'; +import {DebugContext} from '../view/index'; export class EventListener { constructor(public name: string, public callback: Function){}; } @@ -19,7 +19,7 @@ export class DebugNode { listeners: EventListener[]; parent: DebugElement; - constructor(nativeNode: any, parent: DebugNode, private _debugInfo: RenderDebugInfo) { + constructor(nativeNode: any, parent: DebugNode, private _debugContext: DebugContext) { this.nativeNode = nativeNode; if (parent && parent instanceof DebugElement) { parent.addChild(this); @@ -29,19 +29,24 @@ export class DebugNode { this.listeners = []; } - get injector(): Injector { return this._debugInfo ? this._debugInfo.injector : null; } + get injector(): Injector { return this._debugContext ? this._debugContext.injector : null; } - get componentInstance(): any { return this._debugInfo ? this._debugInfo.component : null; } + get componentInstance(): any { return this._debugContext ? this._debugContext.component : null; } - get context(): any { return this._debugInfo ? this._debugInfo.context : null; } + get context(): any { return this._debugContext ? this._debugContext.context : null; } get references(): {[key: string]: any} { - return this._debugInfo ? this._debugInfo.references : null; + return this._debugContext ? this._debugContext.references : null; } - get providerTokens(): any[] { return this._debugInfo ? this._debugInfo.providerTokens : null; } + get providerTokens(): any[] { + return this._debugContext ? this._debugContext.providerTokens : null; + } - get source(): string { return this._debugInfo ? this._debugInfo.source : null; } + /** + * @deprecated since v4 + */ + get source(): string { return 'Deprecated since v4'; } } /** @@ -56,8 +61,8 @@ export class DebugElement extends DebugNode { childNodes: DebugNode[]; nativeElement: any; - constructor(nativeNode: any, parent: any, _debugInfo: RenderDebugInfo) { - super(nativeNode, parent, _debugInfo); + constructor(nativeNode: any, parent: any, _debugContext: DebugContext) { + super(nativeNode, parent, _debugContext); this.properties = {}; this.attributes = {}; this.classes = {}; diff --git a/packages/core/src/error_handler.ts b/packages/core/src/error_handler.ts index 1f816ee712..41d7d02ea3 100644 --- a/packages/core/src/error_handler.ts +++ b/packages/core/src/error_handler.ts @@ -6,7 +6,8 @@ * found in the LICENSE file at https://angular.io/license */ -import {ERROR_ORIGINAL_ERROR, getDebugContext, getOriginalError} from './errors'; +import {ERROR_ORIGINAL_ERROR, getDebugContext, getErrorLogger, getOriginalError} from './errors'; + /** @@ -49,36 +50,23 @@ export class ErrorHandler { constructor(rethrowError: boolean = false) { this.rethrowError = rethrowError; } handleError(error: any): void { - this._console.error(`EXCEPTION: ${this._extractMessage(error)}`); + const originalError = this._findOriginalError(error); + const context = this._findContext(error); + // Note: Browser consoles show the place from where console.error was called. + // We can use this to give users additional information about the error. + const errorLogger = getErrorLogger(error); - if (error instanceof Error) { - const originalError = this._findOriginalError(error); - const originalStack = this._findOriginalStack(error); - const context = this._findContext(error); - - if (originalError) { - this._console.error(`ORIGINAL EXCEPTION: ${this._extractMessage(originalError)}`); - } - - if (originalStack) { - this._console.error('ORIGINAL STACKTRACE:'); - this._console.error(originalStack); - } - - if (context) { - this._console.error('ERROR CONTEXT:'); - this._console.error(context); - } + errorLogger(this._console, `ERROR`, error); + if (originalError) { + errorLogger(this._console, `ORIGINAL ERROR`, originalError); + } + if (context) { + errorLogger(this._console, 'ERROR CONTEXT', context); } if (this.rethrowError) throw error; } - /** @internal */ - _extractMessage(error: any): string { - return error instanceof Error ? error.message : error.toString(); - } - /** @internal */ _findContext(error: any): any { if (error) { @@ -98,20 +86,6 @@ export class ErrorHandler { return e; } - - /** @internal */ - _findOriginalStack(error: Error): string { - let e: any = error; - let stack: string = e.stack; - while (e instanceof Error && getOriginalError(e)) { - e = getOriginalError(e); - if (e instanceof Error && e.stack) { - stack = e.stack; - } - } - - return stack; - } } export function wrappedError(message: string, originalError: any): Error { diff --git a/packages/core/src/errors.ts b/packages/core/src/errors.ts index 29bde2ca76..a8a1ae8928 100644 --- a/packages/core/src/errors.ts +++ b/packages/core/src/errors.ts @@ -12,6 +12,7 @@ export const ERROR_TYPE = 'ngType'; export const ERROR_COMPONENT_TYPE = 'ngComponentType'; export const ERROR_DEBUG_CONTEXT = 'ngDebugContext'; export const ERROR_ORIGINAL_ERROR = 'ngOriginalError'; +export const ERROR_LOGGER = 'ngErrorLogger'; export function getType(error: Error): Function { @@ -25,3 +26,12 @@ export function getDebugContext(error: Error): DebugContext { export function getOriginalError(error: Error): Error { return (error as any)[ERROR_ORIGINAL_ERROR]; } + +export function getErrorLogger(error: Error): (console: Console, ...values: any[]) => void { + return (error as any)[ERROR_LOGGER] || defaultErrorLogger; +} + + +function defaultErrorLogger(console: Console, ...values: any[]) { + (console.error)(...values); +} \ No newline at end of file diff --git a/packages/core/src/view/element.ts b/packages/core/src/view/element.ts index d9eb6996cb..95737968c3 100644 --- a/packages/core/src/view/element.ts +++ b/packages/core/src/view/element.ts @@ -6,14 +6,11 @@ * found in the LICENSE file at https://angular.io/license */ -import {isDevMode} from '../application_ref'; import {Renderer2, RendererType2} from '../render/api'; import {SecurityContext} from '../security'; import {BindingDef, BindingType, DebugContext, DisposableFn, ElementData, ElementHandleEventFn, NodeData, NodeDef, NodeFlags, OutputDef, OutputType, QueryValueType, Services, ViewData, ViewDefinition, ViewDefinitionFactory, ViewFlags, asElementData, asProviderData} from './types'; -import {checkAndUpdateBinding, dispatchEvent, elementEventFullName, filterQueryId, getParentRenderElement, resolveViewDefinition, sliceErrorStack, splitMatchedQueriesDsl, splitNamespace} from './util'; - -const NOOP: any = () => {}; +import {NOOP, checkAndUpdateBinding, dispatchEvent, elementEventFullName, filterQueryId, getParentRenderElement, resolveViewDefinition, splitMatchedQueriesDsl, splitNamespace} from './util'; export function anchorDef( flags: NodeFlags, matchedQueriesDsl: [string | number, QueryValueType][], @@ -24,8 +21,6 @@ export function anchorDef( } flags |= NodeFlags.TypeElement; const {matchedQueries, references, matchedQueryIds} = splitMatchedQueriesDsl(matchedQueriesDsl); - // skip the call to sliceErrorStack itself + the call to this function. - const source = isDevMode() ? sliceErrorStack(2, 3) : ''; const template = templateFactory ? resolveViewDefinition(templateFactory) : null; return { @@ -45,7 +40,7 @@ export function anchorDef( element: { ns: undefined, name: undefined, - attrs: undefined, template, source, + attrs: undefined, template, componentProvider: undefined, componentView: undefined, componentRendererType: undefined, @@ -71,12 +66,10 @@ export function elementDef( string, SecurityContext ])[], outputs?: ([string, string])[], handleEvent?: ElementHandleEventFn, - componentView?: () => ViewDefinition, componentRendererType?: RendererType2): NodeDef { + componentView?: ViewDefinitionFactory, componentRendererType?: RendererType2): NodeDef { if (!handleEvent) { handleEvent = NOOP; } - // skip the call to sliceErrorStack itself + the call to this function. - const source = isDevMode() ? sliceErrorStack(2, 3) : ''; const {matchedQueries, references, matchedQueryIds} = splitMatchedQueriesDsl(matchedQueriesDsl); let ns: string; let name: string; @@ -146,7 +139,6 @@ export function elementDef( ns, name, attrs, - source, template: undefined, // will bet set by the view definition componentProvider: undefined, componentView, componentRendererType, diff --git a/packages/core/src/view/errors.ts b/packages/core/src/view/errors.ts index c0b2c3dab1..96df939fdd 100644 --- a/packages/core/src/view/errors.ts +++ b/packages/core/src/view/errors.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import {ERROR_DEBUG_CONTEXT, ERROR_ORIGINAL_ERROR, getDebugContext} from '../errors'; +import {ERROR_DEBUG_CONTEXT, ERROR_LOGGER, getDebugContext} from '../errors'; import {DebugContext, ViewState} from './types'; export function expressionChangedAfterItHasBeenCheckedError( @@ -21,19 +21,27 @@ export function expressionChangedAfterItHasBeenCheckedError( return viewDebugError(msg, context); } -export function viewWrappedDebugError(originalError: any, context: DebugContext): Error { - const err = viewDebugError(originalError.message, context); - (err as any)[ERROR_ORIGINAL_ERROR] = originalError; +export function viewWrappedDebugError(err: any, context: DebugContext): Error { + if (!(err instanceof Error)) { + // errors that are not Error instances don't have a stack, + // so it is ok to wrap them into a new Error object... + err = new Error(err.toString()); + } + _addDebugContext(err, context); return err; } export function viewDebugError(msg: string, context: DebugContext): Error { const err = new Error(msg); - (err as any)[ERROR_DEBUG_CONTEXT] = context; - err.stack = context.source; + _addDebugContext(err, context); return err; } +function _addDebugContext(err: Error, context: DebugContext) { + (err as any)[ERROR_DEBUG_CONTEXT] = context; + (err as any)[ERROR_LOGGER] = context.logError.bind(context); +} + export function isViewDebugError(err: Error): boolean { return !!getDebugContext(err); } diff --git a/packages/core/src/view/services.ts b/packages/core/src/view/services.ts index 2b70e7fc1a..d42af7dd2f 100644 --- a/packages/core/src/view/services.ts +++ b/packages/core/src/view/services.ts @@ -16,8 +16,8 @@ import {isViewDebugError, viewDestroyedError, viewWrappedDebugError} from './err import {resolveDep} from './provider'; import {dirtyParentQueries, getQueryValue} from './query'; import {createInjector} from './refs'; -import {ArgumentType, BindingType, CheckType, DebugContext, DepFlags, ElementData, NodeCheckFn, NodeData, NodeDef, NodeFlags, RootData, Services, ViewData, ViewDefinition, ViewDefinitionFactory, ViewState, asElementData, asProviderData, asPureExpressionData} from './types'; -import {checkBinding, isComponentView, renderNode, viewParentEl} from './util'; +import {ArgumentType, BindingType, CheckType, DebugContext, DepFlags, ElementData, NodeCheckFn, NodeData, NodeDef, NodeFlags, NodeLogger, RootData, Services, ViewData, ViewDefinition, ViewDefinitionFactory, ViewState, asElementData, asProviderData, asPureExpressionData} from './types'; +import {NOOP, checkBinding, isComponentView, renderNode, viewParentEl} from './util'; import {checkAndUpdateNode, checkAndUpdateView, checkNoChangesNode, checkNoChangesView, createEmbeddedView, createRootView, destroyView} from './view'; let initialized = false; @@ -357,13 +357,6 @@ class DebugContext_ implements DebugContext { } return references; } - get source(): string { - if (this.nodeDef.flags & NodeFlags.TypeText) { - return this.nodeDef.text.source; - } else { - return this.elDef.element.source; - } - } get componentRenderElement() { const elData = findHostElement(this.elOrCompView); return elData ? elData.renderElement : undefined; @@ -372,6 +365,31 @@ class DebugContext_ implements DebugContext { return this.nodeDef.flags & NodeFlags.TypeText ? renderNode(this.view, this.nodeDef) : renderNode(this.elView, this.elDef); } + logError(console: Console, ...values: any[]) { + let logViewFactory: ViewDefinitionFactory; + let logNodeIndex: number; + if (this.nodeDef.flags & NodeFlags.TypeText) { + logViewFactory = this.view.def.factory; + logNodeIndex = this.nodeDef.index; + } else { + logViewFactory = this.elView.def.factory; + logNodeIndex = this.elDef.index; + } + let currNodeIndex = -1; + let nodeLogger: NodeLogger = () => { + currNodeIndex++; + if (currNodeIndex === logNodeIndex) { + return console.error.bind(console, ...values); + } else { + return NOOP; + } + }; + logViewFactory(nodeLogger); + if (currNodeIndex < logNodeIndex) { + console.error('Illegal state: the ViewDefinitionFactory did not call the logger!'); + (console.error)(...values); + } + } } function findHostElement(view: ViewData): ElementData { diff --git a/packages/core/src/view/text.ts b/packages/core/src/view/text.ts index 5819086f92..a96358b1c1 100644 --- a/packages/core/src/view/text.ts +++ b/packages/core/src/view/text.ts @@ -6,15 +6,12 @@ * found in the LICENSE file at https://angular.io/license */ -import {isDevMode} from '../application_ref'; import {looseIdentical} from '../util'; import {BindingDef, BindingType, DebugContext, NodeData, NodeDef, NodeFlags, RootData, Services, TextData, ViewData, ViewFlags, asElementData, asTextData} from './types'; -import {checkAndUpdateBinding, getParentRenderElement, sliceErrorStack} from './util'; +import {checkAndUpdateBinding, getParentRenderElement} from './util'; export function textDef(ngContentIndex: number, constants: string[]): NodeDef { - // skip the call to sliceErrorStack itself + the call to this function. - const source = isDevMode() ? sliceErrorStack(2, 3) : ''; const bindings: BindingDef[] = new Array(constants.length - 1); for (let i = 1; i < constants.length; i++) { bindings[i - 1] = { @@ -46,7 +43,7 @@ export function textDef(ngContentIndex: number, constants: string[]): NodeDef { outputs: [], element: undefined, provider: undefined, - text: {prefix: constants[0], source}, + text: {prefix: constants[0]}, query: undefined, ngContent: undefined }; diff --git a/packages/core/src/view/types.ts b/packages/core/src/view/types.ts index c46fa05b22..ba98e333ce 100644 --- a/packages/core/src/view/types.ts +++ b/packages/core/src/view/types.ts @@ -22,6 +22,7 @@ import {Sanitizer, SecurityContext} from '../security'; // ------------------------------------- export interface ViewDefinition { + factory: ViewDefinitionFactory; flags: ViewFlags; updateDirectives: ViewUpdateFn; updateRenderer: ViewUpdateFn; @@ -45,9 +46,22 @@ export interface ViewDefinition { nodeMatchedQueries: number; } -export type ViewDefinitionFactory = () => ViewDefinition; +/** + * Factory for ViewDefinitions. + * We use a function so we can reexeute it in case an error happens and use the given logger + * function to log the error from the definition of the node, which is shown in all browser + * logs. + */ +export interface ViewDefinitionFactory { (logger: NodeLogger): ViewDefinition; } -export type ViewUpdateFn = (check: NodeCheckFn, view: ViewData) => void; +/** + * Function to call console.error at the right source location. This is an indirection + * via another function as browser will log the location that actually called + * `console.error`. + */ +export interface NodeLogger { (): () => void; } + +export interface ViewUpdateFn { (check: NodeCheckFn, view: ViewData): void; } // helper functions to create an overloaded function type. export interface NodeCheckFn { @@ -57,11 +71,12 @@ export interface NodeCheckFn { v3?: any, v4?: any, v5?: any, v6?: any, v7?: any, v8?: any, v9?: any): any; } -export type ViewHandleEventFn = - (view: ViewData, nodeIndex: number, eventName: string, event: any) => boolean; - export const enum ArgumentType {Inline, Dynamic} +export interface ViewHandleEventFn { + (view: ViewData, nodeIndex: number, eventName: string, event: any): boolean; +} + /** * Bitmask for ViewDefintion.flags. */ @@ -221,11 +236,10 @@ export interface ElementDef { * that are located on this element. */ allProviders: {[tokenKey: string]: NodeDef}; - source: string; handleEvent: ElementHandleEventFn; } -export type ElementHandleEventFn = (view: ViewData, eventName: string, event: any) => boolean; +export interface ElementHandleEventFn { (view: ViewData, eventName: string, event: any): boolean; } export interface ProviderDef { token: any; @@ -250,10 +264,7 @@ export const enum DepFlags { Value = 2 << 2, } -export interface TextDef { - prefix: string; - source: string; -} +export interface TextDef { prefix: string; } export interface QueryDef { id: number; @@ -318,7 +329,7 @@ export const enum ViewState { Destroyed = 1 << 3 } -export type DisposableFn = () => void; +export interface DisposableFn { (): void; } /** * Node instance data. @@ -428,9 +439,9 @@ export abstract class DebugContext { abstract get providerTokens(): any[]; abstract get references(): {[key: string]: any}; abstract get context(): any; - abstract get source(): string; abstract get componentRenderElement(): any; abstract get renderNode(): any; + abstract logError(console: Console, ...values: any[]): void; } // ------------------------------------- diff --git a/packages/core/src/view/util.ts b/packages/core/src/view/util.ts index 8b89bdbca8..8df0fcdb8d 100644 --- a/packages/core/src/view/util.ts +++ b/packages/core/src/view/util.ts @@ -6,7 +6,6 @@ * found in the LICENSE file at https://angular.io/license */ -import {isDevMode} from '../application_ref'; import {WrappedValue, devModeEqual} from '../change_detection/change_detection'; import {SimpleChange} from '../change_detection/change_detection_util'; import {Injector} from '../di'; @@ -18,7 +17,9 @@ import {Renderer, RendererType2} from '../render/api'; import {looseIdentical, stringify} from '../util'; import {expressionChangedAfterItHasBeenCheckedError, isViewDebugError, viewDestroyedError, viewWrappedDebugError} from './errors'; -import {DebugContext, ElementData, NodeData, NodeDef, NodeFlags, QueryValueType, Services, ViewData, ViewDefinition, ViewDefinitionFactory, ViewFlags, ViewState, asElementData, asProviderData, asTextData} from './types'; +import {DebugContext, ElementData, NodeData, NodeDef, NodeFlags, NodeLogger, QueryValueType, Services, ViewData, ViewDefinition, ViewDefinitionFactory, ViewFlags, ViewState, asElementData, asProviderData, asTextData} from './types'; + +export const NOOP: any = () => {}; const _tokenKeyCache = new Map(); @@ -194,29 +195,13 @@ const VIEW_DEFINITION_CACHE = new WeakMap(); export function resolveViewDefinition(factory: ViewDefinitionFactory): ViewDefinition { let value: ViewDefinition = VIEW_DEFINITION_CACHE.get(factory); if (!value) { - value = factory(); + value = factory(() => NOOP); + value.factory = factory; VIEW_DEFINITION_CACHE.set(factory, value); } return value; } -export function sliceErrorStack(start: number, end: number): string { - let err: any; - try { - throw new Error(); - } catch (e) { - err = e; - } - const stack = err.stack || ''; - const lines = stack.split('\n'); - if (lines[0].startsWith('Error')) { - // Chrome always adds the message to the stack as well... - start++; - end++; - } - return lines.slice(start, end).join('\n'); -} - export function rootRenderNodes(view: ViewData): any[] { const renderNodes: any[] = []; visitRootRenderNodes(view, RenderNodeAction.Collect, undefined, undefined, renderNodes); diff --git a/packages/core/src/view/view.ts b/packages/core/src/view/view.ts index 26aaa08723..4cd51be2d8 100644 --- a/packages/core/src/view/view.ts +++ b/packages/core/src/view/view.ts @@ -18,9 +18,7 @@ import {checkAndUpdateQuery, createQuery, queryDef} from './query'; import {createTemplateData, createViewContainerData} from './refs'; import {checkAndUpdateTextDynamic, checkAndUpdateTextInline, createText} from './text'; import {ArgumentType, CheckType, ElementData, ElementDef, NodeData, NodeDef, NodeFlags, ProviderData, ProviderDef, RootData, Services, TextDef, ViewData, ViewDefinition, ViewDefinitionFactory, ViewFlags, ViewHandleEventFn, ViewState, ViewUpdateFn, asElementData, asProviderData, asPureExpressionData, asQueryList, asTextData} from './types'; -import {checkBindingNoChanges, isComponentView, resolveViewDefinition, viewParentEl} from './util'; - -const NOOP = (): any => undefined; +import {NOOP, checkBindingNoChanges, isComponentView, resolveViewDefinition, viewParentEl} from './util'; export function viewDef( flags: ViewFlags, nodes: NodeDef[], updateDirectives?: ViewUpdateFn, @@ -137,6 +135,8 @@ export function viewDef( const handleEvent: ViewHandleEventFn = (view, nodeIndex, eventName, event) => nodes[nodeIndex].element.handleEvent(view, eventName, event); return { + // Will be filled later... + factory: undefined, nodeFlags: viewNodeFlags, rootNodeFlags: viewRootNodeFlags, nodeMatchedQueries: viewMatchedQueries, flags, diff --git a/packages/core/test/application_ref_spec.ts b/packages/core/test/application_ref_spec.ts index fd3c48c7ff..bd00965a65 100644 --- a/packages/core/test/application_ref_spec.ts +++ b/packages/core/test/application_ref_spec.ts @@ -172,7 +172,7 @@ export function main() { ])) .then(() => expect(false).toBe(true), (e) => { expect(e).toBe('Test'); - expect(mockConsole.res).toEqual(['EXCEPTION: Test']); + expect(mockConsole.res[0].join('#')).toEqual('ERROR#Test'); }); })); @@ -213,7 +213,7 @@ export function main() { const expectedErrMsg = `The module MyModule was bootstrapped, but it does not declare "@NgModule.bootstrap" components nor a "ngDoBootstrap" method. Please define one of these.`; expect(e.message).toEqual(expectedErrMsg); - expect(mockConsole.res[0]).toEqual('EXCEPTION: ' + expectedErrMsg); + expect(mockConsole.res[0].join('#')).toEqual('ERROR#Error: ' + expectedErrMsg); }); })); @@ -269,7 +269,7 @@ export function main() { defaultPlatform.bootstrapModuleFactory(moduleFactory) .then(() => expect(false).toBe(true), (e) => { expect(e).toBe('Test'); - expect(mockConsole.res).toEqual(['EXCEPTION: Test']); + expect(mockConsole.res[0].join('#')).toEqual('ERROR#Test'); }); })); }); @@ -509,7 +509,7 @@ export function main() { } class MockConsole { - res: any[] = []; - log(s: any): void { this.res.push(s); } - error(s: any): void { this.res.push(s); } + res: any[][] = []; + log(...args: any[]): void { this.res.push(args); } + error(...args: any[]): void { this.res.push(args); } } diff --git a/packages/core/test/error_handler_spec.ts b/packages/core/test/error_handler_spec.ts index 899313e9cf..d63366884a 100644 --- a/packages/core/test/error_handler_spec.ts +++ b/packages/core/test/error_handler_spec.ts @@ -6,13 +6,13 @@ * found in the LICENSE file at https://angular.io/license */ -import {ERROR_DEBUG_CONTEXT, ERROR_TYPE} from '@angular/core/src/errors'; +import {ERROR_DEBUG_CONTEXT, ERROR_LOGGER, ERROR_TYPE} from '@angular/core/src/errors'; import {ErrorHandler, wrappedError} from '../src/error_handler'; class MockConsole { - res: any[] = []; - error(s: any): void { this.res.push(s); } + res: any[][] = []; + error(...s: any[]): void { this.res.push(s); } } export function main() { @@ -21,15 +21,7 @@ export function main() { const errorHandler = new ErrorHandler(false); errorHandler._console = logger as any; errorHandler.handleError(error); - return logger.res.join('\n'); - } - - function getStack(error: Error): string { - try { - throw error; - } catch (e) { - return e.stack; - } + return logger.res.map(line => line.join('#')).join('\n'); } describe('ErrorHandler', () => { @@ -38,33 +30,15 @@ export function main() { expect(e).toContain('message!'); }); - it('should output stackTrace', () => { - const error = new Error('message!'); - const stack = getStack(error); - if (stack) { - const e = errorToString(error); - expect(e).toContain(stack); - } - }); - describe('context', () => { it('should print nested context', () => { const cause = new Error('message!'); - const stack = getStack(cause); const context = { source: 'context!', toString() { return 'Context'; } } as any; - const original = viewWrappedError(cause, context); + const original = debugError(cause, context); const e = errorToString(wrappedError('message', original)); - expect(e).toEqual( - stack ? `EXCEPTION: message caused by: Error in context! caused by: message! -ORIGINAL EXCEPTION: message! -ORIGINAL STACKTRACE: -${stack} -ERROR CONTEXT: -Context` : - `EXCEPTION: message caused by: Error in context! caused by: message! -ORIGINAL EXCEPTION: message! -ERROR CONTEXT: -Context`); + expect(e).toEqual(`ERROR#Error: message caused by: Error in context! caused by: message! +ORIGINAL ERROR#Error: message! +ERROR CONTEXT#Context`); }); }); @@ -84,23 +58,25 @@ Context`); }); }); - describe('original stack', () => { - it('should print original stack if available', () => { - const realOriginal = new Error('inner'); - const stack = getStack(realOriginal); - if (stack) { - const original = wrappedError('wrapped', realOriginal); - const e = errorToString(wrappedError('wrappedwrapped', original)); - expect(e).toContain(stack); - } - }); + it('should use the error logger on the error', () => { + const err = new Error('test'); + const console = new MockConsole(); + const errorHandler = new ErrorHandler(false); + errorHandler._console = console as any; + const logger = jasmine.createSpy('logger'); + (err as any)[ERROR_LOGGER] = logger; + + errorHandler.handleError(err); + + expect(console.res).toEqual([]); + expect(logger).toHaveBeenCalledWith(console, 'ERROR', err); }); }); } -function viewWrappedError(originalError: any, context: any): Error { +function debugError(originalError: any, context: any): Error { const error = wrappedError(`Error in ${context.source}`, originalError); (error as any)[ERROR_DEBUG_CONTEXT] = context; - (error as any)[ERROR_TYPE] = viewWrappedError; + (error as any)[ERROR_TYPE] = debugError; return error; } diff --git a/packages/core/test/linker/integration_spec.ts b/packages/core/test/linker/integration_spec.ts index 9060280477..026ad9aa8d 100644 --- a/packages/core/test/linker/integration_spec.ts +++ b/packages/core/test/linker/integration_spec.ts @@ -1414,8 +1414,8 @@ function declareTests({useJit}: {useJit: boolean}) { TestBed.createComponent(MyComp); throw 'Should throw'; } catch (e) { - expect(e.message).toEqual( - `Template parse errors:\nCan't bind to 'unknown' since it isn't a known property of 'div'. ("
]unknown="{{ctxProp}}">
"): MyComp@0:5`); + expect(e.message).toMatch( + /Template parse errors:\nCan't bind to 'unknown' since it isn't a known property of 'div'. \("
\]unknown="{{ctxProp}}"><\/div>"\): .*MyComp.html@0:5/); } }); diff --git a/packages/core/test/linker/source_map_integration_node_only_spec.ts b/packages/core/test/linker/source_map_integration_node_only_spec.ts new file mode 100644 index 0000000000..bdcb4d4b6e --- /dev/null +++ b/packages/core/test/linker/source_map_integration_node_only_spec.ts @@ -0,0 +1,222 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {ResourceLoader} from '@angular/compiler'; +import {SourceMap} from '@angular/compiler/src/output/source_map'; +import {extractSourceMap, originalPositionFor} from '@angular/compiler/test/output/source_map_util'; +import {MockResourceLoader} from '@angular/compiler/testing/src/resource_loader_mock'; +import {Component, Directive, ɵglobal} from '@angular/core'; +import {getErrorLogger} from '@angular/core/src/errors'; +import {ComponentFixture, TestBed, fakeAsync, tick} from '@angular/core/testing'; + +export function main() { + describe('jit source mapping', () => { + let jitSpy: jasmine.Spy; + let resourceLoader: MockResourceLoader; + + beforeEach(() => { + jitSpy = spyOn(ɵglobal, 'Function').and.callThrough(); + resourceLoader = new MockResourceLoader(); + TestBed.configureCompiler({providers: [{provide: ResourceLoader, useValue: resourceLoader}]}); + }); + + function getErrorLoggerStack(e: Error): string { + let logStack: string; + getErrorLogger(e)({error: () => logStack = new Error().stack}, e.message); + return logStack; + } + + function getSourceMap(genFile: string): SourceMap { + const jitSources = jitSpy.calls.all().map((call) => call.args[call.args.length - 1]); + return jitSources.map(source => extractSourceMap(source)) + .find(map => map && map.file === genFile); + } + + function getSourcePositionForStack(stack: string): + {source: string, line: number, column: number} { + const ngFactoryLocations = + stack + .split('\n') + // e.g. at View_MyComp_0 (ng:///DynamicTestModule/MyComp.ngfactory.js:153:40) + .map(line => /\((.*\.ngfactory\.js):(\d+):(\d+)/.exec(line)) + .filter(match => !!match) + .map(match => ({ + file: match[1], + line: parseInt(match[2], 10), + column: parseInt(match[3], 10) + })); + const ngFactoryLocation = ngFactoryLocations[0]; + + const sourceMap = getSourceMap(ngFactoryLocation.file); + return originalPositionFor( + sourceMap, {line: ngFactoryLocation.line, column: ngFactoryLocation.column}); + } + + function compileAndCreateComponent(comType: any) { + TestBed.configureTestingModule({declarations: [comType]}); + + let error: any; + TestBed.compileComponents().catch((e) => error = e); + if (resourceLoader.hasPendingRequests()) { + resourceLoader.flush(); + } + tick(); + if (error) { + throw error; + } + return TestBed.createComponent(comType); + } + + describe('inline templates', () => { + const templateUrl = 'ng:///DynamicTestModule/MyComp.html'; + + function templateDecorator(template: string) { return {template}; } + + declareTests({templateUrl, templateDecorator}); + }); + + describe('external templates', () => { + const templateUrl = 'http://localhost:1234:some/url.html'; + + function templateDecorator(template: string) { + resourceLoader.expect(templateUrl, template); + return {templateUrl}; + } + + declareTests({templateUrl, templateDecorator}); + }); + + function declareTests({templateUrl, templateDecorator}: { + templateUrl: string, + templateDecorator: (template: string) => { [key: string]: any } + }) { + it('should use the right source url in html parse errors', fakeAsync(() => { + @Component({...templateDecorator('
\n ')}) + class MyComp { + } + + expect(() => compileAndCreateComponent(MyComp)) + .toThrowError(new RegExp( + `Template parse errors[\\s\\S]*${templateUrl.replace('$', '\\$')}@1:2`)); + })); + + it('should use the right source url in template parse errors', fakeAsync(() => { + @Component({...templateDecorator('
\n
')}) + class MyComp { + } + + expect(() => compileAndCreateComponent(MyComp)) + .toThrowError(new RegExp( + `Template parse errors[\\s\\S]*${templateUrl.replace('$', '\\$')}@1:7`)); + })); + + it('should create a sourceMap for templates', fakeAsync(() => { + const template = `Hello World!`; + + @Component({...templateDecorator(template)}) + class MyComp { + } + + compileAndCreateComponent(MyComp); + + const sourceMap = getSourceMap('ng:///DynamicTestModule/MyComp.ngfactory.js'); + expect(sourceMap.sources).toEqual([templateUrl]); + expect(sourceMap.sourcesContent).toEqual([template]); + })); + + + it('should report source location for di errors', fakeAsync(() => { + const template = `
\n
`; + + @Component({...templateDecorator(template)}) + class MyComp { + } + + @Directive({selector: '[someDir]'}) + class SomeDir { + constructor() { throw new Error('Test'); } + } + + TestBed.configureTestingModule({declarations: [SomeDir]}); + let error: any; + try { + compileAndCreateComponent(MyComp); + } catch (e) { + error = e; + } + // The error should be logged from the element + expect(getSourcePositionForStack(getErrorLoggerStack(error))).toEqual({ + line: 2, + column: 4, + source: templateUrl, + }); + })); + + it('should report source location for binding errors', fakeAsync(() => { + const template = `
\n
`; + + @Component({...templateDecorator(template)}) + class MyComp { + createError() { throw new Error('Test'); } + } + + const comp = compileAndCreateComponent(MyComp); + + let error: any; + try { + comp.detectChanges(); + } catch (e) { + error = e; + } + // the stack should point to the binding + expect(getSourcePositionForStack(error.stack)).toEqual({ + line: 2, + column: 12, + source: templateUrl, + }); + // The error should be logged from the element + expect(getSourcePositionForStack(getErrorLoggerStack(error))).toEqual({ + line: 2, + column: 4, + source: templateUrl, + }); + })); + + it('should report source location for event errors', fakeAsync(() => { + const template = `
\n
`; + + @Component({...templateDecorator(template)}) + class MyComp { + createError() { throw new Error('Test'); } + } + + const comp = compileAndCreateComponent(MyComp); + + let error: any; + try { + comp.debugElement.children[0].children[0].triggerEventHandler('click', 'EVENT'); + } catch (e) { + error = e; + } + // the stack should point to the binding + expect(getSourcePositionForStack(error.stack)).toEqual({ + line: 2, + column: 12, + source: templateUrl, + }); + // The error should be logged from the element + expect(getSourcePositionForStack(getErrorLoggerStack(error))).toEqual({ + line: 2, + column: 4, + source: templateUrl, + }); + + })); + } + }); +} diff --git a/packages/core/test/linker/view_injector_integration_spec.ts b/packages/core/test/linker/view_injector_integration_spec.ts index 6b947b6059..36a46e1928 100644 --- a/packages/core/test/linker/view_injector_integration_spec.ts +++ b/packages/core/test/linker/view_injector_integration_spec.ts @@ -470,7 +470,7 @@ export function main() { TestBed.configureTestingModule({declarations: [CycleDirective]}); expect(() => createComponent('
')) .toThrowError( - 'Template parse errors:\nCannot instantiate cyclic dependency! CycleDirective ("[ERROR ->]
"): TestComp@0:0'); + /Template parse errors:\nCannot instantiate cyclic dependency! CycleDirective \("\[ERROR ->\]
<\/div>"\): .*TestComp.html@0:0/); }); it('should not instantiate a directive in a view that has a host dependency on providers' + @@ -485,7 +485,7 @@ export function main() { expect(() => createComponent('
')) .toThrowError( - `Template parse errors:\nNo provider for service ("[ERROR ->]
"): SimpleComponent@0:0`); + /Template parse errors:\nNo provider for service \("\[ERROR ->\]
"\): .*SimpleComponent.html@0:0/); }); it('should not instantiate a directive in a view that has a host dependency on providers' + @@ -501,7 +501,7 @@ export function main() { expect(() => createComponent('
')) .toThrowError( - `Template parse errors:\nNo provider for service ("[ERROR ->]
"): SimpleComponent@0:0`); + /Template parse errors:\nNo provider for service \("\[ERROR ->\]
"\): .*SimpleComponent.html@0:0/); }); it('should not instantiate a directive in a view that has a self dependency on a parent directive', @@ -512,7 +512,7 @@ export function main() { () => createComponent('
')) .toThrowError( - `Template parse errors:\nNo provider for SimpleDirective ("
[ERROR ->]
"): TestComp@0:21`); + /Template parse errors:\nNo provider for SimpleDirective \("
\[ERROR ->\]
<\/div><\/div>"\): .*TestComp.html@0:21/); }); it('should instantiate directives that depend on other directives', fakeAsync(() => { @@ -560,7 +560,7 @@ export function main() { SimpleComponent, {set: {template: '
'}}); expect(() => createComponent('
')) .toThrowError( - `Template parse errors:\nNo provider for SimpleDirective ("[ERROR ->]
"): SimpleComponent@0:0`); + /Template parse errors:\nNo provider for SimpleDirective \("\[ERROR ->\]
<\/div>"\): .*SimpleComponent.html@0:0/); }); }); diff --git a/packages/core/test/view/services_spec.ts b/packages/core/test/view/services_spec.ts index 9cbf8c911c..66c813a135 100644 --- a/packages/core/test/view/services_spec.ts +++ b/packages/core/test/view/services_spec.ts @@ -58,7 +58,6 @@ export function main() { expect(debugCtx.component).toBe(compView.component); expect(debugCtx.context).toBe(compView.context); expect(debugCtx.providerTokens).toEqual([AService]); - expect(debugCtx.source).toBeTruthy(); expect(debugCtx.references['ref'].nativeElement) .toBe(asElementData(compView, 0).renderElement); }); @@ -74,7 +73,6 @@ export function main() { expect(debugCtx.injector.get(AComp)).toBe(compView.component); expect(debugCtx.component).toBe(compView.component); expect(debugCtx.context).toBe(compView.context); - expect(debugCtx.source).toBeTruthy(); }); it('should provide data for other nodes based on the nearest element parent', () => { @@ -85,6 +83,7 @@ export function main() { expect(debugCtx.renderNode).toBe(asElementData(compView, 0).renderElement); }); + }); }); } diff --git a/packages/platform-browser/test/browser/bootstrap_spec.ts b/packages/platform-browser/test/browser/bootstrap_spec.ts index 27a83bcc30..059cb997c7 100644 --- a/packages/platform-browser/test/browser/bootstrap_spec.ts +++ b/packages/platform-browser/test/browser/bootstrap_spec.ts @@ -98,8 +98,8 @@ class HelloCmpUsingCustomElement { } class MockConsole { - res: any[] = []; - error(s: any): void { this.res.push(s); } + res: any[][] = []; + error(...s: any[]): void { this.res.push(s); } } @@ -208,8 +208,8 @@ export function main() { const refPromise = bootstrap(NonExistentComp, [{provide: ErrorHandler, useValue: errorHandler}]); refPromise.then(null, (reason) => { - expect(logger.res.join('')) - .toContain('The selector "non-existent" did not match any elements'); + expect(logger.res[0].join('#')) + .toContain('ERROR#Error: The selector "non-existent" did not match any elements'); async.done(); return null; }); diff --git a/packages/upgrade/test/static/integration/upgrade_component_spec.ts b/packages/upgrade/test/static/integration/upgrade_component_spec.ts index f7f6c2e6a5..b1690b7126 100644 --- a/packages/upgrade/test/static/integration/upgrade_component_spec.ts +++ b/packages/upgrade/test/static/integration/upgrade_component_spec.ts @@ -1537,24 +1537,21 @@ export function main() { const elementC = html(``); bootstrap(platformBrowserDynamic(), Ng2Module, elementA, ng1Module).then(() => { - expect(mockExceptionHandler).toHaveBeenCalledWith(jasmine.objectContaining({ - ngOriginalError: new Error( - 'Unable to find required \'iDoNotExist\' in upgraded directive \'ng1A\'.') - })); + expect(mockExceptionHandler) + .toHaveBeenCalledWith(new Error( + 'Unable to find required \'iDoNotExist\' in upgraded directive \'ng1A\'.')); }); bootstrap(platformBrowserDynamic(), Ng2Module, elementB, ng1Module).then(() => { - expect(mockExceptionHandler).toHaveBeenCalledWith(jasmine.objectContaining({ - ngOriginalError: new Error( - 'Unable to find required \'^iDoNotExist\' in upgraded directive \'ng1B\'.') - })); + expect(mockExceptionHandler) + .toHaveBeenCalledWith(new Error( + 'Unable to find required \'^iDoNotExist\' in upgraded directive \'ng1B\'.')); }); bootstrap(platformBrowserDynamic(), Ng2Module, elementC, ng1Module).then(() => { - expect(mockExceptionHandler).toHaveBeenCalledWith(jasmine.objectContaining({ - ngOriginalError: new Error( - 'Unable to find required \'^^iDoNotExist\' in upgraded directive \'ng1C\'.') - })); + expect(mockExceptionHandler) + .toHaveBeenCalledWith(new Error( + 'Unable to find required \'^^iDoNotExist\' in upgraded directive \'ng1C\'.')); }); })); diff --git a/tools/public_api_guard/core/typings/core.d.ts b/tools/public_api_guard/core/typings/core.d.ts index 5a933270d3..059e5d9be7 100644 --- a/tools/public_api_guard/core/typings/core.d.ts +++ b/tools/public_api_guard/core/typings/core.d.ts @@ -302,7 +302,7 @@ export declare class DebugElement extends DebugNode { styles: { [key: string]: string; }; - constructor(nativeNode: any, parent: any, _debugInfo: RenderDebugInfo); + constructor(nativeNode: any, parent: any, _debugContext: DebugContext); addChild(child: DebugNode): void; insertBefore(refChild: DebugNode, newChild: DebugNode): void; insertChildrenAfter(child: DebugNode, newChildren: DebugNode[]): void; @@ -325,8 +325,8 @@ export declare class DebugNode { readonly references: { [key: string]: any; }; - readonly source: string; - constructor(nativeNode: any, parent: DebugNode, _debugInfo: RenderDebugInfo); + /** @deprecated */ readonly source: string; + constructor(nativeNode: any, parent: DebugNode, _debugContext: DebugContext); } /** @deprecated */