/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/prefer-nullish-coalescing */ /* eslint-disable @typescript-eslint/strict-boolean-expressions */ /* eslint-disable @typescript-eslint/no-unsafe-member-access */ import { initializeTC39Metadata } from '@aurelia/metadata'; import { isArrayIndex } from './functions'; import { createContainer } from './di.container'; import { Constructable, IDisposable } from './interfaces'; import { getAnnotationKeyFor, ResourceType } from './resource'; import { defineMetadata, getMetadata, isFunction, isString } from './utilities'; import { singletonRegistration, cacheCallbackResult, transientRegistation } from './di.registration'; import { ErrorNames, createMappedError } from './errors'; import type { IAllResolver, ICallableResolver, IFactoryResolver, ILazyResolver, INewInstanceResolver, IOptionalResolver, IResolvedFactory, IResolvedLazy } from './di.resolvers'; export type ResolveCallback = (handler: IContainer, requestor: IContainer, resolver: IResolver) => T; export interface InterfaceSymbol { // We can activate decorator if the argument decorator proposal will be standardized by TC39 (https://github.com/tc39/proposal-class-method-parameter-decorators) // (target: Injectable | AbstractInjectable, property: string | symbol | undefined, index?: number): void; $isInterface: boolean; friendlyName?: string; register?(container: IContainer, key?: K): IResolver; toString?(): string; } // This interface exists only to break a circular type referencing issue in the IServiceLocator interface. // Otherwise IServiceLocator references IResolver, which references IContainer, which extends IServiceLocator. interface IResolverLike { readonly $isResolver: true; resolve(handler: C, requestor: C): Resolved; getFactory? : IFactory>(container: C): T | null; } export interface IResolver extends IResolverLike, Partial { } export interface IDisposableResolver extends IResolver { dispose(): void; } export interface IRegistration extends IResolver { register(container: IContainer, key?: Key): IResolver; } export type Transformer = (instance: Resolved) => Resolved; export interface IFactory { readonly Type: T; registerTransformer(transformer: Transformer): void; construct(container: IContainer, dynamicDependencies?: unknown[]): Resolved; } export interface IServiceLocator { readonly root: IServiceLocator; has(key: K | Key, searchAncestors: boolean): boolean; get(key: IAllResolver): Resolved[]; get(key: INewInstanceResolver): Resolved; get(key: ILazyResolver): IResolvedLazy; get(key: IOptionalResolver): Resolved | undefined; get(key: IFactoryResolver): IResolvedFactory; get(key: ICallableResolver): Resolved; get(key: IResolver): Resolved; get(key: K): Resolved; get(key: Key): Resolved; get(key: K | Key): Resolved; getAll(key: K, searchAncestors?: boolean): Resolved[]; getAll(key: Key, searchAncestors?: boolean): Resolved[]; getAll(key: K | Key, searchAncestors?: boolean): Resolved[]; } export interface IRegistry { register(container: IContainer, ...params: unknown[]): void | IResolver | IContainer; } export interface IContainer extends IServiceLocator, IDisposable { readonly id: number; readonly root: IContainer; readonly parent: IContainer | null; register(...params: any[]): IContainer; registerResolver>(key: K, resolver: T, isDisposable?: boolean): T; // deregisterResolverFor(key: K, searchAncestors: boolean): void; registerTransformer(key: K, transformer: Transformer): boolean; getResolver(key: K | Key, autoRegister?: boolean): IResolver | null; registerFactory(key: Key, factory: IFactory): void; invoke(key: Constructable, dynamicDependencies?: TDeps): T; // eslint-disable-next-line @typescript-eslint/no-unused-vars hasFactory(key: any): boolean; getFactory(key: T): IFactory; createChild(config?: IContainerConfiguration): IContainer; disposeResolvers(): void; deregister(key: Key): void; /** * Register resources from another container, an API for manually registering resources * * This is a semi private API, apps should avoid using it directly */ useResources(container: IContainer): void; find(kind: string, name: string): TResType | null; find(key: string): TResType | null; } export class ResolverBuilder { public constructor( /** @internal */ private _container: IContainer, /** @internal */ private _key: Key, ) {} public instance(value: K): IResolver { return this._registerResolver(ResolverStrategy.instance, value); } public singleton(value: Constructable): IResolver { return this._registerResolver(ResolverStrategy.singleton, value); } public transient(value: Constructable): IResolver { return this._registerResolver(ResolverStrategy.transient, value); } public callback(value: ResolveCallback): IResolver { return this._registerResolver(ResolverStrategy.callback, value); } public cachedCallback(value: ResolveCallback): IResolver { return this._registerResolver(ResolverStrategy.callback, cacheCallbackResult(value)); } public aliasTo(destinationKey: Key): IResolver { return this._registerResolver(ResolverStrategy.alias, destinationKey); } /** @internal */ private _registerResolver(strategy: ResolverStrategy, state: unknown): IResolver { const { _container: container, _key: key } = this; this._container = this._key = (void 0)!; return container.registerResolver(key, new Resolver(key, strategy, state)) as IResolver; } } export type RegisterSelf = { register(container: IContainer): IResolver>; registerInRequestor: boolean; }; export type Key = PropertyKey | object | InterfaceSymbol | Constructable | IResolver; export type Resolved = ( K extends InterfaceSymbol ? T : K extends Constructable ? InstanceType : K extends IResolverLike ? T1 extends Constructable ? InstanceType : T1 : K ); export type Injectable = Constructable & { inject?: Key[] }; export type AbstractInjectable = (abstract new (...args: any[]) => T) & { inject?: Key[] }; const cloneArrayWithPossibleProps = (source: readonly T[]): T[] => { const clone = source.slice(); const keys = Object.keys(source); const len = keys.length; let key: string; for (let i = 0; i < len; ++i) { key = keys[i]; if (!isArrayIndex(key)) { clone[key] = source[key]; } } return clone; }; export interface IContainerConfiguration { /** * If `true`, `createChild` will inherit the resource resolvers from its parent container * instead of only from the root container. * * Setting this flag will not implicitly perpetuate it in the child container hierarchy. * It must be explicitly set on each call to `createChild`. */ inheritParentResources?: boolean; defaultResolver?(key: Key, handler: IContainer): IResolver; } const diParamTypesKeys = getAnnotationKeyFor('di:paramtypes'); const getAnnotationParamtypes = (Type: Constructable | Injectable): readonly Key[] | undefined => { return getMetadata(diParamTypesKeys, Type); }; const getDesignParamtypes = (Type: Constructable | Injectable): readonly Key[] | undefined => getMetadata('design:paramtypes', Type); const getOrCreateAnnotationParamTypes = (context: DecoratorContext): Key[] => { return (context.metadata[diParamTypesKeys] ??= []) as Key[]; }; /** @internal */ export const getDependencies = (Type: Constructable | Injectable): Key[] => { // Note: Every detail of this getDependencies method is pretty deliberate at the moment, and probably not yet 100% tested from every possible angle, // so be careful with making changes here as it can have a huge impact on complex end user apps. // Preferably, only make changes to the dependency resolution process via a RFC. const key = getAnnotationKeyFor('di:dependencies'); let dependencies = getMetadata(key, Type); if (dependencies === void 0) { // Type.length is the number of constructor parameters. If this is 0, it could mean the class has an empty constructor // but it could also mean the class has no constructor at all (in which case it inherits the constructor from the prototype). // Non-zero constructor length + no paramtypes means emitDecoratorMetadata is off, or the class has no decorator. // We're not doing anything with the above right now, but it's good to keep in mind for any future issues. const inject = (Type as Injectable).inject; if (inject === void 0) { // design:paramtypes is set by tsc when emitDecoratorMetadata is enabled. const designParamtypes = getDesignParamtypes(Type); // au:annotation:di:paramtypes is set by the parameter decorator from DI.createInterface or by @inject const annotationParamtypes = getAnnotationParamtypes(Type); if (designParamtypes === void 0) { if (annotationParamtypes === void 0) { // Only go up the prototype if neither static inject nor any of the paramtypes is defined, as // there is no sound way to merge a type's deps with its prototype's deps const Proto = Object.getPrototypeOf(Type); if (isFunction(Proto) && Proto !== Function.prototype) { dependencies = cloneArrayWithPossibleProps(getDependencies(Proto)); } else { dependencies = []; } } else { // No design:paramtypes so just use the au:annotation:di:paramtypes dependencies = cloneArrayWithPossibleProps(annotationParamtypes); } } else if (annotationParamtypes === void 0) { // No au:annotation:di:paramtypes so just use the design:paramtypes dependencies = cloneArrayWithPossibleProps(designParamtypes); } else { // We've got both, so merge them (in case of conflict on same index, au:annotation:di:paramtypes take precedence) dependencies = cloneArrayWithPossibleProps(designParamtypes); let len = annotationParamtypes.length; let auAnnotationParamtype: Key; let i = 0; for (; i < len; ++i) { auAnnotationParamtype = annotationParamtypes[i]; if (auAnnotationParamtype !== void 0) { dependencies[i] = auAnnotationParamtype; } } const keys = Object.keys(annotationParamtypes); let key: string; i = 0; len = keys.length; for (i = 0; i < len; ++i) { key = keys[i]; if (!isArrayIndex(key)) { dependencies[key] = annotationParamtypes[key]; } } } } else { // Ignore paramtypes if we have static inject dependencies = cloneArrayWithPossibleProps(inject); } defineMetadata(dependencies, Type, key); } return dependencies; }; /** * @internal * * @param configureOrName - Use for improving error messaging */ export const createInterface = (configureOrName?: string | ((builder: ResolverBuilder) => IResolver), configuror?: (builder: ResolverBuilder) => IResolver): InterfaceSymbol => { const configure = isFunction(configureOrName) ? configureOrName : configuror; const friendlyName = (isString(configureOrName) ? configureOrName : undefined) ?? '(anonymous)'; const Interface = { // Old code kept with the hope that the argument decorator proposal will be standardized by TC39 (https://github.com/tc39/proposal-class-method-parameter-decorators) // function(_target: Injectable | AbstractInjectable, _property: string | symbol | undefined, _index: number | undefined): void { // if (target == null || new.target !== undefined) { // throw createMappedError(ErrorNames.no_registration_for_interface, friendlyName); // } // const annotationParamtypes = getOrCreateAnnotationParamTypes(target as Injectable); // annotationParamtypes[index!] = Interface; // }, $isInterface: true, friendlyName: friendlyName, toString: (): string => `InterfaceSymbol<${friendlyName}>`, register: configure != null ? (container: IContainer, key?: Key): IResolver => configure(new ResolverBuilder(container, key ?? Interface)) : void 0, }; return Interface; }; export const inject = (...dependencies: Key[]): (decorated: unknown, context: DecoratorContext) => void => { return (decorated: unknown, context: DecoratorContext): void => { switch (context.kind) { case 'class': { const annotationParamtypes = getOrCreateAnnotationParamTypes(context); let dep: Key; let i = 0; for (; i < dependencies.length; ++i) { dep = dependencies[i]; if (dep !== void 0) { annotationParamtypes[i] = dep; } } break; } case 'field': { const annotationParamtypes: any = getOrCreateAnnotationParamTypes(context); const dep = dependencies[0]; if (dep !== void 0) { annotationParamtypes[context.name] = dep; } break; } // TODO(sayan): support getter injection - new feature // TODO: // support method parameter injection when the class-method-parameter-decorators proposal (https://github.com/tc39/proposal-class-method-parameter-decorators) // reaches stage 4 and/or implemented by TS. default: throw createMappedError(ErrorNames.invalid_inject_decorator_usage, String(context.name), context.kind); } }; }; export const DI = /*@__PURE__*/ (() => { // putting this function inside this IIFE as we wants to call it without triggering side effect initializeTC39Metadata(); return { createContainer, getDesignParamtypes, // getAnnotationParamtypes, // getOrCreateAnnotationParamTypes, getDependencies: getDependencies, /** * creates a decorator that also matches an interface and can be used as a {@linkcode Key}. * ```ts * const ILogger = DI.createInterface('Logger'); * container.register(Registration.singleton(ILogger, getSomeLogger())); * const log = container.get(ILogger); * log.info('hello world'); * class Foo { * constructor( @ILogger log: ILogger ) { * log.info('hello world'); * } * } * ``` * you can also build default registrations into your interface. * ```ts * export const ILogger = DI.createInterface('Logger', builder => builder.cachedCallback(LoggerDefault)); * const log = container.get(ILogger); * log.info('hello world'); * class Foo { * constructor( @ILogger log: ILogger ) { * log.info('hello world'); * } * } * ``` * but these default registrations won't work the same with other decorators that take keys, for example * ```ts * export const MyStr = DI.createInterface('MyStr', builder => builder.instance('somestring')); * class Foo { * constructor( @optional(MyStr) public readonly str: string ) { * } * } * container.get(Foo).str; // returns undefined * ``` * to fix this add this line somewhere before you do a `get` * ```ts * container.register(MyStr); * container.get(Foo).str; // returns 'somestring' * ``` * * - @param configureOrName - supply a string to improve error messaging */ createInterface, inject, /** * Registers the `target` class as a transient dependency; each time the dependency is resolved * a new instance will be created. * * @param target - The class / constructor function to register as transient. * @returns The same class, with a static `register` method that takes a container and returns the appropriate resolver. * * @example ```ts * // On an existing class * class Foo { } * DI.transient(Foo); * * // Inline declaration * const Foo = DI.transient(class { }); * // Foo is now strongly typed with register * Foo.register(container); * ``` */ transient(target: T & Partial>): T & RegisterSelf { target.register = function (container: IContainer): IResolver> { const registration = transientRegistation(target as T, target as T); return registration.register(container, target); }; target.registerInRequestor = false; return target as T & RegisterSelf; }, /** * Registers the `target` class as a singleton dependency; the class will only be created once. Each * consecutive time the dependency is resolved, the same instance will be returned. * * @param target - The class / constructor function to register as a singleton. * @returns The same class, with a static `register` method that takes a container and returns the appropriate resolver. * @example ```ts * // On an existing class * class Foo { } * DI.singleton(Foo); * * // Inline declaration * const Foo = DI.singleton(class { }); * // Foo is now strongly typed with register * Foo.register(container); * ``` */ singleton(target: T & Partial>, options: SingletonOptions = defaultSingletonOptions): T & RegisterSelf { target.register = function (container: IContainer): IResolver> { const registration = singletonRegistration(target, target); return registration.register(container, target); }; target.registerInRequestor = options.scoped; return target as T & RegisterSelf; }, }; })(); export const IContainer = /*@__PURE__*/createInterface('IContainer'); export const IServiceLocator = IContainer as unknown as InterfaceSymbol; function transientDecorator(target: T & Partial>, context: ClassDecoratorContext): T & RegisterSelf { return DI.transient(target); } /** * Registers the decorated class as a transient dependency; each time the dependency is resolved * a new instance will be created. * * @example ```ts * @transient() * class Foo { } * ``` */ // eslint-disable-next-line @typescript-eslint/no-unused-vars export function transient(): typeof transientDecorator; /** * Registers the `target` class as a transient dependency; each time the dependency is resolved * a new instance will be created. * * @param target - The class / constructor function to register as transient. * * @example ```ts * @transient() * class Foo { } * ``` */ export function transient(target: T & Partial>, context: ClassDecoratorContext): T & RegisterSelf; export function transient(target?: T & Partial>, context?: ClassDecoratorContext): T & RegisterSelf | typeof transientDecorator { return target == null ? transientDecorator : transientDecorator(target, context!); } type SingletonOptions = { scoped: boolean }; const defaultSingletonOptions = { scoped: false }; const decorateSingleton = DI.singleton; type SingletonDecorator = (target: T & Partial>, context: ClassDecoratorContext) => T & RegisterSelf; /** * Registers the decorated class as a singleton dependency; the class will only be created once. Each * consecutive time the dependency is resolved, the same instance will be returned. * * @example ```ts * @singleton() * class Foo { } * ``` */ // eslint-disable-next-line @typescript-eslint/no-unused-vars export function singleton(): SingletonDecorator; // eslint-disable-next-line @typescript-eslint/no-unused-vars export function singleton(options?: SingletonOptions): SingletonDecorator; /** * Registers the `target` class as a singleton dependency; the class will only be created once. Each * consecutive time the dependency is resolved, the same instance will be returned. * * @param target - The class / constructor function to register as a singleton. * * @example ```ts * @singleton() * class Foo { } * ``` */ export function singleton(target: T & Partial>, context: ClassDecoratorContext): T & RegisterSelf; export function singleton(targetOrOptions?: (T & Partial>) | SingletonOptions, _context?: ClassDecoratorContext): T & RegisterSelf | SingletonDecorator { return isFunction(targetOrOptions) // The decorator is applied without options. Example: `@singleton()` or `@singleton` ? decorateSingleton(targetOrOptions) : function ($target: T, _ctx: ClassDecoratorContext) { return decorateSingleton($target, targetOrOptions); }; } _START_CONST_ENUM(); /** @internal */ export const enum ResolverStrategy { instance = 0, singleton = 1, transient = 2, callback = 3, array = 4, alias = 5, } _END_CONST_ENUM(); /** @internal */ export class Resolver implements IResolver { /** @internal */ public _key: Key; /** @internal */ public _strategy: ResolverStrategy; /** @internal */ public _state: any; public get $isResolver(): true { return true; } /** @internal */ private _resolving: boolean = false; public constructor( key: K, strategy: ResolverStrategy, state: any, ) { this._key = key; this._strategy = strategy; this._state = state; } /** * When resolving a singleton, the internal state is changed, * so cache the original constructable factory for future requests * @internal */ private _cachedFactory: IFactory | null = null; public register(container: IContainer, key?: Key): IResolver { return container.registerResolver(key || this._key, this as IResolver); } public resolve(handler: IContainer, requestor: IContainer): any { switch (this._strategy) { case ResolverStrategy.instance: return this._state; case ResolverStrategy.singleton: { if (this._resolving) { throw createMappedError(ErrorNames.cyclic_dependency, this._state.name); } this._resolving = true; this._state = (this._cachedFactory = handler.getFactory(this._state as Constructable)).construct(requestor); this._strategy = ResolverStrategy.instance; this._resolving = false; return this._state; } case ResolverStrategy.transient: { // Always create transients from the requesting container const factory = handler.getFactory(this._state as Constructable); if (factory === null) { throw createMappedError(ErrorNames.no_factory, this._key); } return factory.construct(requestor); } case ResolverStrategy.callback: return (this._state as ResolveCallback)(handler, requestor, this as IResolver); case ResolverStrategy.array: return (this._state as IResolver[])[0].resolve(handler, requestor); case ResolverStrategy.alias: return requestor.get(this._state); default: throw createMappedError(ErrorNames.invalid_resolver_strategy, this._strategy); } } public getFactory : IFactory>(container: IContainer): T | null { switch (this._strategy) { case ResolverStrategy.singleton: case ResolverStrategy.transient: return container.getFactory(this._state as Constructable) as T; case ResolverStrategy.alias: return container.getResolver(this._state)?.getFactory?.(container) ?? null; case ResolverStrategy.instance: return this._cachedFactory as T; default: return null; } } } /** @internal */ export interface IInvoker { invoke(container: IContainer, fn: T, dependencies: Key[]): Resolved; invokeWithDynamicDependencies( container: IContainer, fn: T, staticDependencies: Key[], dynamicDependencies: Key[] ): Resolved; } export class InstanceProvider implements IDisposableResolver { /** @internal */ private _instance: Resolved | null; /** @internal */ private readonly _name?: string; /** @internal */ private readonly _Type: Constructable | null; public get friendlyName() { return this._name; } public constructor( name?: string, /** * if not undefined, then this is the value this provider will resolve to * until overridden by explicit prepare call */ instance: Resolved | null = null, Type: Constructable | null = null, ) { this._name = name; this._instance = instance; this._Type = Type; } public prepare(instance: Resolved): void { this._instance = instance; } public get $isResolver(): true {return true;} public resolve(): Resolved { if (this._instance == null) { throw createMappedError(ErrorNames.no_instance_provided, this._name); } return this._instance; } public getFactory : IFactory>(container: IContainer): T | null { return this._Type == null ? null : container.getFactory(this._Type) as T; } public dispose(): void { this._instance = null; } } /** * An implementation of IRegistry that delegates registration to a * separately registered class. The ParameterizedRegistry facilitates the * passing of parameters to the final registry. */ export class ParameterizedRegistry implements IRegistry { public constructor( private readonly key: Key, private readonly params: unknown[] ) {} public register(container: IContainer): void { if (container.has(this.key, true)) { const registry = container.get(this.key); registry.register(container, ...this.params); } else { container.register(...this.params.filter(x => typeof x === 'object')); } } }