diff --git a/packages/compiler-cli/integrationtest/bazel/injector_def/ivy_build/app/test/BUILD.bazel b/packages/compiler-cli/integrationtest/bazel/injector_def/ivy_build/app/test/BUILD.bazel index 378a3cd437..6cb9248d7e 100644 --- a/packages/compiler-cli/integrationtest/bazel/injector_def/ivy_build/app/test/BUILD.bazel +++ b/packages/compiler-cli/integrationtest/bazel/injector_def/ivy_build/app/test/BUILD.bazel @@ -24,7 +24,6 @@ jasmine_node_test( name = "test", bootstrap = ["angular/tools/testing/init_node_spec.js"], tags = [ - "fixme-ivy-aot", "ivy-only", ], deps = [ diff --git a/packages/compiler-cli/integrationtest/bazel/injector_def/ivy_build/app/test/module_spec.ts b/packages/compiler-cli/integrationtest/bazel/injector_def/ivy_build/app/test/module_spec.ts index 80b6517a01..f7654591c3 100644 --- a/packages/compiler-cli/integrationtest/bazel/injector_def/ivy_build/app/test/module_spec.ts +++ b/packages/compiler-cli/integrationtest/bazel/injector_def/ivy_build/app/test/module_spec.ts @@ -7,7 +7,6 @@ */ import {Injectable, InjectionToken, Injector, NgModule, createInjector, forwardRef} from '@angular/core'; -import {fixmeIvy} from '@angular/private/testing'; import {AOT_TOKEN, AotModule, AotService} from 'app_built/src/module'; describe('Ivy NgModule', () => { @@ -41,24 +40,23 @@ describe('Ivy NgModule', () => { it('works', () => { createInjector(JitAppModule); }); - fixmeIvy('FW-645: jit doesn\'t support forwardRefs') - .it('throws an error on circular module dependencies', () => { - @NgModule({ - imports: [forwardRef(() => BModule)], - }) - class AModule { - } + it('throws an error on circular module dependencies', () => { + @NgModule({ + imports: [forwardRef(() => BModule)], + }) + class AModule { + } - @NgModule({ - imports: [AModule], - }) - class BModule { - } + @NgModule({ + imports: [AModule], + }) + class BModule { + } - expect(() => createInjector(AModule)) - .toThrowError( - 'Circular dependency in DI detected for type AModule. Dependency path: AModule > BModule > AModule.'); - }); + expect(() => createInjector(AModule)) + .toThrowError( + 'Circular dependency in DI detected for type AModule. Dependency path: AModule > BModule > AModule.'); + }); it('merges imports and exports', () => { const TOKEN = new InjectionToken('TOKEN'); @@ -84,4 +82,4 @@ describe('Ivy NgModule', () => { expect(injector.get(TOKEN)).toEqual('provided from B'); }); }); -}); \ No newline at end of file +}); diff --git a/packages/core/src/render3/jit/directive.ts b/packages/core/src/render3/jit/directive.ts index a51e64e709..b2cd670256 100644 --- a/packages/core/src/render3/jit/directive.ts +++ b/packages/core/src/render3/jit/directive.ts @@ -18,7 +18,7 @@ import {NG_COMPONENT_DEF, NG_DIRECTIVE_DEF} from '../fields'; import {R3DirectiveMetadataFacade, getCompilerFacade} from './compiler_facade'; import {R3ComponentMetadataFacade, R3QueryMetadataFacade} from './compiler_facade_interface'; import {angularCoreEnv} from './environment'; -import {patchComponentDefWithScope, transitiveScopesFor} from './module'; +import {flushModuleScopingQueueAsMuchAsPossible, patchComponentDefWithScope, transitiveScopesFor} from './module'; import {getReflect, reflectDependencies} from './util'; @@ -51,6 +51,7 @@ export function compileComponent(type: Type, metadata: Component): void { error.push(`Did you run and wait for 'resolveComponentResources()'?`); throw new Error(error.join('\n')); } + const meta: R3ComponentMetadataFacade = { ...directiveMetadata(type, metadata), template: metadata.template || '', @@ -67,6 +68,13 @@ export function compileComponent(type: Type, metadata: Component): void { ngComponentDef = compiler.compileComponent( angularCoreEnv, `ng://${stringify(type)}/template.html`, meta); + // When NgModule decorator executed, we enqueued the module definition such that + // it would only dequeue and add itself as module scope to all of its declarations, + // but only if if all of its declarations had resolved. This call runs the check + // to see if any modules that are in the queue can be dequeued and add scope to + // their declarations. + flushModuleScopingQueueAsMuchAsPossible(); + // If component compilation is async, then the @NgModule annotation which declares the // component may execute and set an ngSelectorScope property on the component type. This // allows the component to patch itself with directiveDefs from the module after it diff --git a/packages/core/src/render3/jit/module.ts b/packages/core/src/render3/jit/module.ts index 1bc4c95be3..9ecddd2c75 100644 --- a/packages/core/src/render3/jit/module.ts +++ b/packages/core/src/render3/jit/module.ts @@ -6,6 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ +import {resolveForwardRef} from '../../di/forward_ref'; import {ModuleWithProviders, NgModule, NgModuleDef, NgModuleTransitiveScopes} from '../../metadata/ng_module'; import {Type} from '../../type'; import {assertDefined} from '../assert'; @@ -19,6 +20,55 @@ import {reflectDependencies} from './util'; const EMPTY_ARRAY: Type[] = []; +interface ModuleQueueItem { + moduleType: Type; + ngModule: NgModule; +} + +const moduleQueue: ModuleQueueItem[] = []; + +/** + * Enqueues moduleDef to be checked later to see if scope can be set on its + * component declarations. + */ +function enqueueModuleForDelayedScoping(moduleType: Type, ngModule: NgModule) { + moduleQueue.push({moduleType, ngModule}); +} + +let flushingModuleQueue = false; +/** + * Loops over queued module definitions, if a given module definition has all of its + * declarations resolved, it dequeues that module definition and sets the scope on + * its declarations. + */ +export function flushModuleScopingQueueAsMuchAsPossible() { + if (!flushingModuleQueue) { + flushingModuleQueue = true; + for (let i = moduleQueue.length - 1; i >= 0; i--) { + const {moduleType, ngModule} = moduleQueue[i]; + + if (ngModule.declarations && ngModule.declarations.every(isResolvedDeclaration)) { + // dequeue + moduleQueue.splice(i, 1); + setScopeOnDeclaredComponents(moduleType, ngModule); + } + } + flushingModuleQueue = false; + } +} + +/** + * Returns truthy if a declaration has resolved. If the declaration happens to be + * an array of declarations, it will recurse to check each declaration in that array + * (which may also be arrays). + */ +function isResolvedDeclaration(declaration: any[] | Type): boolean { + if (Array.isArray(declaration)) { + return declaration.every(isResolvedDeclaration); + } + return !!resolveForwardRef(declaration); +} + /** * Compiles a module in JIT mode. * @@ -26,7 +76,12 @@ const EMPTY_ARRAY: Type[] = []; */ export function compileNgModule(moduleType: Type, ngModule: NgModule = {}): void { compileNgModuleDefs(moduleType, ngModule); - setScopeOnDeclaredComponents(moduleType, ngModule); + + // Because we don't know if all declarations have resolved yet at the moment the + // NgModule decorator is executing, we're enqueueing the setting of module scope + // on its declarations to be run at a later time when all declarations for the module, + // including forward refs, have resolved. + enqueueModuleForDelayedScoping(moduleType, ngModule); } /** diff --git a/packages/core/test/render3/ivy/jit_spec.ts b/packages/core/test/render3/ivy/jit_spec.ts index 91e656481e..d9c9aa9253 100644 --- a/packages/core/test/render3/ivy/jit_spec.ts +++ b/packages/core/test/render3/ivy/jit_spec.ts @@ -205,8 +205,8 @@ ivyEnabled && describe('render3 jit', () => { } const moduleDef: NgModuleDef = (Module as any).ngModuleDef; - expect(cmpDef.directiveDefs instanceof Function).toBe(true); - expect((cmpDef.directiveDefs as Function)()).toEqual([cmpDef]); + // directive defs are still null, since no directives were in that component + expect(cmpDef.directiveDefs).toBeNull(); }); it('should add hostbindings and hostlisteners', () => {