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.
This commit is contained in:
Martin Probst 2016-09-01 13:46:08 -07:00 committed by GitHub
parent c9e5b599e4
commit ebc8e808a9
11 changed files with 99 additions and 8 deletions

View File

@ -525,13 +525,14 @@ export class CompileNgModuleMetadata implements CompileMetadataWithIdentifier {
importedModules: CompileNgModuleMetadata[]; importedModules: CompileNgModuleMetadata[];
exportedModules: CompileNgModuleMetadata[]; exportedModules: CompileNgModuleMetadata[];
schemas: SchemaMetadata[]; schemas: SchemaMetadata[];
id: string;
transitiveModule: TransitiveCompileNgModuleMetadata; transitiveModule: TransitiveCompileNgModuleMetadata;
constructor( constructor(
{type, providers, declaredDirectives, exportedDirectives, declaredPipes, exportedPipes, {type, providers, declaredDirectives, exportedDirectives, declaredPipes, exportedPipes,
entryComponents, bootstrapComponents, importedModules, exportedModules, schemas, entryComponents, bootstrapComponents, importedModules, exportedModules, schemas,
transitiveModule}: { transitiveModule, id}: {
type?: CompileTypeMetadata, type?: CompileTypeMetadata,
providers?: providers?:
Array<CompileProviderMetadata|CompileTypeMetadata|CompileIdentifierMetadata|any[]>, Array<CompileProviderMetadata|CompileTypeMetadata|CompileIdentifierMetadata|any[]>,
@ -544,7 +545,8 @@ export class CompileNgModuleMetadata implements CompileMetadataWithIdentifier {
importedModules?: CompileNgModuleMetadata[], importedModules?: CompileNgModuleMetadata[],
exportedModules?: CompileNgModuleMetadata[], exportedModules?: CompileNgModuleMetadata[],
transitiveModule?: TransitiveCompileNgModuleMetadata, transitiveModule?: TransitiveCompileNgModuleMetadata,
schemas?: SchemaMetadata[] schemas?: SchemaMetadata[],
id?: string
} = {}) { } = {}) {
this.type = type; this.type = type;
this.declaredDirectives = _normalizeArray(declaredDirectives); this.declaredDirectives = _normalizeArray(declaredDirectives);
@ -557,6 +559,7 @@ export class CompileNgModuleMetadata implements CompileMetadataWithIdentifier {
this.importedModules = _normalizeArray(importedModules); this.importedModules = _normalizeArray(importedModules);
this.exportedModules = _normalizeArray(exportedModules); this.exportedModules = _normalizeArray(exportedModules);
this.schemas = _normalizeArray(schemas); this.schemas = _normalizeArray(schemas);
this.id = id;
this.transitiveModule = transitiveModule; this.transitiveModule = transitiveModule;
} }

View File

@ -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 {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 {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'; import {assetUrl} from './util';
var APP_VIEW_MODULE_URL = assetUrl('core', 'linker/view'); var APP_VIEW_MODULE_URL = assetUrl('core', 'linker/view');
@ -107,6 +107,11 @@ export class Identifiers {
runtime: NgModuleInjector, runtime: NgModuleInjector,
moduleUrl: assetUrl('core', 'linker/ng_module_factory') moduleUrl: assetUrl('core', 'linker/ng_module_factory')
}; };
static RegisterModuleFactoryFn: IdentifierSpec = {
name: 'registerModuleFactory',
runtime: registerModuleFactory,
moduleUrl: assetUrl('core', 'linker/ng_module_factory_loader')
};
static ValueUnwrapper: static ValueUnwrapper:
IdentifierSpec = {name: 'ValueUnwrapper', moduleUrl: CD_MODULE_URL, runtime: ValueUnwrapper}; IdentifierSpec = {name: 'ValueUnwrapper', moduleUrl: CD_MODULE_URL, runtime: ValueUnwrapper};
static Injector: IdentifierSpec = { static Injector: IdentifierSpec = {

View File

@ -328,7 +328,8 @@ export class CompileMetadataResolver {
exportedPipes: exportedPipes, exportedPipes: exportedPipes,
importedModules: importedModules, importedModules: importedModules,
exportedModules: exportedModules, exportedModules: exportedModules,
transitiveModule: transitiveModule transitiveModule: transitiveModule,
id: meta.id,
}); });
transitiveModule.modules.push(compileMeta); transitiveModule.modules.push(compileMeta);
this._verifyModule(compileMeta); this._verifyModule(compileMeta);

View File

@ -69,8 +69,16 @@ export class NgModuleCompiler {
[o.importType(ngModuleMeta.type)], [o.TypeModifier.Const]))) [o.importType(ngModuleMeta.type)], [o.TypeModifier.Const])))
.toDeclStmt(null, [o.StmtModifier.Final]); .toDeclStmt(null, [o.StmtModifier.Final]);
return new NgModuleCompileResult( let stmts: o.Statement[] = [injectorClass, ngModuleFactoryStmt];
[injectorClass, ngModuleFactoryStmt], ngModuleFactoryVar, deps); 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);
} }
} }

