fix(ivy): module with providers are processed too early (#30688)

Currently with Ivy, `ModuleWithProvider` providers are processed in order
of declaration in the `NgModule` imports. This technically makes makes
sense but is a potential breaking change as `ModuleWithProvider` providers
are processed after all imported modules in View Engine.

In order to keep the behavior of View Engine, the `r3_injector` is updated
to no longer process `ModuleWithProvider` providers egarly.

Resolves FW-1349

PR Close #30688
This commit is contained in:
Paul Gschwendtner 2019-05-27 20:34:01 +02:00 committed by Miško Hevery
parent 1f79c827a0
commit 6d861f240b
5 changed files with 156 additions and 16 deletions

View File

@ -222,14 +222,20 @@ export class R3Injector {
} }
/** /**
* Add an `InjectorType` or `InjectorDefTypeWithProviders` and all of its transitive providers * Add an `InjectorType` or `InjectorTypeWithProviders` and all of its transitive providers
* to this injector. * to this injector.
*
* If an `InjectorTypeWithProviders` that declares providers besides the type is specified,
* the function will return "true" to indicate that the providers of the type definition need
* to be processed. This allows us to process providers of injector types after all imports of
* an injector definition are processed. (following View Engine semantics: see FW-1349)
*/ */
private processInjectorType( private processInjectorType(
defOrWrappedDef: InjectorType<any>|InjectorTypeWithProviders<any>, defOrWrappedDef: InjectorType<any>|InjectorTypeWithProviders<any>,
parents: InjectorType<any>[], dedupStack: InjectorType<any>[]) { parents: InjectorType<any>[],
dedupStack: InjectorType<any>[]): defOrWrappedDef is InjectorTypeWithProviders<any> {
defOrWrappedDef = resolveForwardRef(defOrWrappedDef); defOrWrappedDef = resolveForwardRef(defOrWrappedDef);
if (!defOrWrappedDef) return; if (!defOrWrappedDef) return false;
// Either the defOrWrappedDef is an InjectorType (with ngInjectorDef) or an // Either the defOrWrappedDef is an InjectorType (with ngInjectorDef) or an
// InjectorDefTypeWithProviders (aka ModuleWithProviders). Detecting either is a megamorphic // InjectorDefTypeWithProviders (aka ModuleWithProviders). Detecting either is a megamorphic
@ -259,12 +265,6 @@ export class R3Injector {
// Check for multiple imports of the same module // Check for multiple imports of the same module
const isDuplicate = dedupStack.indexOf(defType) !== -1; const isDuplicate = dedupStack.indexOf(defType) !== -1;
// If defOrWrappedType was an InjectorDefTypeWithProviders, then .providers may hold some
// extra providers.
const providers =
(ngModule !== undefined) && (defOrWrappedDef as InjectorTypeWithProviders<any>).providers ||
EMPTY_ARRAY;
// Finally, if defOrWrappedType was an `InjectorDefTypeWithProviders`, then the actual // Finally, if defOrWrappedType was an `InjectorDefTypeWithProviders`, then the actual
// `InjectorDef` is on its `ngModule`. // `InjectorDef` is on its `ngModule`.
if (ngModule !== undefined) { if (ngModule !== undefined) {
@ -273,7 +273,7 @@ export class R3Injector {
// If no definition was found, it might be from exports. Remove it. // If no definition was found, it might be from exports. Remove it.
if (def == null) { if (def == null) {
return; return false;
} }
// Track the InjectorType and add a provider for it. // Track the InjectorType and add a provider for it.
@ -291,14 +291,33 @@ export class R3Injector {
// Add it to the set of dedups. This way we can detect multiple imports of the same module // Add it to the set of dedups. This way we can detect multiple imports of the same module
dedupStack.push(defType); dedupStack.push(defType);
let importTypesWithProviders: (InjectorTypeWithProviders<any>[])|undefined;
try { try {
deepForEach( deepForEach(def.imports, imported => {
def.imports, imported => this.processInjectorType(imported, parents, dedupStack)); if (this.processInjectorType(imported, parents, dedupStack)) {
if (importTypesWithProviders === undefined) importTypesWithProviders = [];
// If the processed import is an injector type with providers, we store it in the
// list of import types with providers, so that we can process those afterwards.
importTypesWithProviders.push(imported);
}
});
} finally { } finally {
// Remove it from the parents set when finished. // Remove it from the parents set when finished.
// TODO(FW-1307): Re-add ngDevMode when closure can handle it // TODO(FW-1307): Re-add ngDevMode when closure can handle it
parents.pop(); parents.pop();
} }
// Imports which are declared with providers (TypeWithProviders) need to be processed
// after all imported modules are processed. This is similar to how View Engine
// processes/merges module imports in the metadata resolver. See: FW-1349.
if (importTypesWithProviders !== undefined) {
for (let i = 0; i < importTypesWithProviders.length; i++) {
const {ngModule, providers} = importTypesWithProviders[i];
deepForEach(
providers !,
provider => this.processProvider(provider, ngModule, providers || EMPTY_ARRAY));
}
}
} }
// Next, include providers listed on the definition itself. // Next, include providers listed on the definition itself.
@ -309,9 +328,9 @@ export class R3Injector {
defProviders, provider => this.processProvider(provider, injectorType, defProviders)); defProviders, provider => this.processProvider(provider, injectorType, defProviders));
} }
// Finally, include providers from an InjectorDefTypeWithProviders if there was one. return (
const ngModuleType = (defOrWrappedDef as InjectorTypeWithProviders<any>).ngModule; ngModule !== undefined &&
deepForEach(providers, provider => this.processProvider(provider, ngModuleType, providers)); (defOrWrappedDef as InjectorTypeWithProviders<any>).providers !== undefined);
} }
/** /**

View File

@ -25,6 +25,7 @@ ts_library(
"//packages/platform-browser/testing", "//packages/platform-browser/testing",
"//packages/platform-server", "//packages/platform-server",
"//packages/private/testing", "//packages/private/testing",
"//packages/router",
"@npm//rxjs", "@npm//rxjs",
"@npm//zone.js", "@npm//zone.js",
], ],

View File

@ -7,7 +7,7 @@
*/ */
import {CommonModule} from '@angular/common'; import {CommonModule} from '@angular/common';
import {Attribute, ChangeDetectorRef, Component, Directive, ElementRef, EventEmitter, Host, HostBinding, INJECTOR, Inject, Injectable, Injector, Input, LOCALE_ID, Optional, Output, Pipe, PipeTransform, Self, SkipSelf, TemplateRef, ViewChild, ViewContainerRef, forwardRef} from '@angular/core'; import {Attribute, ChangeDetectorRef, Component, Directive, ElementRef, EventEmitter, Host, HostBinding, INJECTOR, Inject, Injectable, InjectionToken, Injector, Input, LOCALE_ID, ModuleWithProviders, NgModule, Optional, Output, Pipe, PipeTransform, Self, SkipSelf, TemplateRef, ViewChild, ViewContainerRef, forwardRef} from '@angular/core';
import {ViewRef} from '@angular/core/src/render3/view_ref'; import {ViewRef} from '@angular/core/src/render3/view_ref';
import {TestBed} from '@angular/core/testing'; import {TestBed} from '@angular/core/testing';
import {ivyEnabled, onlyInIvy} from '@angular/private/testing'; import {ivyEnabled, onlyInIvy} from '@angular/private/testing';
@ -31,6 +31,32 @@ describe('di', () => {
}); });
}); });
describe('multi providers', () => {
it('should process ModuleWithProvider providers after module imports', () => {
const testToken = new InjectionToken<string[]>('test-multi');
@NgModule({providers: [{provide: testToken, useValue: 'A', multi: true}]})
class TestModuleA {
}
@NgModule({providers: [{provide: testToken, useValue: 'B', multi: true}]})
class TestModuleB {
}
TestBed.configureTestingModule({
imports: [
{
ngModule: TestModuleA,
providers: [{provide: testToken, useValue: 'C', multi: true}],
},
TestModuleB,
]
});
expect(TestBed.get(testToken) as string[]).toEqual(['A', 'B', 'C']);
});
});
describe('directive injection', () => { describe('directive injection', () => {
let log: string[] = []; let log: string[] = [];

View File

@ -0,0 +1,64 @@
/**
* @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 {APP_BASE_HREF} from '@angular/common';
import {NgModule} from '@angular/core';
import {TestBed} from '@angular/core/testing';
import {Router, RouterModule} from '@angular/router';
describe('router integration acceptance', () => {
// Test case that ensures that we don't regress in multi-provider ordering
// which is leveraged in the router. See: FW-1349
it('should have correct order for multiple routes declared in different modules', () => {
@NgModule({
imports: [
RouterModule.forChild([
{path: '1a:1', redirectTo: ''},
{path: '1a:2', redirectTo: ''},
]),
],
})
class Level1AModule {
}
@NgModule({
imports: [
RouterModule.forChild([
{path: '1b:1', redirectTo: ''},
{path: '1b:2', redirectTo: ''},
]),
],
})
class Level1BModule {
}
@NgModule({
imports: [
RouterModule.forRoot([{path: 'root', redirectTo: ''}]),
Level1AModule,
Level1BModule,
],
providers: [
{provide: APP_BASE_HREF, useValue: '/'},
]
})
class RootModule {
}
TestBed.configureTestingModule({
imports: [RootModule],
});
expect((TestBed.get(Router) as Router).config.map(r => r.path)).toEqual([
'1a:1',
'1a:2',
'1b:1',
'1b:2',
'root',
]);
});
});

View File

@ -217,6 +217,31 @@ describe('InjectorDef-based createInjector()', () => {
}); });
} }
class MultiProviderA {
static ngInjectorDef = ɵɵdefineInjector({
factory: () => new MultiProviderA(),
providers: [{provide: LOCALE, multi: true, useValue: 'A'}],
});
}
class MultiProviderB {
static ngInjectorDef = ɵɵdefineInjector({
factory: () => new MultiProviderB(),
providers: [{provide: LOCALE, multi: true, useValue: 'B'}],
});
}
class WithProvidersTest {
static ngInjectorDef = ɵɵdefineInjector({
factory: () => new WithProvidersTest(),
imports: [
{ngModule: MultiProviderA, providers: [{provide: LOCALE, multi: true, useValue: 'C'}]},
MultiProviderB
],
providers: [],
});
}
let injector: Injector; let injector: Injector;
beforeEach(() => { beforeEach(() => {
@ -274,6 +299,11 @@ describe('InjectorDef-based createInjector()', () => {
expect(instance.locale).toEqual(['en', 'es']); expect(instance.locale).toEqual(['en', 'es']);
}); });
it('should process "InjectionTypeWithProviders" providers after imports injection type', () => {
injector = createInjector(WithProvidersTest);
expect(injector.get(LOCALE)).toEqual(['A', 'B', 'C']);
});
it('injects an injector with dependencies', () => { it('injects an injector with dependencies', () => {
const instance = injector.get(InjectorWithDep); const instance = injector.get(InjectorWithDep);
expect(instance instanceof InjectorWithDep); expect(instance instanceof InjectorWithDep);