fix(ivy): ngUpgrade should distinguish element and module injectors (#28313)

There are cases where we should check an element injector but don't go
into the associated module injector if a token is not found. In both the
view engine and ngIvy this is acheived by passing the
`NOT_FOUND_CHECK_ONLY_ELEMENT_INJECTOR` as the `notFoundValue`.

Before this fix the view engine and ngIvy were using different objects to
represent `NOT_FOUND_CHECK_ONLY_ELEMENT_INJECTOR`. This was causing problems
as ngUpgrade is using `NOT_FOUND_CHECK_ONLY_ELEMENT_INJECTOR` const in its
`NgAdapterInjector` to prevent searching of module injectors.

This commit makes sure that ngIvy is using the same object to represent
`NOT_FOUND_CHECK_ONLY_ELEMENT_INJECTOR` as the view engine.

PR Close #28313
This commit is contained in:
Pawel Kozlowski 2019-01-23 13:26:48 +01:00 committed by Jason Aden
parent 22a43cff4d
commit 32c61f434c
3 changed files with 140 additions and 145 deletions

View File

@ -19,6 +19,7 @@ import {RendererFactory2} from '../render/api';
import {Sanitizer} from '../sanitization/security'; import {Sanitizer} from '../sanitization/security';
import {assertDefined} from '../util/assert'; import {assertDefined} from '../util/assert';
import {VERSION} from '../version'; import {VERSION} from '../version';
import {NOT_FOUND_CHECK_ONLY_ELEMENT_INJECTOR} from '../view/provider';
import {assertComponentType} from './assert'; import {assertComponentType} from './assert';
import {LifecycleHooksFeature, createRootComponent, createRootComponentView, createRootContext} from './component'; import {LifecycleHooksFeature, createRootComponent, createRootComponentView, createRootContext} from './component';
@ -74,8 +75,6 @@ export const SCHEDULER = new InjectionToken<((fn: () => void) => void)>('SCHEDUL
factory: () => defaultScheduler, factory: () => defaultScheduler,
}); });
const NOT_FOUND_CHECK_ONLY_ELEMENT_INJECTOR = {};
function createChainedInjector(rootViewInjector: Injector, moduleInjector: Injector): Injector { function createChainedInjector(rootViewInjector: Injector, moduleInjector: Injector): Injector {
return { return {
get: <T>(token: Type<T>| InjectionToken<T>, notFoundValue?: T): T => { get: <T>(token: Type<T>| InjectionToken<T>, notFoundValue?: T): T => {

View File

@ -736,56 +736,54 @@ withEachNg1Version(() => {
}); });
})); }));
fixmeIvy( it('should work with ng2 lazy loaded components', async(() => {
'FW-717: Injector on lazy loaded components are not the same as their NgModule\'s injector') let componentInjector: Injector;
.it('should work with ng2 lazy loaded components', async(() => {
let componentInjector: Injector;
@Component({selector: 'ng2', template: ''}) @Component({selector: 'ng2', template: ''})
class Ng2Component { class Ng2Component {
constructor(injector: Injector) { componentInjector = injector; } constructor(injector: Injector) { componentInjector = injector; }
} }
@NgModule({ @NgModule({
declarations: [Ng2Component], declarations: [Ng2Component],
entryComponents: [Ng2Component], entryComponents: [Ng2Component],
imports: [BrowserModule, UpgradeModule], imports: [BrowserModule, UpgradeModule],
}) })
class Ng2Module { class Ng2Module {
ngDoBootstrap() {} ngDoBootstrap() {}
} }
@Component({template: ''}) @Component({template: ''})
class LazyLoadedComponent { class LazyLoadedComponent {
constructor(public module: NgModuleRef<any>) {} constructor(public module: NgModuleRef<any>) {}
} }
@NgModule({ @NgModule({
declarations: [LazyLoadedComponent], declarations: [LazyLoadedComponent],
entryComponents: [LazyLoadedComponent], entryComponents: [LazyLoadedComponent],
}) })
class LazyLoadedModule { class LazyLoadedModule {
} }
const ng1Module = angular.module('ng1', []).directive( const ng1Module = angular.module('ng1', []).directive(
'ng2', downgradeComponent({component: Ng2Component})); 'ng2', downgradeComponent({component: Ng2Component}));
const element = html('<ng2></ng2>'); const element = html('<ng2></ng2>');
bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then(upgrade => { bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then(upgrade => {
const modInjector = upgrade.injector; const modInjector = upgrade.injector;
// Emulate the router lazy loading a module and creating a component // Emulate the router lazy loading a module and creating a component
const compiler = modInjector.get(Compiler); const compiler = modInjector.get(Compiler);
const modFactory = compiler.compileModuleSync(LazyLoadedModule); const modFactory = compiler.compileModuleSync(LazyLoadedModule);
const childMod = modFactory.create(modInjector); const childMod = modFactory.create(modInjector);
const cmpFactory = childMod.componentFactoryResolver.resolveComponentFactory( const cmpFactory =
LazyLoadedComponent) !; childMod.componentFactoryResolver.resolveComponentFactory(LazyLoadedComponent) !;
const lazyCmp = cmpFactory.create(componentInjector); const lazyCmp = cmpFactory.create(componentInjector);
expect(lazyCmp.instance.module.injector === childMod.injector).toBe(true); expect(lazyCmp.instance.module.injector === childMod.injector).toBe(true);
}); });
})); }));
it('should throw if `downgradedModule` is specified', async(() => { it('should throw if `downgradedModule` is specified', async(() => {
@Component({selector: 'ng2', template: ''}) @Component({selector: 'ng2', template: ''})

View File

@ -416,127 +416,125 @@ withEachNg1Version(() => {
}); });
})); }));
fixmeIvy('FW-873: projected component injector hierarchy not wired up correctly') it('should correctly traverse the injector tree of downgraded components (from different modules)',
.it('should correctly traverse the injector tree of downgraded components (from different modules)', async(() => {
async(() => { @Component({
@Component({ selector: 'ng2A',
selector: 'ng2A', template: 'ng2A(<ng-content></ng-content>)',
template: 'ng2A(<ng-content></ng-content>)', providers: [
providers: [ {provide: 'FOO', useValue: 'CompA-foo'},
{provide: 'FOO', useValue: 'CompA-foo'}, {provide: 'BAR', useValue: 'CompA-bar'},
{provide: 'BAR', useValue: 'CompA-bar'}, ],
], })
}) class Ng2ComponentA {
class Ng2ComponentA { }
}
@Component({ @Component({
selector: 'ng2B', selector: 'ng2B',
template: ` template: `
FOO:{{ foo }} FOO:{{ foo }}
BAR:{{ bar }} BAR:{{ bar }}
BAZ:{{ baz }} BAZ:{{ baz }}
QUX:{{ qux }} QUX:{{ qux }}
QUUX:{{ quux }} QUUX:{{ quux }}
`, `,
providers: [ providers: [
{provide: 'FOO', useValue: 'CompB-foo'}, {provide: 'FOO', useValue: 'CompB-foo'},
], ],
}) })
class Ng2ComponentB { class Ng2ComponentB {
constructor( constructor(
@Inject('FOO') public foo: string, @Inject('BAR') public bar: string, @Inject('FOO') public foo: string, @Inject('BAR') public bar: string,
@Inject('BAZ') public baz: string, @Inject('QUX') public qux: string, @Inject('BAZ') public baz: string, @Inject('QUX') public qux: string,
@Inject('QUUX') public quux: string) {} @Inject('QUUX') public quux: string) {}
} }
@NgModule({ @NgModule({
declarations: [Ng2ComponentA], declarations: [Ng2ComponentA],
entryComponents: [Ng2ComponentA], entryComponents: [Ng2ComponentA],
imports: [BrowserModule], imports: [BrowserModule],
providers: [ providers: [
{provide: 'FOO', useValue: 'ModA-foo'}, {provide: 'FOO', useValue: 'ModA-foo'},
{provide: 'BAR', useValue: 'ModA-bar'}, {provide: 'BAR', useValue: 'ModA-bar'},
{provide: 'BAZ', useValue: 'ModA-baz'}, {provide: 'BAZ', useValue: 'ModA-baz'},
{provide: 'QUX', useValue: 'ModA-qux'}, {provide: 'QUX', useValue: 'ModA-qux'},
], ],
}) })
class Ng2ModuleA { class Ng2ModuleA {
ngDoBootstrap() {} ngDoBootstrap() {}
} }
@NgModule({ @NgModule({
declarations: [Ng2ComponentB], declarations: [Ng2ComponentB],
entryComponents: [Ng2ComponentB], entryComponents: [Ng2ComponentB],
imports: [BrowserModule], imports: [BrowserModule],
providers: [ providers: [
{provide: 'FOO', useValue: 'ModB-foo'}, {provide: 'FOO', useValue: 'ModB-foo'},
{provide: 'BAR', useValue: 'ModB-bar'}, {provide: 'BAR', useValue: 'ModB-bar'},
{provide: 'BAZ', useValue: 'ModB-baz'}, {provide: 'BAZ', useValue: 'ModB-baz'},
], ],
}) })
class Ng2ModuleB { class Ng2ModuleB {
ngDoBootstrap() {} ngDoBootstrap() {}
} }
const doDowngradeModule = (module: Type<any>) => { const doDowngradeModule = (module: Type<any>) => {
const bootstrapFn = (extraProviders: StaticProvider[]) => { const bootstrapFn = (extraProviders: StaticProvider[]) => {
const platformRef = getPlatform() || platformBrowserDynamic([ const platformRef = getPlatform() || platformBrowserDynamic([
...extraProviders, ...extraProviders,
{provide: 'FOO', useValue: 'Plat-foo'}, {provide: 'FOO', useValue: 'Plat-foo'},
{provide: 'BAR', useValue: 'Plat-bar'}, {provide: 'BAR', useValue: 'Plat-bar'},
{provide: 'BAZ', useValue: 'Plat-baz'}, {provide: 'BAZ', useValue: 'Plat-baz'},
{provide: 'QUX', useValue: 'Plat-qux'}, {provide: 'QUX', useValue: 'Plat-qux'},
{provide: 'QUUX', useValue: 'Plat-quux'}, {provide: 'QUUX', useValue: 'Plat-quux'},
]); ]);
return platformRef.bootstrapModule(module); return platformRef.bootstrapModule(module);
}; };
return downgradeModule(bootstrapFn); return downgradeModule(bootstrapFn);
}; };
const downModA = doDowngradeModule(Ng2ModuleA); const downModA = doDowngradeModule(Ng2ModuleA);
const downModB = doDowngradeModule(Ng2ModuleB); const downModB = doDowngradeModule(Ng2ModuleB);
const ng1Module = angular.module('ng1', [downModA, downModB]) const ng1Module = angular.module('ng1', [downModA, downModB])
.directive('ng2A', downgradeComponent({ .directive('ng2A', downgradeComponent({
component: Ng2ComponentA, component: Ng2ComponentA,
downgradedModule: downModA, propagateDigest, downgradedModule: downModA, propagateDigest,
})) }))
.directive('ng2B', downgradeComponent({ .directive('ng2B', downgradeComponent({
component: Ng2ComponentB, component: Ng2ComponentB,
downgradedModule: downModB, propagateDigest, downgradedModule: downModB, propagateDigest,
})); }));
const element = html(` const element = html(`
<ng2-a><ng2-b ng-if="showB1"></ng2-b></ng2-a> <ng2-a><ng2-b ng-if="showB1"></ng2-b></ng2-a>
<ng2-b ng-if="showB2"></ng2-b> <ng2-b ng-if="showB2"></ng2-b>
`); `);
const $injector = angular.bootstrap(element, [ng1Module.name]); const $injector = angular.bootstrap(element, [ng1Module.name]);
const $rootScope = $injector.get($ROOT_SCOPE) as angular.IRootScopeService; const $rootScope = $injector.get($ROOT_SCOPE) as angular.IRootScopeService;
// Wait for module A to be bootstrapped. // Wait for module A to be bootstrapped.
setTimeout(() => { setTimeout(() => {
expect(multiTrim(element.textContent)).toBe('ng2A()'); expect(multiTrim(element.textContent)).toBe('ng2A()');
// Nested component B. // Nested component B.
$rootScope.$apply('showB1 = true'); $rootScope.$apply('showB1 = true');
// Wait for module B to be bootstrapped. // Wait for module B to be bootstrapped.
setTimeout(() => { setTimeout(() => {
// It is debatable, whether the order of traversal should be: // It is debatable, whether the order of traversal should be:
// CompB > CompA > ModB > ModA > Plat (similar to how lazy-loaded components // CompB > CompA > ModB > ModA > Plat (similar to how lazy-loaded components
// work) // work)
expect(multiTrim(element.children[0].textContent)) expect(multiTrim(element.children[0].textContent))
.toBe( .toBe(
'ng2A( FOO:CompB-foo BAR:CompA-bar BAZ:ModB-baz QUX:Plat-qux QUUX:Plat-quux )'); 'ng2A( FOO:CompB-foo BAR:CompA-bar BAZ:ModB-baz QUX:Plat-qux QUUX:Plat-quux )');
// Standalone component B. // Standalone component B.
$rootScope.$apply('showB2 = true'); $rootScope.$apply('showB2 = true');
expect(multiTrim(element.children[1].textContent)) expect(multiTrim(element.children[1].textContent))
.toBe( .toBe('FOO:CompB-foo BAR:ModB-bar BAZ:ModB-baz QUX:Plat-qux QUUX:Plat-quux');
'FOO:CompB-foo BAR:ModB-bar BAZ:ModB-baz QUX:Plat-qux QUUX:Plat-quux'); });
}); });
}); }));
}));
it('should support downgrading a component and propagate inputs', async(() => { it('should support downgrading a component and propagate inputs', async(() => {
@Component( @Component(