/** * @license * Copyright Google Inc. All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ import {Type} from '../interface/type'; import {getClosureSafeProperty} from '../util/property'; import {stringify} from '../util/stringify'; import {resolveForwardRef} from './forward_ref'; import {InjectionToken} from './injection_token'; import {ɵɵinject} from './injector_compatibility'; import {ɵɵdefineInjectable} from './interface/defs'; import {InjectFlags} from './interface/injector'; import {ConstructorProvider, ExistingProvider, FactoryProvider, StaticClassProvider, StaticProvider, ValueProvider} from './interface/provider'; import {Inject, Optional, Self, SkipSelf} from './metadata'; export const SOURCE = '__source'; const _THROW_IF_NOT_FOUND = new Object(); export const THROW_IF_NOT_FOUND = _THROW_IF_NOT_FOUND; /** * An InjectionToken that gets the current `Injector` for `createInjector()`-style injectors. * * Requesting this token instead of `Injector` allows `StaticInjector` to be tree-shaken from a * project. * * @publicApi */ export const INJECTOR = new InjectionToken( 'INJECTOR', -1 as any // `-1` is used by Ivy DI system as special value to recognize it as `Injector`. ); export class NullInjector implements Injector { get(token: any, notFoundValue: any = _THROW_IF_NOT_FOUND): any { if (notFoundValue === _THROW_IF_NOT_FOUND) { // Intentionally left behind: With dev tools open the debugger will stop here. There is no // reason why correctly written application should cause this exception. // TODO(misko): uncomment the next line once `ngDevMode` works with closure. // if(ngDevMode) debugger; const error = new Error(`NullInjectorError: No provider for ${stringify(token)}!`); error.name = 'NullInjectorError'; throw error; } return notFoundValue; } } /** * Concrete injectors implement this interface. * * For more details, see the ["Dependency Injection Guide"](guide/dependency-injection). * * @usageNotes * ### Example * * {@example core/di/ts/injector_spec.ts region='Injector'} * * `Injector` returns itself when given `Injector` as a token: * * {@example core/di/ts/injector_spec.ts region='injectInjector'} * * @publicApi */ export abstract class Injector { static THROW_IF_NOT_FOUND = _THROW_IF_NOT_FOUND; static NULL: Injector = new NullInjector(); /** * Retrieves an instance from the injector based on the provided token. * @returns The instance from the injector if defined, otherwise the `notFoundValue`. * @throws When the `notFoundValue` is `undefined` or `Injector.THROW_IF_NOT_FOUND`. */ abstract get(token: Type|InjectionToken, notFoundValue?: T, flags?: InjectFlags): T; /** * @deprecated from v4.0.0 use Type or InjectionToken * @suppress {duplicate} */ abstract get(token: any, notFoundValue?: any): any; /** * @deprecated from v5 use the new signature Injector.create(options) */ static create(providers: StaticProvider[], parent?: Injector): Injector; static create(options: {providers: StaticProvider[], parent?: Injector, name?: string}): Injector; /** * Create a new Injector which is configure using `StaticProvider`s. * * @usageNotes * ### Example * * {@example core/di/ts/provider_spec.ts region='ConstructorProvider'} */ static create( options: StaticProvider[]|{providers: StaticProvider[], parent?: Injector, name?: string}, parent?: Injector): Injector { if (Array.isArray(options)) { return new StaticInjector(options, parent); } else { return new StaticInjector(options.providers, options.parent, options.name || null); } } /** @nocollapse */ static ngInjectableDef = ɵɵdefineInjectable({ providedIn: 'any' as any, factory: () => ɵɵinject(INJECTOR), }); /** * @internal * @nocollapse */ static __NG_ELEMENT_ID__ = -1; } const IDENT = function(value: T): T { return value; }; const EMPTY = []; const CIRCULAR = IDENT; const MULTI_PROVIDER_FN = function(): any[] { return Array.prototype.slice.call(arguments); }; export const USE_VALUE = getClosureSafeProperty({provide: String, useValue: getClosureSafeProperty}); const NG_TOKEN_PATH = 'ngTokenPath'; export const NG_TEMP_TOKEN_PATH = 'ngTempTokenPath'; const enum OptionFlags { Optional = 1 << 0, CheckSelf = 1 << 1, CheckParent = 1 << 2, Default = CheckSelf | CheckParent } const NULL_INJECTOR = Injector.NULL; const NEW_LINE = /\n/gm; const NO_NEW_LINE = 'ɵ'; export class StaticInjector implements Injector { readonly parent: Injector; readonly source: string|null; private _records: Map; constructor( providers: StaticProvider[], parent: Injector = NULL_INJECTOR, source: string|null = null) { this.parent = parent; this.source = source; const records = this._records = new Map(); records.set( Injector, {token: Injector, fn: IDENT, deps: EMPTY, value: this, useNew: false}); records.set( INJECTOR, {token: INJECTOR, fn: IDENT, deps: EMPTY, value: this, useNew: false}); recursivelyProcessProviders(records, providers); } get(token: Type|InjectionToken, notFoundValue?: T, flags?: InjectFlags): T; get(token: any, notFoundValue?: any): any; get(token: any, notFoundValue?: any, flags: InjectFlags = InjectFlags.Default): any { const record = this._records.get(token); try { return tryResolveToken(token, record, this._records, this.parent, notFoundValue, flags); } catch (e) { return catchInjectorError(e, token, 'StaticInjectorError', this.source); } } toString() { const tokens = [], records = this._records; records.forEach((v, token) => tokens.push(stringify(token))); return `StaticInjector[${tokens.join(', ')}]`; } } type SupportedProvider = ValueProvider | ExistingProvider | StaticClassProvider | ConstructorProvider | FactoryProvider; interface Record { fn: Function; useNew: boolean; deps: DependencyRecord[]; value: any; } interface DependencyRecord { token: any; options: number; } function resolveProvider(provider: SupportedProvider): Record { const deps = computeDeps(provider); let fn: Function = IDENT; let value: any = EMPTY; let useNew: boolean = false; let provide = resolveForwardRef(provider.provide); if (USE_VALUE in provider) { // We need to use USE_VALUE in provider since provider.useValue could be defined as undefined. value = (provider as ValueProvider).useValue; } else if ((provider as FactoryProvider).useFactory) { fn = (provider as FactoryProvider).useFactory; } else if ((provider as ExistingProvider).useExisting) { // Just use IDENT } else if ((provider as StaticClassProvider).useClass) { useNew = true; fn = resolveForwardRef((provider as StaticClassProvider).useClass); } else if (typeof provide == 'function') { useNew = true; fn = provide; } else { throw staticError( 'StaticProvider does not have [useValue|useFactory|useExisting|useClass] or [provide] is not newable', provider); } return {deps, fn, useNew, value}; } function multiProviderMixError(token: any) { return staticError('Cannot mix multi providers and regular providers', token); } function recursivelyProcessProviders(records: Map, provider: StaticProvider) { if (provider) { provider = resolveForwardRef(provider); if (provider instanceof Array) { // if we have an array recurse into the array for (let i = 0; i < provider.length; i++) { recursivelyProcessProviders(records, provider[i]); } } else if (typeof provider === 'function') { // Functions were supported in ReflectiveInjector, but are not here. For safety give useful // error messages throw staticError('Function/Class not supported', provider); } else if (provider && typeof provider === 'object' && provider.provide) { // At this point we have what looks like a provider: {provide: ?, ....} let token = resolveForwardRef(provider.provide); const resolvedProvider = resolveProvider(provider); if (provider.multi === true) { // This is a multi provider. let multiProvider: Record|undefined = records.get(token); if (multiProvider) { if (multiProvider.fn !== MULTI_PROVIDER_FN) { throw multiProviderMixError(token); } } else { // Create a placeholder factory which will look up the constituents of the multi provider. records.set(token, multiProvider = { token: provider.provide, deps: [], useNew: false, fn: MULTI_PROVIDER_FN, value: EMPTY }); } // Treat the provider as the token. token = provider; multiProvider.deps.push({token, options: OptionFlags.Default}); } const record = records.get(token); if (record && record.fn == MULTI_PROVIDER_FN) { throw multiProviderMixError(token); } records.set(token, resolvedProvider); } else { throw staticError('Unexpected provider', provider); } } } function tryResolveToken( token: any, record: Record | undefined, records: Map, parent: Injector, notFoundValue: any, flags: InjectFlags): any { try { return resolveToken(token, record, records, parent, notFoundValue, flags); } catch (e) { // ensure that 'e' is of type Error. if (!(e instanceof Error)) { e = new Error(e); } const path: any[] = e[NG_TEMP_TOKEN_PATH] = e[NG_TEMP_TOKEN_PATH] || []; path.unshift(token); if (record && record.value == CIRCULAR) { // Reset the Circular flag. record.value = EMPTY; } throw e; } } function resolveToken( token: any, record: Record | undefined, records: Map, parent: Injector, notFoundValue: any, flags: InjectFlags): any { let value; if (record && !(flags & InjectFlags.SkipSelf)) { // If we don't have a record, this implies that we don't own the provider hence don't know how // to resolve it. value = record.value; if (value == CIRCULAR) { throw Error(NO_NEW_LINE + 'Circular dependency'); } else if (value === EMPTY) { record.value = CIRCULAR; let obj = undefined; let useNew = record.useNew; let fn = record.fn; let depRecords = record.deps; let deps = EMPTY; if (depRecords.length) { deps = []; for (let i = 0; i < depRecords.length; i++) { const depRecord: DependencyRecord = depRecords[i]; const options = depRecord.options; const childRecord = options & OptionFlags.CheckSelf ? records.get(depRecord.token) : undefined; deps.push(tryResolveToken( // Current Token to resolve depRecord.token, // A record which describes how to resolve the token. // If undefined, this means we don't have such a record childRecord, // Other records we know about. records, // If we don't know how to resolve dependency and we should not check parent for it, // than pass in Null injector. !childRecord && !(options & OptionFlags.CheckParent) ? NULL_INJECTOR : parent, options & OptionFlags.Optional ? null : Injector.THROW_IF_NOT_FOUND, InjectFlags.Default)); } } record.value = value = useNew ? new (fn as any)(...deps) : fn.apply(obj, deps); } } else if (!(flags & InjectFlags.Self)) { value = parent.get(token, notFoundValue, InjectFlags.Default); } return value; } function computeDeps(provider: StaticProvider): DependencyRecord[] { let deps: DependencyRecord[] = EMPTY; const providerDeps: any[] = (provider as ExistingProvider & StaticClassProvider & ConstructorProvider).deps; if (providerDeps && providerDeps.length) { deps = []; for (let i = 0; i < providerDeps.length; i++) { let options = OptionFlags.Default; let token = resolveForwardRef(providerDeps[i]); if (token instanceof Array) { for (let j = 0, annotations = token; j < annotations.length; j++) { const annotation = annotations[j]; if (annotation instanceof Optional || annotation == Optional) { options = options | OptionFlags.Optional; } else if (annotation instanceof SkipSelf || annotation == SkipSelf) { options = options & ~OptionFlags.CheckSelf; } else if (annotation instanceof Self || annotation == Self) { options = options & ~OptionFlags.CheckParent; } else if (annotation instanceof Inject) { token = (annotation as Inject).token; } else { token = resolveForwardRef(annotation); } } } deps.push({token, options}); } } else if ((provider as ExistingProvider).useExisting) { const token = resolveForwardRef((provider as ExistingProvider).useExisting); deps = [{token, options: OptionFlags.Default}]; } else if (!providerDeps && !(USE_VALUE in provider)) { // useValue & useExisting are the only ones which are exempt from deps all others need it. throw staticError('\'deps\' required', provider); } return deps; } 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; let context = stringify(obj); if (obj instanceof Array) { context = obj.map(stringify).join(' -> '); } else if (typeof obj === 'object') { let parts = []; for (let key in obj) { if (obj.hasOwnProperty(key)) { let value = obj[key]; parts.push( key + ':' + (typeof value === 'string' ? JSON.stringify(value) : stringify(value))); } } context = `{${parts.join(', ')}}`; } return `${injectorErrorName}${source ? '(' + source + ')' : ''}[${context}]: ${text.replace(NEW_LINE, '\n ')}`; } function staticError(text: string, obj: any): Error { return new Error(formatError(text, obj, 'StaticInjectorError')); }