diff --git a/packages/compiler/src/identifiers.ts b/packages/compiler/src/identifiers.ts index 08bd1adf41..383dae80fd 100644 --- a/packages/compiler/src/identifiers.ts +++ b/packages/compiler/src/identifiers.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import {ANALYZE_FOR_ENTRY_COMPONENTS, ChangeDetectionStrategy, ChangeDetectorRef, ComponentFactory, ComponentFactoryResolver, ComponentRef, ElementRef, Injector, LOCALE_ID, NgModuleFactory, NgModuleRef, QueryList, Renderer, SecurityContext, TRANSLATIONS_FORMAT, TemplateRef, ViewContainerRef, ViewEncapsulation, ɵCodegenComponentFactoryResolver, ɵEMPTY_ARRAY, ɵEMPTY_MAP, ɵNgModuleInjector, ɵand, ɵccf, ɵcrt, ɵdid, ɵeld, ɵinlineInterpolate, ɵinterpolate, ɵncd, ɵnov, ɵpad, ɵpid, ɵpod, ɵppd, ɵprd, ɵqud, ɵreflector, ɵregisterModuleFactory, ɵted, ɵunv, ɵvid} from '@angular/core'; +import {ANALYZE_FOR_ENTRY_COMPONENTS, ChangeDetectionStrategy, ChangeDetectorRef, ComponentFactory, ComponentFactoryResolver, ComponentRef, ElementRef, Injector, LOCALE_ID, NgModuleFactory, NgModuleRef, QueryList, Renderer, SecurityContext, TRANSLATIONS_FORMAT, TemplateRef, ViewContainerRef, ViewEncapsulation, ɵCodegenComponentFactoryResolver, ɵEMPTY_ARRAY, ɵEMPTY_MAP, ɵand, ɵccf, ɵcmf, ɵcrt, ɵdid, ɵeld, ɵinlineInterpolate, ɵinterpolate, ɵmod, ɵmpd, ɵncd, ɵnov, ɵpad, ɵpid, ɵpod, ɵppd, ɵprd, ɵqud, ɵreflector, ɵregisterModuleFactory, ɵted, ɵunv, ɵvid} from '@angular/core'; import {CompileIdentifierMetadata, CompileTokenMetadata} from './compile_metadata'; @@ -48,10 +48,20 @@ export class Identifiers { IdentifierSpec = {name: 'ComponentRef', moduleUrl: CORE, runtime: ComponentRef}; static NgModuleFactory: IdentifierSpec = {name: 'NgModuleFactory', moduleUrl: CORE, runtime: NgModuleFactory}; - static NgModuleInjector: IdentifierSpec = { - name: 'ɵNgModuleInjector', + static createModuleFactory: IdentifierSpec = { + name: 'ɵcmf', moduleUrl: CORE, - runtime: ɵNgModuleInjector, + runtime: ɵcmf, + }; + static moduleDef: IdentifierSpec = { + name: 'ɵmod', + moduleUrl: CORE, + runtime: ɵmod, + }; + static moduleProviderDef: IdentifierSpec = { + name: 'ɵmpd', + moduleUrl: CORE, + runtime: ɵmpd, }; static RegisterModuleFactoryFn: IdentifierSpec = { name: 'ɵregisterModuleFactory', diff --git a/packages/compiler/src/ng_module_compiler.ts b/packages/compiler/src/ng_module_compiler.ts index 0bb5e13e8c..bf34ddd7c3 100644 --- a/packages/compiler/src/ng_module_compiler.ts +++ b/packages/compiler/src/ng_module_compiler.ts @@ -6,67 +6,58 @@ * found in the LICENSE file at https://angular.io/license */ -import {ɵLifecycleHooks} from '@angular/core'; +import {ɵNodeFlags as NodeFlags} from '@angular/core'; -import {CompileDiDependencyMetadata, CompileIdentifierMetadata, CompileNgModuleMetadata, CompileProviderMetadata, CompileTokenMetadata, identifierModuleUrl, identifierName, tokenName, tokenReference} from './compile_metadata'; -import {Identifiers, createIdentifier, resolveIdentifier} from './identifiers'; +import {CompileNgModuleMetadata, CompileProviderMetadata, identifierName} from './compile_metadata'; +import {Identifiers, createIdentifier} from './identifiers'; 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, typeSourceSpan} from './parse_util'; +import {typeSourceSpan} from './parse_util'; import {NgModuleProviderAnalyzer} from './provider_analyzer'; -import {ProviderAst} from './template_parser/template_ast'; - - -/** - * This is currently not read, but will probably be used in the future. - * We keep it as we already pass it through all the rigth places... - */ -export class ComponentFactoryDependency { - constructor(public compType: any) {} -} +import {componentFactoryResolverProviderDef, depDef, providerDef} from './view_compiler/provider_compiler'; export class NgModuleCompileResult { - constructor( - public statements: o.Statement[], public ngModuleFactoryVar: string, - public dependencies: ComponentFactoryDependency[]) {} + constructor(public statements: o.Statement[], public ngModuleFactoryVar: string) {} } +const LOG_VAR = o.variable('_l'); + @CompilerInjectable() export class NgModuleCompiler { compile(ngModuleMeta: CompileNgModuleMetadata, extraProviders: CompileProviderMetadata[]): NgModuleCompileResult { const sourceSpan = typeSourceSpan('NgModule', ngModuleMeta.type); - const deps: ComponentFactoryDependency[] = []; - const bootstrapComponentFactories: CompileIdentifierMetadata[] = []; - const entryComponentFactories = - ngModuleMeta.transitiveModule.entryComponents.map((entryComponent) => { - if (ngModuleMeta.bootstrapComponents.some( - (id) => id.reference === entryComponent.componentType)) { - bootstrapComponentFactories.push({reference: entryComponent.componentFactory}); - } - deps.push(new ComponentFactoryDependency(entryComponent.componentType)); - return {reference: entryComponent.componentFactory}; - }); - const builder = new _InjectorBuilder( - ngModuleMeta, entryComponentFactories, bootstrapComponentFactories, sourceSpan); - + const entryComponentFactories = ngModuleMeta.transitiveModule.entryComponents; + const bootstrapComponents = ngModuleMeta.bootstrapComponents; const providerParser = new NgModuleProviderAnalyzer(ngModuleMeta, extraProviders, sourceSpan); - providerParser.parse().forEach((provider) => builder.addProvider(provider)); - const injectorClass = builder.build(); + const providerDefs = + [componentFactoryResolverProviderDef(NodeFlags.None, entryComponentFactories)] + .concat(providerParser.parse().map((provider) => providerDef(provider))) + .map(({providerExpr, depsExpr, flags, tokenExpr}) => { + return o.importExpr(createIdentifier(Identifiers.moduleProviderDef)).callFn([ + o.literal(flags), tokenExpr, providerExpr, depsExpr + ]); + }); + + const ngModuleDef = + o.importExpr(createIdentifier(Identifiers.moduleDef)).callFn([o.literalArr(providerDefs)]); + const ngModuleDefFactory = o.fn( + [new o.FnParam(LOG_VAR.name !)], [new o.ReturnStatement(ngModuleDef)], o.INFERRED_TYPE); + const ngModuleFactoryVar = `${identifierName(ngModuleMeta.type)}NgFactory`; const ngModuleFactoryStmt = o.variable(ngModuleFactoryVar) - .set(o.importExpr(createIdentifier(Identifiers.NgModuleFactory)) - .instantiate( - [o.variable(injectorClass.name), o.importExpr(ngModuleMeta.type)], - o.importType( - createIdentifier(Identifiers.NgModuleFactory), - [o.importType(ngModuleMeta.type) !], [o.TypeModifier.Const]))) - .toDeclStmt(null, [o.StmtModifier.Final]); + .set(o.importExpr(createIdentifier(Identifiers.createModuleFactory)).callFn([ + o.importExpr(ngModuleMeta.type), + o.literalArr(bootstrapComponents.map(id => o.importExpr(id))), ngModuleDefFactory + ])) + .toDeclStmt( + o.importType( + createIdentifier(Identifiers.NgModuleFactory), + [o.importType(ngModuleMeta.type) !], [o.TypeModifier.Const]), + [o.StmtModifier.Final]); - const stmts: o.Statement[] = [injectorClass, ngModuleFactoryStmt]; + const stmts: o.Statement[] = [ngModuleFactoryStmt]; if (ngModuleMeta.id) { const registerFactoryStmt = o.importExpr(createIdentifier(Identifiers.RegisterModuleFactoryFn)) @@ -75,185 +66,6 @@ export class NgModuleCompiler { stmts.push(registerFactoryStmt); } - return new NgModuleCompileResult(stmts, ngModuleFactoryVar, deps); + return new NgModuleCompileResult(stmts, ngModuleFactoryVar); } } - -class _InjectorBuilder implements ClassBuilder { - fields: o.ClassField[] = []; - getters: o.ClassGetter[] = []; - methods: o.ClassMethod[] = []; - ctorStmts: o.Statement[] = []; - private _lazyProps = new Map(); - private _tokens: CompileTokenMetadata[] = []; - private _instances = new Map(); - private _createStmts: o.Statement[] = []; - private _destroyStmts: o.Statement[] = []; - - constructor( - private _ngModuleMeta: CompileNgModuleMetadata, - private _entryComponentFactories: CompileIdentifierMetadata[], - private _bootstrapComponentFactories: CompileIdentifierMetadata[], - private _sourceSpan: ParseSourceSpan) {} - - addProvider(resolvedProvider: ProviderAst) { - const providerValueExpressions = - resolvedProvider.providers.map((provider) => this._getProviderValue(provider)); - const propName = `_${tokenName(resolvedProvider.token)}_${this._instances.size}`; - const instance = this._createProviderProperty( - propName, resolvedProvider, providerValueExpressions, resolvedProvider.multiProvider, - resolvedProvider.eager); - if (resolvedProvider.lifecycleHooks.indexOf(ɵLifecycleHooks.OnDestroy) !== -1) { - let callNgOnDestroy: o.Expression = instance.callMethod('ngOnDestroy', []); - if (!resolvedProvider.eager) { - callNgOnDestroy = this._lazyProps.get(instance.name) !.and(callNgOnDestroy); - } - this._destroyStmts.push(callNgOnDestroy.toStmt()); - } - this._tokens.push(resolvedProvider.token); - this._instances.set(tokenReference(resolvedProvider.token), instance); - } - - build(): o.ClassStmt { - const getMethodStmts: o.Statement[] = this._tokens.map((token) => { - const providerExpr = this._instances.get(tokenReference(token)) !; - return new o.IfStmt( - InjectMethodVars.token.identical(createDiTokenExpression(token)), - [new o.ReturnStatement(providerExpr)]); - }); - const methods = [ - new o.ClassMethod( - 'createInternal', [], this._createStmts.concat(new o.ReturnStatement( - this._instances.get(this._ngModuleMeta.type.reference) !)), - o.importType(this._ngModuleMeta.type)), - new o.ClassMethod( - 'getInternal', - [ - new o.FnParam(InjectMethodVars.token.name !, o.DYNAMIC_TYPE), - new o.FnParam(InjectMethodVars.notFoundResult.name !, o.DYNAMIC_TYPE) - ], - getMethodStmts.concat([new o.ReturnStatement(InjectMethodVars.notFoundResult)]), - o.DYNAMIC_TYPE), - new o.ClassMethod('destroyInternal', [], this._destroyStmts), - ]; - - const parentArgs = [ - o.variable(InjectorProps.parent.name), - o.literalArr( - this._entryComponentFactories.map((componentFactory) => o.importExpr(componentFactory))), - o.literalArr(this._bootstrapComponentFactories.map( - (componentFactory) => o.importExpr(componentFactory))) - ]; - const injClassName = `${identifierName(this._ngModuleMeta.type)}Injector`; - return createClassStmt({ - name: injClassName, - ctorParams: [new o.FnParam( - InjectorProps.parent.name, o.importType(createIdentifier(Identifiers.Injector)))], - parent: o.importExpr( - createIdentifier(Identifiers.NgModuleInjector), - [o.importType(this._ngModuleMeta.type) !]), - parentArgs: parentArgs, - builders: [{methods}, this] - }); - } - - private _getProviderValue(provider: CompileProviderMetadata): o.Expression { - let result: o.Expression; - if (provider.useExisting != null) { - result = this._getDependency({token: provider.useExisting}); - } else if (provider.useFactory != null) { - const deps = provider.deps || provider.useFactory.diDeps; - const depsExpr = deps.map((dep) => this._getDependency(dep)); - result = o.importExpr(provider.useFactory).callFn(depsExpr); - } else if (provider.useClass != null) { - const deps = provider.deps || provider.useClass.diDeps; - const depsExpr = deps.map((dep) => this._getDependency(dep)); - result = - o.importExpr(provider.useClass).instantiate(depsExpr, o.importType(provider.useClass)); - } else { - result = convertValueToOutputAst(provider.useValue); - } - return result; - } - - - private _createProviderProperty( - propName: string, provider: ProviderAst, providerValueExpressions: o.Expression[], - isMulti: boolean, isEager: boolean): o.ReadPropExpr { - let resolvedProviderValueExpr: o.Expression; - let type: o.Type; - if (isMulti) { - resolvedProviderValueExpr = o.literalArr(providerValueExpressions); - type = new o.ArrayType(o.DYNAMIC_TYPE); - } else { - resolvedProviderValueExpr = providerValueExpressions[0]; - type = providerValueExpressions[0].type !; - } - if (!type) { - type = o.DYNAMIC_TYPE; - } - if (isEager) { - this.fields.push(new o.ClassField(propName, type)); - this._createStmts.push(o.THIS_EXPR.prop(propName).set(resolvedProviderValueExpr).toStmt()); - } else { - const internalFieldProp = o.THIS_EXPR.prop(`_${propName}`); - this.fields.push(new o.ClassField(internalFieldProp.name, type)); - // Note: Equals is important for JS so that it also checks the undefined case! - const getterStmts = [ - new o.IfStmt( - internalFieldProp.isBlank(), - [internalFieldProp.set(resolvedProviderValueExpr).toStmt()]), - new o.ReturnStatement(internalFieldProp) - ]; - this.getters.push(new o.ClassGetter(propName, getterStmts, type)); - this._lazyProps.set(propName, internalFieldProp); - } - return o.THIS_EXPR.prop(propName); - } - - private _getDependency(dep: CompileDiDependencyMetadata): o.Expression { - let result: o.Expression = null !; - if (dep.isValue) { - result = o.literal(dep.value); - } - if (!dep.isSkipSelf) { - if (dep.token) { - if (tokenReference(dep.token) === resolveIdentifier(Identifiers.Injector)) { - result = o.THIS_EXPR; - } else if ( - tokenReference(dep.token) === resolveIdentifier(Identifiers.ComponentFactoryResolver)) { - result = o.THIS_EXPR.prop('componentFactoryResolver'); - } - } - - if (!result) { - result = this._instances.get(tokenReference(dep.token !)) !; - } - } - if (!result) { - const args = [createDiTokenExpression(dep.token !)]; - if (dep.isOptional) { - args.push(o.NULL_EXPR); - } - result = InjectorProps.parent.callMethod('get', args); - } - return result; - } -} - -function createDiTokenExpression(token: CompileTokenMetadata): o.Expression { - if (token.value != null) { - return o.literal(token.value); - } else { - return o.importExpr(token.identifier !); - } -} - -class InjectorProps { - static parent = o.THIS_EXPR.prop('parent'); -} - -class InjectMethodVars { - static token = o.variable('token'); - static notFoundResult = o.variable('notFoundResult'); -} diff --git a/packages/compiler/src/provider_analyzer.ts b/packages/compiler/src/provider_analyzer.ts index 9a24b0dcb7..067fda08f3 100644 --- a/packages/compiler/src/provider_analyzer.ts +++ b/packages/compiler/src/provider_analyzer.ts @@ -96,7 +96,17 @@ export class ProviderElementContext { } get transformProviders(): ProviderAst[] { - return Array.from(this._transformedProviders.values()); + // Note: Maps keep their insertion order. + const lazyProviders: ProviderAst[] = []; + const eagerProviders: ProviderAst[] = []; + this._transformedProviders.forEach(provider => { + if (provider.eager) { + eagerProviders.push(provider); + } else { + lazyProviders.push(provider); + } + }); + return lazyProviders.concat(eagerProviders); } get transformedDirectiveAsts(): DirectiveAst[] { @@ -316,7 +326,17 @@ export class NgModuleProviderAnalyzer { const errorString = this._errors.join('\n'); throw new Error(`Provider parse errors:\n${errorString}`); } - return Array.from(this._transformedProviders.values()); + // Note: Maps keep their insertion order. + const lazyProviders: ProviderAst[] = []; + const eagerProviders: ProviderAst[] = []; + this._transformedProviders.forEach(provider => { + if (provider.eager) { + eagerProviders.push(provider); + } else { + lazyProviders.push(provider); + } + }); + return lazyProviders.concat(eagerProviders); } private _getOrCreateLocalProvider(token: CompileTokenMetadata, eager: boolean): ProviderAst|null { diff --git a/packages/compiler/src/view_compiler/provider_compiler.ts b/packages/compiler/src/view_compiler/provider_compiler.ts new file mode 100644 index 0000000000..4a3c32d547 --- /dev/null +++ b/packages/compiler/src/view_compiler/provider_compiler.ts @@ -0,0 +1,196 @@ +/** + * @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 {ɵDepFlags as DepFlags, ɵLifecycleHooks as LifecycleHooks, ɵNodeFlags as NodeFlags} from '@angular/core'; + +import {CompileDiDependencyMetadata, CompileEntryComponentMetadata, CompileProviderMetadata, CompileTokenMetadata} from '../compile_metadata'; +import {Identifiers, createIdentifier, createIdentifierToken, resolveIdentifier} from '../identifiers'; +import * as o from '../output/output_ast'; +import {convertValueToOutputAst} from '../output/value_util'; +import {ProviderAst, ProviderAstType} from '../template_parser/template_ast'; + +export function providerDef(providerAst: ProviderAst): { + providerExpr: o.Expression, + flags: NodeFlags, + depsExpr: o.Expression, + tokenExpr: o.Expression +} { + let flags = NodeFlags.None; + if (!providerAst.eager) { + flags |= NodeFlags.LazyProvider; + } + if (providerAst.providerType === ProviderAstType.PrivateService) { + flags |= NodeFlags.PrivateProvider; + } + providerAst.lifecycleHooks.forEach((lifecycleHook) => { + // for regular providers, we only support ngOnDestroy + if (lifecycleHook === LifecycleHooks.OnDestroy || + providerAst.providerType === ProviderAstType.Directive || + providerAst.providerType === ProviderAstType.Component) { + flags |= lifecycleHookToNodeFlag(lifecycleHook); + } + }); + const {providerExpr, flags: providerFlags, depsExpr} = providerAst.multiProvider ? + multiProviderDef(flags, providerAst.providers) : + singleProviderDef(flags, providerAst.providerType, providerAst.providers[0]); + return { + providerExpr, + flags: providerFlags, depsExpr, + tokenExpr: tokenExpr(providerAst.token), + }; +} + +function multiProviderDef(flags: NodeFlags, providers: CompileProviderMetadata[]): + {providerExpr: o.Expression, flags: NodeFlags, depsExpr: o.Expression} { + const allDepDefs: o.Expression[] = []; + const allParams: o.FnParam[] = []; + const exprs = providers.map((provider, providerIndex) => { + let expr: o.Expression; + if (provider.useClass) { + const depExprs = convertDeps(providerIndex, provider.deps || provider.useClass.diDeps); + expr = o.importExpr(provider.useClass).instantiate(depExprs); + } else if (provider.useFactory) { + const depExprs = convertDeps(providerIndex, provider.deps || provider.useFactory.diDeps); + expr = o.importExpr(provider.useFactory).callFn(depExprs); + } else if (provider.useExisting) { + const depExprs = convertDeps(providerIndex, [{token: provider.useExisting}]); + expr = depExprs[0]; + } else { + expr = convertValueToOutputAst(provider.useValue); + } + return expr; + }); + const providerExpr = + o.fn(allParams, [new o.ReturnStatement(o.literalArr(exprs))], o.INFERRED_TYPE); + return { + providerExpr, + flags: flags | NodeFlags.TypeFactoryProvider, + depsExpr: o.literalArr(allDepDefs) + }; + + function convertDeps(providerIndex: number, deps: CompileDiDependencyMetadata[]) { + return deps.map((dep, depIndex) => { + const paramName = `p${providerIndex}_${depIndex}`; + allParams.push(new o.FnParam(paramName, o.DYNAMIC_TYPE)); + allDepDefs.push(depDef(dep)); + return o.variable(paramName); + }); + } +} + +function singleProviderDef( + flags: NodeFlags, providerType: ProviderAstType, providerMeta: CompileProviderMetadata): + {providerExpr: o.Expression, flags: NodeFlags, depsExpr: o.Expression} { + let providerExpr: o.Expression; + let deps: CompileDiDependencyMetadata[]; + if (providerType === ProviderAstType.Directive || providerType === ProviderAstType.Component) { + providerExpr = o.importExpr(providerMeta.useClass !); + flags |= NodeFlags.TypeDirective; + deps = providerMeta.deps || providerMeta.useClass !.diDeps; + } else { + if (providerMeta.useClass) { + providerExpr = o.importExpr(providerMeta.useClass); + flags |= NodeFlags.TypeClassProvider; + deps = providerMeta.deps || providerMeta.useClass.diDeps; + } else if (providerMeta.useFactory) { + providerExpr = o.importExpr(providerMeta.useFactory); + flags |= NodeFlags.TypeFactoryProvider; + deps = providerMeta.deps || providerMeta.useFactory.diDeps; + } else if (providerMeta.useExisting) { + providerExpr = o.NULL_EXPR; + flags |= NodeFlags.TypeUseExistingProvider; + deps = [{token: providerMeta.useExisting}]; + } else { + providerExpr = convertValueToOutputAst(providerMeta.useValue); + flags |= NodeFlags.TypeValueProvider; + deps = []; + } + } + const depsExpr = o.literalArr(deps.map(dep => depDef(dep))); + return {providerExpr, flags, depsExpr}; +} + +function tokenExpr(tokenMeta: CompileTokenMetadata): o.Expression { + return tokenMeta.identifier ? o.importExpr(tokenMeta.identifier) : o.literal(tokenMeta.value); +} + +export function depDef(dep: CompileDiDependencyMetadata): o.Expression { + // Note: the following fields have already been normalized out by provider_analyzer: + // - isAttribute, isSelf, isHost + const expr = dep.isValue ? convertValueToOutputAst(dep.value) : tokenExpr(dep.token !); + let flags = DepFlags.None; + if (dep.isSkipSelf) { + flags |= DepFlags.SkipSelf; + } + if (dep.isOptional) { + flags |= DepFlags.Optional; + } + if (dep.isValue) { + flags |= DepFlags.Value; + } + return flags === DepFlags.None ? expr : o.literalArr([o.literal(flags), expr]); +} + +export function lifecycleHookToNodeFlag(lifecycleHook: LifecycleHooks): NodeFlags { + let nodeFlag = NodeFlags.None; + switch (lifecycleHook) { + case LifecycleHooks.AfterContentChecked: + nodeFlag = NodeFlags.AfterContentChecked; + break; + case LifecycleHooks.AfterContentInit: + nodeFlag = NodeFlags.AfterContentInit; + break; + case LifecycleHooks.AfterViewChecked: + nodeFlag = NodeFlags.AfterViewChecked; + break; + case LifecycleHooks.AfterViewInit: + nodeFlag = NodeFlags.AfterViewInit; + break; + case LifecycleHooks.DoCheck: + nodeFlag = NodeFlags.DoCheck; + break; + case LifecycleHooks.OnChanges: + nodeFlag = NodeFlags.OnChanges; + break; + case LifecycleHooks.OnDestroy: + nodeFlag = NodeFlags.OnDestroy; + break; + case LifecycleHooks.OnInit: + nodeFlag = NodeFlags.OnInit; + break; + } + return nodeFlag; +} + +export function componentFactoryResolverProviderDef( + flags: NodeFlags, entryComponents: CompileEntryComponentMetadata[]): { + providerExpr: o.Expression, + flags: NodeFlags, + depsExpr: o.Expression, + tokenExpr: o.Expression +} { + const entryComponentFactories = entryComponents.map( + (entryComponent) => o.importExpr({reference: entryComponent.componentFactory})); + const token = createIdentifierToken(Identifiers.ComponentFactoryResolver); + const classMeta = { + diDeps: [ + {isValue: true, value: o.literalArr(entryComponentFactories)}, + {token: token, isSkipSelf: true, isOptional: true}, + {token: createIdentifierToken(Identifiers.NgModuleRef)}, + ], + lifecycleHooks: [], + reference: resolveIdentifier(Identifiers.CodegenComponentFactoryResolver) + }; + const {providerExpr, flags: providerFlags, depsExpr} = + singleProviderDef(flags, ProviderAstType.PrivateService, { + token, + multi: false, + useClass: classMeta, + }); + return {providerExpr, flags: providerFlags, depsExpr, tokenExpr: tokenExpr(token)}; +} diff --git a/packages/compiler/src/view_compiler/view_compiler.ts b/packages/compiler/src/view_compiler/view_compiler.ts index 7c8efabd2e..87b79a0093 100644 --- a/packages/compiler/src/view_compiler/view_compiler.ts +++ b/packages/compiler/src/view_compiler/view_compiler.ts @@ -21,6 +21,8 @@ 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'; +import {componentFactoryResolverProviderDef, depDef, lifecycleHookToNodeFlag, providerDef} from './provider_compiler'; + const CLASS_ATTR = 'class'; const STYLE_ATTR = 'style'; const IMPLICIT_TEMPLATE_VAR = '\$implicit'; @@ -96,10 +98,10 @@ interface UpdateExpression { value: AST; } -const LOG_VAR = o.variable('l'); -const VIEW_VAR = o.variable('v'); -const CHECK_VAR = o.variable('ck'); -const COMP_VAR = o.variable('co'); +const LOG_VAR = o.variable('_l'); +const VIEW_VAR = o.variable('_v'); +const CHECK_VAR = o.variable('_ck'); +const COMP_VAR = o.variable('_co'); const EVENT_NAME_VAR = o.variable('en'); const ALLOW_DEFAULT_VAR = o.variable(`ad`); @@ -409,10 +411,7 @@ class ViewBuilder implements TemplateAstVisitor, LocalResolver { 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) { - this._visitProvider(componentFactoryResolverProvider, ast.queryMatches); - } + this._visitComponentFactoryResolverProvider(ast.directives); ast.providers.forEach((providerAst, providerIndex) => { let dirAst: DirectiveAst = undefined !; @@ -585,47 +584,58 @@ class ViewBuilder implements TemplateAstVisitor, LocalResolver { } private _visitProvider(providerAst: ProviderAst, queryMatches: QueryMatch[]): void { + this._addProviderNode(this._visitProviderOrDirective(providerAst, queryMatches)); + } + + private _visitComponentFactoryResolverProvider(directives: DirectiveAst[]) { + const componentDirMeta = directives.find(dirAst => dirAst.directive.isComponent); + if (componentDirMeta && componentDirMeta.directive.entryComponents.length) { + const {providerExpr, depsExpr, flags, tokenExpr} = componentFactoryResolverProviderDef( + NodeFlags.PrivateProvider, componentDirMeta.directive.entryComponents); + this._addProviderNode({ + providerExpr, + depsExpr, + flags, + tokenExpr, + queryMatchExprs: [], + sourceSpan: componentDirMeta.sourceSpan + }); + } + } + + private _addProviderNode(data: { + flags: NodeFlags, + queryMatchExprs: o.Expression[], + providerExpr: o.Expression, + depsExpr: o.Expression, + tokenExpr: o.Expression, + sourceSpan: ParseSourceSpan + }) { const nodeIndex = this.nodes.length; - // reserve the space in the nodeDefs array so we can add children - this.nodes.push(null !); - - const {flags, queryMatchExprs, providerExpr, depsExpr} = - this._visitProviderOrDirective(providerAst, queryMatches); - // providerDef( // flags: NodeFlags, matchedQueries: [string, QueryValueType][], token:any, // value: any, deps: ([DepFlags, any] | any)[]): NodeDef; - this.nodes[nodeIndex] = () => ({ - sourceSpan: providerAst.sourceSpan, - nodeFlags: flags, - nodeDef: o.importExpr(createIdentifier(Identifiers.providerDef)).callFn([ - o.literal(flags), queryMatchExprs.length ? o.literalArr(queryMatchExprs) : o.NULL_EXPR, - tokenExpr(providerAst.token), providerExpr, depsExpr - ]) - }); + this.nodes.push( + () => ({ + sourceSpan: data.sourceSpan, + nodeFlags: data.flags, + nodeDef: o.importExpr(createIdentifier(Identifiers.providerDef)).callFn([ + o.literal(data.flags), + data.queryMatchExprs.length ? o.literalArr(data.queryMatchExprs) : o.NULL_EXPR, + data.tokenExpr, data.providerExpr, data.depsExpr + ]) + })); } private _visitProviderOrDirective(providerAst: ProviderAst, queryMatches: QueryMatch[]): { flags: NodeFlags, + tokenExpr: o.Expression, + sourceSpan: ParseSourceSpan, queryMatchExprs: o.Expression[], providerExpr: o.Expression, depsExpr: o.Expression } { let flags = NodeFlags.None; - if (!providerAst.eager) { - flags |= NodeFlags.LazyProvider; - } - if (providerAst.providerType === ProviderAstType.PrivateService) { - flags |= NodeFlags.PrivateProvider; - } - providerAst.lifecycleHooks.forEach((lifecycleHook) => { - // for regular providers, we only support ngOnDestroy - if (lifecycleHook === LifecycleHooks.OnDestroy || - providerAst.providerType === ProviderAstType.Directive || - providerAst.providerType === ProviderAstType.Component) { - flags |= lifecycleHookToNodeFlag(lifecycleHook); - } - }); let queryMatchExprs: o.Expression[] = []; queryMatches.forEach((match) => { @@ -634,8 +644,15 @@ class ViewBuilder implements TemplateAstVisitor, LocalResolver { o.literalArr([o.literal(match.queryId), o.literal(QueryValueType.Provider)])); } }); - const {providerExpr, depsExpr, flags: providerType} = providerDef(providerAst); - return {flags: flags | providerType, queryMatchExprs, providerExpr, depsExpr}; + const {providerExpr, depsExpr, flags: providerFlags, tokenExpr} = providerDef(providerAst); + return { + flags: flags | providerFlags, + queryMatchExprs, + providerExpr, + depsExpr, + tokenExpr, + sourceSpan: providerAst.sourceSpan + }; } getLocal(name: string): o.Expression|null { @@ -885,100 +902,6 @@ class ViewBuilder implements TemplateAstVisitor, LocalResolver { visitAttr(ast: AttrAst, context: any): any {} } -function providerDef(providerAst: ProviderAst): - {providerExpr: o.Expression, flags: NodeFlags, depsExpr: o.Expression} { - return providerAst.multiProvider ? - multiProviderDef(providerAst.providers) : - singleProviderDef(providerAst.providerType, providerAst.providers[0]); -} - -function multiProviderDef(providers: CompileProviderMetadata[]): - {providerExpr: o.Expression, flags: NodeFlags, depsExpr: o.Expression} { - const allDepDefs: o.Expression[] = []; - const allParams: o.FnParam[] = []; - const exprs = providers.map((provider, providerIndex) => { - let expr: o.Expression; - if (provider.useClass) { - const depExprs = convertDeps(providerIndex, provider.deps || provider.useClass.diDeps); - expr = o.importExpr(provider.useClass).instantiate(depExprs); - } else if (provider.useFactory) { - const depExprs = convertDeps(providerIndex, provider.deps || provider.useFactory.diDeps); - expr = o.importExpr(provider.useFactory).callFn(depExprs); - } else if (provider.useExisting) { - const depExprs = convertDeps(providerIndex, [{token: provider.useExisting}]); - expr = depExprs[0]; - } else { - expr = convertValueToOutputAst(provider.useValue); - } - return expr; - }); - const providerExpr = - o.fn(allParams, [new o.ReturnStatement(o.literalArr(exprs))], o.INFERRED_TYPE); - return {providerExpr, flags: NodeFlags.TypeFactoryProvider, depsExpr: o.literalArr(allDepDefs)}; - - function convertDeps(providerIndex: number, deps: CompileDiDependencyMetadata[]) { - return deps.map((dep, depIndex) => { - const paramName = `p${providerIndex}_${depIndex}`; - allParams.push(new o.FnParam(paramName, o.DYNAMIC_TYPE)); - allDepDefs.push(depDef(dep)); - return o.variable(paramName); - }); - } -} - -function singleProviderDef(providerType: ProviderAstType, providerMeta: CompileProviderMetadata): - {providerExpr: o.Expression, flags: NodeFlags, depsExpr: o.Expression} { - let providerExpr: o.Expression; - let flags: NodeFlags; - let deps: CompileDiDependencyMetadata[]; - if (providerType === ProviderAstType.Directive || providerType === ProviderAstType.Component) { - providerExpr = o.importExpr(providerMeta.useClass !); - flags = NodeFlags.TypeDirective; - deps = providerMeta.deps || providerMeta.useClass !.diDeps; - } else { - if (providerMeta.useClass) { - providerExpr = o.importExpr(providerMeta.useClass); - flags = NodeFlags.TypeClassProvider; - deps = providerMeta.deps || providerMeta.useClass.diDeps; - } else if (providerMeta.useFactory) { - providerExpr = o.importExpr(providerMeta.useFactory); - flags = NodeFlags.TypeFactoryProvider; - deps = providerMeta.deps || providerMeta.useFactory.diDeps; - } else if (providerMeta.useExisting) { - providerExpr = o.NULL_EXPR; - flags = NodeFlags.TypeUseExistingProvider; - deps = [{token: providerMeta.useExisting}]; - } else { - providerExpr = convertValueToOutputAst(providerMeta.useValue); - flags = NodeFlags.TypeValueProvider; - deps = []; - } - } - const depsExpr = o.literalArr(deps.map(dep => depDef(dep))); - return {providerExpr, flags, depsExpr}; -} - -function tokenExpr(tokenMeta: CompileTokenMetadata): o.Expression { - return tokenMeta.identifier ? o.importExpr(tokenMeta.identifier) : o.literal(tokenMeta.value); -} - -function depDef(dep: CompileDiDependencyMetadata): o.Expression { - // Note: the following fields have already been normalized out by provider_analyzer: - // - isAttribute, isSelf, isHost - const expr = dep.isValue ? convertValueToOutputAst(dep.value) : tokenExpr(dep.token !); - let flags = DepFlags.None; - if (dep.isSkipSelf) { - flags |= DepFlags.SkipSelf; - } - if (dep.isOptional) { - flags |= DepFlags.Optional; - } - if (dep.isValue) { - flags |= DepFlags.Value; - } - return flags === DepFlags.None ? expr : o.literalArr([o.literal(flags), expr]); -} - function needsAdditionalRootNode(astNodes: TemplateAst[]): boolean { const lastAstNode = astNodes[astNodes.length - 1]; if (lastAstNode instanceof EmbeddedTemplateAst) { @@ -995,36 +918,6 @@ function needsAdditionalRootNode(astNodes: TemplateAst[]): boolean { return lastAstNode instanceof NgContentAst; } -function lifecycleHookToNodeFlag(lifecycleHook: LifecycleHooks): NodeFlags { - let nodeFlag = NodeFlags.None; - switch (lifecycleHook) { - case LifecycleHooks.AfterContentChecked: - nodeFlag = NodeFlags.AfterContentChecked; - break; - case LifecycleHooks.AfterContentInit: - nodeFlag = NodeFlags.AfterContentInit; - break; - case LifecycleHooks.AfterViewChecked: - nodeFlag = NodeFlags.AfterViewChecked; - break; - case LifecycleHooks.AfterViewInit: - nodeFlag = NodeFlags.AfterViewInit; - break; - case LifecycleHooks.DoCheck: - nodeFlag = NodeFlags.DoCheck; - break; - case LifecycleHooks.OnChanges: - nodeFlag = NodeFlags.OnChanges; - break; - case LifecycleHooks.OnDestroy: - nodeFlag = NodeFlags.OnDestroy; - break; - case LifecycleHooks.OnInit: - nodeFlag = NodeFlags.OnInit; - break; - } - return nodeFlag; -} function elementBindingDef(inputAst: BoundElementPropertyAst, dirAst: DirectiveAst): o.Expression { switch (inputAst.type) { @@ -1147,30 +1040,6 @@ function staticViewQueryIds(nodeStaticQueryIds: Map dirAst.directive.isComponent); - if (componentDirMeta && componentDirMeta.directive.entryComponents.length) { - const entryComponentFactories = componentDirMeta.directive.entryComponents.map( - (entryComponent) => o.importExpr({reference: entryComponent.componentFactory})); - - const token = createIdentifierToken(Identifiers.ComponentFactoryResolver); - - const classMeta: CompileTypeMetadata = { - diDeps: [ - {isValue: true, value: o.literalArr(entryComponentFactories)}, - {token: token, isSkipSelf: true, isOptional: true}, - {token: createIdentifierToken(Identifiers.NgModuleRef)}, - ], - lifecycleHooks: [], - reference: resolveIdentifier(Identifiers.CodegenComponentFactoryResolver) - }; - return new ProviderAst( - token, false, true, [{token, multi: false, useClass: classMeta}], - ProviderAstType.PrivateService, [], componentDirMeta.sourceSpan); - } - return null; -} - function elementEventNameAndTarget( eventAst: BoundEventAst, dirAst: DirectiveAst | null): {name: string, target: string | null} { if (eventAst.isAnimation) { diff --git a/packages/compiler/test/aot/regression_spec.ts b/packages/compiler/test/aot/regression_spec.ts new file mode 100644 index 0000000000..5e187fea24 --- /dev/null +++ b/packages/compiler/test/aot/regression_spec.ts @@ -0,0 +1,36 @@ +/** + * @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 {async} from '@angular/core/testing'; + +import {MockDirectory, compile, expectNoDiagnostics, setup} from './test_util'; + +describe('regressions', () => { + let angularFiles = setup(); + + it('should compile components with empty templates', async(() => { + const appDir = { + 'app.module.ts': ` + import { Component, NgModule } from '@angular/core'; + + @Component({template: ''}) + export class EmptyComp {} + + @NgModule({declarations: [EmptyComp]}) + export class MyModule {} + ` + }; + const rootDir = {'app': appDir}; + compile([rootDir, angularFiles], {postCompile: expectNoDiagnostics}, { + noUnusedLocals: true, + noUnusedParameters: true + }).then((result) => { + expect(result.genFiles.find((f) => f.genFileUrl === '/app/app.module.ngfactory.ts')) + .toBeTruthy(); + }); + })); +}); diff --git a/packages/compiler/test/aot/test_util.ts b/packages/compiler/test/aot/test_util.ts index 4b7f75fc1f..c53ac27acd 100644 --- a/packages/compiler/test/aot/test_util.ts +++ b/packages/compiler/test/aot/test_util.ts @@ -600,12 +600,15 @@ function isSource(fileName: string): boolean { return !/\.d\.ts$/.test(fileName) && /\.ts$/.test(fileName); } -export function compile(rootDirs: MockData, options: { - emit?: boolean, - useSummaries?: boolean, - preCompile?: (program: ts.Program) => void, - postCompile?: (program: ts.Program) => void, -}& AotCompilerOptions = {}): Promise<{genFiles: GeneratedFile[], outDir: MockDirectory}> { +export function compile( + rootDirs: MockData, options: { + emit?: boolean, + useSummaries?: boolean, + preCompile?: (program: ts.Program) => void, + postCompile?: (program: ts.Program) => void, + }& AotCompilerOptions = {}, + tsOptions: ts.CompilerOptions = {}): + Promise<{genFiles: GeneratedFile[], outDir: MockDirectory}> { // Make sure we always return errors via the promise... return Promise.resolve(null).then(() => { // when using summaries, always emit so the next step can use the results. @@ -621,8 +624,9 @@ export function compile(rootDirs: MockData, options: { aotHost.hideMetadata(); aotHost.tsFilesOnly(); } + const tsSettings = {...settings, ...tsOptions}; const scripts = host.scriptNames.slice(0); - const program = ts.createProgram(scripts, settings, host); + const program = ts.createProgram(scripts, tsSettings, host); if (preCompile) preCompile(program); const {compiler, reflector} = createAotCompiler(aotHost, options); return compiler.compileAll(program.getSourceFiles().map(sf => sf.fileName)).then(genFiles => { @@ -630,7 +634,7 @@ export function compile(rootDirs: MockData, options: { file => isSource(file.genFileUrl) ? host.addScript(file.genFileUrl, file.source) : host.override(file.genFileUrl, file.source)); const scripts = host.scriptNames.slice(0); - const newProgram = ts.createProgram(scripts, settings, host); + const newProgram = ts.createProgram(scripts, tsSettings, host); if (postCompile) postCompile(newProgram); if (emit) { newProgram.emit(); diff --git a/packages/compiler/test/template_parser/template_parser_spec.ts b/packages/compiler/test/template_parser/template_parser_spec.ts index afd1bdc7ac..2d031bbb1a 100644 --- a/packages/compiler/test/template_parser/template_parser_spec.ts +++ b/packages/compiler/test/template_parser/template_parser_spec.ts @@ -977,8 +977,8 @@ Binding to attribute 'onEvent' is disallowed for security reasons ("parse('
', [dirA])[0]; expect(elAst.providers.length).toBe(2); - expect(elAst.providers[1].providerType).toBe(ProviderAstType.PublicService); - expect(elAst.providers[1].providers).toEqual([provider]); + expect(elAst.providers[0].providerType).toBe(ProviderAstType.PublicService); + expect(elAst.providers[0].providers).toEqual([provider]); }); it('should use the private providers of a component', () => { @@ -986,8 +986,8 @@ Binding to attribute 'onEvent' is disallowed for security reasons ("parse('', [comp])[0]; expect(elAst.providers.length).toBe(2); - expect(elAst.providers[1].providerType).toBe(ProviderAstType.PrivateService); - expect(elAst.providers[1].providers).toEqual([provider]); + expect(elAst.providers[0].providerType).toBe(ProviderAstType.PrivateService); + expect(elAst.providers[0].providers).toEqual([provider]); }); it('should support multi providers', () => { @@ -998,8 +998,8 @@ Binding to attribute 'onEvent' is disallowed for security reasons ("parse('
', [dirA, dirB])[0]; expect(elAst.providers.length).toBe(4); - expect(elAst.providers[2].providers).toEqual([provider0, provider2]); - expect(elAst.providers[3].providers).toEqual([provider1]); + expect(elAst.providers[0].providers).toEqual([provider0, provider2]); + expect(elAst.providers[1].providers).toEqual([provider1]); }); it('should overwrite non multi providers', () => { @@ -1010,8 +1010,8 @@ Binding to attribute 'onEvent' is disallowed for security reasons ("parse('
', [dirA, dirB])[0]; expect(elAst.providers.length).toBe(4); - expect(elAst.providers[2].providers).toEqual([provider3]); - expect(elAst.providers[3].providers).toEqual([provider2]); + expect(elAst.providers[0].providers).toEqual([provider3]); + expect(elAst.providers[1].providers).toEqual([provider2]); }); it('should overwrite component providers by directive providers', () => { @@ -1021,7 +1021,7 @@ Binding to attribute 'onEvent' is disallowed for security reasons ("parse('', [dirA, comp])[0]; expect(elAst.providers.length).toBe(3); - expect(elAst.providers[2].providers).toEqual([dirProvider]); + expect(elAst.providers[0].providers).toEqual([dirProvider]); }); it('should overwrite view providers by directive providers', () => { @@ -1031,7 +1031,7 @@ Binding to attribute 'onEvent' is disallowed for security reasons ("parse('', [dirA, comp])[0]; expect(elAst.providers.length).toBe(3); - expect(elAst.providers[2].providers).toEqual([dirProvider]); + expect(elAst.providers[0].providers).toEqual([dirProvider]); }); it('should overwrite directives by providers', () => { @@ -1053,17 +1053,17 @@ Binding to attribute 'onEvent' is disallowed for security reasons ("]
"): TestComp@0:0`); }); - it('should sort providers by their DI order', () => { + it('should sort providers by their DI order, lazy providers first', () => { const provider0 = createProvider('service0', {deps: ['type:[dir2]']}); const provider1 = createProvider('service1'); const dir2 = createDir('[dir2]', {deps: ['service1']}); const comp = createDir('my-comp', {providers: [provider0, provider1]}); const elAst: ElementAst = parse('', [comp, dir2])[0]; expect(elAst.providers.length).toBe(4); - expect(elAst.providers[0].providers[0].useClass).toEqual(comp.type); - expect(elAst.providers[1].providers).toEqual([provider1]); - expect(elAst.providers[2].providers[0].useClass).toEqual(dir2.type); - expect(elAst.providers[3].providers).toEqual([provider0]); + expect(elAst.providers[1].providers[0].useClass).toEqual(comp.type); + expect(elAst.providers[2].providers).toEqual([provider1]); + expect(elAst.providers[3].providers[0].useClass).toEqual(dir2.type); + expect(elAst.providers[0].providers).toEqual([provider0]); }); it('should sort directives by their DI order', () => { @@ -1086,12 +1086,12 @@ Binding to attribute 'onEvent' is disallowed for security reasons ("parse('
', [dirA])[0]; expect(elAst.providers.length).toBe(3); - expect(elAst.providers[0].providers).toEqual([provider0]); - expect(elAst.providers[0].eager).toBe(true); - expect(elAst.providers[1].providers[0].useClass).toEqual(dirA.type); + expect(elAst.providers[1].providers).toEqual([provider0]); expect(elAst.providers[1].eager).toBe(true); - expect(elAst.providers[2].providers).toEqual([provider1]); - expect(elAst.providers[2].eager).toBe(false); + expect(elAst.providers[2].providers[0].useClass).toEqual(dirA.type); + expect(elAst.providers[2].eager).toBe(true); + expect(elAst.providers[0].providers).toEqual([provider1]); + expect(elAst.providers[0].eager).toBe(false); }); it('should mark dependencies on parent elements as eager', () => { @@ -1102,12 +1102,12 @@ Binding to attribute 'onEvent' is disallowed for security reasons ("parse('
', [dirA, dirB])[0]; expect(elAst.providers.length).toBe(3); - expect(elAst.providers[0].providers[0].useClass).toEqual(dirA.type); - expect(elAst.providers[0].eager).toBe(true); - expect(elAst.providers[1].providers).toEqual([provider0]); + expect(elAst.providers[1].providers[0].useClass).toEqual(dirA.type); expect(elAst.providers[1].eager).toBe(true); - expect(elAst.providers[2].providers).toEqual([provider1]); - expect(elAst.providers[2].eager).toBe(false); + expect(elAst.providers[2].providers).toEqual([provider0]); + expect(elAst.providers[2].eager).toBe(true); + expect(elAst.providers[0].providers).toEqual([provider1]); + expect(elAst.providers[0].eager).toBe(false); }); it('should mark queried providers as eager', () => { @@ -1117,12 +1117,12 @@ Binding to attribute 'onEvent' is disallowed for security reasons ("parse('
', [dirA])[0]; expect(elAst.providers.length).toBe(3); - expect(elAst.providers[0].providers[0].useClass).toEqual(dirA.type); - expect(elAst.providers[0].eager).toBe(true); - expect(elAst.providers[1].providers).toEqual([provider0]); + expect(elAst.providers[1].providers[0].useClass).toEqual(dirA.type); expect(elAst.providers[1].eager).toBe(true); - expect(elAst.providers[2].providers).toEqual([provider1]); - expect(elAst.providers[2].eager).toBe(false); + expect(elAst.providers[2].providers).toEqual([provider0]); + expect(elAst.providers[2].eager).toBe(true); + expect(elAst.providers[0].providers).toEqual([provider1]); + expect(elAst.providers[0].eager).toBe(false); }); it('should not mark dependencies across embedded views as eager', () => { @@ -1132,10 +1132,10 @@ Binding to attribute 'onEvent' is disallowed for security reasons ("parse('
', [dirA, dirB])[0]; expect(elAst.providers.length).toBe(2); - expect(elAst.providers[0].providers[0].useClass).toEqual(dirA.type); - expect(elAst.providers[0].eager).toBe(true); - expect(elAst.providers[1].providers).toEqual([provider0]); - expect(elAst.providers[1].eager).toBe(false); + expect(elAst.providers[1].providers[0].useClass).toEqual(dirA.type); + expect(elAst.providers[1].eager).toBe(true); + expect(elAst.providers[0].providers).toEqual([provider0]); + expect(elAst.providers[0].eager).toBe(false); }); it('should report missing @Self() deps as errors', () => { diff --git a/packages/core/src/application_ref.ts b/packages/core/src/application_ref.ts index 2eaf016a68..ceeb4ee893 100644 --- a/packages/core/src/application_ref.ts +++ b/packages/core/src/application_ref.ts @@ -23,7 +23,7 @@ import {Injectable, InjectionToken, Injector, Provider, ReflectiveInjector} from import {CompilerFactory, CompilerOptions} from './linker/compiler'; import {ComponentFactory, ComponentRef} from './linker/component_factory'; import {ComponentFactoryBoundToModule, ComponentFactoryResolver} from './linker/component_factory_resolver'; -import {NgModuleFactory, NgModuleInjector, NgModuleRef} from './linker/ng_module_factory'; +import {InternalNgModuleRef, NgModuleFactory, NgModuleRef} from './linker/ng_module_factory'; import {InternalViewRef, ViewRef} from './linker/view_ref'; import {WtfScopeFn, wtfCreateScope, wtfLeave} from './profile/profile'; import {Testability, TestabilityRegistry} from './testability/testability'; @@ -293,7 +293,7 @@ export class PlatformRef_ extends PlatformRef { return ngZone.run(() => { const ngZoneInjector = ReflectiveInjector.resolveAndCreate([{provide: NgZone, useValue: ngZone}], this.injector); - const moduleRef = >moduleFactory.create(ngZoneInjector); + const moduleRef = >moduleFactory.create(ngZoneInjector); const exceptionHandler: ErrorHandler = moduleRef.injector.get(ErrorHandler, null); if (!exceptionHandler) { throw new Error('No ErrorHandler. Is platform module (BrowserModule) included?'); @@ -326,10 +326,10 @@ export class PlatformRef_ extends PlatformRef { .then((moduleFactory) => this._bootstrapModuleFactoryWithZone(moduleFactory, ngZone)); } - private _moduleDoBootstrap(moduleRef: NgModuleInjector): void { - const appRef = moduleRef.injector.get(ApplicationRef); - if (moduleRef.bootstrapFactories.length > 0) { - moduleRef.bootstrapFactories.forEach(f => appRef.bootstrap(f)); + private _moduleDoBootstrap(moduleRef: InternalNgModuleRef): void { + const appRef = moduleRef.injector.get(ApplicationRef) as ApplicationRef; + if (moduleRef._bootstrapComponents.length > 0) { + moduleRef._bootstrapComponents.forEach(f => appRef.bootstrap(f)); } else if (moduleRef.instance.ngDoBootstrap) { moduleRef.instance.ngDoBootstrap(appRef); } else { diff --git a/packages/core/src/codegen_private_exports.ts b/packages/core/src/codegen_private_exports.ts index fadaed4122..86c660a5e1 100644 --- a/packages/core/src/codegen_private_exports.ts +++ b/packages/core/src/codegen_private_exports.ts @@ -7,7 +7,6 @@ */ export {CodegenComponentFactoryResolver as ɵCodegenComponentFactoryResolver} from './linker/component_factory_resolver'; -export {NgModuleInjector as ɵNgModuleInjector} from './linker/ng_module_factory'; export {registerModuleFactory as ɵregisterModuleFactory} from './linker/ng_module_factory_loader'; export {reflector as ɵreflector} from './reflection/reflection'; -export {ArgumentType as ɵArgumentType, BindingFlags as ɵBindingFlags, DepFlags as ɵDepFlags, EMPTY_ARRAY as ɵEMPTY_ARRAY, EMPTY_MAP as ɵEMPTY_MAP, NodeFlags as ɵNodeFlags, QueryBindingType as ɵQueryBindingType, QueryValueType as ɵQueryValueType, ViewDefinition as ɵViewDefinition, ViewFlags as ɵViewFlags, anchorDef as ɵand, createComponentFactory as ɵccf, createRendererType2 as ɵcrt, directiveDef as ɵdid, elementDef as ɵeld, elementEventFullName as ɵelementEventFullName, getComponentViewDefinitionFactory as ɵgetComponentViewDefinitionFactory, inlineInterpolate as ɵinlineInterpolate, interpolate as ɵinterpolate, ngContentDef as ɵncd, nodeValue as ɵnov, pipeDef as ɵpid, providerDef as ɵprd, pureArrayDef as ɵpad, pureObjectDef as ɵpod, purePipeDef as ɵppd, queryDef as ɵqud, textDef as ɵted, unwrapValue as ɵunv, viewDef as ɵvid} from './view/index'; +export {ArgumentType as ɵArgumentType, BindingFlags as ɵBindingFlags, DepFlags as ɵDepFlags, EMPTY_ARRAY as ɵEMPTY_ARRAY, EMPTY_MAP as ɵEMPTY_MAP, NodeFlags as ɵNodeFlags, QueryBindingType as ɵQueryBindingType, QueryValueType as ɵQueryValueType, ViewDefinition as ɵViewDefinition, ViewFlags as ɵViewFlags, anchorDef as ɵand, createComponentFactory as ɵccf, createNgModuleFactory as ɵcmf, createRendererType2 as ɵcrt, directiveDef as ɵdid, elementDef as ɵeld, elementEventFullName as ɵelementEventFullName, getComponentViewDefinitionFactory as ɵgetComponentViewDefinitionFactory, inlineInterpolate as ɵinlineInterpolate, interpolate as ɵinterpolate, moduleDef as ɵmod, moduleProvideDef as ɵmpd, ngContentDef as ɵncd, nodeValue as ɵnov, pipeDef as ɵpid, providerDef as ɵprd, pureArrayDef as ɵpad, pureObjectDef as ɵpod, purePipeDef as ɵppd, queryDef as ɵqud, textDef as ɵted, unwrapValue as ɵunv, viewDef as ɵvid} from './view/index'; diff --git a/packages/core/src/di/reflective_errors.ts b/packages/core/src/di/reflective_errors.ts index 2360e84f24..4eaf4be951 100644 --- a/packages/core/src/di/reflective_errors.ts +++ b/packages/core/src/di/reflective_errors.ts @@ -39,20 +39,22 @@ function constructResolvingPath(keys: any[]): string { export interface InjectionError extends Error { keys: ReflectiveKey[]; injectors: ReflectiveInjector[]; - constructResolvingMessage: (this: InjectionError) => string; + constructResolvingMessage: (keys: ReflectiveKey[]) => string; addKey(injector: ReflectiveInjector, key: ReflectiveKey): void; } function injectionError( injector: ReflectiveInjector, key: ReflectiveKey, - constructResolvingMessage: (this: InjectionError) => string, + constructResolvingMessage: (keys: ReflectiveKey[]) => string, originalError?: Error): InjectionError { - const error = (originalError ? wrappedError('', originalError) : Error()) as InjectionError; + const keys = [key]; + const errMsg = constructResolvingMessage(keys); + const error = + (originalError ? wrappedError(errMsg, originalError) : Error(errMsg)) as InjectionError; error.addKey = addKey; - error.keys = [key]; + error.keys = keys; error.injectors = [injector]; error.constructResolvingMessage = constructResolvingMessage; - error.message = error.constructResolvingMessage(); (error as any)[ERROR_ORIGINAL_ERROR] = originalError; return error; } @@ -60,7 +62,8 @@ function injectionError( function addKey(this: InjectionError, injector: ReflectiveInjector, key: ReflectiveKey): void { this.injectors.push(injector); this.keys.push(key); - this.message = this.constructResolvingMessage(); + // Note: This updated message won't be reflected in the `.stack` property + this.message = this.constructResolvingMessage(this.keys); } /** @@ -78,9 +81,9 @@ function addKey(this: InjectionError, injector: ReflectiveInjector, key: Reflect * ``` */ export function noProviderError(injector: ReflectiveInjector, key: ReflectiveKey): InjectionError { - return injectionError(injector, key, function(this: InjectionError) { - const first = stringify(this.keys[0].token); - return `No provider for ${first}!${constructResolvingPath(this.keys)}`; + return injectionError(injector, key, function(keys: ReflectiveKey[]) { + const first = stringify(keys[0].token); + return `No provider for ${first}!${constructResolvingPath(keys)}`; }); } @@ -102,8 +105,8 @@ export function noProviderError(injector: ReflectiveInjector, key: ReflectiveKey */ export function cyclicDependencyError( injector: ReflectiveInjector, key: ReflectiveKey): InjectionError { - return injectionError(injector, key, function(this: InjectionError) { - return `Cannot instantiate cyclic dependency!${constructResolvingPath(this.keys)}`; + return injectionError(injector, key, function(keys: ReflectiveKey[]) { + return `Cannot instantiate cyclic dependency!${constructResolvingPath(keys)}`; }); } @@ -136,9 +139,9 @@ export function cyclicDependencyError( export function instantiationError( injector: ReflectiveInjector, originalException: any, originalStack: any, key: ReflectiveKey): InjectionError { - return injectionError(injector, key, function(this: InjectionError) { - const first = stringify(this.keys[0].token); - return `${getOriginalError(this).message}: Error during instantiation of ${first}!${constructResolvingPath(this.keys)}.`; + return injectionError(injector, key, function(keys: ReflectiveKey[]) { + const first = stringify(keys[0].token); + return `${originalException.message}: Error during instantiation of ${first}!${constructResolvingPath(keys)}.`; }, originalException); } diff --git a/packages/core/src/linker/component_factory_resolver.ts b/packages/core/src/linker/component_factory_resolver.ts index 4edd31232b..32c3a8467c 100644 --- a/packages/core/src/linker/component_factory_resolver.ts +++ b/packages/core/src/linker/component_factory_resolver.ts @@ -54,8 +54,13 @@ export class CodegenComponentFactoryResolver implements ComponentFactoryResolver } resolveComponentFactory(component: {new (...args: any[]): T}): ComponentFactory { - let factory = this._factories.get(component) || this._parent.resolveComponentFactory(component); - + let factory = this._factories.get(component); + if (!factory && this._parent) { + factory = this._parent.resolveComponentFactory(component); + } + if (!factory) { + throw noComponentFactoryError(component); + } return new ComponentFactoryBoundToModule(factory, this._ngModule); } } diff --git a/packages/core/src/linker/ng_module_factory.ts b/packages/core/src/linker/ng_module_factory.ts index 2b18e8068e..0e4925f806 100644 --- a/packages/core/src/linker/ng_module_factory.ts +++ b/packages/core/src/linker/ng_module_factory.ts @@ -6,12 +6,10 @@ * found in the LICENSE file at https://angular.io/license */ -import {Injector, THROW_IF_NOT_FOUND} from '../di/injector'; +import {Injector} from '../di/injector'; import {Type} from '../type'; -import {stringify} from '../util'; -import {ComponentFactory} from './component_factory'; -import {CodegenComponentFactoryResolver, ComponentFactoryBoundToModule, ComponentFactoryResolver} from './component_factory_resolver'; +import {ComponentFactoryResolver} from './component_factory_resolver'; /** @@ -50,76 +48,16 @@ export abstract class NgModuleRef { abstract onDestroy(callback: () => void): void; } +export interface InternalNgModuleRef extends NgModuleRef { + // Note: we are using the prefix _ as NgModuleData is an NgModuleRef and therefore directly + // exposed to the user. + _bootstrapComponents: Type[]; +} + /** * @experimental */ -export class NgModuleFactory { - constructor( - private _injectorClass: {new (parentInjector: Injector): NgModuleInjector}, - private _moduleType: Type) {} - - get moduleType(): Type { return this._moduleType; } - - create(parentInjector: Injector|null): NgModuleRef { - const instance = new this._injectorClass(parentInjector || Injector.NULL); - instance.create(); - return instance; - } -} - -const _UNDEFINED = new Object(); - -export abstract class NgModuleInjector implements Injector, NgModuleRef { - bootstrapFactories: ComponentFactory[]; - instance: T; - - private _destroyListeners: (() => void)[] = []; - private _destroyed: boolean = false; - private _cmpFactoryResolver: CodegenComponentFactoryResolver; - - constructor( - public parent: Injector, factories: ComponentFactory[], - bootstrapFactories: ComponentFactory[]) { - this.bootstrapFactories = - bootstrapFactories.map(f => new ComponentFactoryBoundToModule(f, this)); - this._cmpFactoryResolver = new CodegenComponentFactoryResolver( - factories, parent.get(ComponentFactoryResolver, ComponentFactoryResolver.NULL), this); - } - - create() { this.instance = this.createInternal(); } - - abstract createInternal(): T; - - get(token: any, notFoundValue: any = THROW_IF_NOT_FOUND): any { - if (token === Injector || token === NgModuleRef) { - return this; - } - - if (token === ComponentFactoryResolver) { - return this._cmpFactoryResolver; - } - - const result = this.getInternal(token, _UNDEFINED); - return result === _UNDEFINED ? this.parent.get(token, notFoundValue) : result; - } - - abstract getInternal(token: any, notFoundValue: any): any; - - get injector(): Injector { return this; } - - get componentFactoryResolver(): ComponentFactoryResolver { return this._cmpFactoryResolver; } - - destroy(): void { - if (this._destroyed) { - throw new Error( - `The ng module ${stringify(this.instance.constructor)} has already been destroyed.`); - } - this._destroyed = true; - this.destroyInternal(); - this._destroyListeners.forEach((listener) => listener()); - } - - onDestroy(callback: () => void): void { this._destroyListeners.push(callback); } - - abstract destroyInternal(): void; +export abstract class NgModuleFactory { + abstract get moduleType(): Type; + abstract create(parentInjector: Injector|null): NgModuleRef; } diff --git a/packages/core/src/view/element.ts b/packages/core/src/view/element.ts index f2bf0199e4..cf69acb758 100644 --- a/packages/core/src/view/element.ts +++ b/packages/core/src/view/element.ts @@ -10,7 +10,7 @@ import {RendererType2} from '../render/api'; import {SecurityContext} from '../security'; import {BindingDef, BindingFlags, ElementData, ElementHandleEventFn, NodeDef, NodeFlags, OutputDef, OutputType, QueryValueType, ViewData, ViewDefinitionFactory, asElementData} from './types'; -import {NOOP, calcBindingFlags, checkAndUpdateBinding, dispatchEvent, elementEventFullName, getParentRenderElement, resolveRendererType2, resolveViewDefinition, splitMatchedQueriesDsl, splitNamespace} from './util'; +import {NOOP, calcBindingFlags, checkAndUpdateBinding, dispatchEvent, elementEventFullName, getParentRenderElement, resolveDefinition, resolveRendererType2, splitMatchedQueriesDsl, splitNamespace} from './util'; export function anchorDef( flags: NodeFlags, matchedQueriesDsl: [string | number, QueryValueType][], @@ -18,7 +18,7 @@ export function anchorDef( templateFactory?: ViewDefinitionFactory): NodeDef { flags |= NodeFlags.TypeElement; const {matchedQueries, references, matchedQueryIds} = splitMatchedQueriesDsl(matchedQueriesDsl); - const template = templateFactory ? resolveViewDefinition(templateFactory) : null; + const template = templateFactory ? resolveDefinition(templateFactory) : null; return { // will bet set by the view definition diff --git a/packages/core/src/view/index.ts b/packages/core/src/view/index.ts index 0b647b3ec9..9d15814d6e 100644 --- a/packages/core/src/view/index.ts +++ b/packages/core/src/view/index.ts @@ -8,13 +8,15 @@ export {anchorDef, elementDef} from './element'; export {ngContentDef} from './ng_content'; +export {moduleDef, moduleProvideDef} from './ng_module'; export {directiveDef, pipeDef, providerDef} from './provider'; export {pureArrayDef, pureObjectDef, purePipeDef} from './pure_expression'; export {queryDef} from './query'; export {ViewRef_, createComponentFactory, getComponentViewDefinitionFactory, nodeValue} from './refs'; +export {createNgModuleFactory} from './refs'; export {initServicesIfNeeded} from './services'; export {textDef} from './text'; -export {EMPTY_ARRAY, EMPTY_MAP, createRendererType2, elementEventFullName, inlineInterpolate, interpolate, rootRenderNodes, unwrapValue} from './util'; +export {EMPTY_ARRAY, EMPTY_MAP, createRendererType2, elementEventFullName, inlineInterpolate, interpolate, rootRenderNodes, tokenKey, unwrapValue} from './util'; export {viewDef} from './view'; export {attachEmbeddedView, detachEmbeddedView, moveEmbeddedView} from './view_attach'; diff --git a/packages/core/src/view/ng_module.ts b/packages/core/src/view/ng_module.ts new file mode 100644 index 0000000000..d130b6767b --- /dev/null +++ b/packages/core/src/view/ng_module.ts @@ -0,0 +1,184 @@ +/** + * @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 {Injector, THROW_IF_NOT_FOUND} from '../di/injector'; +import {NgModuleRef} from '../linker/ng_module_factory'; + +import {DepDef, DepFlags, NgModuleData, NgModuleDefinition, NgModuleDefinitionFactory, NgModuleProviderDef, NodeFlags} from './types'; +import {tokenKey} from './util'; + +const NOT_CREATED = new Object(); + +const InjectorRefTokenKey = tokenKey(Injector); +const NgModuleRefTokenKey = tokenKey(NgModuleRef); + +export function moduleProvideDef( + flags: NodeFlags, token: any, value: any, + deps: ([DepFlags, any] | any)[]): NgModuleProviderDef { + const depDefs: DepDef[] = deps.map(value => { + let token: any; + let flags: DepFlags; + if (Array.isArray(value)) { + [flags, token] = value; + } else { + flags = DepFlags.None; + token = value; + } + return {flags, token, tokenKey: tokenKey(token)}; + }); + return { + // will bet set by the module definition + index: -1, + deps: depDefs, flags, token, value + }; +} + +export function moduleDef(providers: NgModuleProviderDef[]): NgModuleDefinition { + const providersByKey: {[key: string]: NgModuleProviderDef} = {}; + for (let i = 0; i < providers.length; i++) { + const provider = providers[i]; + provider.index = i; + providersByKey[tokenKey(provider.token)] = provider; + } + return { + // Will be filled later... + factory: null, + providersByKey, + providers + }; +} + +export function initNgModule(data: NgModuleData) { + const def = data._def; + const providers = data._providers = new Array(def.providers.length); + for (let i = 0; i < def.providers.length; i++) { + const provDef = def.providers[i]; + providers[i] = provDef.flags & NodeFlags.LazyProvider ? NOT_CREATED : + _createProviderInstance(data, provDef); + } +} + +export function resolveNgModuleDep( + data: NgModuleData, depDef: DepDef, notFoundValue: any = Injector.THROW_IF_NOT_FOUND): any { + if (depDef.flags & DepFlags.Value) { + return depDef.token; + } + if (depDef.flags & DepFlags.Optional) { + notFoundValue = null; + } + if (depDef.flags & DepFlags.SkipSelf) { + return data._parent.get(depDef.token, notFoundValue); + } + const tokenKey = depDef.tokenKey; + switch (tokenKey) { + case InjectorRefTokenKey: + case NgModuleRefTokenKey: + return data; + } + const providerDef = data._def.providersByKey[tokenKey]; + if (providerDef) { + let providerInstance = data._providers[providerDef.index]; + if (providerInstance === NOT_CREATED) { + providerInstance = data._providers[providerDef.index] = + _createProviderInstance(data, providerDef); + } + return providerInstance; + } + return data._parent.get(depDef.token, notFoundValue); +} + + +function _createProviderInstance(ngModule: NgModuleData, providerDef: NgModuleProviderDef): any { + let injectable: any; + switch (providerDef.flags & NodeFlags.Types) { + case NodeFlags.TypeClassProvider: + injectable = _createClass(ngModule, providerDef !.value, providerDef !.deps); + break; + case NodeFlags.TypeFactoryProvider: + injectable = _callFactory(ngModule, providerDef !.value, providerDef !.deps); + break; + case NodeFlags.TypeUseExistingProvider: + injectable = resolveNgModuleDep(ngModule, providerDef !.deps[0]); + break; + case NodeFlags.TypeValueProvider: + injectable = providerDef !.value; + break; + } + return injectable; +} + +function _createClass(ngModule: NgModuleData, ctor: any, deps: DepDef[]): any { + const len = deps.length; + let injectable: any; + switch (len) { + case 0: + injectable = new ctor(); + break; + case 1: + injectable = new ctor(resolveNgModuleDep(ngModule, deps[0])); + break; + case 2: + injectable = + new ctor(resolveNgModuleDep(ngModule, deps[0]), resolveNgModuleDep(ngModule, deps[1])); + break; + case 3: + injectable = new ctor( + resolveNgModuleDep(ngModule, deps[0]), resolveNgModuleDep(ngModule, deps[1]), + resolveNgModuleDep(ngModule, deps[2])); + break; + default: + const depValues = new Array(len); + for (let i = 0; i < len; i++) { + depValues[i] = resolveNgModuleDep(ngModule, deps[i]); + } + injectable = new ctor(...depValues); + } + return injectable; +} + +function _callFactory(ngModule: NgModuleData, factory: any, deps: DepDef[]): any { + const len = deps.length; + let injectable: any; + switch (len) { + case 0: + injectable = factory(); + break; + case 1: + injectable = factory(resolveNgModuleDep(ngModule, deps[0])); + break; + case 2: + injectable = + factory(resolveNgModuleDep(ngModule, deps[0]), resolveNgModuleDep(ngModule, deps[1])); + break; + case 3: + injectable = factory( + resolveNgModuleDep(ngModule, deps[0]), resolveNgModuleDep(ngModule, deps[1]), + resolveNgModuleDep(ngModule, deps[2])); + break; + default: + const depValues = Array(len); + for (let i = 0; i < len; i++) { + depValues[i] = resolveNgModuleDep(ngModule, deps[i]); + } + injectable = factory(...depValues); + } + return injectable; +} + +export function callNgModuleLifecycle(ngModule: NgModuleData, lifecycles: NodeFlags) { + const def = ngModule._def; + for (let i = 0; i < def.providers.length; i++) { + const provDef = def.providers[i]; + if (provDef.flags & NodeFlags.OnDestroy) { + const instance = ngModule._providers[i]; + if (instance && instance !== NOT_CREATED) { + instance.ngOnDestroy(); + } + } + } +} diff --git a/packages/core/src/view/provider.ts b/packages/core/src/view/provider.ts index 55236fbd09..9ca128f44e 100644 --- a/packages/core/src/view/provider.ts +++ b/packages/core/src/view/provider.ts @@ -105,7 +105,7 @@ export function _def( ngContentIndex: -1, childCount, bindings, bindingFlags: calcBindingFlags(bindings), outputs, element: null, - provider: {token, tokenKey: tokenKey(token), value, deps: depDefs}, + provider: {token, value, deps: depDefs}, text: null, query: null, ngContent: null diff --git a/packages/core/src/view/refs.ts b/packages/core/src/view/refs.ts index 5c5ff5368f..f38e86f469 100644 --- a/packages/core/src/view/refs.ts +++ b/packages/core/src/view/refs.ts @@ -8,20 +8,22 @@ import {ApplicationRef} from '../application_ref'; import {ChangeDetectorRef} from '../change_detection/change_detection'; -import {Injector} from '../di'; +import {Injector} from '../di/injector'; import {ComponentFactory, ComponentRef} from '../linker/component_factory'; -import {ComponentFactoryBoundToModule} from '../linker/component_factory_resolver'; +import {ComponentFactoryBoundToModule, ComponentFactoryResolver} from '../linker/component_factory_resolver'; import {ElementRef} from '../linker/element_ref'; -import {NgModuleRef} from '../linker/ng_module_factory'; +import {InternalNgModuleRef, NgModuleFactory, NgModuleRef} from '../linker/ng_module_factory'; import {TemplateRef} from '../linker/template_ref'; import {ViewContainerRef} from '../linker/view_container_ref'; import {EmbeddedViewRef, InternalViewRef, ViewRef} from '../linker/view_ref'; import {Renderer as RendererV1, Renderer2} from '../render/api'; import {Type} from '../type'; +import {stringify} from '../util'; import {VERSION} from '../version'; -import {DepFlags, ElementData, NodeDef, NodeFlags, Services, TemplateData, ViewContainerData, ViewData, ViewDefinitionFactory, ViewState, asElementData, asProviderData, asTextData} from './types'; -import {markParentViewsForCheck, resolveViewDefinition, rootRenderNodes, splitNamespace, tokenKey, viewParentEl} from './util'; +import {callNgModuleLifecycle, initNgModule, resolveNgModuleDep} from './ng_module'; +import {DepFlags, ElementData, NgModuleData, NgModuleDefinition, NgModuleDefinitionFactory, NodeDef, NodeFlags, Services, TemplateData, ViewContainerData, ViewData, ViewDefinitionFactory, ViewState, asElementData, asProviderData, asTextData} from './types'; +import {markParentViewsForCheck, resolveDefinition, rootRenderNodes, splitNamespace, tokenKey, viewParentEl} from './util'; import {attachEmbeddedView, detachEmbeddedView, moveEmbeddedView, renderDetachView} from './view_attach'; const EMPTY_CONTEXT = new Object(); @@ -41,6 +43,14 @@ export function getComponentViewDefinitionFactory(componentFactory: ComponentFac return (componentFactory as ComponentFactory_).viewDefFactory; } +// Attention: this function is called as top level function. +// Putting any logic in here will destroy closure tree shaking! +export function createNgModuleFactory( + ngModuleType: Type, bootstrapComponents: Type[], + defFactory: NgModuleDefinitionFactory): NgModuleFactory { + return new NgModuleFactory_(ngModuleType, bootstrapComponents, defFactory); +} + class ComponentFactory_ extends ComponentFactory { /** * @internal @@ -85,7 +95,7 @@ class ComponentFactory_ extends ComponentFactory { if (!ngModule) { throw new Error('ngModule should be provided'); } - const viewDef = resolveViewDefinition(this.viewDefFactory); + const viewDef = resolveDefinition(this.viewDefFactory); const componentNodeIndex = viewDef.nodes[0].element !.componentProvider !.index; const view = Services.createRootView( injector, projectableNodes || [], rootSelectorOrNode, viewDef, ngModule, EMPTY_CONTEXT); @@ -452,3 +462,57 @@ class RendererAdapter implements RendererV1 { animate(): any { throw new Error('Renderer.animate is no longer supported!'); } } + + +class NgModuleFactory_ extends NgModuleFactory { + constructor( + private _moduleType: Type, private _bootstrapComponents: Type[], + private _ngModuleDefFactory: NgModuleDefinitionFactory, ) { + // Attention: this ctor is called as top level function. + // Putting any logic in here will destroy closure tree shaking! + super(); + } + + get moduleType(): Type { return this._moduleType; } + + create(parentInjector: Injector|null): NgModuleRef { + const def = resolveDefinition(this._ngModuleDefFactory); + return new NgModuleRef_( + this._moduleType, parentInjector || Injector.NULL, this._bootstrapComponents, def); + } +} + +class NgModuleRef_ implements NgModuleData, InternalNgModuleRef { + private _destroyListeners: (() => void)[] = []; + private _destroyed: boolean = false; + public _providers: any[]; + + constructor( + private _moduleType: any, public _parent: Injector, public _bootstrapComponents: Type[], + public _def: NgModuleDefinition) { + initNgModule(this); + } + + get(token: any, notFoundValue: any = Injector.THROW_IF_NOT_FOUND): any { + return resolveNgModuleDep( + this, {token: token, tokenKey: tokenKey(token), flags: DepFlags.None}, notFoundValue); + } + + get instance() { return this.get(this._moduleType); } + + get componentFactoryResolver() { return this.get(ComponentFactoryResolver); } + + get injector(): Injector { return this; } + + destroy(): void { + if (this._destroyed) { + throw new Error( + `The ng module ${stringify(this.instance.constructor)} has already been destroyed.`); + } + this._destroyed = true; + callNgModuleLifecycle(this, NodeFlags.OnDestroy); + this._destroyListeners.forEach((listener) => listener()); + } + + onDestroy(callback: () => void): void { this._destroyListeners.push(callback); } +} diff --git a/packages/core/src/view/types.ts b/packages/core/src/view/types.ts index 8746da9c4d..c231d1d4c3 100644 --- a/packages/core/src/view/types.ts +++ b/packages/core/src/view/types.ts @@ -19,8 +19,32 @@ import {Sanitizer, SecurityContext} from '../security'; // Defs // ------------------------------------- -export interface ViewDefinition { - factory: ViewDefinitionFactory|null; +/** + * Factory for ViewDefinitions/NgModuleDefinitions. + * 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 DefinitionFactory> { (logger: NodeLogger): D; } + +/** + * 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 Definition> { factory: DF|null; } + +export interface NgModuleDefinition extends Definition { + providers: NgModuleProviderDef[]; + providersByKey: {[tokenKey: string]: NgModuleProviderDef}; +} + +export interface NgModuleDefinitionFactory extends DefinitionFactory {} +; + +export interface ViewDefinition extends Definition { flags: ViewFlags; updateDirectives: ViewUpdateFn; updateRenderer: ViewUpdateFn; @@ -44,20 +68,8 @@ export interface ViewDefinition { nodeMatchedQueries: number; } -/** - * 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 interface ViewDefinitionFactory extends DefinitionFactory {} -/** - * 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; } @@ -245,7 +257,14 @@ export interface ElementHandleEventFn { (view: ViewData, eventName: string, even export interface ProviderDef { token: any; - tokenKey: string; + value: any; + deps: DepDef[]; +} + +export interface NgModuleProviderDef { + flags: NodeFlags; + index: number; + token: any; value: any; deps: DepDef[]; } @@ -296,6 +315,14 @@ export interface NgContentDef { // Data // ------------------------------------- +export interface NgModuleData extends Injector, NgModuleRef { + // Note: we are using the prefix _ as NgModuleData is an NgModuleRef and therefore directly + // exposed to the user. + _def: NgModuleDefinition; + _parent: Injector; + _providers: any[]; +} + /** * View instance data. * Attention: Adding fields to this is performance sensitive! @@ -379,13 +406,20 @@ export interface ElementData { template: TemplateData; } -export interface ViewContainerData extends ViewContainerRef { _embeddedViews: ViewData[]; } +export interface ViewContainerData extends ViewContainerRef { + // Note: we are using the prefix _ as ViewContainerData is a ViewContainerRef and therefore + // directly + // exposed to the user. + _embeddedViews: ViewData[]; +} export interface TemplateData extends TemplateRef { // views that have been created from the template // of this element, // but inserted into the embeddedViews of another element. // By default, this is undefined. + // Note: we are using the prefix _ as TemplateData is a TemplateRef and therefore directly + // exposed to the user. _projectedViews: ViewData[]; } diff --git a/packages/core/src/view/util.ts b/packages/core/src/view/util.ts index 52210b9a70..7d0a1e2bbf 100644 --- a/packages/core/src/view/util.ts +++ b/packages/core/src/view/util.ts @@ -12,7 +12,7 @@ import {RendererType2} from '../render/api'; import {looseIdentical, stringify} from '../util'; import {expressionChangedAfterItHasBeenCheckedError} from './errors'; -import {BindingDef, BindingFlags, ElementData, NodeDef, NodeFlags, QueryValueType, Services, ViewData, ViewDefinition, ViewDefinitionFactory, ViewFlags, ViewState, asElementData, asTextData} from './types'; +import {BindingDef, BindingFlags, Definition, DefinitionFactory, ElementData, NodeDef, NodeFlags, QueryValueType, Services, ViewData, ViewDefinition, ViewDefinitionFactory, ViewFlags, ViewState, asElementData, asTextData} from './types'; export const NOOP: any = () => {}; @@ -220,14 +220,14 @@ export function getParentRenderElement(view: ViewData, renderHost: any, def: Nod } } -const VIEW_DEFINITION_CACHE = new WeakMap(); +const DEFINITION_CACHE = new WeakMap>(); -export function resolveViewDefinition(factory: ViewDefinitionFactory): ViewDefinition { - let value: ViewDefinition = VIEW_DEFINITION_CACHE.get(factory) !; +export function resolveDefinition>(factory: DefinitionFactory): D { + let value = DEFINITION_CACHE.get(factory) !as D; if (!value) { value = factory(() => NOOP); value.factory = factory; - VIEW_DEFINITION_CACHE.set(factory, value); + DEFINITION_CACHE.set(factory, value); } return value; } diff --git a/packages/core/src/view/view.ts b/packages/core/src/view/view.ts index f624c6c6c4..9a5d288443 100644 --- a/packages/core/src/view/view.ts +++ b/packages/core/src/view/view.ts @@ -17,7 +17,7 @@ import {checkAndUpdateQuery, createQuery} from './query'; import {createTemplateData, createViewContainerData} from './refs'; import {checkAndUpdateTextDynamic, checkAndUpdateTextInline, createText} from './text'; import {ArgumentType, CheckType, ElementData, NodeData, NodeDef, NodeFlags, ProviderData, RootData, Services, ViewData, ViewDefinition, ViewFlags, ViewHandleEventFn, ViewState, ViewUpdateFn, asElementData, asQueryList, asTextData} from './types'; -import {NOOP, checkBindingNoChanges, isComponentView, markParentViewsForCheckProjectedViews, resolveViewDefinition} from './util'; +import {NOOP, checkBindingNoChanges, isComponentView, markParentViewsForCheckProjectedViews, resolveDefinition, tokenKey} from './util'; import {detachProjectedView} from './view_attach'; export function viewDef( @@ -102,7 +102,7 @@ export function viewDef( const isPrivateService = (node.flags & NodeFlags.PrivateProvider) !== 0; const isComponent = (node.flags & NodeFlags.Component) !== 0; if (!isPrivateService || isComponent) { - currentParent !.element !.publicProviders ![node.provider !.tokenKey] = node; + currentParent !.element !.publicProviders ![tokenKey(node.provider !.token)] = node; } else { if (!currentElementHasPrivateProviders) { currentElementHasPrivateProviders = true; @@ -110,7 +110,7 @@ export function viewDef( currentParent !.element !.allProviders = Object.create(currentParent !.element !.publicProviders); } - currentParent !.element !.allProviders ![node.provider !.tokenKey] = node; + currentParent !.element !.allProviders ![tokenKey(node.provider !.token)] = node; } if (isComponent) { currentParent !.element !.componentProvider = node; @@ -240,7 +240,7 @@ function createViewNodes(view: ViewData) { const el = createElement(view, renderHost, nodeDef) as any; let componentView: ViewData = undefined !; if (nodeDef.flags & NodeFlags.ComponentView) { - const compViewDef = resolveViewDefinition(nodeDef.element !.componentView !); + const compViewDef = resolveDefinition(nodeDef.element !.componentView !); const rendererType = nodeDef.element !.componentRendererType; let compRenderer: Renderer2; if (!rendererType) { diff --git a/packages/core/test/linker/ng_module_integration_spec.ts b/packages/core/test/linker/ng_module_integration_spec.ts index cafdf580c2..f62ac13032 100644 --- a/packages/core/test/linker/ng_module_integration_spec.ts +++ b/packages/core/test/linker/ng_module_integration_spec.ts @@ -11,7 +11,7 @@ import {Console} from '@angular/core/src/console'; import {ComponentFixture, TestBed, inject} from '@angular/core/testing'; import {expect} from '@angular/platform-browser/testing/src/matchers'; -import {NgModuleInjector} from '../../src/linker/ng_module_factory'; +import {InternalNgModuleRef} from '../../src/linker/ng_module_factory'; import {clearModulesForTest} from '../../src/linker/ng_module_factory_loader'; import {stringify} from '../../src/util'; @@ -404,9 +404,9 @@ function declareTests({useJit}: {useJit: boolean}) { class SomeModule { } - const ngModule = >createModule(SomeModule); - expect(ngModule.bootstrapFactories.length).toBe(1); - expect(ngModule.bootstrapFactories[0].componentType).toBe(SomeComp); + const ngModule = >createModule(SomeModule); + expect(ngModule._bootstrapComponents.length).toBe(1); + expect(ngModule._bootstrapComponents[0]).toBe(SomeComp); }); }); @@ -787,6 +787,16 @@ function declareTests({useJit}: {useJit: boolean}) { expect(child.get(Injector)).toBe(child); }); + it('should allow to inject lazy providers via Injector.get from an eager provider that is declared earlier', + () => { + @NgModule({providers: [{provide: 'a', useFactory: () => 'aValue'}]}) + class SomeModule { + public a: string; + constructor(injector: Injector) { this.a = injector.get('a'); } + } + expect(createModule(SomeModule).instance.a).toBe('aValue'); + }); + it('should throw when no provider defined', () => { const injector = createInjector([]); expect(() => injector.get('NonExisting')).toThrowError('No provider for NonExisting!'); diff --git a/packages/core/test/linker/view_injector_integration_spec.ts b/packages/core/test/linker/view_injector_integration_spec.ts index 76900b100c..bc261656e4 100644 --- a/packages/core/test/linker/view_injector_integration_spec.ts +++ b/packages/core/test/linker/view_injector_integration_spec.ts @@ -342,6 +342,19 @@ export function main() { expect(created).toBe(true); }); + it('should allow injecting lazy providers via Injector.get from an eager provider that is declared earlier', + () => { + @Component({providers: [{provide: 'a', useFactory: () => 'aValue'}], template: ''}) + class SomeComponent { + public a: string; + constructor(injector: Injector) { this.a = injector.get('a'); } + } + + const comp = TestBed.configureTestingModule({declarations: [SomeComponent]}) + .createComponent(SomeComponent); + expect(comp.componentInstance.a).toBe('aValue'); + }); + it('should support ngOnDestroy for lazy providers', () => { let created = false; let destroyed = false; diff --git a/packages/router/test/router_preloader.spec.ts b/packages/router/test/router_preloader.spec.ts index 5807522d7b..3e12280f3a 100644 --- a/packages/router/test/router_preloader.spec.ts +++ b/packages/router/test/router_preloader.spec.ts @@ -100,12 +100,12 @@ describe('RouterPreloader', () => { const loadedConfig: LoadedRouterConfig = c[0]._loadedConfig !; const module: any = loadedConfig.module; expect(loadedConfig.routes[0].path).toEqual('LoadedModule1'); - expect(module.parent).toBe(testModule); + expect(module._parent).toBe(testModule); const loadedConfig2: LoadedRouterConfig = loadedConfig.routes[0]._loadedConfig !; const module2: any = loadedConfig2.module; expect(loadedConfig2.routes[0].path).toEqual('LoadedModule2'); - expect(module2.parent).toBe(module); + expect(module2._parent).toBe(module); expect(events.map(e => e.toString())).toEqual([ 'RouteConfigLoadStart(path: lazy)', @@ -167,12 +167,12 @@ describe('RouterPreloader', () => { const loadedConfig: LoadedRouterConfig = c[0]._loadedConfig !; const module: any = loadedConfig.module; - expect(module.parent).toBe(testModule); + expect(module._parent).toBe(testModule); const loadedConfig2: LoadedRouterConfig = loadedConfig.routes[0]._loadedConfig !; const loadedConfig3: LoadedRouterConfig = loadedConfig2.routes[0]._loadedConfig !; const module3: any = loadedConfig3.module; - expect(module3.parent).toBe(module2); + expect(module3._parent).toBe(module2); }))); }); diff --git a/tools/public_api_guard/core/core.d.ts b/tools/public_api_guard/core/core.d.ts index 707e591e9d..d25cd55ca6 100644 --- a/tools/public_api_guard/core/core.d.ts +++ b/tools/public_api_guard/core/core.d.ts @@ -612,12 +612,9 @@ export declare type NgIterable = Array | Iterable; export declare const NgModule: NgModuleDecorator; /** @experimental */ -export declare class NgModuleFactory { - readonly moduleType: Type; - constructor(_injectorClass: { - new (parentInjector: Injector): NgModuleInjector; - }, _moduleType: Type); - create(parentInjector: Injector | null): NgModuleRef; +export declare abstract class NgModuleFactory { + readonly abstract moduleType: Type; + abstract create(parentInjector: Injector | null): NgModuleRef; } /** @stable */