From ebc8e808a9efda944c8c7d674d9772641f600b80 Mon Sep 17 00:00:00 2001 From: Martin Probst Date: Thu, 1 Sep 2016 13:46:08 -0700 Subject: [PATCH] feat(router): register NgModuleFactory objects. (#11211) When lazily loading code, users need to be able to get hold of the NgModuleFactory. For SystemJS environments, the SystemJS registry serves this purpose. However other environments, such as modules compiled with Closure compiler, do not expose exports object or a path based registry. For these environments, `@NgModule` objects can include an identifier, and the loading code can then pass `loadModule(id).then(() => getNgModule(id))` to the router. --- .../@angular/compiler/src/compile_metadata.ts | 7 +++-- modules/@angular/compiler/src/identifiers.ts | 7 ++++- .../compiler/src/metadata_resolver.ts | 3 +- .../compiler/src/ng_module_compiler.ts | 12 +++++-- .../compiler/src/private_import_core.ts | 1 + .../@angular/core/src/core_private_export.ts | 3 ++ modules/@angular/core/src/linker.ts | 2 +- .../src/linker/ng_module_factory_loader.ts | 31 +++++++++++++++++++ .../@angular/core/src/metadata/ng_module.ts | 9 ++++++ .../test/linker/ng_module_integration_spec.ts | 27 +++++++++++++++- tools/public_api_guard/core/index.d.ts | 5 +++ 11 files changed, 99 insertions(+), 8 deletions(-) diff --git a/modules/@angular/compiler/src/compile_metadata.ts b/modules/@angular/compiler/src/compile_metadata.ts index 25311ae3a2..e7eddf4f4a 100644 --- a/modules/@angular/compiler/src/compile_metadata.ts +++ b/modules/@angular/compiler/src/compile_metadata.ts @@ -525,13 +525,14 @@ export class CompileNgModuleMetadata implements CompileMetadataWithIdentifier { importedModules: CompileNgModuleMetadata[]; exportedModules: CompileNgModuleMetadata[]; schemas: SchemaMetadata[]; + id: string; transitiveModule: TransitiveCompileNgModuleMetadata; constructor( {type, providers, declaredDirectives, exportedDirectives, declaredPipes, exportedPipes, entryComponents, bootstrapComponents, importedModules, exportedModules, schemas, - transitiveModule}: { + transitiveModule, id}: { type?: CompileTypeMetadata, providers?: Array, @@ -544,7 +545,8 @@ export class CompileNgModuleMetadata implements CompileMetadataWithIdentifier { importedModules?: CompileNgModuleMetadata[], exportedModules?: CompileNgModuleMetadata[], transitiveModule?: TransitiveCompileNgModuleMetadata, - schemas?: SchemaMetadata[] + schemas?: SchemaMetadata[], + id?: string } = {}) { this.type = type; this.declaredDirectives = _normalizeArray(declaredDirectives); @@ -557,6 +559,7 @@ export class CompileNgModuleMetadata implements CompileMetadataWithIdentifier { this.importedModules = _normalizeArray(importedModules); this.exportedModules = _normalizeArray(exportedModules); this.schemas = _normalizeArray(schemas); + this.id = id; this.transitiveModule = transitiveModule; } diff --git a/modules/@angular/compiler/src/identifiers.ts b/modules/@angular/compiler/src/identifiers.ts index 90ec332da1..a81eeb81c9 100644 --- a/modules/@angular/compiler/src/identifiers.ts +++ b/modules/@angular/compiler/src/identifiers.ts @@ -9,7 +9,7 @@ import {ANALYZE_FOR_ENTRY_COMPONENTS, ChangeDetectionStrategy, ChangeDetectorRef, ComponentFactory, ComponentFactoryResolver, ElementRef, Injector, LOCALE_ID as LOCALE_ID_, NgModuleFactory, QueryList, RenderComponentType, Renderer, SecurityContext, SimpleChange, TRANSLATIONS_FORMAT as TRANSLATIONS_FORMAT_, TemplateRef, ViewContainerRef, ViewEncapsulation} from '@angular/core'; import {CompileIdentifierMetadata, CompileTokenMetadata} from './compile_metadata'; -import {AnimationGroupPlayer, AnimationKeyframe, AnimationOutput, AnimationSequencePlayer, AnimationStyles, AppElement, AppView, ChangeDetectorStatus, CodegenComponentFactoryResolver, DebugAppView, DebugContext, EMPTY_ARRAY, EMPTY_MAP, NgModuleInjector, NoOpAnimationPlayer, StaticNodeDebugInfo, TemplateRef_, UNINITIALIZED, ValueUnwrapper, ViewType, ViewUtils, balanceAnimationKeyframes, castByValue, checkBinding, clearStyles, collectAndResolveStyles, devModeEqual, flattenNestedViewRenderNodes, interpolate, prepareFinalAnimationStyles, pureProxy1, pureProxy10, pureProxy2, pureProxy3, pureProxy4, pureProxy5, pureProxy6, pureProxy7, pureProxy8, pureProxy9, reflector, renderStyles} from './private_import_core'; +import {AnimationGroupPlayer, AnimationKeyframe, AnimationOutput, AnimationSequencePlayer, AnimationStyles, AppElement, AppView, ChangeDetectorStatus, CodegenComponentFactoryResolver, DebugAppView, DebugContext, EMPTY_ARRAY, EMPTY_MAP, NgModuleInjector, NoOpAnimationPlayer, StaticNodeDebugInfo, TemplateRef_, UNINITIALIZED, ValueUnwrapper, ViewType, ViewUtils, balanceAnimationKeyframes, castByValue, checkBinding, clearStyles, collectAndResolveStyles, devModeEqual, flattenNestedViewRenderNodes, interpolate, prepareFinalAnimationStyles, pureProxy1, pureProxy10, pureProxy2, pureProxy3, pureProxy4, pureProxy5, pureProxy6, pureProxy7, pureProxy8, pureProxy9, reflector, registerModuleFactory, renderStyles} from './private_import_core'; import {assetUrl} from './util'; var APP_VIEW_MODULE_URL = assetUrl('core', 'linker/view'); @@ -107,6 +107,11 @@ export class Identifiers { runtime: NgModuleInjector, moduleUrl: assetUrl('core', 'linker/ng_module_factory') }; + static RegisterModuleFactoryFn: IdentifierSpec = { + name: 'registerModuleFactory', + runtime: registerModuleFactory, + moduleUrl: assetUrl('core', 'linker/ng_module_factory_loader') + }; static ValueUnwrapper: IdentifierSpec = {name: 'ValueUnwrapper', moduleUrl: CD_MODULE_URL, runtime: ValueUnwrapper}; static Injector: IdentifierSpec = { diff --git a/modules/@angular/compiler/src/metadata_resolver.ts b/modules/@angular/compiler/src/metadata_resolver.ts index 9f9bdc0b96..88070a4543 100644 --- a/modules/@angular/compiler/src/metadata_resolver.ts +++ b/modules/@angular/compiler/src/metadata_resolver.ts @@ -328,7 +328,8 @@ export class CompileMetadataResolver { exportedPipes: exportedPipes, importedModules: importedModules, exportedModules: exportedModules, - transitiveModule: transitiveModule + transitiveModule: transitiveModule, + id: meta.id, }); transitiveModule.modules.push(compileMeta); this._verifyModule(compileMeta); diff --git a/modules/@angular/compiler/src/ng_module_compiler.ts b/modules/@angular/compiler/src/ng_module_compiler.ts index 9ca32b71b6..4c0871818f 100644 --- a/modules/@angular/compiler/src/ng_module_compiler.ts +++ b/modules/@angular/compiler/src/ng_module_compiler.ts @@ -69,8 +69,16 @@ export class NgModuleCompiler { [o.importType(ngModuleMeta.type)], [o.TypeModifier.Const]))) .toDeclStmt(null, [o.StmtModifier.Final]); - return new NgModuleCompileResult( - [injectorClass, ngModuleFactoryStmt], ngModuleFactoryVar, deps); + let stmts: o.Statement[] = [injectorClass, ngModuleFactoryStmt]; + if (ngModuleMeta.id) { + let registerFactoryStmt = + o.importExpr(resolveIdentifier(Identifiers.RegisterModuleFactoryFn)) + .callFn([o.literal(ngModuleMeta.id), o.variable(ngModuleFactoryVar)]) + .toStmt(); + stmts.push(registerFactoryStmt); + } + + return new NgModuleCompileResult(stmts, ngModuleFactoryVar, deps); } } diff --git a/modules/@angular/compiler/src/private_import_core.ts b/modules/@angular/compiler/src/private_import_core.ts index cca7cca9df..5bd4cd4df1 100644 --- a/modules/@angular/compiler/src/private_import_core.ts +++ b/modules/@angular/compiler/src/private_import_core.ts @@ -25,6 +25,7 @@ export const CodegenComponentFactoryResolver: typeof r.CodegenComponentFactoryRe export const AppView: typeof r.AppView = r.AppView; export const DebugAppView: typeof r.DebugAppView = r.DebugAppView; export const NgModuleInjector: typeof r.NgModuleInjector = r.NgModuleInjector; +export const registerModuleFactory: typeof r.registerModuleFactory = r.registerModuleFactory; export type ViewType = typeof r._ViewType; export const ViewType: typeof r.ViewType = r.ViewType; export const MAX_INTERPOLATION_VALUES: typeof r.MAX_INTERPOLATION_VALUES = diff --git a/modules/@angular/core/src/core_private_export.ts b/modules/@angular/core/src/core_private_export.ts index bc491b6350..778b848446 100644 --- a/modules/@angular/core/src/core_private_export.ts +++ b/modules/@angular/core/src/core_private_export.ts @@ -24,6 +24,7 @@ import * as component_factory_resolver from './linker/component_factory_resolver import * as debug_context from './linker/debug_context'; import * as element from './linker/element'; import * as ng_module_factory from './linker/ng_module_factory'; +import * as ng_module_factory_loader from './linker/ng_module_factory_loader'; import * as template_ref from './linker/template_ref'; import * as view from './linker/view'; import * as view_type from './linker/view_type'; @@ -61,6 +62,7 @@ export var __core_private__: { DebugAppView: typeof view.DebugAppView, _DebugAppView?: view.DebugAppView, NgModuleInjector: typeof ng_module_factory.NgModuleInjector, _NgModuleInjector?: ng_module_factory.NgModuleInjector, + registerModuleFactory: typeof ng_module_factory_loader.registerModuleFactory, ViewType: typeof view_type.ViewType, _ViewType?: view_type.ViewType, MAX_INTERPOLATION_VALUES: typeof view_utils.MAX_INTERPOLATION_VALUES, checkBinding: typeof view_utils.checkBinding, @@ -131,6 +133,7 @@ export var __core_private__: { AppView: view.AppView, DebugAppView: view.DebugAppView, NgModuleInjector: ng_module_factory.NgModuleInjector, + registerModuleFactory: ng_module_factory_loader.registerModuleFactory, ViewType: view_type.ViewType, MAX_INTERPOLATION_VALUES: view_utils.MAX_INTERPOLATION_VALUES, checkBinding: view_utils.checkBinding, diff --git a/modules/@angular/core/src/linker.ts b/modules/@angular/core/src/linker.ts index 12d4c98334..b3b25b222e 100644 --- a/modules/@angular/core/src/linker.ts +++ b/modules/@angular/core/src/linker.ts @@ -12,7 +12,7 @@ export {ComponentFactory, ComponentRef} from './linker/component_factory'; export {ComponentFactoryResolver} from './linker/component_factory_resolver'; export {ElementRef} from './linker/element_ref'; export {NgModuleFactory, NgModuleRef} from './linker/ng_module_factory'; -export {NgModuleFactoryLoader} from './linker/ng_module_factory_loader'; +export {NgModuleFactoryLoader, getModuleFactory} from './linker/ng_module_factory_loader'; export {QueryList} from './linker/query_list'; export {SystemJsNgModuleLoader, SystemJsNgModuleLoaderConfig} from './linker/system_js_ng_module_factory_loader'; export {TemplateRef} from './linker/template_ref'; diff --git a/modules/@angular/core/src/linker/ng_module_factory_loader.ts b/modules/@angular/core/src/linker/ng_module_factory_loader.ts index 4f576725c7..951cce9bb8 100644 --- a/modules/@angular/core/src/linker/ng_module_factory_loader.ts +++ b/modules/@angular/core/src/linker/ng_module_factory_loader.ts @@ -15,3 +15,34 @@ import {NgModuleFactory} from './ng_module_factory'; export abstract class NgModuleFactoryLoader { abstract load(path: string): Promise>; } + +let moduleFactories = new Map>(); + +/** + * Registers a loaded module. Should only be called from generated NgModuleFactory code. + * @experimental + */ +export function registerModuleFactory(id: string, factory: NgModuleFactory) { + let existing = moduleFactories.get(id); + if (existing) { + throw new Error(`Duplicate module registered for ${id + } - ${existing.moduleType.name} vs ${factory.moduleType.name}`); + } + moduleFactories.set(id, factory); +} + +export function clearModulesForTest() { + moduleFactories = new Map>(); +} + +/** + * Returns the NgModuleFactory with the given id, if it exists and has been loaded. + * Factories for modules that do not specify an `id` cannot be retrieved. Throws if the module + * cannot be found. + * @experimental + */ +export function getModuleFactory(id: string): NgModuleFactory { + let factory = moduleFactories.get(id); + if (!factory) throw new Error(`No module with ID ${id} loaded`); + return factory; +} diff --git a/modules/@angular/core/src/metadata/ng_module.ts b/modules/@angular/core/src/metadata/ng_module.ts index 165e7ceb17..11c2c44442 100644 --- a/modules/@angular/core/src/metadata/ng_module.ts +++ b/modules/@angular/core/src/metadata/ng_module.ts @@ -59,6 +59,7 @@ export interface NgModuleMetadataType { entryComponents?: Array|any[]>; bootstrap?: Array|any[]>; schemas?: Array; + id?: string; } /** @@ -177,6 +178,13 @@ export class NgModuleMetadata extends InjectableMetadata implements NgModuleMeta */ schemas: Array; + /** + * An opaque ID for this module, e.g. a name or a path. Used to identify modules in + * `getModuleFactory`. If left `undefined`, the `NgModule` will not be registered with + * `getModuleFactory`. + */ + id: string; + constructor(options: NgModuleMetadataType = {}) { // We cannot use destructuring of the constructor argument because `exports` is a // protected symbol in CommonJS and closure tries to aggressively optimize it away. @@ -188,5 +196,6 @@ export class NgModuleMetadata extends InjectableMetadata implements NgModuleMeta this.entryComponents = options.entryComponents; this.bootstrap = options.bootstrap; this.schemas = options.schemas; + this.id = options.id; } } diff --git a/modules/@angular/core/test/linker/ng_module_integration_spec.ts b/modules/@angular/core/test/linker/ng_module_integration_spec.ts index 6c1fc80a0a..d193e22e53 100644 --- a/modules/@angular/core/test/linker/ng_module_integration_spec.ts +++ b/modules/@angular/core/test/linker/ng_module_integration_spec.ts @@ -6,13 +6,14 @@ * found in the LICENSE file at https://angular.io/license */ -import {ANALYZE_FOR_ENTRY_COMPONENTS, CUSTOM_ELEMENTS_SCHEMA, Compiler, Component, ComponentFactoryResolver, Directive, HostBinding, Inject, Injectable, Injector, Input, NgModule, NgModuleRef, Optional, Pipe, Provider, SelfMetadata, Type, forwardRef} from '@angular/core'; +import {ANALYZE_FOR_ENTRY_COMPONENTS, CUSTOM_ELEMENTS_SCHEMA, Compiler, Component, ComponentFactoryResolver, Directive, HostBinding, Inject, Injectable, Injector, Input, NgModule, NgModuleRef, Optional, Pipe, Provider, SelfMetadata, Type, forwardRef, getModuleFactory} from '@angular/core'; import {Console} from '@angular/core/src/console'; import {ComponentFixture, TestBed, inject} from '@angular/core/testing'; import {expect} from '@angular/platform-browser/testing/matchers'; import {stringify} from '../../src/facade/lang'; import {NgModuleInjector} from '../../src/linker/ng_module_factory'; +import {clearModulesForTest} from '../../src/linker/ng_module_factory_loader'; class Engine {} @@ -246,6 +247,30 @@ function declareTests({useJit}: {useJit: boolean}) { }); }); + describe('id', () => { + const token = 'myid'; + @NgModule({id: token}) + class SomeModule { + } + @NgModule({id: token}) + class SomeOtherModule { + } + + afterEach(() => clearModulesForTest()); + + it('should register loaded modules', () => { + createModule(SomeModule); + let factory = getModuleFactory(token); + expect(factory).toBeTruthy(); + expect(factory.moduleType).toBe(SomeModule); + }); + + it('should throw when registering a duplicate module', () => { + createModule(SomeModule); + expect(() => createModule(SomeOtherModule)).toThrowError(/Duplicate module registered/); + }); + }); + describe('entryComponents', () => { it('should create ComponentFactories in root modules', () => { @NgModule({declarations: [SomeComp], entryComponents: [SomeComp]}) diff --git a/tools/public_api_guard/core/index.d.ts b/tools/public_api_guard/core/index.d.ts index c4198427fb..e1b26fc9c5 100644 --- a/tools/public_api_guard/core/index.d.ts +++ b/tools/public_api_guard/core/index.d.ts @@ -539,6 +539,9 @@ export interface ForwardRefFn { /** @experimental */ export declare function getDebugNode(nativeNode: any): DebugNode; +/** @experimental */ +export declare function getModuleFactory(id: string): NgModuleFactory; + /** @experimental */ export declare function getPlatform(): PlatformRef; @@ -749,6 +752,7 @@ export declare class NgModuleMetadata extends InjectableMetadata implements NgMo declarations: Array | any[]>; entryComponents: Array | any[]>; exports: Array | any[]>; + id: string; imports: Array | ModuleWithProviders | any[]>; providers: Provider[]; schemas: Array; @@ -767,6 +771,7 @@ export interface NgModuleMetadataType { declarations?: Array | any[]>; entryComponents?: Array | any[]>; exports?: Array | any[]>; + id?: string; imports?: Array | ModuleWithProviders | any[]>; providers?: Provider[]; schemas?: Array;