feat(core): support `forwardRef` in `providedIn` of `Injectable` declaration (#41426)

Adds support for using a `forwardRef` inside of the `providedIn` of an `Injectable` declaration.

Fixes #41205.

PR Close #41426
This commit is contained in:
Kristiyan Kostadinov 2021-04-02 09:00:26 +02:00 committed by atscott
parent acebe92bad
commit f7c294ee0f
8 changed files with 96 additions and 15 deletions

View File

@ -2303,6 +2303,22 @@ describe('ngc transformer command-line', () => {
`);
expect(source).toMatch(/new Service\(i0\.ɵɵinject\(exports\.TOKEN\)\);/);
});
it('compiles an injectable using `forwardRef` inside `providedIn`', () => {
const source = compileService(`
import {Injectable, forwardRef} from '@angular/core';
import {Module} from './module';
@Injectable({
providedIn: forwardRef(() => Module),
})
export class Service {}
`);
expect(source).toMatch(/ɵprov = .+\.ɵɵdefineInjectable\(/);
expect(source).toMatch(/ɵprov.*token: Service/);
expect(source).toMatch(/ɵprov.*providedIn: .+\.Module/);
});
});
it('libraries should not break strictMetadataEmit', () => {

View File

@ -183,6 +183,41 @@ function allTests(os: string) {
expect(dtsContents).toContain('static ɵfac: i0.ɵɵFactoryDeclaration<Service, never>;');
});
it('should compile Injectables with providedIn using forwardRef without errors', () => {
env.write('test.ts', `
import {Injectable, NgModule, forwardRef} from '@angular/core';
@Injectable()
export class Dep {}
@Injectable({ providedIn: forwardRef(() => Mod) })
export class Service {
constructor(dep: Dep) {}
}
@NgModule()
export class Mod {}
`);
env.driveMain();
const jsContents = env.getContents('test.js');
expect(jsContents).toContain('Dep.ɵprov =');
expect(jsContents).toContain('Service.ɵprov =');
expect(jsContents).toContain('Mod.ɵmod =');
expect(jsContents)
.toContain(
'Service.ɵfac = function Service_Factory(t) { return new (t || Service)(i0.ɵɵinject(Dep)); };');
expect(jsContents).toContain('providedIn: forwardRef(function () { return Mod; }) })');
expect(jsContents).not.toContain('__decorate');
const dtsContents = env.getContents('test.d.ts');
expect(dtsContents).toContain('static ɵprov: i0.ɵɵInjectableDef<Dep>;');
expect(dtsContents).toContain('static ɵprov: i0.ɵɵInjectableDef<Service>;');
expect(dtsContents).toContain('static ɵfac: i0.ɵɵFactoryDeclaration<Dep, never>;');
expect(dtsContents).toContain('static ɵfac: i0.ɵɵFactoryDeclaration<Service, never>;');
expect(dtsContents).toContain('i0.ɵɵFactoryDeclaration<Mod, never>;');
});
it('should compile @Injectable with an @Optional dependency', () => {
env.write('test.ts', `
import {Injectable, Optional as Opt} from '@angular/core';

View File

@ -166,7 +166,7 @@ export class StaticInjector implements Injector {
// This means we have never seen this record, see if it is tree shakable provider.
const injectableDef = getInjectableDef(token);
if (injectableDef) {
const providedIn = injectableDef && injectableDef.providedIn;
const providedIn = injectableDef && resolveForwardRef(injectableDef.providedIn);
if (providedIn === 'any' || providedIn != null && providedIn === this.scope) {
records.set(
token,

View File

@ -420,10 +420,12 @@ export class R3Injector {
private injectableDefInScope(def: ɵɵInjectableDef<any>): boolean {
if (!def.providedIn) {
return false;
} else if (typeof def.providedIn === 'string') {
return def.providedIn === 'any' || (def.providedIn === this.scope);
}
const providedIn = resolveForwardRef(def.providedIn);
if (typeof providedIn === 'string') {
return providedIn === 'any' || (providedIn === this.scope);
} else {
return this.injectorDefTypes.has(def.providedIn);
return this.injectorDefTypes.has(providedIn);
}
}
}

View File

@ -138,7 +138,7 @@ function moduleTransitivelyPresent(ngModule: NgModuleData, scope: any): boolean
}
function targetsModule(ngModule: NgModuleData, def: ɵɵInjectableDef<any>): boolean {
const providedIn = def.providedIn;
const providedIn = resolveForwardRef(def.providedIn);
return providedIn != null &&
(providedIn === 'any' || providedIn === ngModule._def.scope ||
moduleTransitivelyPresent(ngModule, providedIn));

View File

@ -7,7 +7,7 @@
*/
import {DebugElement__PRE_R3__, DebugEventListener, DebugNode__PRE_R3__, getDebugNode, indexDebugNode, removeDebugNodeFromIndex} from '../debug/debug_node';
import {Injector} from '../di';
import {Injector, resolveForwardRef} from '../di';
import {getInjectableDef, InjectableType, ɵɵInjectableDef} from '../di/interface/defs';
import {ErrorHandler} from '../error_handler';
import {Type} from '../interface/type';
@ -282,7 +282,7 @@ function applyProviderOverridesToNgModule(def: NgModuleDefinition): NgModuleDefi
});
def.modules.forEach(module => {
providerOverridesWithScope.forEach((override, token) => {
if (getInjectableDef(token)!.providedIn === module) {
if (resolveForwardRef(getInjectableDef(token)!.providedIn) === module) {
hasOverrides = true;
hasDeprecatedOverrides = hasDeprecatedOverrides || override.deprecatedBehavior;
}
@ -310,7 +310,7 @@ function applyProviderOverridesToNgModule(def: NgModuleDefinition): NgModuleDefi
if (providerOverridesWithScope.size > 0) {
let moduleSet = new Set<any>(def.modules);
providerOverridesWithScope.forEach((override, token) => {
if (moduleSet.has(getInjectableDef(token)!.providedIn)) {
if (moduleSet.has(resolveForwardRef(getInjectableDef(token)!.providedIn))) {
let provider = {
token: token,
flags:

View File

@ -1951,6 +1951,34 @@ describe('di', () => {
const platformService = childInjector.get(PlatformService);
expect(platformService.injector.get(ɵINJECTOR_SCOPE)).toBe('platform');
});
it('should create a provider that uses `forwardRef` inside `providedIn`', () => {
@Injectable()
class ProviderDep {
getNumber() {
return 3;
}
}
@Injectable({providedIn: forwardRef(() => Module)})
class Provider {
constructor(private _dep: ProviderDep) {}
value = this._dep.getNumber() + 2;
}
@Component({template: ''})
class Comp {
constructor(public provider: Provider) {}
}
@NgModule({declarations: [Comp], exports: [Comp], providers: [ProviderDep]})
class Module {
}
TestBed.configureTestingModule({imports: [Module]});
const fixture = TestBed.createComponent(Comp);
expect(fixture.componentInstance.provider.value).toBe(5);
});
});
describe('service injection', () => {

View File

@ -7,7 +7,7 @@
*/
import {ResourceLoader} from '@angular/compiler';
import {ApplicationInitStatus, Compiler, COMPILER_OPTIONS, Component, Directive, Injector, InjectorType, LOCALE_ID, ModuleWithComponentFactories, ModuleWithProviders, NgModule, NgModuleFactory, NgZone, Pipe, PlatformRef, Provider, Type, ɵcompileComponent as compileComponent, ɵcompileDirective as compileDirective, ɵcompileNgModuleDefs as compileNgModuleDefs, ɵcompilePipe as compilePipe, ɵDEFAULT_LOCALE_ID as DEFAULT_LOCALE_ID, ɵDirectiveDef as DirectiveDef, ɵgetInjectableDef as getInjectableDef, ɵNG_COMP_DEF as NG_COMP_DEF, ɵNG_DIR_DEF as NG_DIR_DEF, ɵNG_INJ_DEF as NG_INJ_DEF, ɵNG_MOD_DEF as NG_MOD_DEF, ɵNG_PIPE_DEF as NG_PIPE_DEF, ɵNgModuleFactory as R3NgModuleFactory, ɵNgModuleTransitiveScopes as NgModuleTransitiveScopes, ɵNgModuleType as NgModuleType, ɵpatchComponentDefWithScope as patchComponentDefWithScope, ɵRender3ComponentFactory as ComponentFactory, ɵRender3NgModuleRef as NgModuleRef, ɵsetLocaleId as setLocaleId, ɵtransitiveScopesFor as transitiveScopesFor, ɵɵInjectableDef as InjectableDef} from '@angular/core';
import {ApplicationInitStatus, Compiler, COMPILER_OPTIONS, Component, Directive, Injector, InjectorType, LOCALE_ID, ModuleWithComponentFactories, ModuleWithProviders, NgModule, NgModuleFactory, NgZone, Pipe, PlatformRef, Provider, resolveForwardRef, Type, ɵcompileComponent as compileComponent, ɵcompileDirective as compileDirective, ɵcompileNgModuleDefs as compileNgModuleDefs, ɵcompilePipe as compilePipe, ɵDEFAULT_LOCALE_ID as DEFAULT_LOCALE_ID, ɵDirectiveDef as DirectiveDef, ɵgetInjectableDef as getInjectableDef, ɵNG_COMP_DEF as NG_COMP_DEF, ɵNG_DIR_DEF as NG_DIR_DEF, ɵNG_INJ_DEF as NG_INJ_DEF, ɵNG_MOD_DEF as NG_MOD_DEF, ɵNG_PIPE_DEF as NG_PIPE_DEF, ɵNgModuleFactory as R3NgModuleFactory, ɵNgModuleTransitiveScopes as NgModuleTransitiveScopes, ɵNgModuleType as NgModuleType, ɵpatchComponentDefWithScope as patchComponentDefWithScope, ɵRender3ComponentFactory as ComponentFactory, ɵRender3NgModuleRef as NgModuleRef, ɵsetLocaleId as setLocaleId, ɵtransitiveScopesFor as transitiveScopesFor, ɵɵInjectableDef as InjectableDef} from '@angular/core';
import {clearResolutionOfComponentResourcesQueue, isComponentDefPendingResolution, resolveComponentResources, restoreComponentResolutionQueue} from '../../src/metadata/resource_loading';
@ -176,19 +176,19 @@ export class R3TestBedCompiler {
const injectableDef: InjectableDef<any>|null =
typeof token !== 'string' ? getInjectableDef(token) : null;
const isRoot = injectableDef !== null && injectableDef.providedIn === 'root';
const overridesBucket = isRoot ? this.rootProviderOverrides : this.providerOverrides;
const providedIn = injectableDef === null ? null : resolveForwardRef(injectableDef.providedIn);
const overridesBucket =
providedIn === 'root' ? this.rootProviderOverrides : this.providerOverrides;
overridesBucket.push(providerDef);
// Keep overrides grouped by token as well for fast lookups using token
this.providerOverridesByToken.set(token, providerDef);
if (injectableDef !== null && injectableDef.providedIn !== null &&
typeof injectableDef.providedIn !== 'string') {
const existingOverrides = this.providerOverridesByModule.get(injectableDef.providedIn);
if (injectableDef !== null && providedIn !== null && typeof providedIn !== 'string') {
const existingOverrides = this.providerOverridesByModule.get(providedIn);
if (existingOverrides !== undefined) {
existingOverrides.push(providerDef);
} else {
this.providerOverridesByModule.set(injectableDef.providedIn, [providerDef]);
this.providerOverridesByModule.set(providedIn, [providerDef]);
}
}
}