Skip to content

Commit 7d6f488

Browse files
committed
fix(ivy): properly tree-shake away StaticInjector (angular#30219)
Ivy uses R3Injector, but we are currently pulling in both the StaticInjector (View Engine injector) and the R3Injector when running with Ivy. This commit adds an ivy switch so calling Injector.create() pulls in the correct implementation of the injector depending on whether you are using VE or Ivy. This saves us about 3KB in the bundle. PR Close angular#30219
1 parent b1506a3 commit 7d6f488

File tree

15 files changed

+164
-144
lines changed

15 files changed

+164
-144
lines changed

integration/_payload-limits.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
"master": {
2222
"uncompressed": {
2323
"runtime": 1440,
24-
"main": 149205,
24+
"main": 146225,
2525
"polyfills": 43567
2626
}
2727
}

packages/core/src/core_render3_private_export.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -291,4 +291,6 @@ export {
291291

292292
export {createInjector as ɵcreateInjector} from './di/r3_injector';
293293

294+
export {INJECTOR_IMPL__POST_R3__ as ɵINJECTOR_IMPL__POST_R3__} from './di/injector';
295+
294296
// clang-format on

packages/core/src/di/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@ export {InjectFlags} from './interface/injector';
1717
export {ɵɵdefineInjectable, defineInjectable, ɵɵdefineInjector, InjectableType, InjectorType} from './interface/defs';
1818
export {forwardRef, resolveForwardRef, ForwardRefFn} from './forward_ref';
1919
export {Injectable, InjectableDecorator, InjectableProvider} from './injectable';
20-
export {INJECTOR, Injector} from './injector';
21-
export {ɵɵinject, inject} from './injector_compatibility';
20+
export {Injector} from './injector';
21+
export {ɵɵinject, inject, INJECTOR} from './injector_compatibility';
2222
export {ReflectiveInjector} from './reflective_injector';
2323
export {StaticProvider, ValueProvider, ConstructorSansProvider, ExistingProvider, FactoryProvider, Provider, TypeProvider, ClassProvider} from './interface/provider';
2424
export {ResolvedReflectiveFactory, ResolvedReflectiveProvider} from './reflective_provider';

packages/core/src/di/injector.ts

Lines changed: 16 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -7,48 +7,29 @@
77
*/
88

99
import {Type} from '../interface/type';
10-
import {getClosureSafeProperty} from '../util/property';
1110
import {stringify} from '../util/stringify';
11+
1212
import {resolveForwardRef} from './forward_ref';
1313
import {InjectionToken} from './injection_token';
14-
import {ɵɵinject} from './injector_compatibility';
14+
import {INJECTOR, NG_TEMP_TOKEN_PATH, NullInjector, THROW_IF_NOT_FOUND, USE_VALUE, catchInjectorError, formatError, ɵɵinject} from './injector_compatibility';
1515
import {ɵɵ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';
19+
import {createInjector} from './r3_injector';
1920

20-
export const SOURCE = '__source';
21-
const _THROW_IF_NOT_FOUND = new Object();
22-
export const THROW_IF_NOT_FOUND = _THROW_IF_NOT_FOUND;
23-
24-
/**
25-
* An InjectionToken that gets the current `Injector` for `createInjector()`-style injectors.
26-
*
27-
* Requesting this token instead of `Injector` allows `StaticInjector` to be tree-shaken from a
28-
* project.
29-
*
30-
* @publicApi
31-
*/
32-
export const INJECTOR = new InjectionToken<Injector>(
33-
'INJECTOR',
34-
-1 as any // `-1` is used by Ivy DI system as special value to recognize it as `Injector`.
35-
);
21+
export function INJECTOR_IMPL__PRE_R3__(
22+
providers: StaticProvider[], parent: Injector | undefined, name: string) {
23+
return new StaticInjector(providers, parent, name);
24+
}
3625

37-
export class NullInjector implements Injector {
38-
get(token: any, notFoundValue: any = _THROW_IF_NOT_FOUND): any {
39-
if (notFoundValue === _THROW_IF_NOT_FOUND) {
40-
// Intentionally left behind: With dev tools open the debugger will stop here. There is no
41-
// reason why correctly written application should cause this exception.
42-
// TODO(misko): uncomment the next line once `ngDevMode` works with closure.
43-
// if(ngDevMode) debugger;
44-
const error = new Error(`NullInjectorError: No provider for ${stringify(token)}!`);
45-
error.name = 'NullInjectorError';
46-
throw error;
47-
}
48-
return notFoundValue;
49-
}
26+
export function INJECTOR_IMPL__POST_R3__(
27+
providers: StaticProvider[], parent: Injector | undefined, name: string) {
28+
return createInjector({name: name}, parent, providers, name);
5029
}
5130

31+
export const INJECTOR_IMPL = INJECTOR_IMPL__PRE_R3__;
32+
5233
/**
5334
* Concrete injectors implement this interface.
5435
*
@@ -66,7 +47,7 @@ export class NullInjector implements Injector {
6647
* @publicApi
6748
*/
6849
export abstract class Injector {
69-
static THROW_IF_NOT_FOUND = _THROW_IF_NOT_FOUND;
50+
static THROW_IF_NOT_FOUND = THROW_IF_NOT_FOUND;
7051
static NULL: Injector = new NullInjector();
7152

7253
/**
@@ -100,9 +81,9 @@ export abstract class Injector {
10081
options: StaticProvider[]|{providers: StaticProvider[], parent?: Injector, name?: string},
10182
parent?: Injector): Injector {
10283
if (Array.isArray(options)) {
103-
return new StaticInjector(options, parent);
84+
return INJECTOR_IMPL(options, parent, '');
10485
} else {
105-
return new StaticInjector(options.providers, options.parent, options.name || null);
86+
return INJECTOR_IMPL(options.providers, options.parent, options.name || '');
10687
}
10788
}
10889

@@ -129,18 +110,14 @@ const CIRCULAR = IDENT;
129110
const MULTI_PROVIDER_FN = function(): any[] {
130111
return Array.prototype.slice.call(arguments);
131112
};
132-
export const USE_VALUE =
133-
getClosureSafeProperty<ValueProvider>({provide: String, useValue: getClosureSafeProperty});
134-
const NG_TOKEN_PATH = 'ngTokenPath';
135-
export const NG_TEMP_TOKEN_PATH = 'ngTempTokenPath';
113+
136114
const enum OptionFlags {
137115
Optional = 1 << 0,
138116
CheckSelf = 1 << 1,
139117
CheckParent = 1 << 2,
140118
Default = CheckSelf | CheckParent
141119
}
142120
const NULL_INJECTOR = Injector.NULL;
143-
const NEW_LINE = /\n/gm;
144121
const NO_NEW_LINE = 'ɵ';
145122

146123
export class StaticInjector implements Injector {
@@ -377,38 +354,6 @@ function computeDeps(provider: StaticProvider): DependencyRecord[] {
377354
return deps;
378355
}
379356

380-
export function catchInjectorError(
381-
e: any, token: any, injectorErrorName: string, source: string | null): never {
382-
const tokenPath: any[] = e[NG_TEMP_TOKEN_PATH];
383-
if (token[SOURCE]) {
384-
tokenPath.unshift(token[SOURCE]);
385-
}
386-
e.message = formatError('\n' + e.message, tokenPath, injectorErrorName, source);
387-
e[NG_TOKEN_PATH] = tokenPath;
388-
e[NG_TEMP_TOKEN_PATH] = null;
389-
throw e;
390-
}
391-
392-
function formatError(
393-
text: string, obj: any, injectorErrorName: string, source: string | null = null): string {
394-
text = text && text.charAt(0) === '\n' && text.charAt(1) == NO_NEW_LINE ? text.substr(2) : text;
395-
let context = stringify(obj);
396-
if (obj instanceof Array) {
397-
context = obj.map(stringify).join(' -> ');
398-
} else if (typeof obj === 'object') {
399-
let parts = <string[]>[];
400-
for (let key in obj) {
401-
if (obj.hasOwnProperty(key)) {
402-
let value = obj[key];
403-
parts.push(
404-
key + ':' + (typeof value === 'string' ? JSON.stringify(value) : stringify(value)));
405-
}
406-
}
407-
context = `{${parts.join(', ')}}`;
408-
}
409-
return `${injectorErrorName}${source ? '(' + source + ')' : ''}[${context}]: ${text.replace(NEW_LINE, '\n ')}`;
410-
}
411-
412357
function staticError(text: string, obj: any): Error {
413358
return new Error(formatError(text, obj, 'StaticInjectorError'));
414359
}

packages/core/src/di/injector_compatibility.ts

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,44 @@
77
*/
88

99
import {Type} from '../interface/type';
10+
import {getClosureSafeProperty} from '../util/property';
1011
import {stringify} from '../util/stringify';
1112

1213
import {resolveForwardRef} from './forward_ref';
1314
import {InjectionToken} from './injection_token';
1415
import {Injector} from './injector';
1516
import {getInjectableDef, ɵɵInjectableDef} from './interface/defs';
1617
import {InjectFlags} from './interface/injector';
18+
import {ValueProvider} from './interface/provider';
1719
import {Inject, Optional, Self, SkipSelf} from './metadata';
1820

1921

2022

23+
/**
24+
* An InjectionToken that gets the current `Injector` for `createInjector()`-style injectors.
25+
*
26+
* Requesting this token instead of `Injector` allows `StaticInjector` to be tree-shaken from a
27+
* project.
28+
*
29+
* @publicApi
30+
*/
31+
export const INJECTOR = new InjectionToken<Injector>(
32+
'INJECTOR',
33+
-1 as any // `-1` is used by Ivy DI system as special value to recognize it as `Injector`.
34+
);
35+
36+
const _THROW_IF_NOT_FOUND = new Object();
37+
export const THROW_IF_NOT_FOUND = _THROW_IF_NOT_FOUND;
38+
39+
export const NG_TEMP_TOKEN_PATH = 'ngTempTokenPath';
40+
const NG_TOKEN_PATH = 'ngTokenPath';
41+
const NEW_LINE = /\n/gm;
42+
const NO_NEW_LINE = 'ɵ';
43+
export const SOURCE = '__source';
44+
45+
export const USE_VALUE =
46+
getClosureSafeProperty<ValueProvider>({provide: String, useValue: getClosureSafeProperty});
47+
2148
/**
2249
* Current injector value used by `inject`.
2350
* - `undefined`: it is an error to call `inject`
@@ -166,3 +193,52 @@ export function injectArgs(types: (Type<any>| InjectionToken<any>| any[])[]): an
166193
}
167194
return args;
168195
}
196+
197+
198+
export class NullInjector implements Injector {
199+
get(token: any, notFoundValue: any = THROW_IF_NOT_FOUND): any {
200+
if (notFoundValue === THROW_IF_NOT_FOUND) {
201+
// Intentionally left behind: With dev tools open the debugger will stop here. There is no
202+
// reason why correctly written application should cause this exception.
203+
// TODO(misko): uncomment the next line once `ngDevMode` works with closure.
204+
// if(ngDevMode) debugger;
205+
const error = new Error(`NullInjectorError: No provider for ${stringify(token)}!`);
206+
error.name = 'NullInjectorError';
207+
throw error;
208+
}
209+
return notFoundValue;
210+
}
211+
}
212+
213+
214+
export function catchInjectorError(
215+
e: any, token: any, injectorErrorName: string, source: string | null): never {
216+
const tokenPath: any[] = e[NG_TEMP_TOKEN_PATH];
217+
if (token[SOURCE]) {
218+
tokenPath.unshift(token[SOURCE]);
219+
}
220+
e.message = formatError('\n' + e.message, tokenPath, injectorErrorName, source);
221+
e[NG_TOKEN_PATH] = tokenPath;
222+
e[NG_TEMP_TOKEN_PATH] = null;
223+
throw e;
224+
}
225+
226+
export function formatError(
227+
text: string, obj: any, injectorErrorName: string, source: string | null = null): string {
228+
text = text && text.charAt(0) === '\n' && text.charAt(1) == NO_NEW_LINE ? text.substr(2) : text;
229+
let context = stringify(obj);
230+
if (obj instanceof Array) {
231+
context = obj.map(stringify).join(' -> ');
232+
} else if (typeof obj === 'object') {
233+
let parts = <string[]>[];
234+
for (let key in obj) {
235+
if (obj.hasOwnProperty(key)) {
236+
let value = obj[key];
237+
parts.push(
238+
key + ':' + (typeof value === 'string' ? JSON.stringify(value) : stringify(value)));
239+
}
240+
}
241+
context = `{${parts.join(', ')}}`;
242+
}
243+
return `${injectorErrorName}${source ? '(' + source + ')' : ''}[${context}]: ${text.replace(NEW_LINE, '\n ')}`;
244+
}

packages/core/src/di/r3_injector.ts

Lines changed: 21 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,13 @@
88

99
import {OnDestroy} from '../interface/lifecycle_hooks';
1010
import {Type} from '../interface/type';
11+
import {throwCyclicDependencyError, throwInvalidProviderError, throwMixedMultiProviderError} from '../render3/errors';
1112
import {stringify} from '../util/stringify';
1213

1314
import {resolveForwardRef} from './forward_ref';
1415
import {InjectionToken} from './injection_token';
15-
import {INJECTOR, Injector, NG_TEMP_TOKEN_PATH, NullInjector, USE_VALUE, catchInjectorError} from './injector';
16-
import {injectArgs, setCurrentInjector, ɵɵinject} from './injector_compatibility';
16+
import {Injector} from './injector';
17+
import {INJECTOR, NG_TEMP_TOKEN_PATH, NullInjector, THROW_IF_NOT_FOUND, USE_VALUE, catchInjectorError, injectArgs, setCurrentInjector, ɵɵinject} from './injector_compatibility';
1718
import {InjectableType, InjectorType, InjectorTypeWithProviders, getInjectableDef, getInjectorDef, ɵɵInjectableDef} from './interface/defs';
1819
import {InjectFlags} from './interface/injector';
1920
import {ClassProvider, ConstructorProvider, ExistingProvider, FactoryProvider, StaticClassProvider, StaticProvider, TypeProvider, ValueProvider} from './interface/provider';
@@ -131,7 +132,7 @@ export class R3Injector {
131132
this.injectorDefTypes.forEach(defType => this.get(defType));
132133

133134
// Source name, used for debugging
134-
this.source = source || (def instanceof Array ? null : stringify(def));
135+
this.source = source || (typeof def === 'object' ? null : stringify(def));
135136
}
136137

137138
/**
@@ -157,7 +158,7 @@ export class R3Injector {
157158
}
158159

159160
get<T>(
160-
token: Type<T>|InjectionToken<T>, notFoundValue: any = Injector.THROW_IF_NOT_FOUND,
161+
token: Type<T>|InjectionToken<T>, notFoundValue: any = THROW_IF_NOT_FOUND,
161162
flags = InjectFlags.Default): T {
162163
this.assertNotDestroyed();
163164
// Set the injection context.
@@ -208,6 +209,12 @@ export class R3Injector {
208209
}
209210
}
210211

212+
toString() {
213+
const tokens = <string[]>[], records = this.records;
214+
records.forEach((v, token) => tokens.push(stringify(token)));
215+
return `R3Injector[${tokens.join(', ')}]`;
216+
}
217+
211218
private assertNotDestroyed(): void {
212219
if (this._destroyed) {
213220
throw new Error('Injector has already been destroyed.');
@@ -242,7 +249,8 @@ export class R3Injector {
242249
(ngModule === undefined) ? (defOrWrappedDef as InjectorType<any>) : ngModule;
243250

244251
// Check for circular dependencies.
245-
if (ngDevMode && parents.indexOf(defType) !== -1) {
252+
// TODO(FW-1307): Re-add ngDevMode when closure can handle it
253+
if (parents.indexOf(defType) !== -1) {
246254
const defName = stringify(defType);
247255
throw new Error(
248256
`Circular dependency in DI detected for type ${defName}. Dependency path: ${parents.map(defType => stringify(defType)).join(' > ')} > ${defName}.`);
@@ -278,7 +286,8 @@ export class R3Injector {
278286
if (def.imports != null && !isDuplicate) {
279287
// Before processing defType's imports, add it to the set of parents. This way, if it ends
280288
// up deeply importing itself, this can be detected.
281-
ngDevMode && parents.push(defType);
289+
// TODO(FW-1307): Re-add ngDevMode when closure can handle it
290+
parents.push(defType);
282291
// Add it to the set of dedups. This way we can detect multiple imports of the same module
283292
dedupStack.push(defType);
284293

@@ -287,7 +296,8 @@ export class R3Injector {
287296
def.imports, imported => this.processInjectorType(imported, parents, dedupStack));
288297
} finally {
289298
// Remove it from the parents set when finished.
290-
ngDevMode && parents.pop();
299+
// TODO(FW-1307): Re-add ngDevMode when closure can handle it
300+
parents.pop();
291301
}
292302
}
293303

@@ -325,7 +335,7 @@ export class R3Injector {
325335
if (multiRecord) {
326336
// It has. Throw a nice error if
327337
if (multiRecord.multi === undefined) {
328-
throw new Error(`Mixed multi-provider for ${token}.`);
338+
throwMixedMultiProviderError();
329339
}
330340
} else {
331341
multiRecord = makeRecord(undefined, NOT_YET, true);
@@ -337,15 +347,15 @@ export class R3Injector {
337347
} else {
338348
const existing = this.records.get(token);
339349
if (existing && existing.multi !== undefined) {
340-
throw new Error(`Mixed multi-provider for ${stringify(token)}`);
350+
throwMixedMultiProviderError();
341351
}
342352
}
343353
this.records.set(token, record);
344354
}
345355

346356
private hydrate<T>(token: Type<T>|InjectionToken<T>, record: Record<T>): T {
347357
if (record.value === CIRCULAR) {
348-
throw new Error(`Cannot instantiate cyclic dependency! ${stringify(token)}`);
358+
throwCyclicDependencyError(stringify(token));
349359
} else if (record.value === NOT_YET) {
350360
record.value = CIRCULAR;
351361
record.value = record.factory !();
@@ -421,14 +431,7 @@ export function providerToFactory(
421431
provider &&
422432
((provider as StaticClassProvider | ClassProvider).useClass || provider.provide));
423433
if (!classRef) {
424-
let ngModuleDetail = '';
425-
if (ngModuleType && providers) {
426-
const providerDetail = providers.map(v => v == provider ? '?' + provider + '?' : '...');
427-
ngModuleDetail =
428-
` - only instances of Provider and Type are allowed, got: [${providerDetail.join(', ')}]`;
429-
}
430-
throw new Error(
431-
`Invalid provider for the NgModule '${stringify(ngModuleType)}'` + ngModuleDetail);
434+
throwInvalidProviderError(ngModuleType, providers, provider);
432435
}
433436
if (hasDeps(provider)) {
434437
factory = () => new (classRef)(...injectArgs(provider.deps));

packages/core/src/di/reflective_injector.ts

Lines changed: 2 additions & 1 deletion
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 {Injector, THROW_IF_NOT_FOUND} from './injector';
9+
import {Injector} from './injector';
10+
import {THROW_IF_NOT_FOUND} from './injector_compatibility';
1011
import {Provider} from './interface/provider';
1112
import {Self, SkipSelf} from './metadata';
1213
import {cyclicDependencyError, instantiationError, noProviderError, outOfBoundsError} from './reflective_errors';

0 commit comments

Comments
 (0)