first commit
Security: Sync from Public / sync-from-public (push) Has been cancelled
Test: Benchmark Nightly / build (push) Has been cancelled
Test: Benchmark Nightly / Notify Cats on failure (push) Has been cancelled
CI: Python / Checks (push) Has been cancelled
Test: Evals Python / Workflow Comparison Python (push) Has been cancelled
Util: Check Docs URLs / check-docs-urls (push) Has been cancelled
Test: Visual Storybook / Cloudflare Pages (push) Has been cancelled
Test: E2E Performance / build-and-test-performance (push) Has been cancelled
Test: Workflows Nightly / Run Workflow Tests (push) Has been cancelled
Util: Cleanup CI Docker Images / Delete stale CI images (push) Has been cancelled
Test: Benchmark Destroy Env / build (push) Has been cancelled
Util: Update Node Popularity / update-popularity (push) Has been cancelled
Test: E2E Coverage Weekly / Coverage Tests (push) Has been cancelled

This commit is contained in:
2026-03-17 16:22:57 +03:30
commit 3d5eaf9445
15349 changed files with 2847338 additions and 0 deletions
@@ -0,0 +1,17 @@
import { Container } from '../di';
import { ServiceA } from './fixtures/service-a';
import { ServiceB } from './fixtures/service-b';
describe('DI Container', () => {
describe('circular dependency', () => {
it('should detect multilevel circular dependencies', () => {
expect(() => Container.get(ServiceA)).toThrow(
'[DI] Circular dependency detected in ServiceB at index 0.\nServiceA -> ServiceB',
);
expect(() => Container.get(ServiceB)).toThrow(
'[DI] Circular dependency detected in ServiceB at index 0.\nServiceB',
);
});
});
});
+300
View File
@@ -0,0 +1,300 @@
import { Container, Service } from '../di';
@Service()
class SimpleService {
getValue() {
return 'simple';
}
}
@Service()
class DependentService {
constructor(readonly simple: SimpleService) {}
getValue() {
return this.simple.getValue() + '-dependent';
}
}
class CustomFactory {
getValue() {
return 'factory-made';
}
}
@Service({ factory: () => new CustomFactory() })
class FactoryService {
getValue() {
return 'should-not-be-called';
}
}
abstract class AbstractService {
abstract getValue(): string;
}
@Service()
class ConcreteService extends AbstractService {
getValue(): string {
return 'concrete';
}
}
describe('DI Container', () => {
beforeEach(() => {
jest.clearAllMocks();
Container.reset();
});
describe('basic functionality', () => {
it('should create a simple instance', () => {
const instance = Container.get(SimpleService);
expect(instance).toBeInstanceOf(SimpleService);
expect(instance.getValue()).toBe('simple');
});
it('should return same instance on multiple gets', () => {
const instance1 = Container.get(SimpleService);
const instance2 = Container.get(SimpleService);
expect(instance1).toBe(instance2);
});
it('should handle classes with no dependencies (empty constructor)', () => {
@Service()
class EmptyConstructorService {}
const instance = Container.get(EmptyConstructorService);
expect(instance).toBeInstanceOf(EmptyConstructorService);
});
it('should throw when trying to resolve an undecorated class', () => {
class UnDecoratedService {}
expect(() => Container.get(UnDecoratedService)).toThrow();
});
});
describe('dependency injection', () => {
it('should inject dependencies correctly', () => {
const dependent = Container.get(DependentService);
expect(dependent).toBeInstanceOf(DependentService);
expect(dependent.getValue()).toBe('simple-dependent');
expect(dependent.simple).toBeInstanceOf(SimpleService);
});
it('should handle deep dependency chains', () => {
@Service()
class ServiceC {
getValue() {
return 'C';
}
}
@Service()
class ServiceB {
constructor(private c: ServiceC) {}
getValue() {
return this.c.getValue() + 'B';
}
}
@Service()
class ServiceA {
constructor(private b: ServiceB) {}
getValue() {
return this.b.getValue() + 'A';
}
}
const instance = Container.get(ServiceA);
expect(instance.getValue()).toBe('CBA');
});
it('should return undefined for non-decorated dependencies in resolution chain', () => {
class NonDecoratedDep {}
@Service()
class ServiceWithNonDecoratedDep {
constructor(readonly dep: NonDecoratedDep) {}
}
const instance = Container.get(ServiceWithNonDecoratedDep);
expect(instance).toBeInstanceOf(ServiceWithNonDecoratedDep);
expect(instance.dep).toBeUndefined();
});
});
describe('factory handling', () => {
it('should use factory when provided', () => {
const instance = Container.get(FactoryService);
expect(instance).toBeInstanceOf(CustomFactory);
expect(instance.getValue()).toBe('factory-made');
});
it('should preserve factory metadata when setting instance', () => {
const customInstance = new CustomFactory();
Container.set(FactoryService, customInstance);
const instance = Container.get(FactoryService);
expect(instance).toBe(customInstance);
});
it('should preserve factory when resetting container', () => {
const factoryInstance1 = Container.get(FactoryService);
Container.reset();
const factoryInstance2 = Container.get(FactoryService);
expect(factoryInstance1).not.toBe(factoryInstance2);
expect(factoryInstance2.getValue()).toBe('factory-made');
});
it('should throw error when factory throws', () => {
@Service({
factory: () => {
throw new Error('Factory error');
},
})
class ErrorFactoryService {}
expect(() => Container.get(ErrorFactoryService)).toThrow('Factory error');
});
it('should handle factory with dependencies', () => {
const factory = jest.fn().mockReturnValue({});
@Service({ factory })
class FactoryWithDependencies {
constructor(readonly simpleService: SimpleService) {}
}
const instance = Container.get(FactoryWithDependencies);
expect(instance.simpleService).toBeUndefined();
expect(factory).toHaveBeenCalledWith(Container.get(SimpleService));
});
});
describe('instance management', () => {
it('should allow manual instance setting', () => {
const customInstance = new SimpleService();
Container.set(SimpleService, customInstance);
const instance = Container.get(SimpleService);
expect(instance).toBe(customInstance);
});
});
describe('abstract classes', () => {
it('should throw when trying to instantiate an abstract class directly', () => {
@Service()
abstract class TestAbstractClass {
abstract doSomething(): void;
// Add a concrete method to make the class truly abstract at runtime
constructor() {
if (this.constructor === TestAbstractClass) {
throw new TypeError('Abstract class "TestAbstractClass" cannot be instantiated');
}
}
}
expect(() => Container.get(TestAbstractClass)).toThrow(
'[DI] TestAbstractClass is an abstract class, and cannot be instantiated',
);
});
it('should allow setting an implementation for an abstract class', () => {
const concrete = new ConcreteService();
Container.set(AbstractService, concrete);
const instance = Container.get(AbstractService);
expect(instance).toBe(concrete);
expect(instance.getValue()).toBe('concrete');
});
it('should allow factory for abstract class', () => {
@Service({ factory: () => new ConcreteService() })
abstract class FactoryAbstractService {
abstract getValue(): string;
}
const instance = Container.get(FactoryAbstractService);
expect(instance).toBeInstanceOf(ConcreteService);
expect(instance.getValue()).toBe('concrete');
});
});
describe('inheritance', () => {
it('should handle inheritance in injectable classes', () => {
@Service()
class BaseService {
getValue() {
return 'base';
}
}
@Service()
class DerivedService extends BaseService {
getValue() {
return 'derived-' + super.getValue();
}
}
const instance = Container.get(DerivedService);
expect(instance.getValue()).toBe('derived-base');
});
it('should maintain separate instances for base and derived classes', () => {
@Service()
class BaseService {
getValue() {
return 'base';
}
}
@Service()
class DerivedService extends BaseService {}
const baseInstance = Container.get(BaseService);
const derivedInstance = Container.get(DerivedService);
expect(baseInstance).not.toBe(derivedInstance);
expect(baseInstance).toBeInstanceOf(BaseService);
expect(derivedInstance).toBeInstanceOf(DerivedService);
});
});
describe('type registration checking', () => {
it('should return true for registered classes', () => {
expect(Container.has(SimpleService)).toBe(true);
});
it('should return false for unregistered classes', () => {
class UnregisteredService {}
expect(Container.has(UnregisteredService)).toBe(false);
});
it('should return true for abstract classes with implementations', () => {
const concrete = new ConcreteService();
Container.set(AbstractService, concrete);
expect(Container.has(AbstractService)).toBe(true);
});
it('should return true for factory-provided services before instantiation', () => {
expect(Container.has(FactoryService)).toBe(true);
});
it('should maintain registration after reset', () => {
expect(Container.has(SimpleService)).toBe(true);
Container.reset();
expect(Container.has(SimpleService)).toBe(true);
});
it('should return true after manual instance setting', () => {
class ManualService {}
expect(Container.has(ManualService)).toBe(false);
Container.set(ManualService, new ManualService());
expect(Container.has(ManualService)).toBe(true);
});
});
});
@@ -0,0 +1,8 @@
// eslint-disable-next-line import-x/no-cycle
import { ServiceB } from './service-b';
import { Service } from '../../di';
@Service()
export class ServiceA {
constructor(readonly b: ServiceB) {}
}
@@ -0,0 +1,8 @@
// eslint-disable-next-line import-x/no-cycle
import { ServiceA } from './service-a';
import { Service } from '../../di';
@Service()
export class ServiceB {
constructor(readonly a: ServiceA) {}
}
+143
View File
@@ -0,0 +1,143 @@
import 'reflect-metadata';
/**
* Represents a class constructor type that can be instantiated with 'new'
* @template T The type of instance the constructor creates
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type Constructable<T = unknown> = new (...args: any[]) => T;
type AbstractConstructable<T = unknown> = abstract new (...args: unknown[]) => T;
type ServiceIdentifier<T = unknown> = Constructable<T> | AbstractConstructable<T>;
type Factory<T = unknown> = (...args: unknown[]) => T;
interface Metadata<T = unknown> {
instance?: T;
factory?: Factory<T>;
}
interface Options<T> {
factory?: Factory<T>;
}
const instances = new Map<ServiceIdentifier, Metadata>();
/**
* Decorator that marks a class as available for dependency injection.
* @param options Configuration options for the injectable class
* @param options.factory Optional factory function to create instances of this class
* @returns A class decorator to be applied to the target class
*/
// eslint-disable-next-line @typescript-eslint/no-restricted-types
export function Service<T = unknown>(): Function;
// eslint-disable-next-line @typescript-eslint/no-restricted-types
export function Service<T = unknown>(options: Options<T>): Function;
export function Service<T>({ factory }: Options<T> = {}) {
return function (target: Constructable<T>) {
instances.set(target, { factory });
return target;
};
}
class DIError extends Error {
constructor(message: string) {
super(`[DI] ${message}`);
}
}
class ContainerClass {
/** Stack to track types being resolved to detect circular dependencies */
private readonly resolutionStack: ServiceIdentifier[] = [];
/**
* Checks if a type is registered in the container
* @template T The type to check for
* @param type The constructor of the type to check
* @returns True if the type is registered (has metadata), false otherwise
*/
has<T>(type: ServiceIdentifier<T>): boolean {
return instances.has(type);
}
/**
* Retrieves or creates an instance of the specified type from the container
* @template T The type of instance to retrieve
* @param type The constructor of the type to retrieve
* @returns An instance of the specified type with all dependencies injected
* @throws {DIError} If circular dependencies are detected or if the type is not injectable
*/
get<T>(type: ServiceIdentifier<T>): T {
const { resolutionStack } = this;
const metadata = instances.get(type) as Metadata<T>;
if (!metadata) {
// Special case: Allow undefined returns for non-decorated constructor params
// when resolving a dependency chain (i.e., resolutionStack not empty)
if (resolutionStack.length) return undefined as T;
throw new DIError(`${type.name} is not decorated with ${Service.name}`);
}
if (metadata?.instance) return metadata.instance as T;
// Add current type to resolution stack before resolving dependencies
resolutionStack.push(type);
try {
let instance: T;
const paramTypes = (Reflect.getMetadata('design:paramtypes', type) ?? []) as Constructable[];
const dependencies = paramTypes.map(<P>(paramType: Constructable<P>, index: number) => {
if (paramType === undefined) {
throw new DIError(
`Circular dependency detected in ${type.name} at index ${index}.\n${resolutionStack.map((t) => t.name).join(' -> ')}\n`,
);
}
return this.get(paramType);
});
if (metadata?.factory) {
instance = metadata.factory(...dependencies);
} else {
// Create new instance with resolved dependencies
instance = new (type as Constructable)(...dependencies) as T;
}
instances.set(type, { ...metadata, instance });
return instance;
} catch (error) {
if (error instanceof TypeError && error.message.toLowerCase().includes('abstract')) {
throw new DIError(`${type.name} is an abstract class, and cannot be instantiated`);
}
throw error;
} finally {
resolutionStack.pop();
}
}
/**
* Manually sets an instance for a specific type in the container
* @template T The type of instance being set
* @param type The constructor of the type to set. This can also be an abstract class
* @param instance The instance to store in the container
*/
set<T>(type: ServiceIdentifier<T>, instance: T): void {
// Preserve any existing metadata (like factory) when setting new instance
const metadata = instances.get(type) ?? {};
instances.set(type, { ...metadata, instance });
}
/** Clears all instantiated instances from the container while preserving type registrations */
reset(): void {
for (const metadata of instances.values()) {
delete metadata.instance;
}
}
}
/**
* Global dependency injection container instance
* Used to retrieve and manage class instances and their dependencies
*/
export const Container = new ContainerClass();