Skip to content

Commit db56836

Browse files
alxhubkara
authored andcommitted
feat: tree-shakeable providers API updates (angular#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 angular#22655
1 parent 21e44c6 commit db56836

File tree

29 files changed

+219
-114
lines changed

29 files changed

+219
-114
lines changed

integration/injectable-def/src/lib1.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import {Injectable, NgModule} from '@angular/core';
33
@NgModule({})
44
export class Lib1Module {}
55

6-
@Injectable({scope: Lib1Module})
6+
@Injectable({providedIn: Lib1Module})
77
export class Service {
88
static instance = 0;
99
readonly instance = Service.instance++;

packages/compiler-cli/integrationtest/bazel/injectable_def/app/src/dep.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ export class AppComponent {
3535
export class DepAppModule {
3636
}
3737

38-
@Injectable({scope: DepAppModule})
38+
@Injectable({providedIn: DepAppModule})
3939
export class ShakeableService {
4040
constructor(readonly normal: NormalService) {}
4141
}

packages/compiler-cli/integrationtest/bazel/injectable_def/app/src/root.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9-
import {APP_ROOT_SCOPE, Component, Injectable, NgModule, Optional, Self} from '@angular/core';
9+
import {Component, Injectable, NgModule, Optional, Self} from '@angular/core';
1010
import {BrowserModule} from '@angular/platform-browser';
1111
import {ServerModule} from '@angular/platform-server';
1212
import {RouterModule} from '@angular/router';

packages/compiler-cli/integrationtest/bazel/injectable_def/app/src/root_service.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,10 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9-
import {APP_ROOT_SCOPE, Injectable} from '@angular/core';
9+
import {Injectable} from '@angular/core';
1010

1111
@Injectable({
12-
scope: APP_ROOT_SCOPE,
12+
providedIn: 'root',
1313
})
1414
export class Service {
1515
}

packages/compiler-cli/integrationtest/bazel/injectable_def/app/src/self.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,6 @@ export class AppComponent {
3636
export class SelfAppModule {
3737
}
3838

39-
@Injectable({scope: SelfAppModule})
39+
@Injectable({providedIn: SelfAppModule})
4040
export class ShakeableService {
4141
}

packages/compiler-cli/integrationtest/bazel/injectable_def/app/src/string.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ export class AppComponent {
3232
export class StringAppModule {
3333
}
3434

35-
@Injectable({scope: StringAppModule})
35+
@Injectable({providedIn: StringAppModule})
3636
export class Service {
3737
constructor(@Inject('someStringToken') readonly data: string) {}
3838
}

packages/compiler-cli/integrationtest/bazel/injectable_def/app/src/token.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ export class TokenModule {
1717
}
1818

1919
export const TOKEN = new InjectionToken('test', {
20-
scope: TokenModule,
20+
providedIn: TokenModule,
2121
factory: () => new Service(inject(Dep)),
2222
});
2323

packages/compiler-cli/integrationtest/bazel/injectable_def/app/test/BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ ts_library(
1414
deps = [
1515
"//packages/compiler-cli/integrationtest/bazel/injectable_def/app",
1616
"//packages/core",
17+
"//packages/core/testing",
1718
"//packages/platform-server",
1819
],
1920
)

packages/compiler-cli/integrationtest/bazel/injectable_def/app/test/app_spec.ts

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9-
import {enableProdMode} from '@angular/core';
9+
import {Component, Injectable, NgModule} from '@angular/core';
10+
import {TestBed} from '@angular/core/testing';
1011
import {renderModuleFactory} from '@angular/platform-server';
1112
import {BasicAppModuleNgFactory} from 'app_built/src/basic.ngfactory';
1213
import {DepAppModuleNgFactory} from 'app_built/src/dep.ngfactory';
@@ -16,8 +17,6 @@ import {SelfAppModuleNgFactory} from 'app_built/src/self.ngfactory';
1617
import {StringAppModuleNgFactory} from 'app_built/src/string.ngfactory';
1718
import {TokenAppModuleNgFactory} from 'app_built/src/token.ngfactory';
1819

19-
enableProdMode();
20-
2120
describe('ngInjectableDef Bazel Integration', () => {
2221
it('works in AOT', done => {
2322
renderModuleFactory(BasicAppModuleNgFactory, {
@@ -88,4 +87,41 @@ describe('ngInjectableDef Bazel Integration', () => {
8887
done();
8988
});
9089
});
90+
91+
it('allows provider override in JIT for root-scoped @Injectables', () => {
92+
@Injectable({
93+
providedIn: 'root',
94+
useValue: new Service('default'),
95+
})
96+
class Service {
97+
constructor(readonly value: string) {}
98+
}
99+
100+
TestBed.configureTestingModule({});
101+
TestBed.overrideProvider(Service, {useValue: new Service('overridden')});
102+
103+
expect(TestBed.get(Service).value).toEqual('overridden');
104+
});
105+
106+
it('allows provider override in JIT for module-scoped @Injectables', () => {
107+
108+
@NgModule()
109+
class Module {
110+
}
111+
112+
@Injectable({
113+
providedIn: Module,
114+
useValue: new Service('default'),
115+
})
116+
class Service {
117+
constructor(readonly value: string) {}
118+
}
119+
120+
TestBed.configureTestingModule({
121+
imports: [Module],
122+
});
123+
TestBed.overrideProvider(Service, {useValue: new Service('overridden')});
124+
125+
expect(TestBed.get(Service).value).toEqual('overridden');
126+
});
91127
});

packages/compiler-cli/integrationtest/bazel/injectable_def/lib1/module.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ export class Lib1Module {
1313
}
1414

1515
@Injectable({
16-
scope: Lib1Module,
16+
providedIn: Lib1Module,
1717
})
1818
export class Service {
1919
static instanceCount = 0;

packages/compiler-cli/test/ngc_spec.ts

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2058,13 +2058,13 @@ describe('ngc transformer command-line', () => {
20582058
import {Module} from './module';
20592059
20602060
@Injectable({
2061-
scope: Module,
2061+
providedIn: Module,
20622062
})
20632063
export class Service {}
20642064
`);
20652065
expect(source).toMatch(/ngInjectableDef = .+\.defineInjectable\(/);
20662066
expect(source).toMatch(/ngInjectableDef.*token: Service/);
2067-
expect(source).toMatch(/ngInjectableDef.*scope: .+\.Module/);
2067+
expect(source).toMatch(/ngInjectableDef.*providedIn: .+\.Module/);
20682068
});
20692069

20702070
it('ngInjectableDef in es5 mode is annotated @nocollapse when closure options are enabled',
@@ -2081,7 +2081,7 @@ describe('ngc transformer command-line', () => {
20812081
import {Module} from './module';
20822082
20832083
@Injectable({
2084-
scope: Module,
2084+
providedIn: Module,
20852085
})
20862086
export class Service {}
20872087
`);
@@ -2096,7 +2096,7 @@ describe('ngc transformer command-line', () => {
20962096
export const CONST_SERVICE: Service = null;
20972097
20982098
@Injectable({
2099-
scope: Module,
2099+
providedIn: Module,
21002100
useValue: CONST_SERVICE
21012101
})
21022102
export class Service {}
@@ -2113,7 +2113,7 @@ describe('ngc transformer command-line', () => {
21132113
export class Existing {}
21142114
21152115
@Injectable({
2116-
scope: Module,
2116+
providedIn: Module,
21172117
useExisting: Existing,
21182118
})
21192119
export class Service {}
@@ -2130,7 +2130,7 @@ describe('ngc transformer command-line', () => {
21302130
export class Existing {}
21312131
21322132
@Injectable({
2133-
scope: Module,
2133+
providedIn: Module,
21342134
useFactory: (existing: Existing|null) => new Service(existing),
21352135
deps: [[new Optional(), Existing]],
21362136
})
@@ -2150,7 +2150,7 @@ describe('ngc transformer command-line', () => {
21502150
export class Existing {}
21512151
21522152
@Injectable({
2153-
scope: Module,
2153+
providedIn: Module,
21542154
useFactory: (existing: Existing) => new Service(existing),
21552155
deps: [[new SkipSelf(), Existing]],
21562156
})
@@ -2166,18 +2166,18 @@ describe('ngc transformer command-line', () => {
21662166
import {Inject, Injectable, InjectionToken} from '@angular/core';
21672167
import {Module} from './module';
21682168
2169-
export const TOKEN = new InjectionToken('desc', {scope: Module, factory: () => true});
2169+
export const TOKEN = new InjectionToken('desc', {providedIn: Module, factory: () => true});
21702170
21712171
@Injectable({
2172-
scope: Module,
2172+
providedIn: Module,
21732173
})
21742174
export class Service {
21752175
constructor(@Inject(TOKEN) value: boolean) {}
21762176
}
21772177
`);
21782178
expect(source).toMatch(/ngInjectableDef = .+\.defineInjectable\(/);
21792179
expect(source).toMatch(/ngInjectableDef.*token: Service/);
2180-
expect(source).toMatch(/ngInjectableDef.*scope: .+\.Module/);
2180+
expect(source).toMatch(/ngInjectableDef.*providedIn: .+\.Module/);
21812181
});
21822182

21832183
it('generates exports.* references when outputting commonjs', () => {
@@ -2189,15 +2189,15 @@ describe('ngc transformer command-line', () => {
21892189
"files": ["service.ts"]
21902190
}`);
21912191
const source = compileService(`
2192-
import {Inject, Injectable, InjectionToken, APP_ROOT_SCOPE} from '@angular/core';
2192+
import {Inject, Injectable, InjectionToken} from '@angular/core';
21932193
import {Module} from './module';
21942194
21952195
export const TOKEN = new InjectionToken<string>('test token', {
2196-
scope: APP_ROOT_SCOPE,
2196+
providedIn: 'root',
21972197
factory: () => 'this is a test',
21982198
});
21992199
2200-
@Injectable({scope: APP_ROOT_SCOPE})
2200+
@Injectable({providedIn: 'root'})
22012201
export class Service {
22022202
constructor(@Inject(TOKEN) token: any) {}
22032203
}

packages/compiler/src/compile_metadata.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,7 @@ export interface CompileInjectableMetadata {
140140
symbol: StaticSymbol;
141141
type: CompileTypeMetadata;
142142

143-
module?: StaticSymbol;
143+
providedIn?: StaticSymbol;
144144

145145
useValue?: any;
146146
useClass?: StaticSymbol;

packages/compiler/src/core.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ export interface ModuleWithProviders {
127127
providers?: Provider[];
128128
}
129129
export interface Injectable {
130-
scope?: Type|any;
130+
providedIn?: Type|'root'|any;
131131
useClass?: Type|any;
132132
useExisting?: Type|any;
133133
useValue?: any;

packages/compiler/src/injectable_compiler.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -89,16 +89,24 @@ export class InjectableCompiler {
8989
}
9090

9191
injectableDef(injectable: CompileInjectableMetadata, ctx: OutputContext): o.Expression {
92+
let providedIn: o.Expression = o.NULL_EXPR;
93+
if (injectable.providedIn) {
94+
if (typeof injectable.providedIn === 'string') {
95+
providedIn = o.literal(injectable.providedIn);
96+
} else {
97+
providedIn = ctx.importExpr(injectable.providedIn);
98+
}
99+
}
92100
const def: MapLiteral = [
93101
mapEntry('factory', this.factoryFor(injectable, ctx)),
94102
mapEntry('token', ctx.importExpr(injectable.type.reference)),
95-
mapEntry('scope', ctx.importExpr(injectable.module !)),
103+
mapEntry('providedIn', providedIn),
96104
];
97105
return o.importExpr(Identifiers.defineInjectable).callFn([o.literalMap(def)]);
98106
}
99107

100108
compile(injectable: CompileInjectableMetadata, ctx: OutputContext): void {
101-
if (injectable.module) {
109+
if (injectable.providedIn) {
102110
const className = identifierName(injectable.type) !;
103111
const clazz = new o.ClassStmt(
104112
className, null,

packages/compiler/src/metadata_resolver.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -801,7 +801,7 @@ export class CompileMetadataResolver {
801801
return {
802802
symbol: type,
803803
type: typeMetadata,
804-
module: meta.scope || undefined,
804+
providedIn: meta.providedIn,
805805
useValue: meta.useValue,
806806
useClass: meta.useClass,
807807
useExisting: meta.useExisting,

packages/core/src/core_private_export.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ export {isListLikeIterable as ɵisListLikeIterable} from './change_detection/cha
1313
export {ChangeDetectorStatus as ɵChangeDetectorStatus, isDefaultChangeDetectionStrategy as ɵisDefaultChangeDetectionStrategy} from './change_detection/constants';
1414
export {Console as ɵConsole} from './console';
1515
export {setCurrentInjector as ɵsetCurrentInjector} from './di/injector';
16+
export {APP_ROOT as ɵAPP_ROOT} from './di/scope';
1617
export {ComponentFactory as ɵComponentFactory} from './linker/component_factory';
1718
export {CodegenComponentFactoryResolver as ɵCodegenComponentFactoryResolver} from './linker/component_factory_resolver';
1819
export {ReflectionCapabilities as ɵReflectionCapabilities} from './reflection/reflection_capabilities';

packages/core/src/di.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,4 +23,3 @@ export {StaticProvider, ValueProvider, ExistingProvider, FactoryProvider, Provid
2323
export {ResolvedReflectiveFactory, ResolvedReflectiveProvider} from './di/reflective_provider';
2424
export {ReflectiveKey} from './di/reflective_key';
2525
export {InjectionToken} from './di/injection_token';
26-
export {APP_ROOT_SCOPE} from './di/scope';

packages/core/src/di/injectable.ts

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -55,9 +55,9 @@ export interface InjectableDecorator {
5555
* @stable
5656
*/
5757
(): any;
58-
(options?: {scope: Type<any>}&InjectableProvider): any;
58+
(options?: {providedIn: Type<any>| 'root' | null}&InjectableProvider): any;
5959
new (): Injectable;
60-
new (options?: {scope: Type<any>}&InjectableProvider): Injectable;
60+
new (options?: {providedIn: Type<any>| 'root' | null}&InjectableProvider): Injectable;
6161
}
6262

6363
/**
@@ -66,7 +66,7 @@ export interface InjectableDecorator {
6666
* @experimental
6767
*/
6868
export interface Injectable {
69-
scope?: Type<any>;
69+
providedIn?: Type<any>|'root'|null;
7070
factory: () => any;
7171
}
7272

@@ -109,12 +109,19 @@ export function convertInjectableProviderToFactory(
109109
}
110110

111111
/**
112-
* Define injectable
112+
* Construct an `InjectableDef` which defines how a token will be constructed by the DI system, and
113+
* in which injectors (if any) it will be available.
113114
*
114115
* @experimental
115116
*/
116-
export function defineInjectable(opts: Injectable): Injectable {
117-
return opts;
117+
export function defineInjectable<T>(opts: {
118+
providedIn?: Type<any>| 'root' | null,
119+
factory: () => T,
120+
}): InjectableDef<T> {
121+
return {
122+
providedIn: opts.providedIn || null,
123+
factory: opts.factory,
124+
};
118125
}
119126

120127
/**
@@ -125,19 +132,24 @@ export function defineInjectable(opts: Injectable): Injectable {
125132
*/
126133
export const Injectable: InjectableDecorator = makeDecorator(
127134
'Injectable', undefined, undefined, undefined,
128-
(injectableType: Type<any>, options: {scope: Type<any>} & InjectableProvider) => {
129-
if (options && options.scope) {
135+
(injectableType: Type<any>,
136+
options: {providedIn?: Type<any>| 'root' | null} & InjectableProvider) => {
137+
if (options && options.providedIn) {
130138
(injectableType as InjectableType<any>).ngInjectableDef = defineInjectable({
131-
scope: options.scope,
139+
providedIn: options.providedIn,
132140
factory: convertInjectableProviderToFactory(injectableType, options)
133141
});
134142
}
135143
});
136144

145+
export interface InjectableDef<T> {
146+
providedIn: Type<any>|'root'|null;
147+
factory: () => T;
148+
}
137149

138150
/**
139151
* Type representing injectable service.
140152
*
141153
* @experimental
142154
*/
143-
export interface InjectableType<T> extends Type<T> { ngInjectableDef?: Injectable; }
155+
export interface InjectableType<T> extends Type<T> { ngInjectableDef: InjectableDef<T>; }

0 commit comments

Comments
 (0)