feat(ivy): improve stacktrace for `R3Injector` errors (#28207)
Improve the stacktrace for `R3Injector` errors by adding the source component (or module) that tried to inject the missing provider, as well as the name of the injector which triggered the error (`R3Injector`). e.g.: ``` R3InjectorError(SomeModule)[car -> SportsCar]: NullInjectorError: No provider for SportsCar! ``` FW-807 #resolve FW-875 #resolve PR Close #28207
This commit is contained in:
parent
7219639ff3
commit
728fe69625
|
@ -9,7 +9,6 @@
|
||||||
import {Type} from '../interface/type';
|
import {Type} from '../interface/type';
|
||||||
import {getClosureSafeProperty} from '../util/property';
|
import {getClosureSafeProperty} from '../util/property';
|
||||||
import {stringify} from '../util/stringify';
|
import {stringify} from '../util/stringify';
|
||||||
|
|
||||||
import {resolveForwardRef} from './forward_ref';
|
import {resolveForwardRef} from './forward_ref';
|
||||||
import {InjectionToken} from './injection_token';
|
import {InjectionToken} from './injection_token';
|
||||||
import {inject} from './injector_compatibility';
|
import {inject} from './injector_compatibility';
|
||||||
|
@ -42,7 +41,9 @@ export class NullInjector implements Injector {
|
||||||
// reason why correctly written application should cause this exception.
|
// reason why correctly written application should cause this exception.
|
||||||
// TODO(misko): uncomment the next line once `ngDevMode` works with closure.
|
// TODO(misko): uncomment the next line once `ngDevMode` works with closure.
|
||||||
// if(ngDevMode) debugger;
|
// if(ngDevMode) debugger;
|
||||||
throw new Error(`NullInjectorError: No provider for ${stringify(token)}!`);
|
const error = new Error(`NullInjectorError: No provider for ${stringify(token)}!`);
|
||||||
|
error.name = 'NullInjectorError';
|
||||||
|
throw error;
|
||||||
}
|
}
|
||||||
return notFoundValue;
|
return notFoundValue;
|
||||||
}
|
}
|
||||||
|
@ -131,7 +132,7 @@ const MULTI_PROVIDER_FN = function(): any[] {
|
||||||
export const USE_VALUE =
|
export const USE_VALUE =
|
||||||
getClosureSafeProperty<ValueProvider>({provide: String, useValue: getClosureSafeProperty});
|
getClosureSafeProperty<ValueProvider>({provide: String, useValue: getClosureSafeProperty});
|
||||||
const NG_TOKEN_PATH = 'ngTokenPath';
|
const NG_TOKEN_PATH = 'ngTokenPath';
|
||||||
const NG_TEMP_TOKEN_PATH = 'ngTempTokenPath';
|
export const NG_TEMP_TOKEN_PATH = 'ngTempTokenPath';
|
||||||
const enum OptionFlags {
|
const enum OptionFlags {
|
||||||
Optional = 1 << 0,
|
Optional = 1 << 0,
|
||||||
CheckSelf = 1 << 1,
|
CheckSelf = 1 << 1,
|
||||||
|
@ -167,14 +168,7 @@ export class StaticInjector implements Injector {
|
||||||
try {
|
try {
|
||||||
return tryResolveToken(token, record, this._records, this.parent, notFoundValue, flags);
|
return tryResolveToken(token, record, this._records, this.parent, notFoundValue, flags);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
const tokenPath: any[] = e[NG_TEMP_TOKEN_PATH];
|
return catchInjectorError(e, token, 'StaticInjectorError', this.source);
|
||||||
if (token[SOURCE]) {
|
|
||||||
tokenPath.unshift(token[SOURCE]);
|
|
||||||
}
|
|
||||||
e.message = formatError('\n' + e.message, tokenPath, this.source);
|
|
||||||
e[NG_TOKEN_PATH] = tokenPath;
|
|
||||||
e[NG_TEMP_TOKEN_PATH] = null;
|
|
||||||
throw e;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -200,8 +194,6 @@ interface DependencyRecord {
|
||||||
options: number;
|
options: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
type TokenPath = Array<any>;
|
|
||||||
|
|
||||||
function resolveProvider(provider: SupportedProvider): Record {
|
function resolveProvider(provider: SupportedProvider): Record {
|
||||||
const deps = computeDeps(provider);
|
const deps = computeDeps(provider);
|
||||||
let fn: Function = IDENT;
|
let fn: Function = IDENT;
|
||||||
|
@ -385,7 +377,20 @@ function computeDeps(provider: StaticProvider): DependencyRecord[] {
|
||||||
return deps;
|
return deps;
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatError(text: string, obj: any, source: string | null = null): string {
|
export function catchInjectorError(
|
||||||
|
e: any, token: any, injectorErrorName: string, source: string | null): never {
|
||||||
|
const tokenPath: any[] = e[NG_TEMP_TOKEN_PATH];
|
||||||
|
if (token[SOURCE]) {
|
||||||
|
tokenPath.unshift(token[SOURCE]);
|
||||||
|
}
|
||||||
|
e.message = formatError('\n' + e.message, tokenPath, injectorErrorName, source);
|
||||||
|
e[NG_TOKEN_PATH] = tokenPath;
|
||||||
|
e[NG_TEMP_TOKEN_PATH] = null;
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatError(
|
||||||
|
text: string, obj: any, injectorErrorName: string, source: string | null = null): string {
|
||||||
text = text && text.charAt(0) === '\n' && text.charAt(1) == NO_NEW_LINE ? text.substr(2) : text;
|
text = text && text.charAt(0) === '\n' && text.charAt(1) == NO_NEW_LINE ? text.substr(2) : text;
|
||||||
let context = stringify(obj);
|
let context = stringify(obj);
|
||||||
if (obj instanceof Array) {
|
if (obj instanceof Array) {
|
||||||
|
@ -401,9 +406,9 @@ function formatError(text: string, obj: any, source: string | null = null): stri
|
||||||
}
|
}
|
||||||
context = `{${parts.join(', ')}}`;
|
context = `{${parts.join(', ')}}`;
|
||||||
}
|
}
|
||||||
return `StaticInjectorError${source ? '(' + source + ')' : ''}[${context}]: ${text.replace(NEW_LINE, '\n ')}`;
|
return `${injectorErrorName}${source ? '(' + source + ')' : ''}[${context}]: ${text.replace(NEW_LINE, '\n ')}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function staticError(text: string, obj: any): Error {
|
function staticError(text: string, obj: any): Error {
|
||||||
return new Error(formatError(text, obj));
|
return new Error(formatError(text, obj, 'StaticInjectorError'));
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,8 +15,8 @@
|
||||||
export enum InjectFlags {
|
export enum InjectFlags {
|
||||||
// TODO(alxhub): make this 'const' when ngc no longer writes exports of it into ngfactory files.
|
// TODO(alxhub): make this 'const' when ngc no longer writes exports of it into ngfactory files.
|
||||||
|
|
||||||
|
/** Check self and check parent injector if needed */
|
||||||
Default = 0b0000,
|
Default = 0b0000,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Specifies that an injector should retrieve a dependency from any injector until reaching the
|
* Specifies that an injector should retrieve a dependency from any injector until reaching the
|
||||||
* host element of the current component. (Only used with Element Injector)
|
* host element of the current component. (Only used with Element Injector)
|
||||||
|
|
|
@ -9,10 +9,9 @@
|
||||||
import {OnDestroy} from '../interface/lifecycle_hooks';
|
import {OnDestroy} from '../interface/lifecycle_hooks';
|
||||||
import {Type} from '../interface/type';
|
import {Type} from '../interface/type';
|
||||||
import {stringify} from '../util/stringify';
|
import {stringify} from '../util/stringify';
|
||||||
|
|
||||||
import {resolveForwardRef} from './forward_ref';
|
import {resolveForwardRef} from './forward_ref';
|
||||||
import {InjectionToken} from './injection_token';
|
import {InjectionToken} from './injection_token';
|
||||||
import {INJECTOR, Injector, NullInjector, THROW_IF_NOT_FOUND, USE_VALUE} from './injector';
|
import {INJECTOR, Injector, NG_TEMP_TOKEN_PATH, NullInjector, USE_VALUE, catchInjectorError} from './injector';
|
||||||
import {inject, injectArgs, setCurrentInjector} from './injector_compatibility';
|
import {inject, injectArgs, setCurrentInjector} from './injector_compatibility';
|
||||||
import {InjectableDef, InjectableType, InjectorType, InjectorTypeWithProviders, getInjectableDef, getInjectorDef} from './interface/defs';
|
import {InjectableDef, InjectableType, InjectorType, InjectorTypeWithProviders, getInjectableDef, getInjectorDef} from './interface/defs';
|
||||||
import {InjectFlags} from './interface/injector';
|
import {InjectFlags} from './interface/injector';
|
||||||
|
@ -20,7 +19,6 @@ import {ClassProvider, ConstructorProvider, ExistingProvider, FactoryProvider, S
|
||||||
import {APP_ROOT} from './scope';
|
import {APP_ROOT} from './scope';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Internal type for a single provider in a deep provider array.
|
* Internal type for a single provider in a deep provider array.
|
||||||
*/
|
*/
|
||||||
|
@ -72,9 +70,9 @@ interface Record<T> {
|
||||||
*/
|
*/
|
||||||
export function createInjector(
|
export function createInjector(
|
||||||
defType: /* InjectorType<any> */ any, parent: Injector | null = null,
|
defType: /* InjectorType<any> */ any, parent: Injector | null = null,
|
||||||
additionalProviders: StaticProvider[] | null = null): Injector {
|
additionalProviders: StaticProvider[] | null = null, name?: string): Injector {
|
||||||
parent = parent || getNullInjector();
|
parent = parent || getNullInjector();
|
||||||
return new R3Injector(defType, additionalProviders, parent);
|
return new R3Injector(defType, additionalProviders, parent, name);
|
||||||
}
|
}
|
||||||
|
|
||||||
export class R3Injector {
|
export class R3Injector {
|
||||||
|
@ -99,6 +97,8 @@ export class R3Injector {
|
||||||
*/
|
*/
|
||||||
private readonly isRootInjector: boolean;
|
private readonly isRootInjector: boolean;
|
||||||
|
|
||||||
|
readonly source: string|null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Flag indicating that this injector was previously destroyed.
|
* Flag indicating that this injector was previously destroyed.
|
||||||
*/
|
*/
|
||||||
|
@ -106,8 +106,8 @@ export class R3Injector {
|
||||||
private _destroyed = false;
|
private _destroyed = false;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
def: InjectorType<any>, additionalProviders: StaticProvider[]|null,
|
def: InjectorType<any>, additionalProviders: StaticProvider[]|null, readonly parent: Injector,
|
||||||
readonly parent: Injector) {
|
source: string|null = null) {
|
||||||
// Start off by creating Records for every provider declared in every InjectorType
|
// Start off by creating Records for every provider declared in every InjectorType
|
||||||
// included transitively in `def`.
|
// included transitively in `def`.
|
||||||
const dedupStack: InjectorType<any>[] = [];
|
const dedupStack: InjectorType<any>[] = [];
|
||||||
|
@ -127,6 +127,9 @@ export class R3Injector {
|
||||||
|
|
||||||
// Eagerly instantiate the InjectorType classes themselves.
|
// Eagerly instantiate the InjectorType classes themselves.
|
||||||
this.injectorDefTypes.forEach(defType => this.get(defType));
|
this.injectorDefTypes.forEach(defType => this.get(defType));
|
||||||
|
|
||||||
|
// Source name, used for debugging
|
||||||
|
this.source = source || (def instanceof Array ? null : stringify(def));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -152,7 +155,7 @@ export class R3Injector {
|
||||||
}
|
}
|
||||||
|
|
||||||
get<T>(
|
get<T>(
|
||||||
token: Type<T>|InjectionToken<T>, notFoundValue: any = THROW_IF_NOT_FOUND,
|
token: Type<T>|InjectionToken<T>, notFoundValue: any = Injector.THROW_IF_NOT_FOUND,
|
||||||
flags = InjectFlags.Default): T {
|
flags = InjectFlags.Default): T {
|
||||||
this.assertNotDestroyed();
|
this.assertNotDestroyed();
|
||||||
// Set the injection context.
|
// Set the injection context.
|
||||||
|
@ -182,7 +185,21 @@ export class R3Injector {
|
||||||
// Select the next injector based on the Self flag - if self is set, the next injector is
|
// Select the next injector based on the Self flag - if self is set, the next injector is
|
||||||
// the NullInjector, otherwise it's the parent.
|
// the NullInjector, otherwise it's the parent.
|
||||||
const nextInjector = !(flags & InjectFlags.Self) ? this.parent : getNullInjector();
|
const nextInjector = !(flags & InjectFlags.Self) ? this.parent : getNullInjector();
|
||||||
return nextInjector.get(token, notFoundValue);
|
return nextInjector.get(token, flags & InjectFlags.Optional ? null : notFoundValue);
|
||||||
|
} catch (e) {
|
||||||
|
if (e.name === 'NullInjectorError') {
|
||||||
|
const path: any[] = e[NG_TEMP_TOKEN_PATH] = e[NG_TEMP_TOKEN_PATH] || [];
|
||||||
|
path.unshift(stringify(token));
|
||||||
|
if (previousInjector) {
|
||||||
|
// We still have a parent injector, keep throwing
|
||||||
|
throw e;
|
||||||
|
} else {
|
||||||
|
// Format & throw the final error message when we don't have any previous injector
|
||||||
|
return catchInjectorError(e, token, 'R3InjectorError', this.source);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
// Lastly, clean up the state by restoring the previous injector.
|
// Lastly, clean up the state by restoring the previous injector.
|
||||||
setCurrentInjector(previousInjector);
|
setCurrentInjector(previousInjector);
|
||||||
|
|
|
@ -16,7 +16,6 @@ import {InternalNgModuleRef, NgModuleFactory as viewEngine_NgModuleFactory, NgMo
|
||||||
import {NgModuleDef} from '../metadata/ng_module';
|
import {NgModuleDef} from '../metadata/ng_module';
|
||||||
import {assertDefined} from '../util/assert';
|
import {assertDefined} from '../util/assert';
|
||||||
import {stringify} from '../util/stringify';
|
import {stringify} from '../util/stringify';
|
||||||
|
|
||||||
import {ComponentFactoryResolver} from './component_ref';
|
import {ComponentFactoryResolver} from './component_ref';
|
||||||
import {getNgModuleDef} from './definition';
|
import {getNgModuleDef} from './definition';
|
||||||
|
|
||||||
|
@ -52,7 +51,8 @@ export class NgModuleRef<T> extends viewEngine_NgModuleRef<T> implements Interna
|
||||||
},
|
},
|
||||||
COMPONENT_FACTORY_RESOLVER
|
COMPONENT_FACTORY_RESOLVER
|
||||||
];
|
];
|
||||||
this._r3Injector = createInjector(ngModuleType, _parent, additionalProviders) as R3Injector;
|
this._r3Injector = createInjector(
|
||||||
|
ngModuleType, _parent, additionalProviders, stringify(ngModuleType)) as R3Injector;
|
||||||
this.instance = this.get(ngModuleType);
|
this.instance = this.get(ngModuleType);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -5,12 +5,21 @@
|
||||||
{
|
{
|
||||||
"name": "CIRCULAR"
|
"name": "CIRCULAR"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "CIRCULAR"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "EMPTY"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "EMPTY_ARRAY"
|
"name": "EMPTY_ARRAY"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "EmptyErrorImpl"
|
"name": "EmptyErrorImpl"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "IDENT"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "INJECTOR"
|
"name": "INJECTOR"
|
||||||
},
|
},
|
||||||
|
@ -23,15 +32,36 @@
|
||||||
{
|
{
|
||||||
"name": "InjectionToken"
|
"name": "InjectionToken"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "Injector"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "MULTI_PROVIDER_FN"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "NEW_LINE"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "NG_INJECTABLE_DEF"
|
"name": "NG_INJECTABLE_DEF"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "NG_INJECTOR_DEF"
|
"name": "NG_INJECTOR_DEF"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "NG_TEMP_TOKEN_PATH"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "NG_TOKEN_PATH"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "NOT_YET"
|
"name": "NOT_YET"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "NO_NEW_LINE"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "NULL_INJECTOR"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "NULL_INJECTOR"
|
"name": "NULL_INJECTOR"
|
||||||
},
|
},
|
||||||
|
@ -50,6 +80,9 @@
|
||||||
{
|
{
|
||||||
"name": "R3Injector"
|
"name": "R3Injector"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "SOURCE"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "ScopedService"
|
"name": "ScopedService"
|
||||||
},
|
},
|
||||||
|
@ -60,7 +93,7 @@
|
||||||
"name": "SkipSelf"
|
"name": "SkipSelf"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "THROW_IF_NOT_FOUND"
|
"name": "StaticInjector"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "USE_VALUE"
|
"name": "USE_VALUE"
|
||||||
|
@ -83,6 +116,12 @@
|
||||||
{
|
{
|
||||||
"name": "_currentInjector"
|
"name": "_currentInjector"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "catchInjectorError"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "computeDeps"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "couldBeInjectableType"
|
"name": "couldBeInjectableType"
|
||||||
},
|
},
|
||||||
|
@ -98,6 +137,9 @@
|
||||||
{
|
{
|
||||||
"name": "defineInjector"
|
"name": "defineInjector"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "formatError"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "forwardRef"
|
"name": "forwardRef"
|
||||||
},
|
},
|
||||||
|
@ -155,19 +197,37 @@
|
||||||
{
|
{
|
||||||
"name": "makeRecord"
|
"name": "makeRecord"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "multiProviderMixError"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "providerToFactory"
|
"name": "providerToFactory"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "providerToRecord"
|
"name": "providerToRecord"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "recursivelyProcessProviders"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "resolveForwardRef"
|
"name": "resolveForwardRef"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "resolveProvider"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "resolveToken"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "setCurrentInjector"
|
"name": "setCurrentInjector"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "staticError"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "stringify"
|
"name": "stringify"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "tryResolveToken"
|
||||||
}
|
}
|
||||||
]
|
]
|
|
@ -6,11 +6,9 @@
|
||||||
* found in the LICENSE file at https://angular.io/license
|
* found in the LICENSE file at https://angular.io/license
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {InjectionToken} from '../../src/di/injection_token';
|
import {INJECTOR, InjectFlags, InjectionToken, Injector, Optional, defineInjectable, defineInjector, inject} from '@angular/core';
|
||||||
import {INJECTOR, Injector} from '../../src/di/injector';
|
import {R3Injector, createInjector} from '@angular/core/src/di/r3_injector';
|
||||||
import {inject} from '../../src/di/injector_compatibility';
|
import {expect} from '@angular/platform-browser/testing/src/matchers';
|
||||||
import {defineInjectable, defineInjector} from '../../src/di/interface/defs';
|
|
||||||
import {R3Injector, createInjector} from '../../src/di/r3_injector';
|
|
||||||
|
|
||||||
describe('InjectorDef-based createInjector()', () => {
|
describe('InjectorDef-based createInjector()', () => {
|
||||||
class CircularA {
|
class CircularA {
|
||||||
|
@ -34,6 +32,13 @@ describe('InjectorDef-based createInjector()', () => {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class OptionalService {
|
||||||
|
static ngInjectableDef = defineInjectable({
|
||||||
|
providedIn: null,
|
||||||
|
factory: () => new OptionalService(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
class StaticService {
|
class StaticService {
|
||||||
constructor(readonly dep: Service) {}
|
constructor(readonly dep: Service) {}
|
||||||
}
|
}
|
||||||
|
@ -56,6 +61,24 @@ describe('InjectorDef-based createInjector()', () => {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class ServiceWithOptionalDep {
|
||||||
|
constructor(@Optional() readonly service: OptionalService|null) {}
|
||||||
|
|
||||||
|
static ngInjectableDef = defineInjectable({
|
||||||
|
providedIn: null,
|
||||||
|
factory: () => new ServiceWithOptionalDep(inject(OptionalService, InjectFlags.Optional)),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
class ServiceWithMissingDep {
|
||||||
|
constructor(readonly service: Service) {}
|
||||||
|
|
||||||
|
static ngInjectableDef = defineInjectable({
|
||||||
|
providedIn: null,
|
||||||
|
factory: () => new ServiceWithMissingDep(inject(Service)),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
class ServiceWithMultiDep {
|
class ServiceWithMultiDep {
|
||||||
constructor(readonly locale: string[]) {}
|
constructor(readonly locale: string[]) {}
|
||||||
|
|
||||||
|
@ -135,6 +158,7 @@ describe('InjectorDef-based createInjector()', () => {
|
||||||
imports: [IntermediateModule],
|
imports: [IntermediateModule],
|
||||||
providers: [
|
providers: [
|
||||||
ServiceWithDep,
|
ServiceWithDep,
|
||||||
|
ServiceWithOptionalDep,
|
||||||
ServiceWithMultiDep,
|
ServiceWithMultiDep,
|
||||||
{provide: LOCALE, multi: true, useValue: 'en'},
|
{provide: LOCALE, multi: true, useValue: 'en'},
|
||||||
{provide: LOCALE, multi: true, useValue: 'es'},
|
{provide: LOCALE, multi: true, useValue: 'es'},
|
||||||
|
@ -158,6 +182,14 @@ describe('InjectorDef-based createInjector()', () => {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class ModuleWithMissingDep {
|
||||||
|
static ngInjectorDef = defineInjector({
|
||||||
|
factory: () => new ModuleWithMissingDep(),
|
||||||
|
imports: undefined,
|
||||||
|
providers: [ServiceWithMissingDep],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
class NotAModule {}
|
class NotAModule {}
|
||||||
|
|
||||||
class ImportsNotAModule {
|
class ImportsNotAModule {
|
||||||
|
@ -198,12 +230,41 @@ describe('InjectorDef-based createInjector()', () => {
|
||||||
it('returns the default value if a provider isn\'t present',
|
it('returns the default value if a provider isn\'t present',
|
||||||
() => { expect(injector.get(ServiceTwo, null)).toBeNull(); });
|
() => { expect(injector.get(ServiceTwo, null)).toBeNull(); });
|
||||||
|
|
||||||
|
it('should throw when no provider defined', () => {
|
||||||
|
expect(() => injector.get(ServiceTwo))
|
||||||
|
.toThrowError(
|
||||||
|
`R3InjectorError(Module)[ServiceTwo]: \n` +
|
||||||
|
` NullInjectorError: No provider for ServiceTwo!`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw without the module name when no module', () => {
|
||||||
|
const injector = createInjector([ServiceTwo]);
|
||||||
|
expect(() => injector.get(ServiceTwo))
|
||||||
|
.toThrowError(
|
||||||
|
`R3InjectorError[ServiceTwo]: \n` +
|
||||||
|
` NullInjectorError: No provider for ServiceTwo!`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw with the full path when no provider', () => {
|
||||||
|
const injector = createInjector(ModuleWithMissingDep);
|
||||||
|
expect(() => injector.get(ServiceWithMissingDep))
|
||||||
|
.toThrowError(
|
||||||
|
`R3InjectorError(ModuleWithMissingDep)[ServiceWithMissingDep -> Service]: \n` +
|
||||||
|
` NullInjectorError: No provider for Service!`);
|
||||||
|
});
|
||||||
|
|
||||||
it('injects a service with dependencies', () => {
|
it('injects a service with dependencies', () => {
|
||||||
const instance = injector.get(ServiceWithDep);
|
const instance = injector.get(ServiceWithDep);
|
||||||
expect(instance instanceof ServiceWithDep);
|
expect(instance instanceof ServiceWithDep);
|
||||||
expect(instance.service).toBe(injector.get(Service));
|
expect(instance.service).toBe(injector.get(Service));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('injects a service with optional dependencies', () => {
|
||||||
|
const instance = injector.get(ServiceWithOptionalDep);
|
||||||
|
expect(instance instanceof ServiceWithOptionalDep);
|
||||||
|
expect(instance.service).toBe(null);
|
||||||
|
});
|
||||||
|
|
||||||
it('injects a service with dependencies on multi-providers', () => {
|
it('injects a service with dependencies on multi-providers', () => {
|
||||||
const instance = injector.get(ServiceWithMultiDep);
|
const instance = injector.get(ServiceWithMultiDep);
|
||||||
expect(instance instanceof ServiceWithMultiDep);
|
expect(instance instanceof ServiceWithMultiDep);
|
||||||
|
|
|
@ -6,8 +6,7 @@
|
||||||
* found in the LICENSE file at https://angular.io/license
|
* found in the LICENSE file at https://angular.io/license
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {Inject, InjectionToken, Injector, Optional, ReflectiveKey, Self, SkipSelf, forwardRef} from '@angular/core';
|
import {Inject, InjectionToken, Injector, Optional, Self, SkipSelf, forwardRef} from '@angular/core';
|
||||||
import {getOriginalError} from '@angular/core/src/errors';
|
|
||||||
import {expect} from '@angular/platform-browser/testing/src/matchers';
|
import {expect} from '@angular/platform-browser/testing/src/matchers';
|
||||||
|
|
||||||
import {stringify} from '../../src/util/stringify';
|
import {stringify} from '../../src/util/stringify';
|
||||||
|
|
|
@ -734,8 +734,11 @@ function declareTests(config?: {useJit: boolean}) {
|
||||||
|
|
||||||
it('should throw when the aliased provider does not exist', () => {
|
it('should throw when the aliased provider does not exist', () => {
|
||||||
const injector = createInjector([{provide: 'car', useExisting: SportsCar}]);
|
const injector = createInjector([{provide: 'car', useExisting: SportsCar}]);
|
||||||
const e = `NullInjectorError: No provider for ${stringify(SportsCar)}!`;
|
let errorMsg = `NullInjectorError: No provider for ${stringify(SportsCar)}!`;
|
||||||
expect(() => injector.get('car')).toThrowError(e);
|
if (ivyEnabled) {
|
||||||
|
errorMsg = `R3InjectorError(SomeModule)[car -> SportsCar]: \n ` + errorMsg;
|
||||||
|
}
|
||||||
|
expect(() => injector.get('car')).toThrowError(errorMsg);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle forwardRef in useExisting', () => {
|
it('should handle forwardRef in useExisting', () => {
|
||||||
|
@ -930,8 +933,11 @@ function declareTests(config?: {useJit: boolean}) {
|
||||||
|
|
||||||
it('should throw when no provider defined', () => {
|
it('should throw when no provider defined', () => {
|
||||||
const injector = createInjector([]);
|
const injector = createInjector([]);
|
||||||
expect(() => injector.get('NonExisting'))
|
let errorMsg = 'NullInjectorError: No provider for NonExisting!';
|
||||||
.toThrowError('NullInjectorError: No provider for NonExisting!');
|
if (ivyEnabled) {
|
||||||
|
errorMsg = `R3InjectorError(SomeModule)[NonExisting]: \n ` + errorMsg;
|
||||||
|
}
|
||||||
|
expect(() => injector.get('NonExisting')).toThrowError(errorMsg);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should throw when trying to instantiate a cyclic dependency', () => {
|
it('should throw when trying to instantiate a cyclic dependency', () => {
|
||||||
|
|
|
@ -11,6 +11,7 @@ import {getDebugContext} from '@angular/core/src/errors';
|
||||||
import {ArgumentType, DepFlags, NodeFlags, Services, anchorDef, asElementData, directiveDef, elementDef, providerDef, textDef} from '@angular/core/src/view/index';
|
import {ArgumentType, DepFlags, NodeFlags, Services, anchorDef, asElementData, directiveDef, elementDef, providerDef, textDef} from '@angular/core/src/view/index';
|
||||||
import {TestBed, withModule} from '@angular/core/testing';
|
import {TestBed, withModule} from '@angular/core/testing';
|
||||||
import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter';
|
import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter';
|
||||||
|
import {ivyEnabled} from '@angular/private/testing';
|
||||||
|
|
||||||
import {ARG_TYPE_VALUES, checkNodeInlineOrDynamic, createRootView, createAndGetRootNodes, compViewDef, compViewDefFactory} from './helper';
|
import {ARG_TYPE_VALUES, checkNodeInlineOrDynamic, createRootView, createAndGetRootNodes, compViewDef, compViewDefFactory} from './helper';
|
||||||
|
|
||||||
|
@ -147,7 +148,7 @@ import {ARG_TYPE_VALUES, checkNodeInlineOrDynamic, createRootView, createAndGetR
|
||||||
|
|
||||||
expect(() => createAndGetRootNodes(compViewDef(rootElNodes)))
|
expect(() => createAndGetRootNodes(compViewDef(rootElNodes)))
|
||||||
.toThrowError(
|
.toThrowError(
|
||||||
'StaticInjectorError(DynamicTestModule)[SomeService -> Dep]: \n' +
|
`${ivyEnabled ? 'R3InjectorError' : 'StaticInjectorError'}(DynamicTestModule)[SomeService -> Dep]: \n` +
|
||||||
' StaticInjectorError(Platform: core)[SomeService -> Dep]: \n' +
|
' StaticInjectorError(Platform: core)[SomeService -> Dep]: \n' +
|
||||||
' NullInjectorError: No provider for Dep!');
|
' NullInjectorError: No provider for Dep!');
|
||||||
|
|
||||||
|
@ -161,7 +162,7 @@ import {ARG_TYPE_VALUES, checkNodeInlineOrDynamic, createRootView, createAndGetR
|
||||||
|
|
||||||
expect(() => createAndGetRootNodes(compViewDef(nonRootElNodes)))
|
expect(() => createAndGetRootNodes(compViewDef(nonRootElNodes)))
|
||||||
.toThrowError(
|
.toThrowError(
|
||||||
'StaticInjectorError(DynamicTestModule)[SomeService -> Dep]: \n' +
|
`${ivyEnabled ? 'R3InjectorError' : 'StaticInjectorError'}(DynamicTestModule)[SomeService -> Dep]: \n` +
|
||||||
' StaticInjectorError(Platform: core)[SomeService -> Dep]: \n' +
|
' StaticInjectorError(Platform: core)[SomeService -> Dep]: \n' +
|
||||||
' NullInjectorError: No provider for Dep!');
|
' NullInjectorError: No provider for Dep!');
|
||||||
});
|
});
|
||||||
|
@ -186,7 +187,7 @@ import {ARG_TYPE_VALUES, checkNodeInlineOrDynamic, createRootView, createAndGetR
|
||||||
directiveDef(1, NodeFlags.None, null, 0, SomeService, ['nonExistingDep'])
|
directiveDef(1, NodeFlags.None, null, 0, SomeService, ['nonExistingDep'])
|
||||||
])))
|
])))
|
||||||
.toThrowError(
|
.toThrowError(
|
||||||
'StaticInjectorError(DynamicTestModule)[nonExistingDep]: \n' +
|
`${ivyEnabled ? 'R3InjectorError' : 'StaticInjectorError'}(DynamicTestModule)[nonExistingDep]: \n` +
|
||||||
' StaticInjectorError(Platform: core)[nonExistingDep]: \n' +
|
' StaticInjectorError(Platform: core)[nonExistingDep]: \n' +
|
||||||
' NullInjectorError: No provider for nonExistingDep!');
|
' NullInjectorError: No provider for nonExistingDep!');
|
||||||
});
|
});
|
||||||
|
|
|
@ -18,7 +18,7 @@ import {platformBrowserDynamic} from '@angular/platform-browser-dynamic';
|
||||||
import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter';
|
import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter';
|
||||||
import {DOCUMENT} from '@angular/platform-browser/src/dom/dom_tokens';
|
import {DOCUMENT} from '@angular/platform-browser/src/dom/dom_tokens';
|
||||||
import {expect} from '@angular/platform-browser/testing/src/matchers';
|
import {expect} from '@angular/platform-browser/testing/src/matchers';
|
||||||
import {fixmeIvy, modifiedInIvy, onlyInIvy} from '@angular/private/testing';
|
import {ivyEnabled, modifiedInIvy, onlyInIvy} from '@angular/private/testing';
|
||||||
|
|
||||||
@Component({selector: 'non-existent', template: ''})
|
@Component({selector: 'non-existent', template: ''})
|
||||||
class NonExistentComp {
|
class NonExistentComp {
|
||||||
|
@ -205,44 +205,48 @@ function bootstrap(
|
||||||
});
|
});
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// TODO(misko): can't use `fixmeIvy.it` because the `it` is somehow special here.
|
it('should throw if no provider', inject([AsyncTestCompleter], (async: AsyncTestCompleter) => {
|
||||||
fixmeIvy('FW-875: The source of the error is missing in the `StaticInjectorError` message')
|
const logger = new MockConsole();
|
||||||
.isEnabled &&
|
const errorHandler = new ErrorHandler();
|
||||||
it('should throw if no provider',
|
(errorHandler as any)._console = logger as any;
|
||||||
inject([AsyncTestCompleter], (async: AsyncTestCompleter) => {
|
|
||||||
const logger = new MockConsole();
|
|
||||||
const errorHandler = new ErrorHandler();
|
|
||||||
(errorHandler as any)._console = logger as any;
|
|
||||||
|
|
||||||
class IDontExist {}
|
class IDontExist {}
|
||||||
|
|
||||||
@Component({selector: 'cmp', template: 'Cmp'})
|
@Component({selector: 'cmp', template: 'Cmp'})
|
||||||
class CustomCmp {
|
class CustomCmp {
|
||||||
constructor(iDontExist: IDontExist) {}
|
constructor(iDontExist: IDontExist) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'hello-app',
|
selector: 'hello-app',
|
||||||
template: '<cmp></cmp>',
|
template: '<cmp></cmp>',
|
||||||
})
|
})
|
||||||
class RootCmp {
|
class RootCmp {
|
||||||
}
|
}
|
||||||
|
|
||||||
@NgModule({declarations: [CustomCmp], exports: [CustomCmp]})
|
@NgModule({declarations: [CustomCmp], exports: [CustomCmp]})
|
||||||
class CustomModule {
|
class CustomModule {
|
||||||
}
|
}
|
||||||
|
|
||||||
bootstrap(RootCmp, [{provide: ErrorHandler, useValue: errorHandler}], [], [
|
bootstrap(RootCmp, [{provide: ErrorHandler, useValue: errorHandler}], [], [
|
||||||
CustomModule
|
CustomModule
|
||||||
]).then(null, (e: Error) => {
|
]).then(null, (e: Error) => {
|
||||||
expect(e.message).toContain(
|
let errorMsg: string;
|
||||||
'StaticInjectorError(TestModule)[CustomCmp -> IDontExist]: \n' +
|
if (ivyEnabled) {
|
||||||
' StaticInjectorError(Platform: core)[CustomCmp -> IDontExist]: \n' +
|
errorMsg = `R3InjectorError(TestModule)[IDontExist]: \n` +
|
||||||
' NullInjectorError: No provider for IDontExist!');
|
' StaticInjectorError(TestModule)[IDontExist]: \n' +
|
||||||
async.done();
|
' StaticInjectorError(Platform: core)[IDontExist]: \n' +
|
||||||
return null;
|
' NullInjectorError: No provider for IDontExist!';
|
||||||
});
|
} else {
|
||||||
}));
|
errorMsg = `StaticInjectorError(TestModule)[CustomCmp -> IDontExist]: \n` +
|
||||||
|
' StaticInjectorError(Platform: core)[CustomCmp -> IDontExist]: \n' +
|
||||||
|
' NullInjectorError: No provider for IDontExist!';
|
||||||
|
}
|
||||||
|
expect(e.message).toContain(errorMsg);
|
||||||
|
async.done();
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
}));
|
||||||
|
|
||||||
if (getDOM().supportsDOMEvents()) {
|
if (getDOM().supportsDOMEvents()) {
|
||||||
it('should forward the error to promise when bootstrap fails',
|
it('should forward the error to promise when bootstrap fails',
|
||||||
|
|
Loading…
Reference in New Issue