feat(ivy): Forward refs now supported (#27439)

Adds deferred execution of scope setting for modules such that forward refs can be supported in ivy. Design docs can be found at https://docs.google.com/document/d/11KTbybis9rt0cZgMKd1wo_IKb6y1PMU-RoTDVLTXK4Y/edit#

PR Close #27439
This commit is contained in:
Ben Lesh 2018-12-03 13:58:07 -08:00 committed by Igor Minar
parent 2bc39860bb
commit cd858323f2
5 changed files with 83 additions and 23 deletions

View File

@ -24,7 +24,6 @@ jasmine_node_test(
name = "test",
bootstrap = ["angular/tools/testing/init_node_spec.js"],
tags = [
"fixme-ivy-aot",
"ivy-only",
],
deps = [

View File

@ -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<string>('TOKEN');
@ -84,4 +82,4 @@ describe('Ivy NgModule', () => {
expect(injector.get(TOKEN)).toEqual('provided from B');
});
});
});
});

View File

@ -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<any>, 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<any>, 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

View File

@ -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<any>[] = [];
interface ModuleQueueItem {
moduleType: Type<any>;
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<any>, 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<any>): 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<any>[] = [];
*/
export function compileNgModule(moduleType: Type<any>, 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);
}
/**

View File

@ -205,8 +205,8 @@ ivyEnabled && describe('render3 jit', () => {
}
const moduleDef: NgModuleDef<Module> = (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', () => {