From f7c294ee0f4131dae83d4b6a7fa4e497df84aa57 Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Fri, 2 Apr 2021 09:00:26 +0200 Subject: [PATCH] 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 --- packages/compiler-cli/test/ngc_spec.ts | 16 +++++++++ .../compiler-cli/test/ngtsc/ngtsc_spec.ts | 35 +++++++++++++++++++ packages/core/src/di/injector.ts | 2 +- packages/core/src/di/r3_injector.ts | 8 +++-- packages/core/src/view/ng_module.ts | 2 +- packages/core/src/view/services.ts | 6 ++-- packages/core/test/acceptance/di_spec.ts | 28 +++++++++++++++ .../core/testing/src/r3_test_bed_compiler.ts | 14 ++++---- 8 files changed, 96 insertions(+), 15 deletions(-) diff --git a/packages/compiler-cli/test/ngc_spec.ts b/packages/compiler-cli/test/ngc_spec.ts index 0f8cd65c1a..a18743add7 100644 --- a/packages/compiler-cli/test/ngc_spec.ts +++ b/packages/compiler-cli/test/ngc_spec.ts @@ -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', () => { diff --git a/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts b/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts index 34da420eff..95c0ef5209 100644 --- a/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts +++ b/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts @@ -183,6 +183,41 @@ function allTests(os: string) { expect(dtsContents).toContain('static ɵfac: i0.ɵɵFactoryDeclaration;'); }); + 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;'); + expect(dtsContents).toContain('static ɵprov: i0.ɵɵInjectableDef;'); + expect(dtsContents).toContain('static ɵfac: i0.ɵɵFactoryDeclaration;'); + expect(dtsContents).toContain('static ɵfac: i0.ɵɵFactoryDeclaration;'); + expect(dtsContents).toContain('i0.ɵɵFactoryDeclaration;'); + }); + it('should compile @Injectable with an @Optional dependency', () => { env.write('test.ts', ` import {Injectable, Optional as Opt} from '@angular/core'; diff --git a/packages/core/src/di/injector.ts b/packages/core/src/di/injector.ts index eff4badb3d..50cc2a6dac 100644 --- a/packages/core/src/di/injector.ts +++ b/packages/core/src/di/injector.ts @@ -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, diff --git a/packages/core/src/di/r3_injector.ts b/packages/core/src/di/r3_injector.ts index 625f5f4012..1cf239b9a7 100644 --- a/packages/core/src/di/r3_injector.ts +++ b/packages/core/src/di/r3_injector.ts @@ -420,10 +420,12 @@ export class R3Injector { private injectableDefInScope(def: ɵɵInjectableDef): 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); } } } diff --git a/packages/core/src/view/ng_module.ts b/packages/core/src/view/ng_module.ts index 5a414d4548..1c038d78ae 100644 --- a/packages/core/src/view/ng_module.ts +++ b/packages/core/src/view/ng_module.ts @@ -138,7 +138,7 @@ function moduleTransitivelyPresent(ngModule: NgModuleData, scope: any): boolean } function targetsModule(ngModule: NgModuleData, def: ɵɵInjectableDef): boolean { - const providedIn = def.providedIn; + const providedIn = resolveForwardRef(def.providedIn); return providedIn != null && (providedIn === 'any' || providedIn === ngModule._def.scope || moduleTransitivelyPresent(ngModule, providedIn)); diff --git a/packages/core/src/view/services.ts b/packages/core/src/view/services.ts index c15f0a4aa1..9879646758 100644 --- a/packages/core/src/view/services.ts +++ b/packages/core/src/view/services.ts @@ -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(def.modules); providerOverridesWithScope.forEach((override, token) => { - if (moduleSet.has(getInjectableDef(token)!.providedIn)) { + if (moduleSet.has(resolveForwardRef(getInjectableDef(token)!.providedIn))) { let provider = { token: token, flags: diff --git a/packages/core/test/acceptance/di_spec.ts b/packages/core/test/acceptance/di_spec.ts index fe1fe122c8..69cda5ce1b 100644 --- a/packages/core/test/acceptance/di_spec.ts +++ b/packages/core/test/acceptance/di_spec.ts @@ -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', () => { diff --git a/packages/core/testing/src/r3_test_bed_compiler.ts b/packages/core/testing/src/r3_test_bed_compiler.ts index 6c0c1f6fef..f5efc15cab 100644 --- a/packages/core/testing/src/r3_test_bed_compiler.ts +++ b/packages/core/testing/src/r3_test_bed_compiler.ts @@ -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|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]); } } }