From db56836425fe200f42e299bce3e76bca0a6021e9 Mon Sep 17 00:00:00 2001 From: Alex Rickabaugh Date: Wed, 7 Mar 2018 15:10:38 -0800 Subject: [PATCH] feat: tree-shakeable providers API updates (#22655) Rename @Injectable({scope -> providedIn}). Instead of {providedIn: APP_ROOT_SCOPE}, accept {providedIn: 'root'}. Also, {providedIn: null} implies the injectable should not be added to any scope. PR Close #22655 --- integration/injectable-def/src/lib1.ts | 2 +- .../bazel/injectable_def/app/src/dep.ts | 2 +- .../bazel/injectable_def/app/src/root.ts | 2 +- .../injectable_def/app/src/root_service.ts | 4 +- .../bazel/injectable_def/app/src/self.ts | 2 +- .../bazel/injectable_def/app/src/string.ts | 2 +- .../bazel/injectable_def/app/src/token.ts | 2 +- .../bazel/injectable_def/app/test/BUILD.bazel | 1 + .../bazel/injectable_def/app/test/app_spec.ts | 42 +++++++++++++++++-- .../bazel/injectable_def/lib1/module.ts | 2 +- packages/compiler-cli/test/ngc_spec.ts | 26 ++++++------ packages/compiler/src/compile_metadata.ts | 2 +- packages/compiler/src/core.ts | 2 +- packages/compiler/src/injectable_compiler.ts | 12 +++++- packages/compiler/src/metadata_resolver.ts | 2 +- packages/core/src/core_private_export.ts | 1 + packages/core/src/di.ts | 1 - packages/core/src/di/injectable.ts | 32 +++++++++----- packages/core/src/di/injection_token.ts | 11 +++-- packages/core/src/di/scope.ts | 18 +++----- packages/core/src/view/ng_module.ts | 21 ++++------ packages/core/src/view/services.ts | 34 ++++++++++++++- packages/core/src/view/types.ts | 5 --- .../compiler_canonical/ng_module_spec.ts | 4 +- packages/core/test/view/ng_module_spec.ts | 35 ++++++++-------- packages/core/testing/src/test_bed.ts | 36 ++++++++++++++-- packages/examples/core/di/ts/injector_spec.ts | 8 ++-- packages/platform-browser/src/browser.ts | 4 +- tools/public_api_guard/core/core.d.ts | 18 ++++---- 29 files changed, 219 insertions(+), 114 deletions(-) diff --git a/integration/injectable-def/src/lib1.ts b/integration/injectable-def/src/lib1.ts index 093339129f..c2df128273 100644 --- a/integration/injectable-def/src/lib1.ts +++ b/integration/injectable-def/src/lib1.ts @@ -3,7 +3,7 @@ import {Injectable, NgModule} from '@angular/core'; @NgModule({}) export class Lib1Module {} -@Injectable({scope: Lib1Module}) +@Injectable({providedIn: Lib1Module}) export class Service { static instance = 0; readonly instance = Service.instance++; diff --git a/packages/compiler-cli/integrationtest/bazel/injectable_def/app/src/dep.ts b/packages/compiler-cli/integrationtest/bazel/injectable_def/app/src/dep.ts index a02cbe6e2e..4cc1c02b7d 100644 --- a/packages/compiler-cli/integrationtest/bazel/injectable_def/app/src/dep.ts +++ b/packages/compiler-cli/integrationtest/bazel/injectable_def/app/src/dep.ts @@ -35,7 +35,7 @@ export class AppComponent { export class DepAppModule { } -@Injectable({scope: DepAppModule}) +@Injectable({providedIn: DepAppModule}) export class ShakeableService { constructor(readonly normal: NormalService) {} } \ No newline at end of file diff --git a/packages/compiler-cli/integrationtest/bazel/injectable_def/app/src/root.ts b/packages/compiler-cli/integrationtest/bazel/injectable_def/app/src/root.ts index d04e603618..3d56d26bec 100644 --- a/packages/compiler-cli/integrationtest/bazel/injectable_def/app/src/root.ts +++ b/packages/compiler-cli/integrationtest/bazel/injectable_def/app/src/root.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import {APP_ROOT_SCOPE, Component, Injectable, NgModule, Optional, Self} from '@angular/core'; +import {Component, Injectable, NgModule, Optional, Self} from '@angular/core'; import {BrowserModule} from '@angular/platform-browser'; import {ServerModule} from '@angular/platform-server'; import {RouterModule} from '@angular/router'; diff --git a/packages/compiler-cli/integrationtest/bazel/injectable_def/app/src/root_service.ts b/packages/compiler-cli/integrationtest/bazel/injectable_def/app/src/root_service.ts index 77a749b42f..487b40ab0b 100644 --- a/packages/compiler-cli/integrationtest/bazel/injectable_def/app/src/root_service.ts +++ b/packages/compiler-cli/integrationtest/bazel/injectable_def/app/src/root_service.ts @@ -6,10 +6,10 @@ * found in the LICENSE file at https://angular.io/license */ -import {APP_ROOT_SCOPE, Injectable} from '@angular/core'; +import {Injectable} from '@angular/core'; @Injectable({ - scope: APP_ROOT_SCOPE, + providedIn: 'root', }) export class Service { } diff --git a/packages/compiler-cli/integrationtest/bazel/injectable_def/app/src/self.ts b/packages/compiler-cli/integrationtest/bazel/injectable_def/app/src/self.ts index 9a211bc030..fb37c76e40 100644 --- a/packages/compiler-cli/integrationtest/bazel/injectable_def/app/src/self.ts +++ b/packages/compiler-cli/integrationtest/bazel/injectable_def/app/src/self.ts @@ -36,6 +36,6 @@ export class AppComponent { export class SelfAppModule { } -@Injectable({scope: SelfAppModule}) +@Injectable({providedIn: SelfAppModule}) export class ShakeableService { } \ No newline at end of file diff --git a/packages/compiler-cli/integrationtest/bazel/injectable_def/app/src/string.ts b/packages/compiler-cli/integrationtest/bazel/injectable_def/app/src/string.ts index 91cc13ca7b..fca98267bd 100644 --- a/packages/compiler-cli/integrationtest/bazel/injectable_def/app/src/string.ts +++ b/packages/compiler-cli/integrationtest/bazel/injectable_def/app/src/string.ts @@ -32,7 +32,7 @@ export class AppComponent { export class StringAppModule { } -@Injectable({scope: StringAppModule}) +@Injectable({providedIn: StringAppModule}) export class Service { constructor(@Inject('someStringToken') readonly data: string) {} } diff --git a/packages/compiler-cli/integrationtest/bazel/injectable_def/app/src/token.ts b/packages/compiler-cli/integrationtest/bazel/injectable_def/app/src/token.ts index b458c405c6..3b93c3e5d8 100644 --- a/packages/compiler-cli/integrationtest/bazel/injectable_def/app/src/token.ts +++ b/packages/compiler-cli/integrationtest/bazel/injectable_def/app/src/token.ts @@ -17,7 +17,7 @@ export class TokenModule { } export const TOKEN = new InjectionToken('test', { - scope: TokenModule, + providedIn: TokenModule, factory: () => new Service(inject(Dep)), }); diff --git a/packages/compiler-cli/integrationtest/bazel/injectable_def/app/test/BUILD.bazel b/packages/compiler-cli/integrationtest/bazel/injectable_def/app/test/BUILD.bazel index 570c6fa0e9..5e4bb4bd12 100644 --- a/packages/compiler-cli/integrationtest/bazel/injectable_def/app/test/BUILD.bazel +++ b/packages/compiler-cli/integrationtest/bazel/injectable_def/app/test/BUILD.bazel @@ -14,6 +14,7 @@ ts_library( deps = [ "//packages/compiler-cli/integrationtest/bazel/injectable_def/app", "//packages/core", + "//packages/core/testing", "//packages/platform-server", ], ) diff --git a/packages/compiler-cli/integrationtest/bazel/injectable_def/app/test/app_spec.ts b/packages/compiler-cli/integrationtest/bazel/injectable_def/app/test/app_spec.ts index bfcf4b26ea..452d437a9c 100644 --- a/packages/compiler-cli/integrationtest/bazel/injectable_def/app/test/app_spec.ts +++ b/packages/compiler-cli/integrationtest/bazel/injectable_def/app/test/app_spec.ts @@ -6,7 +6,8 @@ * found in the LICENSE file at https://angular.io/license */ -import {enableProdMode} from '@angular/core'; +import {Component, Injectable, NgModule} from '@angular/core'; +import {TestBed} from '@angular/core/testing'; import {renderModuleFactory} from '@angular/platform-server'; import {BasicAppModuleNgFactory} from 'app_built/src/basic.ngfactory'; import {DepAppModuleNgFactory} from 'app_built/src/dep.ngfactory'; @@ -16,8 +17,6 @@ import {SelfAppModuleNgFactory} from 'app_built/src/self.ngfactory'; import {StringAppModuleNgFactory} from 'app_built/src/string.ngfactory'; import {TokenAppModuleNgFactory} from 'app_built/src/token.ngfactory'; -enableProdMode(); - describe('ngInjectableDef Bazel Integration', () => { it('works in AOT', done => { renderModuleFactory(BasicAppModuleNgFactory, { @@ -88,4 +87,41 @@ describe('ngInjectableDef Bazel Integration', () => { done(); }); }); + + it('allows provider override in JIT for root-scoped @Injectables', () => { + @Injectable({ + providedIn: 'root', + useValue: new Service('default'), + }) + class Service { + constructor(readonly value: string) {} + } + + TestBed.configureTestingModule({}); + TestBed.overrideProvider(Service, {useValue: new Service('overridden')}); + + expect(TestBed.get(Service).value).toEqual('overridden'); + }); + + it('allows provider override in JIT for module-scoped @Injectables', () => { + + @NgModule() + class Module { + } + + @Injectable({ + providedIn: Module, + useValue: new Service('default'), + }) + class Service { + constructor(readonly value: string) {} + } + + TestBed.configureTestingModule({ + imports: [Module], + }); + TestBed.overrideProvider(Service, {useValue: new Service('overridden')}); + + expect(TestBed.get(Service).value).toEqual('overridden'); + }); }); diff --git a/packages/compiler-cli/integrationtest/bazel/injectable_def/lib1/module.ts b/packages/compiler-cli/integrationtest/bazel/injectable_def/lib1/module.ts index 297dc07804..f9baf45121 100644 --- a/packages/compiler-cli/integrationtest/bazel/injectable_def/lib1/module.ts +++ b/packages/compiler-cli/integrationtest/bazel/injectable_def/lib1/module.ts @@ -13,7 +13,7 @@ export class Lib1Module { } @Injectable({ - scope: Lib1Module, + providedIn: Lib1Module, }) export class Service { static instanceCount = 0; diff --git a/packages/compiler-cli/test/ngc_spec.ts b/packages/compiler-cli/test/ngc_spec.ts index 619dd8615a..260439207f 100644 --- a/packages/compiler-cli/test/ngc_spec.ts +++ b/packages/compiler-cli/test/ngc_spec.ts @@ -2058,13 +2058,13 @@ describe('ngc transformer command-line', () => { import {Module} from './module'; @Injectable({ - scope: Module, + providedIn: Module, }) export class Service {} `); expect(source).toMatch(/ngInjectableDef = .+\.defineInjectable\(/); expect(source).toMatch(/ngInjectableDef.*token: Service/); - expect(source).toMatch(/ngInjectableDef.*scope: .+\.Module/); + expect(source).toMatch(/ngInjectableDef.*providedIn: .+\.Module/); }); it('ngInjectableDef in es5 mode is annotated @nocollapse when closure options are enabled', @@ -2081,7 +2081,7 @@ describe('ngc transformer command-line', () => { import {Module} from './module'; @Injectable({ - scope: Module, + providedIn: Module, }) export class Service {} `); @@ -2096,7 +2096,7 @@ describe('ngc transformer command-line', () => { export const CONST_SERVICE: Service = null; @Injectable({ - scope: Module, + providedIn: Module, useValue: CONST_SERVICE }) export class Service {} @@ -2113,7 +2113,7 @@ describe('ngc transformer command-line', () => { export class Existing {} @Injectable({ - scope: Module, + providedIn: Module, useExisting: Existing, }) export class Service {} @@ -2130,7 +2130,7 @@ describe('ngc transformer command-line', () => { export class Existing {} @Injectable({ - scope: Module, + providedIn: Module, useFactory: (existing: Existing|null) => new Service(existing), deps: [[new Optional(), Existing]], }) @@ -2150,7 +2150,7 @@ describe('ngc transformer command-line', () => { export class Existing {} @Injectable({ - scope: Module, + providedIn: Module, useFactory: (existing: Existing) => new Service(existing), deps: [[new SkipSelf(), Existing]], }) @@ -2166,10 +2166,10 @@ describe('ngc transformer command-line', () => { import {Inject, Injectable, InjectionToken} from '@angular/core'; import {Module} from './module'; - export const TOKEN = new InjectionToken('desc', {scope: Module, factory: () => true}); + export const TOKEN = new InjectionToken('desc', {providedIn: Module, factory: () => true}); @Injectable({ - scope: Module, + providedIn: Module, }) export class Service { constructor(@Inject(TOKEN) value: boolean) {} @@ -2177,7 +2177,7 @@ describe('ngc transformer command-line', () => { `); expect(source).toMatch(/ngInjectableDef = .+\.defineInjectable\(/); expect(source).toMatch(/ngInjectableDef.*token: Service/); - expect(source).toMatch(/ngInjectableDef.*scope: .+\.Module/); + expect(source).toMatch(/ngInjectableDef.*providedIn: .+\.Module/); }); it('generates exports.* references when outputting commonjs', () => { @@ -2189,15 +2189,15 @@ describe('ngc transformer command-line', () => { "files": ["service.ts"] }`); const source = compileService(` - import {Inject, Injectable, InjectionToken, APP_ROOT_SCOPE} from '@angular/core'; + import {Inject, Injectable, InjectionToken} from '@angular/core'; import {Module} from './module'; export const TOKEN = new InjectionToken('test token', { - scope: APP_ROOT_SCOPE, + providedIn: 'root', factory: () => 'this is a test', }); - @Injectable({scope: APP_ROOT_SCOPE}) + @Injectable({providedIn: 'root'}) export class Service { constructor(@Inject(TOKEN) token: any) {} } diff --git a/packages/compiler/src/compile_metadata.ts b/packages/compiler/src/compile_metadata.ts index 2a8edf12e6..d915bc8405 100644 --- a/packages/compiler/src/compile_metadata.ts +++ b/packages/compiler/src/compile_metadata.ts @@ -140,7 +140,7 @@ export interface CompileInjectableMetadata { symbol: StaticSymbol; type: CompileTypeMetadata; - module?: StaticSymbol; + providedIn?: StaticSymbol; useValue?: any; useClass?: StaticSymbol; diff --git a/packages/compiler/src/core.ts b/packages/compiler/src/core.ts index 264043cfeb..f834d18ad8 100644 --- a/packages/compiler/src/core.ts +++ b/packages/compiler/src/core.ts @@ -127,7 +127,7 @@ export interface ModuleWithProviders { providers?: Provider[]; } export interface Injectable { - scope?: Type|any; + providedIn?: Type|'root'|any; useClass?: Type|any; useExisting?: Type|any; useValue?: any; diff --git a/packages/compiler/src/injectable_compiler.ts b/packages/compiler/src/injectable_compiler.ts index afa8ea54cf..900b566bbc 100644 --- a/packages/compiler/src/injectable_compiler.ts +++ b/packages/compiler/src/injectable_compiler.ts @@ -89,16 +89,24 @@ export class InjectableCompiler { } injectableDef(injectable: CompileInjectableMetadata, ctx: OutputContext): o.Expression { + let providedIn: o.Expression = o.NULL_EXPR; + if (injectable.providedIn) { + if (typeof injectable.providedIn === 'string') { + providedIn = o.literal(injectable.providedIn); + } else { + providedIn = ctx.importExpr(injectable.providedIn); + } + } const def: MapLiteral = [ mapEntry('factory', this.factoryFor(injectable, ctx)), mapEntry('token', ctx.importExpr(injectable.type.reference)), - mapEntry('scope', ctx.importExpr(injectable.module !)), + mapEntry('providedIn', providedIn), ]; return o.importExpr(Identifiers.defineInjectable).callFn([o.literalMap(def)]); } compile(injectable: CompileInjectableMetadata, ctx: OutputContext): void { - if (injectable.module) { + if (injectable.providedIn) { const className = identifierName(injectable.type) !; const clazz = new o.ClassStmt( className, null, diff --git a/packages/compiler/src/metadata_resolver.ts b/packages/compiler/src/metadata_resolver.ts index b4d50aa321..c8746e01f4 100644 --- a/packages/compiler/src/metadata_resolver.ts +++ b/packages/compiler/src/metadata_resolver.ts @@ -801,7 +801,7 @@ export class CompileMetadataResolver { return { symbol: type, type: typeMetadata, - module: meta.scope || undefined, + providedIn: meta.providedIn, useValue: meta.useValue, useClass: meta.useClass, useExisting: meta.useExisting, diff --git a/packages/core/src/core_private_export.ts b/packages/core/src/core_private_export.ts index 5ae5166e2a..d9178c5c13 100644 --- a/packages/core/src/core_private_export.ts +++ b/packages/core/src/core_private_export.ts @@ -13,6 +13,7 @@ export {isListLikeIterable as ɵisListLikeIterable} from './change_detection/cha export {ChangeDetectorStatus as ɵChangeDetectorStatus, isDefaultChangeDetectionStrategy as ɵisDefaultChangeDetectionStrategy} from './change_detection/constants'; export {Console as ɵConsole} from './console'; export {setCurrentInjector as ɵsetCurrentInjector} from './di/injector'; +export {APP_ROOT as ɵAPP_ROOT} from './di/scope'; export {ComponentFactory as ɵComponentFactory} from './linker/component_factory'; export {CodegenComponentFactoryResolver as ɵCodegenComponentFactoryResolver} from './linker/component_factory_resolver'; export {ReflectionCapabilities as ɵReflectionCapabilities} from './reflection/reflection_capabilities'; diff --git a/packages/core/src/di.ts b/packages/core/src/di.ts index 756904237a..2ce39f9ae4 100644 --- a/packages/core/src/di.ts +++ b/packages/core/src/di.ts @@ -23,4 +23,3 @@ export {StaticProvider, ValueProvider, ExistingProvider, FactoryProvider, Provid export {ResolvedReflectiveFactory, ResolvedReflectiveProvider} from './di/reflective_provider'; export {ReflectiveKey} from './di/reflective_key'; export {InjectionToken} from './di/injection_token'; -export {APP_ROOT_SCOPE} from './di/scope'; diff --git a/packages/core/src/di/injectable.ts b/packages/core/src/di/injectable.ts index b9f7ec22c7..8b6c667554 100644 --- a/packages/core/src/di/injectable.ts +++ b/packages/core/src/di/injectable.ts @@ -55,9 +55,9 @@ export interface InjectableDecorator { * @stable */ (): any; - (options?: {scope: Type}&InjectableProvider): any; + (options?: {providedIn: Type| 'root' | null}&InjectableProvider): any; new (): Injectable; - new (options?: {scope: Type}&InjectableProvider): Injectable; + new (options?: {providedIn: Type| 'root' | null}&InjectableProvider): Injectable; } /** @@ -66,7 +66,7 @@ export interface InjectableDecorator { * @experimental */ export interface Injectable { - scope?: Type; + providedIn?: Type|'root'|null; factory: () => any; } @@ -109,12 +109,19 @@ export function convertInjectableProviderToFactory( } /** -* Define injectable +* Construct an `InjectableDef` which defines how a token will be constructed by the DI system, and +* in which injectors (if any) it will be available. * * @experimental */ -export function defineInjectable(opts: Injectable): Injectable { - return opts; +export function defineInjectable(opts: { + providedIn?: Type| 'root' | null, + factory: () => T, +}): InjectableDef { + return { + providedIn: opts.providedIn || null, + factory: opts.factory, + }; } /** @@ -125,19 +132,24 @@ export function defineInjectable(opts: Injectable): Injectable { */ export const Injectable: InjectableDecorator = makeDecorator( 'Injectable', undefined, undefined, undefined, - (injectableType: Type, options: {scope: Type} & InjectableProvider) => { - if (options && options.scope) { + (injectableType: Type, + options: {providedIn?: Type| 'root' | null} & InjectableProvider) => { + if (options && options.providedIn) { (injectableType as InjectableType).ngInjectableDef = defineInjectable({ - scope: options.scope, + providedIn: options.providedIn, factory: convertInjectableProviderToFactory(injectableType, options) }); } }); +export interface InjectableDef { + providedIn: Type|'root'|null; + factory: () => T; +} /** * Type representing injectable service. * * @experimental */ -export interface InjectableType extends Type { ngInjectableDef?: Injectable; } +export interface InjectableType extends Type { ngInjectableDef: InjectableDef; } diff --git a/packages/core/src/di/injection_token.ts b/packages/core/src/di/injection_token.ts index 933051c340..430fb9b26a 100644 --- a/packages/core/src/di/injection_token.ts +++ b/packages/core/src/di/injection_token.ts @@ -8,7 +8,7 @@ import {Type} from '../type'; -import {Injectable, defineInjectable} from './injectable'; +import {InjectableDef, defineInjectable} from './injectable'; /** * Creates a token that can be used in a DI Provider. @@ -36,12 +36,15 @@ export class InjectionToken { /** @internal */ readonly ngMetadataName = 'InjectionToken'; - readonly ngInjectableDef: Injectable|undefined; + readonly ngInjectableDef: InjectableDef|undefined; - constructor(protected _desc: string, options?: {scope: Type, factory: () => T}) { + constructor(protected _desc: string, options?: { + providedIn?: Type| 'root' | null, + factory: () => T + }) { if (options !== undefined) { this.ngInjectableDef = defineInjectable({ - scope: options.scope, + providedIn: options.providedIn || 'root', factory: options.factory, }); } else { diff --git a/packages/core/src/di/scope.ts b/packages/core/src/di/scope.ts index b9f546ea50..e4a3badc71 100644 --- a/packages/core/src/di/scope.ts +++ b/packages/core/src/di/scope.ts @@ -10,18 +10,10 @@ import {Type} from '../type'; import {InjectionToken} from './injection_token'; -// APP_ROOT_SCOPE is cast as a Type to allow for its usage as the scope parameter of @Injectable(). - /** - * A scope which targets the root injector. - * - * When specified as the `scope` parameter to `@Injectable` or `InjectionToken`, this special - * scope indicates the provider for the service or token being configured belongs in the root - * injector. This is loosely equivalent to the convention of having a `forRoot()` static - * function within a module that configures the provider, and expecting users to only import that - * module via its `forRoot()` function in the root injector. - * - * @experimental + * An internal token whose presence in an injector indicates that the injector should treat itself + * as a root scoped injector when processing requests for unknown tokens which may indicate + * they are provided in the root scope. */ -export const APP_ROOT_SCOPE: Type = new InjectionToken( - 'The presence of this token marks an injector as being the root injector.') as any; +export const APP_ROOT = new InjectionToken( + 'The presence of this token marks an injector as being the root injector.'); diff --git a/packages/core/src/view/ng_module.ts b/packages/core/src/view/ng_module.ts index f145e6c4c4..ecb9b02830 100644 --- a/packages/core/src/view/ng_module.ts +++ b/packages/core/src/view/ng_module.ts @@ -7,12 +7,13 @@ */ import {resolveForwardRef} from '../di/forward_ref'; +import {InjectableDef} from '../di/injectable'; import {InjectFlags, Injector, setCurrentInjector} from '../di/injector'; -import {APP_ROOT_SCOPE} from '../di/scope'; +import {APP_ROOT} from '../di/scope'; import {NgModuleRef} from '../linker/ng_module_factory'; import {stringify} from '../util'; -import {DepDef, DepFlags, InjectableDef, NgModuleData, NgModuleDefinition, NgModuleProviderDef, NodeFlags} from './types'; +import {DepDef, DepFlags, NgModuleData, NgModuleDefinition, NgModuleProviderDef, NodeFlags} from './types'; import {splitDepsDsl, tokenKey} from './util'; const UNDEFINED_VALUE = new Object(); @@ -20,12 +21,6 @@ const UNDEFINED_VALUE = new Object(); const InjectorRefTokenKey = tokenKey(Injector); const NgModuleRefTokenKey = tokenKey(NgModuleRef); -export function injectableDef(scope: any, factory: () => any): InjectableDef { - return { - scope, factory, - }; -} - export function moduleProvideDef( flags: NodeFlags, token: any, value: any, deps: ([DepFlags, any] | any)[]): NgModuleProviderDef { @@ -47,7 +42,7 @@ export function moduleDef(providers: NgModuleProviderDef[]): NgModuleDefinition let isRoot: boolean = false; for (let i = 0; i < providers.length; i++) { const provider = providers[i]; - if (provider.token === APP_ROOT_SCOPE) { + if (provider.token === APP_ROOT) { isRoot = true; } if (provider.flags & NodeFlags.TypeNgModule) { @@ -103,7 +98,7 @@ export function resolveNgModuleDep( } return providerInstance === UNDEFINED_VALUE ? undefined : providerInstance; } else if (depDef.token.ngInjectableDef && targetsModule(data, depDef.token.ngInjectableDef)) { - const injectableDef = depDef.token.ngInjectableDef as InjectableDef; + const injectableDef = depDef.token.ngInjectableDef as InjectableDef; const key = tokenKey; const index = data._providers.length; data._def.providersByKey[depDef.tokenKey] = { @@ -129,9 +124,9 @@ function moduleTransitivelyPresent(ngModule: NgModuleData, scope: any): boolean return ngModule._def.modules.indexOf(scope) > -1; } -function targetsModule(ngModule: NgModuleData, def: InjectableDef): boolean { - return def.scope != null && (moduleTransitivelyPresent(ngModule, def.scope) || - def.scope === APP_ROOT_SCOPE && ngModule._def.isRoot); +function targetsModule(ngModule: NgModuleData, def: InjectableDef): boolean { + return def.providedIn != null && (moduleTransitivelyPresent(ngModule, def.providedIn) || + def.providedIn === 'root' && ngModule._def.isRoot); } function _createProviderInstance(ngModule: NgModuleData, providerDef: NgModuleProviderDef): any { diff --git a/packages/core/src/view/services.ts b/packages/core/src/view/services.ts index f5230c8701..bc3fd20019 100644 --- a/packages/core/src/view/services.ts +++ b/packages/core/src/view/services.ts @@ -8,13 +8,14 @@ import {isDevMode} from '../application_ref'; import {DebugElement, DebugNode, EventListener, getDebugNode, indexDebugNode, removeDebugNodeFromIndex} from '../debug/debug_node'; -import {Injector} from '../di'; +import {InjectableType, Injector} from '../di'; import {ErrorHandler} from '../error_handler'; import {ComponentFactory} from '../linker/component_factory'; import {NgModuleRef} from '../linker/ng_module_factory'; import {Renderer2, RendererFactory2, RendererStyleFlags2, RendererType2} from '../render/api'; import {Sanitizer} from '../sanitization/security'; import {Type} from '../type'; +import {tokenKey} from '../view/util'; import {isViewDebugError, viewDestroyedError, viewWrappedDebugError} from './errors'; import {resolveDep} from './provider'; @@ -162,10 +163,15 @@ function debugCreateNgModuleRef( } const providerOverrides = new Map(); +const providerOverridesWithScope = new Map, ProviderOverride>(); const viewDefOverrides = new Map(); function debugOverrideProvider(override: ProviderOverride) { providerOverrides.set(override.token, override); + if (typeof override.token === 'function' && override.token.ngInjectableDef && + typeof override.token.ngInjectableDef.providedIn === 'function') { + providerOverridesWithScope.set(override.token as InjectableType, override); + } } function debugOverrideComponentView(comp: any, compFactory: ComponentFactory) { @@ -176,6 +182,7 @@ function debugOverrideComponentView(comp: any, compFactory: ComponentFactory { + providerOverridesWithScope.forEach((override, token) => { + if (token.ngInjectableDef.providedIn === module) { + hasOverrides = true; + hasDeprecatedOverrides = hasDeprecatedOverrides || override.deprecatedBehavior; + } + }); + }); return {hasOverrides, hasDeprecatedOverrides}; } @@ -285,6 +300,23 @@ function applyProviderOverridesToNgModule(def: NgModuleDefinition): NgModuleDefi provider.value = override.value; } } + if (providerOverridesWithScope.size > 0) { + let moduleSet = new Set(def.modules); + providerOverridesWithScope.forEach((override, token) => { + if (moduleSet.has(token.ngInjectableDef.providedIn)) { + let provider = { + token: token, + flags: + override.flags | (hasDeprecatedOverrides ? NodeFlags.LazyProvider : NodeFlags.None), + deps: splitDepsDsl(override.deps), + value: override.value, + index: def.providers.length, + }; + def.providers.push(provider); + def.providersByKey[tokenKey(token)] = provider; + } + }); + } } } diff --git a/packages/core/src/view/types.ts b/packages/core/src/view/types.ts index 94728946f4..870a7df872 100644 --- a/packages/core/src/view/types.ts +++ b/packages/core/src/view/types.ts @@ -297,11 +297,6 @@ export const enum DepFlags { Value = 1 << 3, } -export interface InjectableDef { - scope: any; - factory: () => any; -} - export interface TextDef { prefix: string; } export interface QueryDef { diff --git a/packages/core/test/render3/compiler_canonical/ng_module_spec.ts b/packages/core/test/render3/compiler_canonical/ng_module_spec.ts index b69c3e08ab..16b60e801a 100644 --- a/packages/core/test/render3/compiler_canonical/ng_module_spec.ts +++ b/packages/core/test/render3/compiler_canonical/ng_module_spec.ts @@ -14,7 +14,7 @@ import {renderComponent, toHtml} from '../render_util'; xdescribe('NgModule', () => { interface Injectable { - scope?: /*InjectorDefType*/ any; + providedIn?: /*InjectorDefType*/ any; factory: Function; } @@ -67,7 +67,7 @@ xdescribe('NgModule', () => { constructor(@Optional() toast: Toast|null, name: String) {} // NORMATIVE static ngInjectableDef = defineInjectable({ - scope: MyModule, + providedIn: MyModule, factory: () => new BurntToast( $r3$.ɵdirectiveInject(Toast, $r3$.ɵInjectFlags.Optional), $r3$.ɵdirectiveInject(String)), diff --git a/packages/core/test/view/ng_module_spec.ts b/packages/core/test/view/ng_module_spec.ts index 8e5c8c2ea2..373433f47d 100644 --- a/packages/core/test/view/ng_module_spec.ts +++ b/packages/core/test/view/ng_module_spec.ts @@ -7,9 +7,10 @@ */ import {NgModuleRef} from '@angular/core'; +import {InjectableDef} from '@angular/core/src/di/injectable'; import {InjectFlags, Injector, inject} from '@angular/core/src/di/injector'; import {makePropDecorator} from '@angular/core/src/util/decorators'; -import {InjectableDef, NgModuleDefinition, NgModuleProviderDef, NodeFlags} from '@angular/core/src/view'; +import {NgModuleDefinition, NgModuleProviderDef, NodeFlags} from '@angular/core/src/view'; import {moduleDef, moduleProvideDef, resolveNgModuleDep} from '@angular/core/src/view/ng_module'; import {createNgModuleRef} from '@angular/core/src/view/refs'; import {tokenKey} from '@angular/core/src/view/util'; @@ -23,67 +24,67 @@ class MyChildModule {} class NotMyModule {} class Bar { - static ngInjectableDef: InjectableDef = { + static ngInjectableDef: InjectableDef = { factory: () => new Bar(), - scope: MyModule, + providedIn: MyModule, }; } class Baz { - static ngInjectableDef: InjectableDef = { + static ngInjectableDef: InjectableDef = { factory: () => new Baz(), - scope: NotMyModule, + providedIn: NotMyModule, }; } class HasNormalDep { constructor(public foo: Foo) {} - static ngInjectableDef: InjectableDef = { + static ngInjectableDef: InjectableDef = { factory: () => new HasNormalDep(inject(Foo)), - scope: MyModule, + providedIn: MyModule, }; } class HasDefinedDep { constructor(public bar: Bar) {} - static ngInjectableDef: InjectableDef = { + static ngInjectableDef: InjectableDef = { factory: () => new HasDefinedDep(inject(Bar)), - scope: MyModule, + providedIn: MyModule, }; } class HasOptionalDep { constructor(public baz: Baz|null) {} - static ngInjectableDef: InjectableDef = { + static ngInjectableDef: InjectableDef = { factory: () => new HasOptionalDep(inject(Baz, null)), - scope: MyModule, + providedIn: MyModule, }; } class ChildDep { - static ngInjectableDef: InjectableDef = { + static ngInjectableDef: InjectableDef = { factory: () => new ChildDep(), - scope: MyChildModule, + providedIn: MyChildModule, }; } class FromChildWithOptionalDep { constructor(public baz: Baz|null) {} - static ngInjectableDef: InjectableDef = { + static ngInjectableDef: InjectableDef = { factory: () => new FromChildWithOptionalDep(inject(Baz, null, InjectFlags.Default)), - scope: MyChildModule, + providedIn: MyChildModule, }; } class FromChildWithSkipSelfDep { constructor(public depFromParent: ChildDep|null, public depFromChild: Bar|null) {} - static ngInjectableDef: InjectableDef = { + static ngInjectableDef: InjectableDef = { factory: () => new FromChildWithSkipSelfDep( inject(ChildDep, null, InjectFlags.SkipSelf), inject(Bar, null, InjectFlags.Self)), - scope: MyChildModule, + providedIn: MyChildModule, }; } diff --git a/packages/core/testing/src/test_bed.ts b/packages/core/testing/src/test_bed.ts index 266dcb5c85..17e6fce535 100644 --- a/packages/core/testing/src/test_bed.ts +++ b/packages/core/testing/src/test_bed.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import {ApplicationInitStatus, CompilerOptions, Component, Directive, InjectionToken, Injector, ModuleWithComponentFactories, NgModule, NgModuleFactory, NgModuleRef, NgZone, Optional, Pipe, PlatformRef, Provider, SchemaMetadata, SkipSelf, StaticProvider, Type, ɵDepFlags as DepFlags, ɵNodeFlags as NodeFlags, ɵclearOverrides as clearOverrides, ɵgetComponentViewDefinitionFactory as getComponentViewDefinitionFactory, ɵoverrideComponentView as overrideComponentView, ɵoverrideProvider as overrideProvider, ɵstringify as stringify} from '@angular/core'; +import {ApplicationInitStatus, CompilerOptions, Component, Directive, InjectionToken, Injector, ModuleWithComponentFactories, NgModule, NgModuleFactory, NgModuleRef, NgZone, Optional, Pipe, PlatformRef, Provider, SchemaMetadata, SkipSelf, StaticProvider, Type, ɵAPP_ROOT as APP_ROOT, ɵDepFlags as DepFlags, ɵNodeFlags as NodeFlags, ɵclearOverrides as clearOverrides, ɵgetComponentViewDefinitionFactory as getComponentViewDefinitionFactory, ɵoverrideComponentView as overrideComponentView, ɵoverrideProvider as overrideProvider, ɵstringify as stringify} from '@angular/core'; import {AsyncTestCompleter} from './async_test_completer'; import {ComponentFixture} from './component_fixture'; @@ -224,6 +224,9 @@ export class TestBed implements Injector { private _aotSummaries: Array<() => any[]> = []; private _templateOverrides: Array<{component: Type, templateOf: Type}> = []; + private _isRoot: boolean = true; + private _rootProviderOverrides: Provider[] = []; + platform: PlatformRef = null !; ngModule: Type|Type[] = null !; @@ -275,6 +278,9 @@ export class TestBed implements Injector { this._directiveOverrides = []; this._pipeOverrides = []; + this._isRoot = true; + this._rootProviderOverrides = []; + this._moduleRef = null !; this._moduleFactory = null !; this._compilerOptions = []; @@ -375,7 +381,22 @@ export class TestBed implements Injector { const providers = this._providers.concat([{provide: TestBed, useValue: this}]); const declarations = [...this._declarations, ...this._templateOverrides.map(entry => entry.templateOf)]; - const imports = [this.ngModule, this._imports]; + + const rootScopeImports = []; + const rootProviderOverrides = this._rootProviderOverrides; + if (this._isRoot) { + @NgModule({ + providers: [ + ...rootProviderOverrides, + ], + }) + class RootScopeModule { + } + rootScopeImports.push(RootScopeModule); + } + providers.push({provide: APP_ROOT, useValue: this._isRoot}); + + const imports = [rootScopeImports, this.ngModule, this._imports]; const schemas = this._schemas; @NgModule({providers, declarations, imports, schemas}) @@ -477,6 +498,15 @@ export class TestBed implements Injector { deps?: any[], }, deprecated = false): void { + if (typeof token !== 'string' && token.ngInjectableDef && + token.ngInjectableDef.providedIn === 'root') { + if (provider.useFactory) { + this._rootProviderOverrides.push( + {provide: token, useFactory: provider.useFactory, deps: provider.deps || []}); + } else { + this._rootProviderOverrides.push({provide: token, useValue: provider.useValue}); + } + } let flags: NodeFlags = 0; let value: any; if (provider.useFactory) { @@ -638,4 +668,4 @@ export function withModule(moduleDef: TestModuleMetadata, fn?: Function | null): }; } return new InjectSetupWrapper(() => moduleDef); -} +} \ No newline at end of file diff --git a/packages/examples/core/di/ts/injector_spec.ts b/packages/examples/core/di/ts/injector_spec.ts index 729f5123e0..7446721ebe 100644 --- a/packages/examples/core/di/ts/injector_spec.ts +++ b/packages/examples/core/di/ts/injector_spec.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import {APP_ROOT_SCOPE, InjectFlags, InjectionToken, Injector, ReflectiveInjector, Type, inject, ɵsetCurrentInjector as setCurrentInjector} from '@angular/core'; +import {InjectFlags, InjectionToken, Injector, ReflectiveInjector, Type, inject, ɵsetCurrentInjector as setCurrentInjector} from '@angular/core'; class MockRootScopeInjector implements Injector { constructor(readonly parent: Injector) {} @@ -14,7 +14,7 @@ class MockRootScopeInjector implements Injector { get( token: Type|InjectionToken, defaultValue?: any, flags: InjectFlags = InjectFlags.Default): T { - if ((token as any).ngInjectableDef && (token as any).ngInjectableDef.scope === APP_ROOT_SCOPE) { + if ((token as any).ngInjectableDef && (token as any).ngInjectableDef.providedIn === 'root') { const old = setCurrentInjector(this); try { return (token as any).ngInjectableDef.factory(); @@ -56,7 +56,7 @@ class MockRootScopeInjector implements Injector { // #enddocregion }); - it('injects a tree-shaekable InjectionToken', () => { + it('injects a tree-shakeable InjectionToken', () => { class MyDep {} const injector = new MockRootScopeInjector(ReflectiveInjector.resolveAndCreate([MyDep])); @@ -66,7 +66,7 @@ class MockRootScopeInjector implements Injector { } const MY_SERVICE_TOKEN = new InjectionToken('Manually constructed MyService', { - scope: APP_ROOT_SCOPE, + providedIn: 'root', factory: () => new MyService(inject(MyDep)), }); diff --git a/packages/platform-browser/src/browser.ts b/packages/platform-browser/src/browser.ts index 7aaefaf46e..4ec340c715 100644 --- a/packages/platform-browser/src/browser.ts +++ b/packages/platform-browser/src/browser.ts @@ -7,7 +7,7 @@ */ import {CommonModule, PlatformLocation, ɵPLATFORM_BROWSER_ID as PLATFORM_BROWSER_ID} from '@angular/common'; -import {APP_ID, APP_ROOT_SCOPE, ApplicationModule, ErrorHandler, ModuleWithProviders, NgModule, Optional, PLATFORM_ID, PLATFORM_INITIALIZER, PlatformRef, RendererFactory2, RootRenderer, Sanitizer, SkipSelf, StaticProvider, Testability, createPlatformFactory, platformCore} from '@angular/core'; +import {APP_ID, ApplicationModule, ErrorHandler, ModuleWithProviders, NgModule, Optional, PLATFORM_ID, PLATFORM_INITIALIZER, PlatformRef, RendererFactory2, RootRenderer, Sanitizer, SkipSelf, StaticProvider, Testability, createPlatformFactory, platformCore, ɵAPP_ROOT as APP_ROOT} from '@angular/core'; import {BrowserDomAdapter} from './browser/browser_adapter'; import {BrowserPlatformLocation} from './browser/location/browser_platform_location'; @@ -71,7 +71,7 @@ export function _document(): any { @NgModule({ providers: [ BROWSER_SANITIZATION_PROVIDERS, - {provide: APP_ROOT_SCOPE, useValue: true}, + {provide: APP_ROOT, useValue: true}, {provide: ErrorHandler, useFactory: errorHandler, deps: []}, {provide: EVENT_MANAGER_PLUGINS, useClass: DomEventsPlugin, multi: true}, {provide: EVENT_MANAGER_PLUGINS, useClass: KeyEventsPlugin, multi: true}, diff --git a/tools/public_api_guard/core/core.d.ts b/tools/public_api_guard/core/core.d.ts index 83d605e4ec..8c9ccdc549 100644 --- a/tools/public_api_guard/core/core.d.ts +++ b/tools/public_api_guard/core/core.d.ts @@ -112,9 +112,6 @@ export declare const APP_ID: InjectionToken; /** @experimental */ export declare const APP_INITIALIZER: InjectionToken<(() => void)[]>; -/** @experimental */ -export declare const APP_ROOT_SCOPE: Type; - /** @experimental */ export declare class ApplicationInitStatus { readonly done: boolean; @@ -347,7 +344,10 @@ export declare class DefaultIterableDiffer implements IterableDiffer, Iter } /** @experimental */ -export declare function defineInjectable(opts: Injectable): Injectable; +export declare function defineInjectable(opts: { + providedIn?: Type | 'root' | null; + factory: () => T; +}): InjectableDef; /** @experimental */ export declare function destroyPlatform(): void; @@ -461,11 +461,11 @@ export declare const Injectable: InjectableDecorator; export interface InjectableDecorator { /** @stable */ (): any; (options?: { - scope: Type; + providedIn: Type | 'root' | null; } & InjectableProvider): any; new (): Injectable; new (options?: { - scope: Type; + providedIn: Type | 'root' | null; } & InjectableProvider): Injectable; } @@ -474,7 +474,7 @@ export declare type InjectableProvider = ValueSansProvider | ExistingSansProvide /** @experimental */ export interface InjectableType extends Type { - ngInjectableDef?: Injectable; + ngInjectableDef: InjectableDef; } /** @stable */ @@ -493,9 +493,9 @@ export declare const enum InjectFlags { /** @stable */ export declare class InjectionToken { protected _desc: string; - readonly ngInjectableDef: Injectable | undefined; + readonly ngInjectableDef: InjectableDef | undefined; constructor(_desc: string, options?: { - scope: Type; + providedIn?: Type | 'root' | null; factory: () => T; }); toString(): string;