@akala/core
Version:
230 lines (210 loc) • 9.03 kB
text/typescript
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);
}
}