From d9d00bd9b51e57e90f4419fb95ef498749ec0e02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mis=CC=8Cko=20Hevery?= Date: Thu, 27 Jul 2017 13:49:33 -0700 Subject: [PATCH] feat(core): Create StaticInjector which does not depend on Reflect polyfill. --- packages/core/src/di.ts | 2 +- packages/core/src/di/injector.ts | 308 ++++++++++- packages/core/src/di/provider.ts | 174 +++++-- packages/core/src/di/reflective_injector.ts | 2 + packages/core/src/di/reflective_key.ts | 2 +- packages/core/src/util.ts | 4 + packages/core/test/di/injector_spec.ts | 5 +- packages/core/test/di/static_injector_spec.ts | 479 ++++++++++++++++++ .../test/linker/ng_module_integration_spec.ts | 5 +- packages/examples/core/di/ts/provider_spec.ts | 78 ++- .../src/platform-webworker-dynamic.ts | 2 +- tools/public_api_guard/core/core.d.ts | 4 + 12 files changed, 1013 insertions(+), 52 deletions(-) create mode 100644 packages/core/test/di/static_injector_spec.ts diff --git a/packages/core/src/di.ts b/packages/core/src/di.ts index cbfcbaf62d..976fd3f5e4 100644 --- a/packages/core/src/di.ts +++ b/packages/core/src/di.ts @@ -18,7 +18,7 @@ export {forwardRef, resolveForwardRef, ForwardRefFn} from './di/forward_ref'; export {Injector} from './di/injector'; export {ReflectiveInjector} from './di/reflective_injector'; -export {Provider, TypeProvider, ValueProvider, ClassProvider, ExistingProvider, FactoryProvider} from './di/provider'; +export {StaticProvider, ValueProvider, ExistingProvider, FactoryProvider, Provider, TypeProvider, ClassProvider} from './di/provider'; export {ResolvedReflectiveFactory, ResolvedReflectiveProvider} from './di/reflective_provider'; export {ReflectiveKey} from './di/reflective_key'; export {InjectionToken, OpaqueToken} from './di/injection_token'; diff --git a/packages/core/src/di/injector.ts b/packages/core/src/di/injector.ts index d640c81a08..a0a380a414 100644 --- a/packages/core/src/di/injector.ts +++ b/packages/core/src/di/injector.ts @@ -9,7 +9,10 @@ import {Type} from '../type'; import {stringify} from '../util'; +import {resolveForwardRef} from './forward_ref'; import {InjectionToken} from './injection_token'; +import {Inject, Optional, Self, SkipSelf} from './metadata'; +import {ConstructorProvider, ExistingProvider, FactoryProvider, StaticClassProvider, StaticProvider, ValueProvider} from './provider'; const _THROW_IF_NOT_FOUND = new Object(); export const THROW_IF_NOT_FOUND = _THROW_IF_NOT_FOUND; @@ -17,7 +20,7 @@ export const THROW_IF_NOT_FOUND = _THROW_IF_NOT_FOUND; class _NullInjector implements Injector { get(token: any, notFoundValue: any = _THROW_IF_NOT_FOUND): any { if (notFoundValue === _THROW_IF_NOT_FOUND) { - throw new Error(`No provider for ${stringify(token)}!`); + throw new Error(`NullInjectorError: No provider for ${stringify(token)}!`); } return notFoundValue; } @@ -60,4 +63,307 @@ export abstract class Injector { * @suppress {duplicate} */ abstract get(token: any, notFoundValue?: any): any; + + /** + * Create a new Injector which is configure using `StaticProvider`s. + * + * ### Example + * + * {@example core/di/ts/provider_spec.ts region='ConstructorProvider'} + */ + static create(providers: StaticProvider[], parent?: Injector): Injector { + return new StaticInjector(providers, parent); + } +} + + + +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); +}; +const GET_PROPERTY_NAME = {} as any; +const USE_VALUE = + getClosureSafeProperty({provide: String, useValue: GET_PROPERTY_NAME}); +const NG_TOKEN_PATH = 'ngTokenPath'; +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; + + private _records: Map; + + constructor(providers: StaticProvider[], parent: Injector = NULL_INJECTOR) { + this.parent = parent; + const records = this._records = new Map(); + records.set( + Injector, {token: Injector, fn: IDENT, deps: EMPTY, value: this, useNew: false}); + recursivelyProcessProviders(records, providers); + } + + get(token: Type|InjectionToken, notFoundValue?: T): T; + get(token: any, notFoundValue?: any): any; + get(token: any, notFoundValue?: any): any { + const record = this._records.get(token); + try { + return tryResolveToken(token, record, this._records, this.parent, notFoundValue); + } catch (e) { + const tokenPath: any[] = e[NG_TEMP_TOKEN_PATH]; + e.message = formatError('\n' + e.message, tokenPath); + e[NG_TOKEN_PATH] = tokenPath; + e[NG_TEMP_TOKEN_PATH] = null; + throw e; + } + } + + 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; +} + +type TokenPath = Array; + +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): any { + try { + return resolveToken(token, record, records, parent, notFoundValue); + } 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): any { + let value; + if (record) { + // 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)); + } + } + record.value = value = useNew ? new (fn as any)(...deps) : fn.apply(obj, deps); + } + } else { + value = parent.get(token, notFoundValue); + } + 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; +} + +function formatError(text: string, obj: any): 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 `StaticInjectorError[${context}]: ${text.replace(NEW_LINE, '\n ')}`; +} + +function staticError(text: string, obj: any): Error { + return new Error(formatError(text, obj)); +} + +function getClosureSafeProperty(objWithPropertyToExtract: T): string { + for (let key in objWithPropertyToExtract) { + if (objWithPropertyToExtract[key] === GET_PROPERTY_NAME) { + return key; + } + } + throw Error('!prop'); } diff --git a/packages/core/src/di/provider.ts b/packages/core/src/di/provider.ts index acb8f23cb3..9ad449e3ff 100644 --- a/packages/core/src/di/provider.ts +++ b/packages/core/src/di/provider.ts @@ -8,32 +8,6 @@ import {Type} from '../type'; -/** - * @whatItDoes Configures the {@link Injector} to return an instance of `Type` when `Type' is used - * as token. - * @howToUse - * ``` - * @Injectable() - * class MyService {} - * - * const provider: TypeProvider = MyService; - * ``` - * - * @description - * - * Create an instance by invoking the `new` operator and supplying additional arguments. - * This form is a short form of `TypeProvider`; - * - * For more details, see the {@linkDocs guide/dependency-injection "Dependency Injection Guide"}. - * - * ### Example - * - * {@example core/di/ts/provider_spec.ts region='TypeProvider'} - * - * @stable - */ -export interface TypeProvider extends Type {} - /** * @whatItDoes Configures the {@link Injector} to return a value for a token. * @howToUse @@ -79,7 +53,7 @@ export interface ValueProvider { * @Injectable() * class MyService {} * - * const provider: ClassProvider = {provide: 'someToken', useClass: MyService}; + * const provider: ClassProvider = {provide: 'someToken', useClass: MyService, deps: []}; * ``` * * @description @@ -87,24 +61,74 @@ export interface ValueProvider { * * ### Example * - * {@example core/di/ts/provider_spec.ts region='ClassProvider'} + * {@example core/di/ts/provider_spec.ts region='StaticClassProvider'} * * Note that following two providers are not equal: - * {@example core/di/ts/provider_spec.ts region='ClassProviderDifference'} + * {@example core/di/ts/provider_spec.ts region='StaticClassProviderDifference'} * * @stable */ -export interface ClassProvider { +export interface StaticClassProvider { /** * An injection token. (Typically an instance of `Type` or `InjectionToken`, but can be `any`). */ provide: any; /** - * Class to instantiate for the `token`. + * An optional class to instantiate for the `token`. (If not provided `provide` is assumed to be a + * class to + * instantiate) */ useClass: Type; + /** + * A list of `token`s which need to be resolved by the injector. The list of values is then + * used as arguments to the `useClass` constructor. + */ + deps: any[]; + + /** + * If true, then injector returns an array of instances. This is useful to allow multiple + * providers spread across many files to provide configuration information to a common token. + * + * ### Example + * + * {@example core/di/ts/provider_spec.ts region='MultiProviderAspect'} + */ + multi?: boolean; +} + +/** + * @whatItDoes Configures the {@link Injector} to return an instance of a token. + * @howToUse + * ``` + * @Injectable() + * class MyService {} + * + * const provider: ClassProvider = {provide: MyClass, deps: []}; + * ``` + * + * @description + * For more details, see the {@linkDocs guide/dependency-injection "Dependency Injection Guide"}. + * + * ### Example + * + * {@example core/di/ts/provider_spec.ts region='ConstructorProvider'} + * + * @stable + */ +export interface ConstructorProvider { + /** + * An injection token. (Typically an instance of `Type` or `InjectionToken`, but can be `any`). + */ + provide: Type; + + /** + * A list of `token`s which need to be resolved by the injector. The list of values is then + * used as arguments to the `useClass` constructor. + */ + deps: any[]; + /** * If true, then injector returns an array of instances. This is useful to allow multiple * providers spread across many files to provide configuration information to a common token. @@ -205,11 +229,95 @@ export interface FactoryProvider { multi?: boolean; } +/** + * @whatItDoes Describes how the {@link Injector} should be configured in a static way (Without + * reflection). + * @howToUse + * See {@link ValueProvider}, {@link ExistingProvider}, {@link FactoryProvider}. + * + * @description + * For more details, see the {@linkDocs guide/dependency-injection "Dependency Injection Guide"}. + * + * @stable + */ +export type StaticProvider = ValueProvider | ExistingProvider | StaticClassProvider | + ConstructorProvider | FactoryProvider | any[]; + + +/** + * @whatItDoes Configures the {@link Injector} to return an instance of `Type` when `Type' is used + * as token. + * @howToUse + * ``` + * @Injectable() + * class MyService {} + * + * const provider: TypeProvider = MyService; + * ``` + * + * @description + * + * Create an instance by invoking the `new` operator and supplying additional arguments. + * This form is a short form of `TypeProvider`; + * + * For more details, see the {@linkDocs guide/dependency-injection "Dependency Injection Guide"}. + * + * ### Example + * + * {@example core/di/ts/provider_spec.ts region='TypeProvider'} + * + * @stable + */ +export interface TypeProvider extends Type {} + +/** + * @whatItDoes Configures the {@link Injector} to return an instance of `useClass` for a token. + * @howToUse + * ``` + * @Injectable() + * class MyService {} + * + * const provider: ClassProvider = {provide: 'someToken', useClass: MyService}; + * ``` + * + * @description + * For more details, see the {@linkDocs guide/dependency-injection "Dependency Injection Guide"}. + * + * ### Example + * + * {@example core/di/ts/provider_spec.ts region='ClassProvider'} + * + * Note that following two providers are not equal: + * {@example core/di/ts/provider_spec.ts region='ClassProviderDifference'} + * + * @stable + */ +export interface ClassProvider { + /** + * An injection token. (Typically an instance of `Type` or `InjectionToken`, but can be `any`). + */ + provide: any; + + /** + * Class to instantiate for the `token`. + */ + useClass: Type; + + /** + * If true, then injector returns an array of instances. This is useful to allow multiple + * providers spread across many files to provide configuration information to a common token. + * + * ### Example + * + * {@example core/di/ts/provider_spec.ts region='MultiProviderAspect'} + */ + multi?: boolean; +} + /** * @whatItDoes Describes how the {@link Injector} should be configured. * @howToUse - * See {@link TypeProvider}, {@link ValueProvider}, {@link ClassProvider}, {@link ExistingProvider}, - * {@link FactoryProvider}. + * See {@link TypeProvider}, {@link ClassProvider}, {@link StaticProvider}. * * @description * For more details, see the {@linkDocs guide/dependency-injection "Dependency Injection Guide"}. diff --git a/packages/core/src/di/reflective_injector.ts b/packages/core/src/di/reflective_injector.ts index a0e939dddd..2dd381b657 100644 --- a/packages/core/src/di/reflective_injector.ts +++ b/packages/core/src/di/reflective_injector.ts @@ -13,6 +13,8 @@ import {cyclicDependencyError, instantiationError, noProviderError, outOfBoundsE import {ReflectiveKey} from './reflective_key'; import {ReflectiveDependency, ResolvedReflectiveFactory, ResolvedReflectiveProvider, resolveReflectiveProviders} from './reflective_provider'; + + // Threshold for the dynamic version const UNDEFINED = new Object(); diff --git a/packages/core/src/di/reflective_key.ts b/packages/core/src/di/reflective_key.ts index e23df7ad0a..765f3fded6 100644 --- a/packages/core/src/di/reflective_key.ts +++ b/packages/core/src/di/reflective_key.ts @@ -24,7 +24,7 @@ import {resolveForwardRef} from './forward_ref'; * `Key` should not be created directly. {@link ReflectiveInjector} creates keys automatically when * resolving * providers. - * @experimental + * @deprecated No replacement */ export class ReflectiveKey { /** diff --git a/packages/core/src/util.ts b/packages/core/src/util.ts index 3e256b4797..393cf40488 100644 --- a/packages/core/src/util.ts +++ b/packages/core/src/util.ts @@ -60,6 +60,10 @@ export function stringify(token: any): string { return token; } + if (token instanceof Array) { + return '[' + token.map(stringify).join(', ') + ']'; + } + if (token == null) { return '' + token; } diff --git a/packages/core/test/di/injector_spec.ts b/packages/core/test/di/injector_spec.ts index 753741fe25..820d5069da 100644 --- a/packages/core/test/di/injector_spec.ts +++ b/packages/core/test/di/injector_spec.ts @@ -13,12 +13,13 @@ import {describe, expect, it} from '@angular/core/testing/src/testing_internal'; export function main() { describe('Injector.NULL', () => { it('should throw if no arg is given', () => { - expect(() => Injector.NULL.get('someToken')).toThrowError('No provider for someToken!'); + expect(() => Injector.NULL.get('someToken')) + .toThrowError('NullInjectorError: No provider for someToken!'); }); it('should throw if THROW_IF_NOT_FOUND is given', () => { expect(() => Injector.NULL.get('someToken', Injector.THROW_IF_NOT_FOUND)) - .toThrowError('No provider for someToken!'); + .toThrowError('NullInjectorError: No provider for someToken!'); }); it('should return the default value', diff --git a/packages/core/test/di/static_injector_spec.ts b/packages/core/test/di/static_injector_spec.ts new file mode 100644 index 0000000000..f2b8df2f54 --- /dev/null +++ b/packages/core/test/di/static_injector_spec.ts @@ -0,0 +1,479 @@ +/** + * @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 {Inject, InjectionToken, Injector, Optional, ReflectiveKey, Self, SkipSelf, forwardRef} from '@angular/core'; +import {getOriginalError} from '@angular/core/src/errors'; +import {expect} from '@angular/platform-browser/testing/src/matchers'; + +import {stringify} from '../../src/util'; + +class Engine { + static PROVIDER = {provide: Engine, useClass: Engine, deps: []}; +} + +class BrokenEngine { + static PROVIDER = {provide: Engine, useClass: BrokenEngine, deps: []}; + constructor() { throw new Error('Broken Engine'); } +} + +class DashboardSoftware { + static PROVIDER = {provide: DashboardSoftware, useClass: DashboardSoftware, deps: []}; +} + +class Dashboard { + static PROVIDER = {provide: Dashboard, useClass: Dashboard, deps: [DashboardSoftware]}; + constructor(software: DashboardSoftware) {} +} + +class TurboEngine extends Engine { + static PROVIDER = {provide: Engine, useClass: TurboEngine, deps: []}; +} + +class Car { + static PROVIDER = {provide: Car, useClass: Car, deps: [Engine]}; + constructor(public engine: Engine) {} +} + +class CarWithOptionalEngine { + static PROVIDER = { + provide: CarWithOptionalEngine, + useClass: CarWithOptionalEngine, + deps: [[new Optional(), Engine]] + }; + constructor(public engine: Engine) {} +} + +class CarWithDashboard { + static PROVIDER = { + provide: CarWithDashboard, + useClass: CarWithDashboard, + deps: [Engine, Dashboard] + }; + engine: Engine; + dashboard: Dashboard; + constructor(engine: Engine, dashboard: Dashboard) { + this.engine = engine; + this.dashboard = dashboard; + } +} + +class SportsCar extends Car { + static PROVIDER = {provide: Car, useClass: SportsCar, deps: [Engine]}; +} + +class CyclicEngine { + static PROVIDER = {provide: Engine, useClass: CyclicEngine, deps: [Car]}; + constructor(car: Car) {} +} + +class NoAnnotations { + constructor(secretDependency: any) {} +} + +function factoryFn(a: any) {} + +export function main() { + const dynamicProviders = [ + {provide: 'provider0', useValue: 1}, {provide: 'provider1', useValue: 1}, + {provide: 'provider2', useValue: 1}, {provide: 'provider3', useValue: 1}, + {provide: 'provider4', useValue: 1}, {provide: 'provider5', useValue: 1}, + {provide: 'provider6', useValue: 1}, {provide: 'provider7', useValue: 1}, + {provide: 'provider8', useValue: 1}, {provide: 'provider9', useValue: 1}, + {provide: 'provider10', useValue: 1} + ]; + + describe(`StaticInjector`, () => { + + it('should instantiate a class without dependencies', () => { + const injector = Injector.create([Engine.PROVIDER]); + const engine = injector.get(Engine); + + expect(engine).toBeAnInstanceOf(Engine); + }); + + it('should resolve dependencies based on type information', () => { + const injector = Injector.create([Engine.PROVIDER, Car.PROVIDER]); + const car = injector.get(Car); + + expect(car).toBeAnInstanceOf(Car); + expect(car.engine).toBeAnInstanceOf(Engine); + }); + + it('should cache instances', () => { + const injector = Injector.create([Engine.PROVIDER]); + + const e1 = injector.get(Engine); + const e2 = injector.get(Engine); + + expect(e1).toBe(e2); + }); + + it('should provide to a value', () => { + const injector = Injector.create([{provide: Engine, useValue: 'fake engine'}]); + + const engine = injector.get(Engine); + expect(engine).toEqual('fake engine'); + }); + + it('should inject dependencies instance of InjectionToken', () => { + const TOKEN = new InjectionToken('token'); + + const injector = Injector.create([ + {provide: TOKEN, useValue: 'by token'}, + {provide: Engine, useFactory: (v: string) => v, deps: [[TOKEN]]}, + ]); + + const engine = injector.get(Engine); + expect(engine).toEqual('by token'); + }); + + it('should provide to a factory', () => { + function sportsCarFactory(e: any) { return new SportsCar(e); } + + const injector = Injector.create( + [Engine.PROVIDER, {provide: Car, useFactory: sportsCarFactory, deps: [Engine]}]); + + const car = injector.get(Car); + expect(car).toBeAnInstanceOf(SportsCar); + expect(car.engine).toBeAnInstanceOf(Engine); + }); + + it('should supporting provider to null', () => { + const injector = Injector.create([{provide: Engine, useValue: null}]); + const engine = injector.get(Engine); + expect(engine).toBeNull(); + }); + + it('should provide to an alias', () => { + const injector = Injector.create([ + Engine.PROVIDER, {provide: SportsCar, useClass: SportsCar, deps: [Engine]}, + {provide: Car, useExisting: SportsCar} + ]); + + const car = injector.get(Car); + const sportsCar = injector.get(SportsCar); + expect(car).toBeAnInstanceOf(SportsCar); + expect(car).toBe(sportsCar); + }); + + it('should support multiProviders', () => { + const injector = Injector.create([ + Engine.PROVIDER, {provide: Car, useClass: SportsCar, deps: [Engine], multi: true}, + {provide: Car, useClass: CarWithOptionalEngine, deps: [Engine], multi: true} + ]); + + const cars = injector.get(Car) as any as Car[]; + expect(cars.length).toEqual(2); + expect(cars[0]).toBeAnInstanceOf(SportsCar); + expect(cars[1]).toBeAnInstanceOf(CarWithOptionalEngine); + }); + + it('should support multiProviders that are created using useExisting', () => { + const injector = Injector.create([ + Engine.PROVIDER, {provide: SportsCar, useClass: SportsCar, deps: [Engine]}, + {provide: Car, useExisting: SportsCar, multi: true} + ]); + + const cars = injector.get(Car) as any as Car[]; + expect(cars.length).toEqual(1); + expect(cars[0]).toBe(injector.get(SportsCar)); + }); + + it('should throw when the aliased provider does not exist', () => { + const injector = Injector.create([{provide: 'car', useExisting: SportsCar}]); + const e = + `StaticInjectorError[car -> ${stringify(SportsCar)}]: \n NullInjectorError: No provider for ${stringify(SportsCar)}!`; + expect(() => injector.get('car')).toThrowError(e); + }); + + it('should handle forwardRef in useExisting', () => { + const injector = Injector.create([ + {provide: 'originalEngine', useClass: forwardRef(() => Engine), deps: []}, { + provide: 'aliasedEngine', + useExisting: forwardRef(() => 'originalEngine'), + deps: [] + } + ]); + expect(injector.get('aliasedEngine')).toBeAnInstanceOf(Engine); + }); + + it('should support overriding factory dependencies', () => { + const injector = Injector.create([ + Engine.PROVIDER, + {provide: Car, useFactory: (e: Engine) => new SportsCar(e), deps: [Engine]} + ]); + + const car = injector.get(Car); + expect(car).toBeAnInstanceOf(SportsCar); + expect(car.engine).toBeAnInstanceOf(Engine); + }); + + it('should support optional dependencies', () => { + const injector = Injector.create([CarWithOptionalEngine.PROVIDER]); + + const car = injector.get(CarWithOptionalEngine); + expect(car.engine).toEqual(null); + }); + + it('should flatten passed-in providers', () => { + const injector = Injector.create([[[Engine.PROVIDER, Car.PROVIDER]]]); + + const car = injector.get(Car); + expect(car).toBeAnInstanceOf(Car); + }); + + it('should use the last provider when there are multiple providers for same token', () => { + const injector = Injector.create([ + {provide: Engine, useClass: Engine, deps: []}, + {provide: Engine, useClass: TurboEngine, deps: []} + ]); + + expect(injector.get(Engine)).toBeAnInstanceOf(TurboEngine); + }); + + it('should use non-type tokens', () => { + const injector = Injector.create([{provide: 'token', useValue: 'value'}]); + + expect(injector.get('token')).toEqual('value'); + }); + + it('should throw when given invalid providers', () => { + expect(() => Injector.create(['blah'])) + .toThrowError('StaticInjectorError[blah]: Unexpected provider'); + }); + + it('should throw when missing deps', () => { + expect(() => Injector.create([{provide: Engine, useClass: Engine}])) + .toThrowError( + 'StaticInjectorError[{provide:Engine, useClass:Engine}]: \'deps\' required'); + }); + + it('should throw when using reflective API', () => { + expect(() => Injector.create([Engine])) + .toThrowError('StaticInjectorError[Engine]: Function/Class not supported'); + }); + + it('should throw when unknown provider shape API', () => { + expect(() => Injector.create([{provide: 'abc', deps: [Engine]}])) + .toThrowError( + 'StaticInjectorError[{provide:"abc", deps:[Engine]}]: StaticProvider does not have [useValue|useFactory|useExisting|useClass] or [provide] is not newable'); + }); + + it('should throw when given invalid providers and serialize the provider', () => { + expect(() => Injector.create([{foo: 'bar', bar: Car}])) + .toThrowError('StaticInjectorError[{foo:"bar", bar:Car}]: Unexpected provider'); + }); + + it('should provide itself', () => { + const parent = Injector.create([]); + const child = Injector.create([], parent); + + expect(child.get(Injector)).toBe(child); + }); + + it('should throw when no provider defined', () => { + const injector = Injector.create([]); + expect(() => injector.get('NonExisting')) + .toThrowError( + 'StaticInjectorError[NonExisting]: \n NullInjectorError: No provider for NonExisting!'); + }); + + it('should show the full path when no provider', () => { + const injector = + Injector.create([CarWithDashboard.PROVIDER, Engine.PROVIDER, Dashboard.PROVIDER]); + expect(() => injector.get(CarWithDashboard)) + .toThrowError( + `StaticInjectorError[${stringify(CarWithDashboard)} -> ${stringify(Dashboard)} -> DashboardSoftware]: + NullInjectorError: No provider for DashboardSoftware!`); + }); + + it('should throw when trying to instantiate a cyclic dependency', () => { + const injector = Injector.create([Car.PROVIDER, CyclicEngine.PROVIDER]); + + expect(() => injector.get(Car)) + .toThrowError( + `StaticInjectorError[${stringify(Car)} -> ${stringify(Engine)} -> ${stringify(Car)}]: Circular dependency`); + }); + + it('should show the full path when error happens in a constructor', () => { + const error = new Error('MyError'); + const injector = Injector.create( + [Car.PROVIDER, {provide: Engine, useFactory: () => { throw error; }, deps: []}]); + + try { + injector.get(Car); + throw 'Must throw'; + } catch (e) { + expect(e).toBe(error); + expect(e.message).toContain( + `StaticInjectorError[${stringify(Car)} -> Engine]: \n MyError`); + expect(e.ngTokenPath[0]).toEqual(Car); + expect(e.ngTokenPath[1]).toEqual(Engine); + } + }); + + it('should instantiate an object after a failed attempt', () => { + let isBroken = true; + + const injector = Injector.create([ + Car.PROVIDER, { + provide: Engine, + useFactory: (() => isBroken ? new BrokenEngine() : new Engine()), + deps: [] + } + ]); + + expect(() => injector.get(Car)) + .toThrowError('StaticInjectorError[Car -> Engine]: \n Broken Engine'); + + isBroken = false; + + expect(injector.get(Car)).toBeAnInstanceOf(Car); + }); + + it('should support null/undefined values', () => { + const injector = Injector.create([ + {provide: 'null', useValue: null}, + {provide: 'undefined', useValue: undefined}, + ]); + expect(injector.get('null')).toBe(null); + expect(injector.get('undefined')).toBe(undefined); + }); + + }); + + + describe('child', () => { + it('should load instances from parent injector', () => { + const parent = Injector.create([Engine.PROVIDER]); + const child = Injector.create([], parent); + + const engineFromParent = parent.get(Engine); + const engineFromChild = child.get(Engine); + + expect(engineFromChild).toBe(engineFromParent); + }); + + it('should not use the child providers when resolving the dependencies of a parent provider', + () => { + const parent = Injector.create([Car.PROVIDER, Engine.PROVIDER]); + const child = Injector.create([TurboEngine.PROVIDER], parent); + + const carFromChild = child.get(Car); + expect(carFromChild.engine).toBeAnInstanceOf(Engine); + }); + + it('should create new instance in a child injector', () => { + const parent = Injector.create([Engine.PROVIDER]); + const child = Injector.create([TurboEngine.PROVIDER], parent); + + const engineFromParent = parent.get(Engine); + const engineFromChild = child.get(Engine); + + expect(engineFromParent).not.toBe(engineFromChild); + expect(engineFromChild).toBeAnInstanceOf(TurboEngine); + }); + + it('should give access to parent', () => { + const parent = Injector.create([]); + const child = Injector.create([], parent); + expect((child as any).parent).toBe(parent); + }); + }); + + + describe('instantiate', () => { + it('should instantiate an object in the context of the injector', () => { + const inj = Injector.create([Engine.PROVIDER]); + const childInj = Injector.create([Car.PROVIDER], inj); + const car = childInj.get(Car); + expect(car).toBeAnInstanceOf(Car); + expect(car.engine).toBe(inj.get(Engine)); + }); + }); + + describe('depedency resolution', () => { + describe('@Self()', () => { + it('should return a dependency from self', () => { + const inj = Injector.create([ + Engine.PROVIDER, + {provide: Car, useFactory: (e: Engine) => new Car(e), deps: [[Engine, new Self()]]} + ]); + + expect(inj.get(Car)).toBeAnInstanceOf(Car); + }); + + it('should throw when not requested provider on self', () => { + const parent = Injector.create([Engine.PROVIDER]); + const child = Injector.create( + [{provide: Car, useFactory: (e: Engine) => new Car(e), deps: [[Engine, new Self()]]}], + parent); + + expect(() => child.get(Car)) + .toThrowError(`StaticInjectorError[${stringify(Car)} -> ${stringify(Engine)}]: + NullInjectorError: No provider for Engine!`); + }); + }); + + describe('default', () => { + it('should skip self', () => { + const parent = Injector.create([Engine.PROVIDER]); + const child = Injector.create( + [ + TurboEngine.PROVIDER, + {provide: Car, useFactory: (e: Engine) => new Car(e), deps: [[SkipSelf, Engine]]} + ], + parent); + + expect(child.get(Car).engine).toBeAnInstanceOf(Engine); + }); + }); + }); + + describe('resolve', () => { + it('should throw when mixing multi providers with regular providers', () => { + expect(() => { + Injector.create( + [{provide: Engine, useClass: BrokenEngine, deps: [], multi: true}, Engine.PROVIDER]); + }).toThrowError(/Cannot mix multi providers and regular providers/); + + expect(() => { + Injector.create( + [Engine.PROVIDER, {provide: Engine, useClass: BrokenEngine, deps: [], multi: true}]); + }).toThrowError(/Cannot mix multi providers and regular providers/); + }); + + it('should resolve forward references', () => { + const injector = Injector.create([ + [{provide: forwardRef(() => BrokenEngine), useClass: forwardRef(() => Engine), deps: []}], { + provide: forwardRef(() => String), + useFactory: (e: any) => e, + deps: [forwardRef(() => BrokenEngine)] + } + ]); + expect(injector.get(String)).toBeAnInstanceOf(Engine); + expect(injector.get(BrokenEngine)).toBeAnInstanceOf(Engine); + }); + + it('should support overriding factory dependencies with dependency annotations', () => { + const injector = Injector.create([ + Engine.PROVIDER, + {provide: 'token', useFactory: (e: any) => e, deps: [[new Inject(Engine)]]} + ]); + + expect(injector.get('token')).toBeAnInstanceOf(Engine); + }); + }); + + describe('displayName', () => { + it('should work', () => { + expect(Injector.create([Engine.PROVIDER, {provide: BrokenEngine, useValue: null}]).toString()) + .toEqual('StaticInjector[Injector, Engine, BrokenEngine]'); + }); + }); +} diff --git a/packages/core/test/linker/ng_module_integration_spec.ts b/packages/core/test/linker/ng_module_integration_spec.ts index 0991efe7dc..56660ed27e 100644 --- a/packages/core/test/linker/ng_module_integration_spec.ts +++ b/packages/core/test/linker/ng_module_integration_spec.ts @@ -720,7 +720,7 @@ function declareTests({useJit}: {useJit: boolean}) { it('should throw when the aliased provider does not exist', () => { const injector = createInjector([{provide: 'car', useExisting: SportsCar}]); - const e = `No provider for ${stringify(SportsCar)}!`; + const e = `NullInjectorError: No provider for ${stringify(SportsCar)}!`; expect(() => injector.get('car')).toThrowError(e); }); @@ -830,7 +830,8 @@ function declareTests({useJit}: {useJit: boolean}) { it('should throw when no provider defined', () => { const injector = createInjector([]); - expect(() => injector.get('NonExisting')).toThrowError('No provider for NonExisting!'); + expect(() => injector.get('NonExisting')) + .toThrowError('NullInjectorError: No provider for NonExisting!'); }); it('should throw when trying to instantiate a cyclic dependency', () => { diff --git a/packages/examples/core/di/ts/provider_spec.ts b/packages/examples/core/di/ts/provider_spec.ts index fce21ac041..4a273df1dc 100644 --- a/packages/examples/core/di/ts/provider_spec.ts +++ b/packages/examples/core/di/ts/provider_spec.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import {Inject, Injectable, InjectionToken, Optional, ReflectiveInjector} from '@angular/core'; +import {Injectable, InjectionToken, Injector, Optional, ReflectiveInjector} from '@angular/core'; export function main() { describe('Provider examples', () => { @@ -30,8 +30,7 @@ export function main() { describe('ValueProvider', () => { it('works', () => { // #docregion ValueProvider - const injector = - ReflectiveInjector.resolveAndCreate([{provide: String, useValue: 'Hello'}]); + const injector = Injector.create([{provide: String, useValue: 'Hello'}]); expect(injector.get(String)).toEqual('Hello'); // #enddocregion @@ -41,12 +40,13 @@ export function main() { describe('MultiProviderAspect', () => { it('works', () => { // #docregion MultiProviderAspect - const injector = ReflectiveInjector.resolveAndCreate([ - {provide: 'local', multi: true, useValue: 'en'}, - {provide: 'local', multi: true, useValue: 'sk'}, + const locale = new InjectionToken('locale'); + const injector = Injector.create([ + {provide: locale, multi: true, useValue: 'en'}, + {provide: locale, multi: true, useValue: 'sk'}, ]); - const locales: string[] = injector.get('local'); + const locales: string[] = injector.get(locale); expect(locales).toEqual(['en', 'sk']); // #enddocregion }); @@ -89,6 +89,61 @@ export function main() { }); }); + describe('StaticClassProvider', () => { + it('works', () => { + // #docregion StaticClassProvider + abstract class Shape { name: string; } + + class Square extends Shape { + name = 'square'; + } + + const injector = Injector.create([{provide: Shape, useClass: Square, deps: []}]); + + const shape: Shape = injector.get(Shape); + expect(shape.name).toEqual('square'); + expect(shape instanceof Square).toBe(true); + // #enddocregion + }); + + it('is different then useExisting', () => { + // #docregion StaticClassProviderDifference + class Greeting { + salutation = 'Hello'; + } + + class FormalGreeting extends Greeting { + salutation = 'Greetings'; + } + + const injector = Injector.create([ + {provide: FormalGreeting, useClass: FormalGreeting, deps: []}, + {provide: Greeting, useClass: FormalGreeting, deps: []} + ]); + + // The injector returns different instances. + // See: {provide: ?, useExisting: ?} if you want the same instance. + expect(injector.get(FormalGreeting)).not.toBe(injector.get(Greeting)); + // #enddocregion + }); + }); + + describe('ConstructorProvider', () => { + it('works', () => { + // #docregion ConstructorProvider + class Square { + name = 'square'; + } + + const injector = Injector.create([{provide: Square, deps: []}]); + + const shape: Square = injector.get(Square); + expect(shape.name).toEqual('square'); + expect(shape instanceof Square).toBe(true); + // #enddocregion + }); + }); + describe('ExistingProvider', () => { it('works', () => { // #docregion ExistingProvider @@ -100,8 +155,9 @@ export function main() { salutation = 'Greetings'; } - const injector = ReflectiveInjector.resolveAndCreate( - [FormalGreeting, {provide: Greeting, useExisting: FormalGreeting}]); + const injector = Injector.create([ + {provide: FormalGreeting, deps: []}, {provide: Greeting, useExisting: FormalGreeting} + ]); expect(injector.get(Greeting).salutation).toEqual('Greetings'); expect(injector.get(FormalGreeting).salutation).toEqual('Greetings'); @@ -116,7 +172,7 @@ export function main() { const Location = new InjectionToken('location'); const Hash = new InjectionToken('hash'); - const injector = ReflectiveInjector.resolveAndCreate([ + const injector = Injector.create([ {provide: Location, useValue: 'http://angular.io/#someLocation'}, { provide: Hash, useFactory: (location: string) => location.split('#')[1], @@ -133,7 +189,7 @@ export function main() { const Location = new InjectionToken('location'); const Hash = new InjectionToken('hash'); - const injector = ReflectiveInjector.resolveAndCreate([{ + const injector = Injector.create([{ provide: Hash, useFactory: (location: string) => `Hash for: ${location}`, // use a nested array to define metadata for dependencies. diff --git a/packages/platform-webworker-dynamic/src/platform-webworker-dynamic.ts b/packages/platform-webworker-dynamic/src/platform-webworker-dynamic.ts index 72aeb121f4..1472e95f28 100644 --- a/packages/platform-webworker-dynamic/src/platform-webworker-dynamic.ts +++ b/packages/platform-webworker-dynamic/src/platform-webworker-dynamic.ts @@ -19,7 +19,7 @@ export const platformWorkerAppDynamic = createPlatformFactory(platformCoreDynamic, 'workerAppDynamic', [ { provide: COMPILER_OPTIONS, - useValue: {providers: [{provide: ResourceLoader, useClass: ResourceLoaderImpl}]}, + useValue: {providers: [{provide: ResourceLoader, useClass: ResourceLoaderImpl, deps: []}]}, multi: true }, {provide: PLATFORM_ID, useValue: PLATFORM_WORKER_UI_ID} diff --git a/tools/public_api_guard/core/core.d.ts b/tools/public_api_guard/core/core.d.ts index 609b804790..d542f0a860 100644 --- a/tools/public_api_guard/core/core.d.ts +++ b/tools/public_api_guard/core/core.d.ts @@ -491,6 +491,7 @@ export declare abstract class Injector { /** @deprecated */ abstract get(token: any, notFoundValue?: any): any; static NULL: Injector; static THROW_IF_NOT_FOUND: Object; + static create(providers: StaticProvider[], parent?: Injector): Injector; } /** @stable */ @@ -951,6 +952,9 @@ export interface SkipSelfDecorator { /** @deprecated */ export declare function state(name: string, styles: AnimationStyleMetadata): AnimationStateMetadata; +/** @stable */ +export declare type StaticProvider = ValueProvider | ExistingProvider | StaticClassProvider | ConstructorProvider | FactoryProvider | any[]; + /** @deprecated */ export declare function style(tokens: { [key: string]: string | number;