View File

@ -25,6 +25,7 @@ export const CodegenComponentFactoryResolver: typeof r.CodegenComponentFactoryRe
export const AppView: typeof r.AppView = r.AppView; export const AppView: typeof r.AppView = r.AppView;
export const DebugAppView: typeof r.DebugAppView = r.DebugAppView; export const DebugAppView: typeof r.DebugAppView = r.DebugAppView;
export const NgModuleInjector: typeof r.NgModuleInjector = r.NgModuleInjector; export const NgModuleInjector: typeof r.NgModuleInjector = r.NgModuleInjector;
export const registerModuleFactory: typeof r.registerModuleFactory = r.registerModuleFactory;
export type ViewType = typeof r._ViewType; export type ViewType = typeof r._ViewType;
export const ViewType: typeof r.ViewType = r.ViewType; export const ViewType: typeof r.ViewType = r.ViewType;
export const MAX_INTERPOLATION_VALUES: typeof r.MAX_INTERPOLATION_VALUES = export const MAX_INTERPOLATION_VALUES: typeof r.MAX_INTERPOLATION_VALUES =

View File

@ -24,6 +24,7 @@ import * as component_factory_resolver from './linker/component_factory_resolver
import * as debug_context from './linker/debug_context'; import * as debug_context from './linker/debug_context';
import * as element from './linker/element'; import * as element from './linker/element';
import * as ng_module_factory from './linker/ng_module_factory'; 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 template_ref from './linker/template_ref';
import * as view from './linker/view'; import * as view from './linker/view';
import * as view_type from './linker/view_type'; import * as view_type from './linker/view_type';
@ -61,6 +62,7 @@ export var __core_private__: {
DebugAppView: typeof view.DebugAppView, _DebugAppView?: view.DebugAppView<any>, DebugAppView: typeof view.DebugAppView, _DebugAppView?: view.DebugAppView<any>,
NgModuleInjector: typeof ng_module_factory.NgModuleInjector, NgModuleInjector: typeof ng_module_factory.NgModuleInjector,
_NgModuleInjector?: ng_module_factory.NgModuleInjector<any>, _NgModuleInjector?: ng_module_factory.NgModuleInjector<any>,
registerModuleFactory: typeof ng_module_factory_loader.registerModuleFactory,
ViewType: typeof view_type.ViewType, _ViewType?: view_type.ViewType, ViewType: typeof view_type.ViewType, _ViewType?: view_type.ViewType,
MAX_INTERPOLATION_VALUES: typeof view_utils.MAX_INTERPOLATION_VALUES, MAX_INTERPOLATION_VALUES: typeof view_utils.MAX_INTERPOLATION_VALUES,
checkBinding: typeof view_utils.checkBinding, checkBinding: typeof view_utils.checkBinding,
@ -131,6 +133,7 @@ export var __core_private__: {
AppView: view.AppView, AppView: view.AppView,
DebugAppView: view.DebugAppView, DebugAppView: view.DebugAppView,
NgModuleInjector: ng_module_factory.NgModuleInjector, NgModuleInjector: ng_module_factory.NgModuleInjector,
registerModuleFactory: ng_module_factory_loader.registerModuleFactory,
ViewType: view_type.ViewType, ViewType: view_type.ViewType,
MAX_INTERPOLATION_VALUES: view_utils.MAX_INTERPOLATION_VALUES, MAX_INTERPOLATION_VALUES: view_utils.MAX_INTERPOLATION_VALUES,
checkBinding: view_utils.checkBinding, checkBinding: view_utils.checkBinding,

View File

@ -12,7 +12,7 @@ export {ComponentFactory, ComponentRef} from './linker/component_factory';
export {ComponentFactoryResolver} from './linker/component_factory_resolver'; export {ComponentFactoryResolver} from './linker/component_factory_resolver';
export {ElementRef} from './linker/element_ref'; export {ElementRef} from './linker/element_ref';
export {NgModuleFactory, NgModuleRef} from './linker/ng_module_factory'; 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 {QueryList} from './linker/query_list';
export {SystemJsNgModuleLoader, SystemJsNgModuleLoaderConfig} from './linker/system_js_ng_module_factory_loader'; export {SystemJsNgModuleLoader, SystemJsNgModuleLoaderConfig} from './linker/system_js_ng_module_factory_loader';
export {TemplateRef} from './linker/template_ref'; export {TemplateRef} from './linker/template_ref';

View File

@ -15,3 +15,34 @@ import {NgModuleFactory} from './ng_module_factory';
export abstract class NgModuleFactoryLoader { export abstract class NgModuleFactoryLoader {
abstract load(path: string): Promise<NgModuleFactory<any>>; abstract load(path: string): Promise<NgModuleFactory<any>>;
} }
let moduleFactories = new Map<string, NgModuleFactory<any>>();
/**
* Registers a loaded module. Should only be called from generated NgModuleFactory code.
* @experimental
*/
export function registerModuleFactory(id: string, factory: NgModuleFactory<any>) {
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<string, NgModuleFactory<any>>();
}
/**
* 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<any> {
let factory = moduleFactories.get(id);
if (!factory) throw new Error(`No module with ID ${id} loaded`);
return factory;
}

View File

@ -59,6 +59,7 @@ export interface NgModuleMetadataType {
entryComponents?: Array<Type<any>|any[]>; entryComponents?: Array<Type<any>|any[]>;
bootstrap?: Array<Type<any>|any[]>; bootstrap?: Array<Type<any>|any[]>;
schemas?: Array<SchemaMetadata|any[]>; schemas?: Array<SchemaMetadata|any[]>;
id?: string;
} }
/** /**
@ -177,6 +178,13 @@ export class NgModuleMetadata extends InjectableMetadata implements NgModuleMeta
*/ */
schemas: Array<SchemaMetadata|any[]>; schemas: Array<SchemaMetadata|any[]>;
/**
* 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 = {}) { constructor(options: NgModuleMetadataType = {}) {
// We cannot use destructuring of the constructor argument because `exports` is a // We cannot use destructuring of the constructor argument because `exports` is a
// protected symbol in CommonJS and closure tries to aggressively optimize it away. // 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.entryComponents = options.entryComponents;
this.bootstrap = options.bootstrap; this.bootstrap = options.bootstrap;
this.schemas = options.schemas; this.schemas = options.schemas;
this.id = options.id;
} }
} }

View File

@ -6,13 +6,14 @@
* found in the LICENSE file at https://angular.io/license * 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 {Console} from '@angular/core/src/console';
import {ComponentFixture, TestBed, inject} from '@angular/core/testing'; import {ComponentFixture, TestBed, inject} from '@angular/core/testing';
import {expect} from '@angular/platform-browser/testing/matchers'; import {expect} from '@angular/platform-browser/testing/matchers';
import {stringify} from '../../src/facade/lang'; import {stringify} from '../../src/facade/lang';
import {NgModuleInjector} from '../../src/linker/ng_module_factory'; import {NgModuleInjector} from '../../src/linker/ng_module_factory';
import {clearModulesForTest} from '../../src/linker/ng_module_factory_loader';
class Engine {} 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', () => { describe('entryComponents', () => {
it('should create ComponentFactories in root modules', () => { it('should create ComponentFactories in root modules', () => {
@NgModule({declarations: [SomeComp], entryComponents: [SomeComp]}) @NgModule({declarations: [SomeComp], entryComponents: [SomeComp]})

View File

@ -539,6 +539,9 @@ export interface ForwardRefFn {
/** @experimental */ /** @experimental */
export declare function getDebugNode(nativeNode: any): DebugNode; export declare function getDebugNode(nativeNode: any): DebugNode;
/** @experimental */
export declare function getModuleFactory(id: string): NgModuleFactory<any>;
/** @experimental */ /** @experimental */
export declare function getPlatform(): PlatformRef; export declare function getPlatform(): PlatformRef;
@ -749,6 +752,7 @@ export declare class NgModuleMetadata extends InjectableMetadata implements NgMo
declarations: Array<Type<any> | any[]>; declarations: Array<Type<any> | any[]>;
entryComponents: Array<Type<any> | any[]>; entryComponents: Array<Type<any> | any[]>;
exports: Array<Type<any> | any[]>; exports: Array<Type<any> | any[]>;
id: string;
imports: Array<Type<any> | ModuleWithProviders | any[]>; imports: Array<Type<any> | ModuleWithProviders | any[]>;
providers: Provider[]; providers: Provider[];
schemas: Array<SchemaMetadata | any[]>; schemas: Array<SchemaMetadata | any[]>;
@ -767,6 +771,7 @@ export interface NgModuleMetadataType {
declarations?: Array<Type<any> | any[]>; declarations?: Array<Type<any> | any[]>;
entryComponents?: Array<Type<any> | any[]>; entryComponents?: Array<Type<any> | any[]>;
exports?: Array<Type<any> | any[]>; exports?: Array<Type<any> | any[]>;
id?: string;
imports?: Array<Type<any> | ModuleWithProviders | any[]>; imports?: Array<Type<any> | ModuleWithProviders | any[]>;
providers?: Provider[]; providers?: Provider[];
schemas?: Array<SchemaMetadata | any[]>; schemas?: Array<SchemaMetadata | any[]>;