UNPKG

@akala/core

Version:
230 lines (210 loc) 9.03 kB
import "reflect-metadata"; import { SimpleInjector } from './simple-injector.js'; import type { InjectedParameter } from "./shared.js"; export type PropertyInjection = ((i: SimpleInjector) => void); export type ParameterInjection = ((i: SimpleInjector) => InjectedParameter<unknown>); export const injectSymbol = Symbol('inject'); export const afterInjectSymbol = Symbol('after-inject'); export interface InjectableObject { [injectSymbol]: ((i: SimpleInjector) => void)[]; } /** * Decorator to mark class properties or constructor parameters for dependency injection. * * @param name - Optional dependency name. If omitted, uses the property name as the dependency key. * @returns Decorator function to apply injection metadata. */ export function inject(names: string[]): <T>(target: T, propertyKey?: keyof T) => void export function inject(name?: string): (target: object | (new (...args: unknown[]) => unknown), propertyKey: string, parameterIndex?: number) => void export function inject(name?: string | string[]) { if (Array.isArray(name)) return function (target: (new (...args: unknown[]) => unknown) | object, propertyKey: string) { name.forEach((name, i) => inject(name)(target, propertyKey, i)); } return function (target: object | (new (...args: unknown[]) => unknown), propertyKey: string, parameterIndex?: number) { if (typeof parameterIndex == 'number') { if (!name) throw new Error('name is required as parameter names are not available in reflection'); const injections: { [key: string]: (PropertyInjection | ParameterInjection)[] } = Reflect.getOwnMetadata(injectSymbol, target) || { [propertyKey]: [] }; if (!injections[propertyKey]) injections[propertyKey] = []; injections[propertyKey].push(function (injector: SimpleInjector) { const resolved = injector.resolve(name); return { index: parameterIndex, value: resolved }; }); if (propertyKey) Reflect.defineMetadata(injectSymbol, injections[propertyKey], target[propertyKey]); Reflect.defineMetadata(injectSymbol, injections, target) } else { const injections: { [key: string]: (PropertyInjection | ParameterInjection)[] } = Reflect.getOwnMetadata(injectSymbol, target) || { [propertyKey]: [] }; if (!injections[propertyKey]) injections[propertyKey] = []; injections[propertyKey].push(function (injector: SimpleInjector) { this[propertyKey] = injector.resolve(name || propertyKey); }); Reflect.defineMetadata(injectSymbol, injections, target) } } } /** * Processes dependency injection metadata for an object and its prototype chain. * * @param {SimpleInjector} injector - The injector to resolve dependencies. * @param {object} obj - Target object to apply injections to. * @param {object} [prototype] - Optional prototype to traverse (used internally). */ export function applyInjector(injector: SimpleInjector, obj: object, prototype?: object) { const injections: { [key: string]: (PropertyInjection | ParameterInjection)[] } = Reflect.getOwnMetadata(injectSymbol, prototype || obj); // if (injections && injections.length) // injections.forEach(f => f(injector)); if (prototype !== Object.prototype) applyInjector(injector, obj, Reflect.getPrototypeOf(prototype || obj)); for (const property in injections) { if (property?.length) { let descriptor = Reflect.getOwnPropertyDescriptor(obj, property); if (!descriptor && prototype) { descriptor = Reflect.getOwnPropertyDescriptor(prototype, property); if (descriptor && typeof descriptor.value !== 'function') descriptor = null; } if (!descriptor) { let valueSet = false; let value: unknown; Reflect.defineProperty(obj, property, { get() { if (valueSet) return value; injections[property].forEach(i => i.call(obj, injector)); return obj[property]; }, set(v) { value = v; valueSet = true; } }) } else if (descriptor.value) { const oldFunction = descriptor.value; Object.defineProperty(obj, property, { value: function injected(...args: unknown[]) { const mergedArgs = SimpleInjector.mergeArrays(injections[property].map(p => (p as ParameterInjection)(injector)), ...args); return oldFunction.apply(this, mergedArgs.args); } }) } else { injections[property].forEach(p => (p as PropertyInjection).call(obj, injector)); } } } } /** * Decorator to make a class injectable with dependency resolution. * * @template TInstance - Type of the class instance. * @template TClass - Type of the class constructor. * @param {TClass} ctor - Class constructor to wrap. * @param {SimpleInjector} injector - Optional injector instance (default uses constructor argument). * @returns {TClass} Injectable class with dependency injection setup. */ export function injectable<TInstance, TClass extends { new(...args: unknown[]): TInstance }>(ctor: TClass, injector?: SimpleInjector): TClass { // eslint-disable-next-line @typescript-eslint/ban-ts-comment //@ts-expect-error const result = class DynamicProxy extends ctor { // eslint-disable-next-line @typescript-eslint/no-explicit-any constructor(...args: any[]) { const injectionObj: { [key: string]: ParameterInjection[] } = Reflect.getOwnMetadata(injectSymbol, ctor); if (injectionObj) var injections = injectionObj['undefined']; if (!injector) { injector = args.shift(); if ((!injector || !(injector instanceof SimpleInjector)) && injections && injections.length) throw new Error(`No injector was provided while it is required to construct ${ctor}`) // if (injector) // Reflect.defineMetadata(injectSymbol, injector, new.target); } let injected = injections && injections.map(f => f(injector)) || []; injected = injected.filter(p => typeof p.index == 'number'); super(...SimpleInjector.mergeArrays(injected, injector, ...args).args) // Reflect.deleteMetadata(injectSymbol, new.target); // Object.setPrototypeOf(this, Object.create(ctor.prototype)); // if (new.target == result) // { applyInjector(injector, this); if (typeof (this[afterInjectSymbol]) != 'undefined') this[afterInjectSymbol](); // } } } // Object.setPrototypeOf(result, Object.create(ctor.prototype)); Object.assign(result, ctor); return result; } export type InjectableClass<T> = T & { new(injector: SimpleInjector): T; }; /** * Creates a class decorator to apply an injector to a class. * * @param {SimpleInjector} injector - Injector instance to bind to the class. * @returns {Function} Class decorator that configures the class for dependency injection. */ export function useInjector(injector: SimpleInjector) { return function classInjectorDecorator<TClass extends { new(...args: unknown[]): object }>(ctor: TClass): TClass { return injectable(ctor, injector); }; } /** * Extends a class with an injector for dependency resolution. * * @template TClass - Type of the class to extend. * @param {SimpleInjector} injector - Injector instance to apply. * @param {TClass} constructor - Class to extend. * @returns {TClass} Extended class with injector configuration. */ export function extendInject<TClass extends { new(...args: unknown[]): object }>(injector: SimpleInjector, constructor: TClass): TClass { return useInjector(injector)<TClass>(constructor); } /** * Reflection-based injector that resolves dependencies using metadata. * * @extends SimpleInjector */ export class ReflectionInjector extends SimpleInjector { /** * Creates a new ReflectionInjector instance. * * @param {SimpleInjector} [parent] - Optional parent injector to delegate unresolved dependencies to. */ constructor(protected parent?: SimpleInjector) { super(parent); } }