Skip to content

Commit 77c382c

Browse files
committed
feat(core): Adds DI support for providedIn: 'platform'|'any' (#32154)
Extend the vocabulary of the `providedIn` to also include `'platform'` and `'any'`` scope. ``` @Injectable({ providedId: 'platform', // tree shakable injector for platform injector }) class MyService {...} ``` PR Close #32154
1 parent 8a47b48 commit 77c382c

File tree

16 files changed

+138
-64
lines changed

16 files changed

+138
-64
lines changed

packages/core/src/core_private_export.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ export {ChangeDetectorStatus as ɵChangeDetectorStatus, isDefaultChangeDetection
1515
export {Console as ɵConsole} from './console';
1616
export {inject, setCurrentInjector as ɵsetCurrentInjector, ɵɵinject} from './di/injector_compatibility';
1717
export {getInjectableDef as ɵgetInjectableDef, ɵɵInjectableDef, ɵɵInjectorDef} from './di/interface/defs';
18-
export {APP_ROOT as ɵAPP_ROOT} from './di/scope';
18+
export {INJECTOR_SCOPE as ɵINJECTOR_SCOPE} from './di/scope';
1919
export {DEFAULT_LOCALE_ID as ɵDEFAULT_LOCALE_ID} from './i18n/localization';
2020
export {ivyEnabled as ɵivyEnabled} from './ivy_switch';
2121
export {ComponentFactory as ɵComponentFactory} from './linker/component_factory';

packages/core/src/di/injectable.ts

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -50,9 +50,11 @@ export interface InjectableDecorator {
5050
*
5151
*/
5252
(): TypeDecorator;
53-
(options?: {providedIn: Type<any>| 'root' | null}&InjectableProvider): TypeDecorator;
53+
(options?: {providedIn: Type<any>| 'root' | 'platform' | 'any' | null}&
54+
InjectableProvider): TypeDecorator;
5455
new (): Injectable;
55-
new (options?: {providedIn: Type<any>| 'root' | null}&InjectableProvider): Injectable;
56+
new (options?: {providedIn: Type<any>| 'root' | 'platform' | 'any' | null}&
57+
InjectableProvider): Injectable;
5658
}
5759

5860
/**
@@ -64,10 +66,14 @@ export interface Injectable {
6466
/**
6567
* Determines which injectors will provide the injectable,
6668
* by either associating it with an @NgModule or other `InjectorType`,
67-
* or by specifying that this injectable should be provided in the
68-
* 'root' injector, which will be the application-level injector in most apps.
69+
* or by specifying that this injectable should be provided in the:
70+
* - 'root' injector, which will be the application-level injector in most apps.
71+
* - 'platform' injector, which would be the special singleton platform injector shared by all
72+
* applications on the page.
73+
* - 'any` injector, which would be the injector which receives the resolution. (Note this only
74+
* works on NgModule Injectors and not on Element Injector)
6975
*/
70-
providedIn?: Type<any>|'root'|null;
76+
providedIn?: Type<any>|'root'|'platform'|'any'|null;
7177
}
7278

7379
/**
@@ -90,9 +96,9 @@ export interface InjectableType<T> extends Type<T> { ngInjectableDef: ɵɵInject
9096
/**
9197
* Supports @Injectable() in JIT mode for Render2.
9298
*/
93-
function render2CompileInjectable(
94-
injectableType: Type<any>,
95-
options?: {providedIn?: Type<any>| 'root' | null} & InjectableProvider): void {
99+
function render2CompileInjectable(injectableType: Type<any>, options?: {
100+
providedIn?: Type<any>| 'root' | 'platform' | 'any' | null
101+
} & InjectableProvider): void {
96102
if (options && options.providedIn !== undefined && !getInjectableDef(injectableType)) {
97103
(injectableType as InjectableType<any>).ngInjectableDef = ɵɵdefineInjectable({
98104
token: injectableType,

packages/core/src/di/injection_token.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ export class InjectionToken<T> {
5757
readonly ngInjectableDef: never|undefined;
5858

5959
constructor(protected _desc: string, options?: {
60-
providedIn?: Type<any>| 'root' | null,
60+
providedIn?: Type<any>| 'root' | 'platform' | 'any' | null,
6161
factory: () => T
6262
}) {
6363
this.ngInjectableDef = undefined;

packages/core/src/di/injector.ts

Lines changed: 38 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,13 @@ import {stringify} from '../util/stringify';
1111

1212
import {resolveForwardRef} from './forward_ref';
1313
import {InjectionToken} from './injection_token';
14-
import {INJECTOR, NG_TEMP_TOKEN_PATH, NullInjector, THROW_IF_NOT_FOUND, USE_VALUE, catchInjectorError, formatError, ɵɵinject} from './injector_compatibility';
15-
import {ɵɵdefineInjectable} from './interface/defs';
14+
import {INJECTOR, NG_TEMP_TOKEN_PATH, NullInjector, THROW_IF_NOT_FOUND, USE_VALUE, catchInjectorError, formatError, setCurrentInjector, ɵɵinject} from './injector_compatibility';
15+
import {getInjectableDef, ɵɵdefineInjectable} from './interface/defs';
1616
import {InjectFlags} from './interface/injector';
1717
import {ConstructorProvider, ExistingProvider, FactoryProvider, StaticClassProvider, StaticProvider, ValueProvider} from './interface/provider';
1818
import {Inject, Optional, Self, SkipSelf} from './metadata';
1919
import {createInjector} from './r3_injector';
20+
import {INJECTOR_SCOPE} from './scope';
2021

2122
export function INJECTOR_IMPL__PRE_R3__(
2223
providers: StaticProvider[], parent: Injector | undefined, name: string) {
@@ -124,8 +125,9 @@ const NO_NEW_LINE = 'ɵ';
124125
export class StaticInjector implements Injector {
125126
readonly parent: Injector;
126127
readonly source: string|null;
128+
readonly scope: string|null;
127129

128-
private _records: Map<any, Record>;
130+
private _records: Map<any, Record|null>;
129131

130132
constructor(
131133
providers: StaticProvider[], parent: Injector = Injector.NULL, source: string|null = null) {
@@ -136,17 +138,37 @@ export class StaticInjector implements Injector {
136138
Injector, <Record>{token: Injector, fn: IDENT, deps: EMPTY, value: this, useNew: false});
137139
records.set(
138140
INJECTOR, <Record>{token: INJECTOR, fn: IDENT, deps: EMPTY, value: this, useNew: false});
139-
recursivelyProcessProviders(records, providers);
141+
this.scope = recursivelyProcessProviders(records, providers);
140142
}
141143

142144
get<T>(token: Type<T>|InjectionToken<T>, notFoundValue?: T, flags?: InjectFlags): T;
143145
get(token: any, notFoundValue?: any): any;
144146
get(token: any, notFoundValue?: any, flags: InjectFlags = InjectFlags.Default): any {
145-
const record = this._records.get(token);
147+
const records = this._records;
148+
let record = records.get(token);
149+
if (record === undefined) {
150+
// This means we have never seen this record, see if it is tree shakable provider.
151+
const injectableDef = getInjectableDef(token);
152+
if (injectableDef) {
153+
const providedIn = injectableDef && injectableDef.providedIn;
154+
if (providedIn === 'any' || providedIn != null && providedIn === this.scope) {
155+
records.set(
156+
token, record = resolveProvider(
157+
{provide: token, useFactory: injectableDef.factory, deps: EMPTY}));
158+
}
159+
}
160+
if (record === undefined) {
161+
// Set record to null to make sure that we don't go through expensive lookup above again.
162+
records.set(token, null);
163+
}
164+
}
165+
let lastInjector = setCurrentInjector(this);
146166
try {
147-
return tryResolveToken(token, record, this._records, this.parent, notFoundValue, flags);
167+
return tryResolveToken(token, record, records, this.parent, notFoundValue, flags);
148168
} catch (e) {
149169
return catchInjectorError(e, token, 'StaticInjectorError', this.source);
170+
} finally {
171+
setCurrentInjector(lastInjector);
150172
}
151173
}
152174

@@ -203,13 +225,15 @@ function multiProviderMixError(token: any) {
203225
return staticError('Cannot mix multi providers and regular providers', token);
204226
}
205227

206-
function recursivelyProcessProviders(records: Map<any, Record>, provider: StaticProvider) {
228+
function recursivelyProcessProviders(records: Map<any, Record>, provider: StaticProvider): string|
229+
null {
230+
let scope: string|null = null;
207231
if (provider) {
208232
provider = resolveForwardRef(provider);
209233
if (provider instanceof Array) {
210234
// if we have an array recurse into the array
211235
for (let i = 0; i < provider.length; i++) {
212-
recursivelyProcessProviders(records, provider[i]);
236+
scope = recursivelyProcessProviders(records, provider[i]) || scope;
213237
}
214238
} else if (typeof provider === 'function') {
215239
// Functions were supported in ReflectiveInjector, but are not here. For safety give useful
@@ -244,15 +268,19 @@ function recursivelyProcessProviders(records: Map<any, Record>, provider: Static
244268
if (record && record.fn == MULTI_PROVIDER_FN) {
245269
throw multiProviderMixError(token);
246270
}
271+
if (token === INJECTOR_SCOPE) {
272+
scope = resolvedProvider.value;
273+
}
247274
records.set(token, resolvedProvider);
248275
} else {
249276
throw staticError('Unexpected provider', provider);
250277
}
251278
}
279+
return scope;
252280
}
253281

254282
function tryResolveToken(
255-
token: any, record: Record | undefined, records: Map<any, Record>, parent: Injector,
283+
token: any, record: Record | undefined | null, records: Map<any, Record|null>, parent: Injector,
256284
notFoundValue: any, flags: InjectFlags): any {
257285
try {
258286
return resolveToken(token, record, records, parent, notFoundValue, flags);
@@ -272,7 +300,7 @@ function tryResolveToken(
272300
}
273301

274302
function resolveToken(
275-
token: any, record: Record | undefined, records: Map<any, Record>, parent: Injector,
303+
token: any, record: Record | undefined | null, records: Map<any, Record|null>, parent: Injector,
276304
notFoundValue: any, flags: InjectFlags): any {
277305
let value;
278306
if (record && !(flags & InjectFlags.SkipSelf)) {

packages/core/src/di/interface/defs.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ export interface ɵɵInjectableDef<T> {
3535
* - `null`, does not belong to any injector. Must be explicitly listed in the injector
3636
* `providers`.
3737
*/
38-
providedIn: InjectorType<any>|'root'|'any'|null;
38+
providedIn: InjectorType<any>|'root'|'platform'|'any'|null;
3939

4040
/**
4141
* The token to which this definition belongs.
@@ -140,7 +140,7 @@ export interface InjectorTypeWithProviders<T> {
140140
*/
141141
export function ɵɵdefineInjectable<T>(opts: {
142142
token: unknown,
143-
providedIn?: Type<any>| 'root' | 'any' | null,
143+
providedIn?: Type<any>| 'root' | 'platform' | 'any' | null,
144144
factory: () => T,
145145
}): never {
146146
return ({

packages/core/src/di/r3_injector.ts

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import {INJECTOR, NG_TEMP_TOKEN_PATH, NullInjector, THROW_IF_NOT_FOUND, USE_VALU
2121
import {InjectorType, InjectorTypeWithProviders, getInheritedInjectableDef, getInjectableDef, getInjectorDef, ɵɵInjectableDef} from './interface/defs';
2222
import {InjectFlags} from './interface/injector';
2323
import {ClassProvider, ConstructorProvider, ExistingProvider, FactoryProvider, StaticClassProvider, StaticProvider, TypeProvider, ValueProvider} from './interface/provider';
24-
import {APP_ROOT} from './scope';
24+
import {INJECTOR_SCOPE} from './scope';
2525

2626

2727

@@ -84,8 +84,10 @@ export function createInjector(
8484
export class R3Injector {
8585
/**
8686
* Map of tokens to records which contain the instances of those tokens.
87+
* - `null` value implies that we don't have the record. Used by tree-shakable injectors
88+
* to prevent further searches.
8789
*/
88-
private records = new Map<Type<any>|InjectionToken<any>, Record<any>>();
90+
private records = new Map<Type<any>|InjectionToken<any>, Record<any>|null>();
8991

9092
/**
9193
* The transitive set of `InjectorType`s which define this injector.
@@ -101,7 +103,7 @@ export class R3Injector {
101103
* Flag indicating this injector provides the APP_ROOT_SCOPE token, and thus counts as the
102104
* root scope.
103105
*/
104-
private readonly isRootInjector: boolean;
106+
private readonly scope: 'root'|'platform'|null;
105107

106108
readonly source: string|null;
107109

@@ -129,7 +131,8 @@ export class R3Injector {
129131

130132
// Detect whether this injector has the APP_ROOT_SCOPE token and thus should provide
131133
// any injectable scoped to APP_ROOT_SCOPE.
132-
this.isRootInjector = this.records.has(APP_ROOT);
134+
const record = this.records.get(INJECTOR_SCOPE);
135+
this.scope = record != null ? record.value : null;
133136

134137
// Eagerly instantiate the InjectorType classes themselves.
135138
this.injectorDefTypes.forEach(defType => this.get(defType));
@@ -170,7 +173,7 @@ export class R3Injector {
170173
// Check for the SkipSelf flag.
171174
if (!(flags & InjectFlags.SkipSelf)) {
172175
// SkipSelf isn't set, check if the record belongs to this injector.
173-
let record: Record<T>|undefined = this.records.get(token);
176+
let record: Record<T>|undefined|null = this.records.get(token);
174177
if (record === undefined) {
175178
// No record, but maybe the token is scoped to this injector. Look for an ngInjectableDef
176179
// with a scope matching this injector.
@@ -179,11 +182,13 @@ export class R3Injector {
179182
// Found an ngInjectableDef and it's scoped to this injector. Pretend as if it was here
180183
// all along.
181184
record = makeRecord(injectableDefOrInjectorDefFactory(token), NOT_YET);
182-
this.records.set(token, record);
185+
} else {
186+
record = null;
183187
}
188+
this.records.set(token, record);
184189
}
185190
// If a record was found, get the instance for it and return it.
186-
if (record !== undefined) {
191+
if (record != null /* NOT null || undefined */) {
187192
return this.hydrate(token, record);
188193
}
189194
}
@@ -389,7 +394,7 @@ export class R3Injector {
389394
if (!def.providedIn) {
390395
return false;
391396
} else if (typeof def.providedIn === 'string') {
392-
return def.providedIn === 'any' || (def.providedIn === 'root' && this.isRootInjector);
397+
return def.providedIn === 'any' || (def.providedIn === this.scope);
393398
} else {
394399
return this.injectorDefTypes.has(def.providedIn);
395400
}

packages/core/src/di/scope.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,5 +14,4 @@ import {InjectionToken} from './injection_token';
1414
* as a root scoped injector when processing requests for unknown tokens which may indicate
1515
* they are provided in the root scope.
1616
*/
17-
export const APP_ROOT = new InjectionToken<boolean>(
18-
'The presence of this token marks an injector as being the root injector.');
17+
export const INJECTOR_SCOPE = new InjectionToken<'root'|'platform'|null>('Set Injector scope.');

packages/core/src/view/entrypoint.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ function cloneNgModuleDefinition(def: NgModuleDefinition): NgModuleDefinition {
4848

4949
return {
5050
factory: def.factory,
51-
isRoot: def.isRoot, providers, modules, providersByKey,
51+
scope: def.scope, providers, modules, providersByKey,
5252
};
5353
}
5454

packages/core/src/view/ng_module.ts

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import {resolveForwardRef} from '../di/forward_ref';
1010
import {Injector} from '../di/injector';
1111
import {INJECTOR, setCurrentInjector} from '../di/injector_compatibility';
1212
import {getInjectableDef, ɵɵInjectableDef} from '../di/interface/defs';
13-
import {APP_ROOT} from '../di/scope';
13+
import {INJECTOR_SCOPE} from '../di/scope';
1414
import {NgModuleRef} from '../linker/ng_module_factory';
1515
import {newArray} from '../util/array_utils';
1616
import {stringify} from '../util/stringify';
@@ -42,11 +42,11 @@ export function moduleProvideDef(
4242
export function moduleDef(providers: NgModuleProviderDef[]): NgModuleDefinition {
4343
const providersByKey: {[key: string]: NgModuleProviderDef} = {};
4444
const modules = [];
45-
let isRoot: boolean = false;
45+
let scope: 'root'|'platform'|null = null;
4646
for (let i = 0; i < providers.length; i++) {
4747
const provider = providers[i];
48-
if (provider.token === APP_ROOT && provider.value === true) {
49-
isRoot = true;
48+
if (provider.token === INJECTOR_SCOPE) {
49+
scope = provider.value;
5050
}
5151
if (provider.flags & NodeFlags.TypeNgModule) {
5252
modules.push(provider.token);
@@ -60,7 +60,7 @@ export function moduleDef(providers: NgModuleProviderDef[]): NgModuleDefinition
6060
providersByKey,
6161
providers,
6262
modules,
63-
isRoot,
63+
scope: scope,
6464
};
6565
}
6666

@@ -134,8 +134,9 @@ function moduleTransitivelyPresent(ngModule: NgModuleData, scope: any): boolean
134134
}
135135

136136
function targetsModule(ngModule: NgModuleData, def: ɵɵInjectableDef<any>): boolean {
137-
return def.providedIn != null && (moduleTransitivelyPresent(ngModule, def.providedIn) ||
138-
def.providedIn === 'root' && ngModule._def.isRoot);
137+
const providedIn = def.providedIn;
138+
return providedIn != null && (providedIn === 'any' || providedIn === ngModule._def.scope ||
139+
moduleTransitivelyPresent(ngModule, providedIn));
139140
}
140141

141142
function _createProviderInstance(ngModule: NgModuleData, providerDef: NgModuleProviderDef): any {

packages/core/src/view/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ export interface NgModuleDefinition extends Definition<NgModuleDefinitionFactory
4545
providers: NgModuleProviderDef[];
4646
providersByKey: {[tokenKey: string]: NgModuleProviderDef};
4747
modules: any[];
48-
isRoot: boolean;
48+
scope: 'root'|'platform'|null;
4949
}
5050

5151
export interface NgModuleDefinitionFactory extends DefinitionFactory<NgModuleDefinition> {}

packages/core/test/acceptance/di_spec.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
import {CommonModule} from '@angular/common';
1010
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, ɵDEFAULT_LOCALE_ID as DEFAULT_LOCALE_ID} from '@angular/core';
11+
import {ɵINJECTOR_SCOPE} from '@angular/core/src/core';
1112
import {ViewRef} from '@angular/core/src/render3/view_ref';
1213
import {TestBed} from '@angular/core/testing';
1314
import {ivyEnabled, onlyInIvy} from '@angular/private/testing';
@@ -866,6 +867,37 @@ describe('di', () => {
866867
});
867868
});
868869

870+
describe('Tree shakable injectors', () => {
871+
it('should support tree shakable injectors scopes', () => {
872+
@Injectable({providedIn: 'any'})
873+
class AnyService {
874+
constructor(public injector: Injector) {}
875+
}
876+
877+
@Injectable({providedIn: 'root'})
878+
class RootService {
879+
constructor(public injector: Injector) {}
880+
}
881+
882+
@Injectable({providedIn: 'platform'})
883+
class PlatformService {
884+
constructor(public injector: Injector) {}
885+
}
886+
887+
const testBedInjector: Injector = TestBed.get(Injector);
888+
const childInjector = Injector.create([], testBedInjector);
889+
890+
const anyService = childInjector.get(AnyService);
891+
expect(anyService.injector).toBe(childInjector);
892+
893+
const rootService = childInjector.get(RootService);
894+
expect(rootService.injector.get(ɵINJECTOR_SCOPE)).toBe('root');
895+
896+
const platformService = childInjector.get(PlatformService);
897+
expect(platformService.injector.get(ɵINJECTOR_SCOPE)).toBe('platform');
898+
});
899+
});
900+
869901
describe('service injection', () => {
870902

871903
it('should create instance even when no injector present', () => {

packages/core/test/bundling/injection/bundle.golden_symbols.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[
22
{
3-
"name": "APP_ROOT"
3+
"name": "INJECTOR_SCOPE"
44
},
55
{
66
"name": "CIRCULAR"

0 commit comments

Comments
 (0)