Skip to content

Commit b7738e1

Browse files
ocombejasonaden
authored andcommitted
feat(core): add source to StaticInjectorError message (#20817)
Closes #19302 PR Close #20817
1 parent 634d33f commit b7738e1

File tree

11 files changed

+106
-34
lines changed

11 files changed

+106
-34
lines changed

packages/core/src/application_ref.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -102,16 +102,18 @@ export function createPlatformFactory(
102102
parentPlatformFactory: ((extraProviders?: StaticProvider[]) => PlatformRef) | null,
103103
name: string, providers: StaticProvider[] = []): (extraProviders?: StaticProvider[]) =>
104104
PlatformRef {
105-
const marker = new InjectionToken(`Platform: ${name}`);
105+
const desc = `Platform: ${name}`;
106+
const marker = new InjectionToken(desc);
106107
return (extraProviders: StaticProvider[] = []) => {
107108
let platform = getPlatform();
108109
if (!platform || platform.injector.get(ALLOW_MULTIPLE_PLATFORMS, false)) {
109110
if (parentPlatformFactory) {
110111
parentPlatformFactory(
111112
providers.concat(extraProviders).concat({provide: marker, useValue: true}));
112113
} else {
113-
createPlatform(Injector.create(
114-
providers.concat(extraProviders).concat({provide: marker, useValue: true})));
114+
const injectedProviders: StaticProvider[] =
115+
providers.concat(extraProviders).concat({provide: marker, useValue: true});
116+
createPlatform(Injector.create({providers: injectedProviders, name: desc}));
115117
}
116118
}
117119
return assertPlatform(marker);
@@ -224,10 +226,12 @@ export class PlatformRef {
224226
// pass that as parent to the NgModuleFactory.
225227
const ngZoneOption = options ? options.ngZone : undefined;
226228
const ngZone = getNgZone(ngZoneOption);
229+
const providers: StaticProvider[] = [{provide: NgZone, useValue: ngZone}];
227230
// Attention: Don't use ApplicationRef.run here,
228231
// as we want to be sure that all possible constructor calls are inside `ngZone.run`!
229232
return ngZone.run(() => {
230-
const ngZoneInjector = Injector.create([{provide: NgZone, useValue: ngZone}], this.injector);
233+
const ngZoneInjector = Injector.create(
234+
{providers: providers, parent: this.injector, name: moduleFactory.moduleType.name});
231235
const moduleRef = <InternalNgModuleRef<M>>moduleFactory.create(ngZoneInjector);
232236
const exceptionHandler: ErrorHandler = moduleRef.injector.get(ErrorHandler, null);
233237
if (!exceptionHandler) {

packages/core/src/di/injector.ts

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,12 @@
88

99
import {Type} from '../type';
1010
import {stringify} from '../util';
11-
1211
import {resolveForwardRef} from './forward_ref';
1312
import {InjectionToken} from './injection_token';
1413
import {Inject, Optional, Self, SkipSelf} from './metadata';
1514
import {ConstructorProvider, ExistingProvider, FactoryProvider, StaticClassProvider, StaticProvider, ValueProvider} from './provider';
1615

16+
export const SOURCE = '__source';
1717
const _THROW_IF_NOT_FOUND = new Object();
1818
export const THROW_IF_NOT_FOUND = _THROW_IF_NOT_FOUND;
1919

@@ -64,15 +64,28 @@ export abstract class Injector {
6464
*/
6565
abstract get(token: any, notFoundValue?: any): any;
6666

67+
/**
68+
* @deprecated from v5 use the new signature Injector.create(options)
69+
*/
70+
static create(providers: StaticProvider[], parent?: Injector): Injector;
71+
72+
static create(options: {providers: StaticProvider[], parent?: Injector, name?: string}): Injector;
73+
6774
/**
6875
* Create a new Injector which is configure using `StaticProvider`s.
6976
*
7077
* ### Example
7178
*
7279
* {@example core/di/ts/provider_spec.ts region='ConstructorProvider'}
7380
*/
74-
static create(providers: StaticProvider[], parent?: Injector): Injector {
75-
return new StaticInjector(providers, parent);
81+
static create(
82+
options: StaticProvider[]|{providers: StaticProvider[], parent?: Injector, name?: string},
83+
parent?: Injector): Injector {
84+
if (Array.isArray(options)) {
85+
return new StaticInjector(options, parent);
86+
} else {
87+
return new StaticInjector(options.providers, options.parent, options.name || null);
88+
}
7689
}
7790
}
7891

@@ -103,11 +116,14 @@ const NO_NEW_LINE = 'ɵ';
103116

104117
export class StaticInjector implements Injector {
105118
readonly parent: Injector;
119+
readonly source: string|null;
106120

107121
private _records: Map<any, Record>;
108122

109-
constructor(providers: StaticProvider[], parent: Injector = NULL_INJECTOR) {
123+
constructor(
124+
providers: StaticProvider[], parent: Injector = NULL_INJECTOR, source: string|null = null) {
110125
this.parent = parent;
126+
this.source = source;
111127
const records = this._records = new Map<any, Record>();
112128
records.set(
113129
Injector, <Record>{token: Injector, fn: IDENT, deps: EMPTY, value: this, useNew: false});
@@ -122,7 +138,10 @@ export class StaticInjector implements Injector {
122138
return tryResolveToken(token, record, this._records, this.parent, notFoundValue);
123139
} catch (e) {
124140
const tokenPath: any[] = e[NG_TEMP_TOKEN_PATH];
125-
e.message = formatError('\n' + e.message, tokenPath);
141+
if (token[SOURCE]) {
142+
tokenPath.unshift(token[SOURCE]);
143+
}
144+
e.message = formatError('\n' + e.message, tokenPath, this.source);
126145
e[NG_TOKEN_PATH] = tokenPath;
127146
e[NG_TEMP_TOKEN_PATH] = null;
128147
throw e;
@@ -336,7 +355,7 @@ function computeDeps(provider: StaticProvider): DependencyRecord[] {
336355
return deps;
337356
}
338357

339-
function formatError(text: string, obj: any): string {
358+
function formatError(text: string, obj: any, source: string | null = null): string {
340359
text = text && text.charAt(0) === '\n' && text.charAt(1) == NO_NEW_LINE ? text.substr(2) : text;
341360
let context = stringify(obj);
342361
if (obj instanceof Array) {
@@ -352,7 +371,7 @@ function formatError(text: string, obj: any): string {
352371
}
353372
context = `{${parts.join(', ')}}`;
354373
}
355-
return `StaticInjectorError[${context}]: ${text.replace(NEW_LINE, '\n ')}`;
374+
return `StaticInjectorError${source ? '(' + source + ')' : ''}[${context}]: ${text.replace(NEW_LINE, '\n ')}`;
356375
}
357376

358377
function staticError(text: string, obj: any): Error {

packages/core/src/view/ng_module.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import {resolveForwardRef} from '../di/forward_ref';
1010
import {Injector} from '../di/injector';
1111
import {NgModuleRef} from '../linker/ng_module_factory';
12+
import {stringify} from '../util';
1213

1314
import {DepDef, DepFlags, NgModuleData, NgModuleDefinition, NgModuleProviderDef, NodeFlags} from './types';
1415
import {splitDepsDsl, tokenKey} from './util';
@@ -25,7 +26,7 @@ export function moduleProvideDef(
2526
// lowered the expression and then stopped evaluating it,
2627
// i.e. also didn't unwrap it.
2728
value = resolveForwardRef(value);
28-
const depDefs = splitDepsDsl(deps);
29+
const depDefs = splitDepsDsl(deps, stringify(token));
2930
return {
3031
// will bet set by the module definition
3132
index: -1,

packages/core/src/view/provider.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import {ElementRef} from '../linker/element_ref';
1212
import {TemplateRef} from '../linker/template_ref';
1313
import {ViewContainerRef} from '../linker/view_container_ref';
1414
import {Renderer as RendererV1, Renderer2} from '../render/api';
15-
15+
import {stringify} from '../util';
1616
import {createChangeDetectorRef, createInjector, createRendererV1} from './refs';
1717
import {BindingDef, BindingFlags, DepDef, DepFlags, NodeDef, NodeFlags, OutputDef, OutputType, ProviderData, QueryValueType, Services, ViewData, ViewFlags, ViewState, asElementData, asProviderData, shouldCallLifecycleInitHook} from './types';
1818
import {calcBindingFlags, checkBinding, dispatchEvent, isComponentView, splitDepsDsl, splitMatchedQueriesDsl, tokenKey, viewParentEl} from './util';
@@ -83,7 +83,7 @@ export function _def(
8383
// i.e. also didn't unwrap it.
8484
value = resolveForwardRef(value);
8585

86-
const depDefs = splitDepsDsl(deps);
86+
const depDefs = splitDepsDsl(deps, stringify(token));
8787

8888
return {
8989
// will bet set by the view definition

packages/core/src/view/util.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,10 @@
77
*/
88

99
import {WrappedValue, devModeEqual} from '../change_detection/change_detection';
10+
import {SOURCE} from '../di/injector';
1011
import {ViewEncapsulation} from '../metadata/view';
1112
import {RendererType2} from '../render/api';
1213
import {looseIdentical, stringify} from '../util';
13-
1414
import {expressionChangedAfterItHasBeenCheckedError} from './errors';
1515
import {BindingDef, BindingFlags, Definition, DefinitionFactory, DepDef, DepFlags, ElementData, NodeDef, NodeFlags, QueryValueType, Services, ViewData, ViewDefinition, ViewDefinitionFactory, ViewFlags, ViewState, asElementData, asTextData} from './types';
1616

@@ -209,7 +209,7 @@ export function splitMatchedQueriesDsl(
209209
return {matchedQueries, references, matchedQueryIds};
210210
}
211211

212-
export function splitDepsDsl(deps: ([DepFlags, any] | any)[]): DepDef[] {
212+
export function splitDepsDsl(deps: ([DepFlags, any] | any)[], sourceName?: string): DepDef[] {
213213
return deps.map(value => {
214214
let token: any;
215215
let flags: DepFlags;
@@ -219,6 +219,9 @@ export function splitDepsDsl(deps: ([DepFlags, any] | any)[]): DepDef[] {
219219
flags = DepFlags.None;
220220
token = value;
221221
}
222+
if (token && (typeof token === 'function' || typeof token === 'object') && sourceName) {
223+
Object.defineProperty(token, SOURCE, {value: sourceName, configurable: true});
224+
}
222225
return {flags, token, tokenKey: tokenKey(token)};
223226
});
224227
}

packages/core/test/view/provider_spec.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -147,8 +147,8 @@ export function main() {
147147

148148
expect(() => createAndGetRootNodes(compViewDef(rootElNodes)))
149149
.toThrowError(
150-
'StaticInjectorError[Dep]: \n' +
151-
' StaticInjectorError[Dep]: \n' +
150+
'StaticInjectorError(DynamicTestModule)[SomeService -> Dep]: \n' +
151+
' StaticInjectorError(Platform: core)[SomeService -> Dep]: \n' +
152152
' NullInjectorError: No provider for Dep!');
153153

154154
const nonRootElNodes = [
@@ -161,8 +161,8 @@ export function main() {
161161

162162
expect(() => createAndGetRootNodes(compViewDef(nonRootElNodes)))
163163
.toThrowError(
164-
'StaticInjectorError[Dep]: \n' +
165-
' StaticInjectorError[Dep]: \n' +
164+
'StaticInjectorError(DynamicTestModule)[SomeService -> Dep]: \n' +
165+
' StaticInjectorError(Platform: core)[SomeService -> Dep]: \n' +
166166
' NullInjectorError: No provider for Dep!');
167167
});
168168

@@ -186,8 +186,8 @@ export function main() {
186186
directiveDef(1, NodeFlags.None, null, 0, SomeService, ['nonExistingDep'])
187187
])))
188188
.toThrowError(
189-
'StaticInjectorError[nonExistingDep]: \n' +
190-
' StaticInjectorError[nonExistingDep]: \n' +
189+
'StaticInjectorError(DynamicTestModule)[nonExistingDep]: \n' +
190+
' StaticInjectorError(Platform: core)[nonExistingDep]: \n' +
191191
' NullInjectorError: No provider for nonExistingDep!');
192192
});
193193

packages/core/testing/src/test_bed.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -355,8 +355,12 @@ export class TestBed implements Injector {
355355
}
356356

357357
const ngZone = new NgZone({enableLongStackTrace: true});
358-
const ngZoneInjector =
359-
Injector.create([{provide: NgZone, useValue: ngZone}], this.platform.injector);
358+
const providers: StaticProvider[] = [{provide: NgZone, useValue: ngZone}];
359+
const ngZoneInjector = Injector.create({
360+
providers: providers,
361+
parent: this.platform.injector,
362+
name: this._moduleFactory.moduleType.name
363+
});
360364
this._moduleRef = this._moduleFactory.create(ngZoneInjector);
361365
// ApplicationInitStatus.runInitializers() is marked @internal to core. So casting to any
362366
// before accessing it.

packages/examples/core/di/ts/provider_spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@ export function main() {
135135
name = 'square';
136136
}
137137

138-
const injector = Injector.create([{provide: Square, deps: []}]);
138+
const injector = Injector.create({providers: [{provide: Square, deps: []}]});
139139

140140
const shape: Square = injector.get(Square);
141141
expect(shape.name).toEqual('square');

packages/platform-browser/test/browser/bootstrap_spec.ts

Lines changed: 40 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,12 @@
77
*/
88

99
import {isPlatformBrowser} from '@angular/common';
10-
import {APP_INITIALIZER, CUSTOM_ELEMENTS_SCHEMA, Compiler, Component, Directive, ErrorHandler, Inject, Input, LOCALE_ID, NgModule, OnDestroy, PLATFORM_ID, PLATFORM_INITIALIZER, Pipe, Provider, StaticProvider, VERSION, createPlatformFactory, ɵstringify as stringify} from '@angular/core';
10+
import {APP_INITIALIZER, CUSTOM_ELEMENTS_SCHEMA, Compiler, Component, Directive, ErrorHandler, Inject, Input, LOCALE_ID, NgModule, OnDestroy, PLATFORM_ID, PLATFORM_INITIALIZER, Pipe, Provider, StaticProvider, Type, VERSION, createPlatformFactory} from '@angular/core';
1111
import {ApplicationRef, destroyPlatform} from '@angular/core/src/application_ref';
1212
import {Console} from '@angular/core/src/console';
1313
import {ComponentRef} from '@angular/core/src/linker/component_factory';
1414
import {Testability, TestabilityRegistry} from '@angular/core/src/testability/testability';
15-
import {AsyncTestCompleter, Log, afterEach, beforeEach, beforeEachProviders, ddescribe, describe, iit, inject, it} from '@angular/core/testing/src/testing_internal';
15+
import {AsyncTestCompleter, Log, afterEach, beforeEach, beforeEachProviders, describe, iit, inject, it} from '@angular/core/testing/src/testing_internal';
1616
import {BrowserModule} from '@angular/platform-browser';
1717
import {platformBrowserDynamic} from '@angular/platform-browser-dynamic';
1818
import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter';
@@ -112,10 +112,11 @@ class DummyConsole implements Console {
112112

113113

114114
class TestModule {}
115-
function bootstrap(cmpType: any, providers: Provider[] = [], platformProviders: StaticProvider[] = [
116-
]): Promise<any> {
115+
function bootstrap(
116+
cmpType: any, providers: Provider[] = [], platformProviders: StaticProvider[] = [],
117+
imports: Type<any>[] = []): Promise<any> {
117118
@NgModule({
118-
imports: [BrowserModule],
119+
imports: [BrowserModule, ...imports],
119120
declarations: [cmpType],
120121
bootstrap: [cmpType],
121122
providers: providers,
@@ -183,6 +184,40 @@ export function main() {
183184
});
184185
}));
185186

187+
it('should throw if no provider', inject([AsyncTestCompleter], (async: AsyncTestCompleter) => {
188+
const logger = new MockConsole();
189+
const errorHandler = new ErrorHandler();
190+
errorHandler._console = logger as any;
191+
192+
class IDontExist {}
193+
194+
@Component({selector: 'cmp', template: 'Cmp'})
195+
class CustomCmp {
196+
constructor(iDontExist: IDontExist) {}
197+
}
198+
199+
@Component({
200+
selector: 'hello-app',
201+
template: '<cmp></cmp>',
202+
})
203+
class RootCmp {
204+
}
205+
206+
@NgModule({declarations: [CustomCmp], exports: [CustomCmp]})
207+
class CustomModule {
208+
}
209+
210+
bootstrap(RootCmp, [{provide: ErrorHandler, useValue: errorHandler}], [], [
211+
CustomModule
212+
]).then(null, (e: Error) => {
213+
expect(e.message).toContain(`StaticInjectorError(TestModule)[CustomCmp -> IDontExist]:
214+
StaticInjectorError(Platform: core)[CustomCmp -> IDontExist]:
215+
NullInjectorError: No provider for IDontExist!`);
216+
async.done();
217+
return null;
218+
});
219+
}));
220+
186221
if (getDOM().supportsDOMEvents()) {
187222
it('should forward the error to promise when bootstrap fails',
188223
inject([AsyncTestCompleter], (async: AsyncTestCompleter) => {

packages/upgrade/src/common/downgrade_component_adapter.ts

Lines changed: 4 additions & 3 deletions
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 {ApplicationRef, ChangeDetectorRef, ComponentFactory, ComponentRef, EventEmitter, Injector, OnChanges, SimpleChange, SimpleChanges, Testability, TestabilityRegistry, Type} from '@angular/core';
9+
import {ApplicationRef, ChangeDetectorRef, ComponentFactory, ComponentRef, EventEmitter, Injector, OnChanges, SimpleChange, SimpleChanges, StaticProvider, Testability, TestabilityRegistry, Type} from '@angular/core';
1010

1111
import * as angular from './angular1';
1212
import {PropertyBinding} from './component_info';
@@ -54,8 +54,9 @@ export class DowngradeComponentAdapter {
5454
}
5555

5656
createComponent(projectableNodes: Node[][]) {
57-
const childInjector =
58-
Injector.create([{provide: $SCOPE, useValue: this.componentScope}], this.parentInjector);
57+
const providers: StaticProvider[] = [{provide: $SCOPE, useValue: this.componentScope}];
58+
const childInjector = Injector.create(
59+
{providers: providers, parent: this.parentInjector, name: 'DowngradeComponentAdapter'});
5960

6061
this.componentRef =
6162
this.componentFactory.create(childInjector, projectableNodes, this.element[0]);

0 commit comments

Comments
 (0)