UNPKG

aurelia-dependency-injection

Version:

A lightweight, extensible dependency injection container for JavaScript.

655 lines (600 loc) 20.5 kB
import { protocol } from 'aurelia-metadata'; import { Container } from './container'; import { autoinject } from './injection'; import type { PrimitiveOrDependencyCtor, DependencyCtorOrFunctor, PrimitiveOrDependencyCtorOrFunctor, DependencyCtor, DependencyFunctor, ImplOrAny, Impl, Args } from './types'; /** * Decorator: Indicates that the decorated class/object is a custom resolver. */ export const resolver: { decorates?: (key: any) => key is { get(container: Container, key: any): any }; } & (() => any) = ((protocol as unknown) as { create(...args: any[]): any }).create( 'aurelia:resolver', (target): string | boolean => { if (!(typeof target.get === 'function')) { return 'Resolvers must implement: get(container: Container, key: any): any'; } return true; } ); /** * Used to allow functions/classes to specify custom dependency resolution logic. */ export interface Resolver { /** * Called by the container to allow custom resolution of dependencies for a * function/class. * @param container The container to resolve from. * @param key The key that the resolver was registered as. * @return Returns the resolved object. */ get(container: Container, key: any): any; } export enum Strategy { instance = 0, singleton = 1, transient = 2, function = 3, array = 4, alias = 5 } export type IStrategy = 1 | 2 | 3 | 4 | 5; export type StrategyFunctor< TBase, TImpl extends Impl<TBase> = Impl<TBase>, TArgs extends Args<TBase> = Args<TBase> > = ( container?: Container, ctor?: PrimitiveOrDependencyCtorOrFunctor<TBase, TImpl, TArgs>, strategyResolver?: any ) => TImpl; export interface StrategyState< TBase, TImpl extends Impl<TBase> = Impl<TBase>, TArgs extends Args<TBase> = Args<TBase> > { [Strategy.instance]: TImpl; [Strategy.singleton]: DependencyCtorOrFunctor<TBase, TImpl, TArgs>; [Strategy.transient]: DependencyCtorOrFunctor<TBase, TImpl, TArgs>; [Strategy.function]: StrategyFunctor<TBase, TImpl, TArgs>; /** * For typings purposes, this is done as ({ get: StrategyFunctor } | TImpl)[] * But it should be understood, and used as [{ get: StrategyFunctor }, ...TImp[]] */ [Strategy.array]: ({ get: ( container: Container, key: PrimitiveOrDependencyCtor<TBase, TImpl, TArgs> ) => TImpl } | TImpl)[]; [Strategy.alias]: any; } function isStrategy< TBase, TImpl extends Impl<TBase>, TArgs extends Args<TBase>, TKey extends keyof StrategyState<TBase, TImpl, TArgs> >( actual: Strategy, expected: TKey, state): state is StrategyState<TBase, TImpl, TArgs>[TKey] { return actual === expected; } /** * Used to resolve instances, singletons, transients, aliases */ @resolver() export class StrategyResolver< TBase, TImpl extends Impl<TBase>, TArgs extends Args<TBase>, TStrategyKey extends keyof StrategyState<TBase, TImpl, TArgs>> { public strategy: keyof StrategyState<TBase, TImpl, TArgs>; public state: StrategyState<TBase, TImpl, TArgs>[keyof StrategyState<TBase, TImpl, TArgs>]; /** * Creates an instance of the StrategyResolver class. * @param strategy The type of resolution strategy. * @param state The state associated with the resolution strategy. */ constructor(strategy: TStrategyKey, state: StrategyState<TBase, TImpl, TArgs>[TStrategyKey]) { this.strategy = strategy; this.state = state; } /** * Called by the container to allow custom resolution of dependencies for a * function/class. * @param container The container to resolve from. * @param key The key that the resolver was registered as. * @return Returns the resolved object. */ public get(container: Container, key: PrimitiveOrDependencyCtor<TBase, TImpl, TArgs>) { if (isStrategy<TBase, TImpl, TArgs, Strategy.instance>(this.strategy, Strategy.instance, this.state)) { return this.state; } if (isStrategy<TBase, TImpl, TArgs, Strategy.singleton>(this.strategy, Strategy.singleton, this.state)) { const singleton = container.invoke<TBase, TImpl, TArgs>(this.state); this.state = singleton; this.strategy = 0; return singleton; } if (isStrategy<TBase, TImpl, TArgs, Strategy.transient>(this.strategy, Strategy.transient, this.state)) { return container.invoke<TBase, TImpl, TArgs>(this.state); } if (isStrategy<TBase, TImpl, TArgs, Strategy.function>(this.strategy, Strategy.function, this.state)) { return this.state(container, key, this); } if (isStrategy<TBase, TImpl, TArgs, Strategy.array>(this.strategy, Strategy.array, this.state)) { return (this.state[0] as { get: StrategyFunctor<TBase, TImpl, TArgs> }).get(container, key); } if (isStrategy<TBase, TImpl, TArgs, Strategy.alias>(this.strategy, Strategy.alias, this.state)) { return container.get(this.state) as TImpl; } throw new Error('Invalid strategy: ' + this.strategy); } } /** * Used to allow functions/classes to specify lazy resolution logic. */ @resolver() export class Lazy<TBase, TImpl extends Impl<TBase> = Impl<TBase>, TArgs extends Args<TBase> = Args<TBase>> { /** @internal */ public _key: PrimitiveOrDependencyCtor<TBase, TImpl, TArgs>; /** * A non existent property to help TS distinguish resolvers * * This is semi-private, and should not be used by application */ __resolver_type__!: 'lazy'; /** * Creates an instance of the Lazy class. * @param key The key to lazily resolve. */ constructor(key: PrimitiveOrDependencyCtor<TBase, TImpl, TArgs>) { this._key = key; } /** * Called by the container to lazily resolve the dependency into a lazy locator * function. * @param container The container to resolve from. * @return Returns a function which can be invoked at a later time to obtain * the actual dependency. */ public get(container: Container): () => ImplOrAny<TImpl> { return () => container.get<TBase, TImpl, TArgs>(this._key); } /** * Creates a Lazy Resolver for the supplied key. * @param key The key to lazily resolve. * @return Returns an instance of Lazy for the key. */ public static of< TBase, TImpl extends Impl<TBase> = Impl<TBase>, TArgs extends Args<TBase> = Args<TBase>>( key: PrimitiveOrDependencyCtor<TBase, TImpl, TArgs> ) { return new Lazy<TBase, TImpl, TArgs>(key); } } /** * Used to allow functions/classes to specify resolution of all matches to a key. */ @resolver() export class All<TBase, TImpl extends Impl<TBase> = Impl<TBase>, TArgs extends Args<TBase> = Args<TBase>> { /** @internal */ public _key: PrimitiveOrDependencyCtor<TBase, TImpl, TArgs>; /** * A non existent property to help TS distinguish resolvers * * This is semi-private, and should not be used by application */ __resolver_type__!: 'all'; /** * Creates an instance of the All class. * @param key The key to lazily resolve all matches for. */ constructor(key: PrimitiveOrDependencyCtor<TBase, TImpl, TArgs>) { this._key = key; } /** * Called by the container to resolve all matching dependencies as an array of * instances. * @param container The container to resolve from. * @return Returns an array of all matching instances. */ public get(container: Container): TImpl[] { return container.getAll(this._key); } /** * Creates an All Resolver for the supplied key. * @param key The key to resolve all instances for. * @return Returns an instance of All for the key. */ public static of<TBase, TImpl extends Impl<TBase> = Impl<TBase>, TArgs extends Args<TBase> = Args<TBase>>( key: PrimitiveOrDependencyCtor<TBase, TImpl, TArgs> ): All<TBase, TImpl, TArgs> { return new All(key); } } /** * Used to allow functions/classes to specify an optional dependency, which will * be resolved only if already registred with the container. */ @resolver() export class Optional<TBase, TImpl extends Impl<TBase> = Impl<TBase>, TArgs extends Args<TBase> = Args<TBase>> { /** @internal */ public _key: PrimitiveOrDependencyCtor<TBase, TImpl, TArgs>; /** @internal */ public _checkParent: boolean; /** * A non existent property to help TS distinguish resolvers * * This is semi-private, and should not be used by application */ __resolver_type__!: 'optional'; /** * Creates an instance of the Optional class. * @param key The key to optionally resolve for. * @param checkParent Indicates whether or not the parent container hierarchy * should be checked. */ constructor(key: PrimitiveOrDependencyCtor<TBase, TImpl, TArgs>, checkParent: boolean = true) { this._key = key; this._checkParent = checkParent; } /** * Called by the container to provide optional resolution of the key. * @param container The container to resolve from. * @return Returns the instance if found; otherwise null. */ public get(container: Container): TImpl | null { if (container.hasResolver(this._key, this._checkParent)) { return container.get(this._key); } return null; } /** * Creates an Optional Resolver for the supplied key. * @param key The key to optionally resolve for. * @param [checkParent=true] Indicates whether or not the parent container * hierarchy should be checked. * @return Returns an instance of Optional for the key. */ public static of<TBase, TImpl extends Impl<TBase> = Impl<TBase>, TArgs extends Args<TBase> = Args<TBase>>( key: PrimitiveOrDependencyCtor<TBase, TImpl, TArgs>, checkParent: boolean = true): Optional<TBase, TImpl, TArgs> { return new Optional(key, checkParent); } } /** * Used to inject the dependency from the parent container instead of the current * one. */ @resolver() export class Parent<TBase, TImpl extends Impl<TBase> = Impl<TBase>, TArgs extends Args<TBase> = Args<TBase>> { /** @internal */ public _key: PrimitiveOrDependencyCtor<TBase, TImpl, TArgs>; /** * A non existent property to help TS distinguish resolvers * * This is semi-private, and should not be used by application */ __resolver_type__!: 'parent'; /** * Creates an instance of the Parent class. * @param key The key to resolve from the parent container. */ constructor(key: PrimitiveOrDependencyCtor<TBase, TImpl, TArgs>) { this._key = key; } /** * Called by the container to load the dependency from the parent container * @param container The container to resolve the parent from. * @return Returns the matching instance from the parent container */ public get(container: Container): TImpl | null { return container.parent ? container.parent.get(this._key) : null; } /** * Creates a Parent Resolver for the supplied key. * @param key The key to resolve. * @return Returns an instance of Parent for the key. */ public static of<TBase, TImpl extends Impl<TBase> = Impl<TBase>, TArgs extends Args<TBase> = Args<TBase>>( key: PrimitiveOrDependencyCtor<TBase, TImpl, TArgs> ): Parent<TBase, TImpl, TArgs> { return new Parent(key); } } /** * Used to allow injecting dependencies but also passing data to the constructor. */ @resolver() export class Factory<TBase, TImpl extends Impl<TBase> = Impl<TBase>, TArgs extends Args<TBase> = Args<TBase>> { /** @internal */ public _key: PrimitiveOrDependencyCtorOrFunctor<TBase, TImpl, TArgs>; /** * A non existent property to help TS distinguish resolvers * * This is semi-private, and should not be used by application */ __resolver_type__!: 'factory'; /** * Creates an instance of the Factory class. * @param key The key to resolve from the parent container. */ constructor(key: PrimitiveOrDependencyCtorOrFunctor<TBase, TImpl, TArgs>) { this._key = key; } /** * Called by the container to pass the dependencies to the constructor. * @param container The container to invoke the constructor with dependencies * and other parameters. * @return Returns a function that can be invoked to resolve dependencies * later, and the rest of the parameters. */ public get(container: Container): DependencyFunctor<TBase, TImpl, TArgs> { let fn = this._key; const resolver = container.getResolver(fn); if (resolver && resolver.strategy === Strategy.function) { fn = resolver.state; } return (...rest) => container.invoke(fn as DependencyCtorOrFunctor<TBase, TImpl, TArgs>, rest); } /** * Creates a Factory Resolver for the supplied key. * @param key The key to resolve. * @return Returns an instance of Factory for the key. */ public static of<TBase, TImpl extends Impl<TBase> = Impl<TBase>, TArgs extends Args<TBase> = Args<TBase>>( key: DependencyCtor<TBase, TImpl, TArgs> ): Factory<TBase, TImpl, TArgs> { return new Factory(key); } } /** * Used to inject a new instance of a dependency, without regard for existing * instances in the container. Instances can optionally be registered in the * container * under a different key by supplying a key using the `as` method. */ @resolver() export class NewInstance< TBase, TImpl extends Impl<TBase> = Impl<TBase>, TArgs extends Args<TBase> = Args<TBase>> { /** @internal */ public key: PrimitiveOrDependencyCtorOrFunctor<TBase, TImpl, TArgs>; /** @internal */ public asKey: PrimitiveOrDependencyCtorOrFunctor<TBase, TImpl, TArgs>; /** @internal */ public dynamicDependencies: TArgs[number][]; /** * A non existent property to help TS distinguish resolvers * * This is semi-private, and should not be used by application */ __resolver_type__!: 'newInstance'; /** * Creates an instance of the NewInstance class. * @param key The key to resolve/instantiate. * @param dynamicDependencies An optional list of dynamic dependencies. */ constructor(key: PrimitiveOrDependencyCtorOrFunctor<TBase, TImpl, TArgs>, ...dynamicDependencies: TArgs[number][]) { this.key = key; this.asKey = key; this.dynamicDependencies = dynamicDependencies; } /** * Called by the container to instantiate the dependency and potentially * register * as another key if the `as` method was used. * @param container The container to resolve the parent from. * @return Returns the matching instance from the parent container */ public get(container: Container) { const dynamicDependencies = this.dynamicDependencies.length > 0 ? this.dynamicDependencies.map(dependency => dependency['protocol:aurelia:resolver'] ? dependency.get(container) : container.get(dependency) ) : undefined; let fn = this.key; const resolver = container.getResolver(fn); if (resolver && resolver.strategy === 3) { fn = resolver.state; } const instance = container.invoke(fn as DependencyCtorOrFunctor<TBase, TImpl, TArgs>, dynamicDependencies); container.registerInstance(this.asKey, instance); return instance; } /** * Instructs the NewInstance resolver to register the resolved instance using * the supplied key. * @param key The key to register the instance with. * @return Returns the NewInstance resolver. */ public as(key: PrimitiveOrDependencyCtorOrFunctor<TBase, TImpl, TArgs>) { this.asKey = key; return this; } /** * Creates an NewInstance Resolver for the supplied key. * @param key The key to resolve/instantiate. * @param dynamicDependencies An optional list of dynamic dependencies. * @return Returns an instance of NewInstance for the key. */ public static of<TBase, TImpl extends Impl<TBase> = Impl<TBase>, TArgs extends Args<TBase> = Args<TBase>>( key: PrimitiveOrDependencyCtorOrFunctor<TBase, TImpl, TArgs>, ...dynamicDependencies: TArgs[number][]): NewInstance<TBase, TImpl, TArgs> { return new NewInstance(key, ...dynamicDependencies); } } /** * Used by parameter decorators to call autoinject for the target and retrieve * the target's inject property. * @param target The target class. * @return Returns the target's own inject property. */ export function getDecoratorDependencies< TBase, TImpl extends Impl<TBase> = Impl<TBase>, TArgs extends Args<TBase> = Args<TBase>>( target: DependencyCtor<TBase, TImpl, TArgs> & { inject?: TArgs[number][] } ) { autoinject(target); return target.inject; } /** * Decorator: Specifies the dependency should be lazy loaded */ export function lazy< TBase, TImpl extends Impl<TBase> = Impl<TBase>, TArgs extends Args<TBase> = Args<TBase>>( keyValue: any ) { return ( target: { new (...args: TArgs): TBase | TImpl }, _key, index: number ) => { const inject = getDecoratorDependencies(target)!; inject[index] = Lazy.of(keyValue); }; } /** * Decorator: Specifies the dependency should load all instances of the given * key. */ export function all< TBase, TImpl extends Impl<TBase> = Impl<TBase>, TArgs extends Args<TBase> = Args<TBase>>( keyValue: any ) { return ( target: DependencyCtor<TBase, TImpl, TArgs> & { inject?: TArgs[number][] }, _key, index: number ) => { const inject = getDecoratorDependencies(target)!; inject[index] = All.of(keyValue); }; } /** * Decorator: Specifies the dependency as optional */ export function optional< TBase, TImpl extends Impl<TBase> = Impl<TBase>, TArgs extends Args<TBase> = Args<TBase>>( checkParentOrTarget: boolean = true) { const deco = (checkParent: boolean) => { return ( target: DependencyCtor<TBase, TImpl, TArgs> & { inject?: TArgs[number][] }, _key, index: number) => { const inject = getDecoratorDependencies(target)!; inject[index] = Optional.of(inject[index], checkParent); }; }; if (typeof checkParentOrTarget === 'boolean') { return deco(checkParentOrTarget); } return deco(true); } /** * Decorator: Specifies the dependency to look at the parent container for * resolution */ export function parent< TBase, TImpl extends Impl<TBase> = Impl<TBase>, TArgs extends Args<TBase> = Args<TBase>>( target: DependencyCtor<TBase, TImpl, TArgs> & { inject?: TArgs[number][] }, _key, index: number) { const inject = getDecoratorDependencies(target)!; inject[index] = Parent.of(inject[index]); } /** * Decorator: Specifies the dependency to create a factory method, that can * accept optional arguments */ export function factory< TBase, TImpl extends Impl<TBase> = Impl<TBase>, TArgs extends Args<TBase> = Args<TBase>>( keyValue: any ) { return ( target: DependencyCtor<TBase, TImpl, TArgs> & { inject?: TArgs[number][] }, _key, index: number ) => { const inject = getDecoratorDependencies(target)!; inject[index] = Factory.of(keyValue); }; } /** * Decorator: Specifies the dependency as a new instance. Instances can optionally be registered in the container * under a different key and/or use dynamic dependencies */ export function newInstance< TBase, TImpl extends Impl<TBase> = Impl<TBase>, TArgs extends Args<TBase> = Args<TBase> >( asKeyOrTarget?: PrimitiveOrDependencyCtor<TBase, TImpl, TArgs> & { inject?: TArgs[number][] }, ...dynamicDependencies: TArgs[number][] ) { const deco = (asKey?: typeof asKeyOrTarget) => { return ( target: { new (...args: any[]): any }, _key, index: number ) => { const inject = getDecoratorDependencies(target)!; inject[index] = NewInstance.of(inject[index], ...dynamicDependencies); if (!!asKey) { inject[index].as(asKey); } }; }; if (arguments.length >= 1) { return deco(asKeyOrTarget); } return deco(); }