/** * @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 {Expression, R3InjectorMetadata, R3NgModuleMetadata, R3Reference, WrappedNodeExpr, compileInjector, compileNgModule as compileR3NgModule, jitExpression} from '@angular/compiler'; import {ModuleWithProviders, NgModule, NgModuleDefInternal, NgModuleTransitiveScopes} from '../../metadata/ng_module'; import {Type} from '../../type'; import {getComponentDef, getDirectiveDef, getNgModuleDef, getPipeDef} from '../definition'; import {NG_COMPONENT_DEF, NG_DIRECTIVE_DEF, NG_INJECTOR_DEF, NG_MODULE_DEF, NG_PIPE_DEF} from '../fields'; import {ComponentDefInternal} from '../interfaces/definition'; import {angularCoreEnv} from './environment'; import {reflectDependencies} from './util'; const EMPTY_ARRAY: Type[] = []; /** * Compiles a module in JIT mode. * * This function automatically gets called when a class has a `@NgModule` decorator. */ export function compileNgModule(moduleType: Type, ngModule: NgModule): void { compileNgModuleDefs(moduleType, ngModule); setScopeOnDeclaredComponents(moduleType, ngModule); } /** * Compiles and adds the `ngModuleDef` and `ngInjectorDef` properties to the module class. */ export function compileNgModuleDefs(moduleType: Type, ngModule: NgModule): void { const declarations: Type[] = flatten(ngModule.declarations || EMPTY_ARRAY); let ngModuleDef: any = null; Object.defineProperty(moduleType, NG_MODULE_DEF, { get: () => { if (ngModuleDef === null) { const meta: R3NgModuleMetadata = { type: wrap(moduleType), bootstrap: flatten(ngModule.bootstrap || EMPTY_ARRAY).map(wrapReference), declarations: declarations.map(wrapReference), imports: flatten(ngModule.imports || EMPTY_ARRAY) .map(expandModuleWithProviders) .map(wrapReference), exports: flatten(ngModule.exports || EMPTY_ARRAY) .map(expandModuleWithProviders) .map(wrapReference), emitInline: true, }; const res = compileR3NgModule(meta); ngModuleDef = jitExpression( res.expression, angularCoreEnv, `ng://${moduleType.name}/ngModuleDef.js`, []); } return ngModuleDef; }, // Make the property configurable in dev mode to allow overriding in tests configurable: !!ngDevMode, }); let ngInjectorDef: any = null; Object.defineProperty(moduleType, NG_INJECTOR_DEF, { get: () => { if (ngInjectorDef === null) { const meta: R3InjectorMetadata = { name: moduleType.name, type: wrap(moduleType), deps: reflectDependencies(moduleType), providers: new WrappedNodeExpr(ngModule.providers || EMPTY_ARRAY), imports: new WrappedNodeExpr([ ngModule.imports || EMPTY_ARRAY, ngModule.exports || EMPTY_ARRAY, ]), }; const res = compileInjector(meta); ngInjectorDef = jitExpression( res.expression, angularCoreEnv, `ng://${moduleType.name}/ngInjectorDef.js`, res.statements); } return ngInjectorDef; }, // Make the property configurable in dev mode to allow overriding in tests configurable: !!ngDevMode, }); } /** * Some declared components may be compiled asynchronously, and thus may not have their * ngComponentDef set yet. If this is the case, then a reference to the module is written into * the `ngSelectorScope` property of the declared type. */ function setScopeOnDeclaredComponents(moduleType: Type, ngModule: NgModule) { const declarations: Type[] = flatten(ngModule.declarations || EMPTY_ARRAY); const transitiveScopes = transitiveScopesFor(moduleType); declarations.forEach(declaration => { if (declaration.hasOwnProperty(NG_COMPONENT_DEF)) { // An `ngComponentDef` field exists - go ahead and patch the component directly. const component = declaration as Type& {ngComponentDef: ComponentDefInternal}; const componentDef = getComponentDef(component) !; patchComponentDefWithScope(componentDef, transitiveScopes); } else if ( !declaration.hasOwnProperty(NG_DIRECTIVE_DEF) && !declaration.hasOwnProperty(NG_PIPE_DEF)) { // Set `ngSelectorScope` for future reference when the component compilation finishes. (declaration as Type& {ngSelectorScope?: any}).ngSelectorScope = moduleType; } }); } /** * Patch the definition of a component with directives and pipes from the compilation scope of * a given module. */ export function patchComponentDefWithScope( componentDef: ComponentDefInternal, transitiveScopes: NgModuleTransitiveScopes) { componentDef.directiveDefs = () => Array.from(transitiveScopes.compilation.directives) .map(dir => getDirectiveDef(dir) || getComponentDef(dir) !) .filter(def => !!def); componentDef.pipeDefs = () => Array.from(transitiveScopes.compilation.pipes).map(pipe => getPipeDef(pipe) !); } /** * Compute the pair of transitive scopes (compilation scope and exported scope) for a given module. * * This operation is memoized and the result is cached on the module's definition. It can be called * on modules with components that have not fully compiled yet, but the result should not be used * until they have. */ export function transitiveScopesFor(moduleType: Type): NgModuleTransitiveScopes { if (!isNgModule(moduleType)) { throw new Error(`${moduleType.name} does not have an ngModuleDef`); } const def = getNgModuleDef(moduleType) !; if (def.transitiveCompileScopes !== null) { return def.transitiveCompileScopes; } const scopes: NgModuleTransitiveScopes = { compilation: { directives: new Set(), pipes: new Set(), }, exported: { directives: new Set(), pipes: new Set(), }, }; def.declarations.forEach(declared => { const declaredWithDefs = declared as Type& { ngPipeDef?: any; }; if (getPipeDef(declaredWithDefs)) { scopes.compilation.pipes.add(declared); } else { // Either declared has an ngComponentDef or ngDirectiveDef, or it's a component which hasn't // had its template compiled yet. In either case, it gets added to the compilation's // directives. scopes.compilation.directives.add(declared); } }); def.imports.forEach((imported: Type) => { const importedTyped = imported as Type& { // If imported is an @NgModule: ngModuleDef?: NgModuleDefInternal; }; if (!isNgModule(importedTyped)) { throw new Error(`Importing ${importedTyped.name} which does not have an ngModuleDef`); } // When this module imports another, the imported module's exported directives and pipes are // added to the compilation scope of this module. const importedScope = transitiveScopesFor(importedTyped); importedScope.exported.directives.forEach(entry => scopes.compilation.directives.add(entry)); importedScope.exported.pipes.forEach(entry => scopes.compilation.pipes.add(entry)); }); def.exports.forEach((exported: Type) => { const exportedTyped = exported as Type& { // Components, Directives, NgModules, and Pipes can all be exported. ngComponentDef?: any; ngDirectiveDef?: any; ngModuleDef?: NgModuleDefInternal; ngPipeDef?: any; }; // Either the type is a module, a pipe, or a component/directive (which may not have an // ngComponentDef as it might be compiled asynchronously). if (isNgModule(exportedTyped)) { // When this module exports another, the exported module's exported directives and pipes are // added to both the compilation and exported scopes of this module. const exportedScope = transitiveScopesFor(exportedTyped); exportedScope.exported.directives.forEach(entry => { scopes.compilation.directives.add(entry); scopes.exported.directives.add(entry); }); exportedScope.exported.pipes.forEach(entry => { scopes.compilation.pipes.add(entry); scopes.exported.pipes.add(entry); }); } else if (getNgModuleDef(exportedTyped)) { scopes.exported.pipes.add(exportedTyped); } else { scopes.exported.directives.add(exportedTyped); } }); def.transitiveCompileScopes = scopes; return scopes; } function flatten(values: any[]): T[] { const out: T[] = []; values.forEach(value => { if (Array.isArray(value)) { out.push(...flatten(value)); } else { out.push(value); } }); return out; } function expandModuleWithProviders(value: Type| ModuleWithProviders<{}>): Type { if (isModuleWithProviders(value)) { return value.ngModule; } return value; } function wrap(value: Type): Expression { return new WrappedNodeExpr(value); } function wrapReference(value: Type): R3Reference { const wrapped = wrap(value); return {value: wrapped, type: wrapped}; } function isModuleWithProviders(value: any): value is ModuleWithProviders<{}> { return (value as{ngModule?: any}).ngModule !== undefined; } function isNgModule(value: Type): value is Type&{ngModuleDef: NgModuleDefInternal} { return !!getNgModuleDef(value); }