@davidcromianski-dev/ant-di
v2.0.1
Published
Simple JavaScript Dependency Injection Container inspired by PHP Pimple with advanced features like auto-wiring and service providers
Maintainers
Readme
Ant DI
Simple JavaScript Dependency Injection Package
This package is a simple dependency injection container for JavaScript and TypeScript. It is inspired by the PHP Pimple package and provides advanced features like wiring and service providers.
Features
- Simple and lightweight - Easy to use dependency injection container
- Auto-wiring - Manual dependency resolution for TypeScript classes with automatic singleton caching
- Singleton behavior - Automatic instance caching ensures classes return the same instance
- Factory support - Create new instances on each request
- Protected callables - Store functions without executing them
- Frozen keys - Prevent modification after first resolution
- Service providers - Modular service registration
- ValueObject management - Isolated dependency management with advanced features
- Circular dependency detection - Built-in prevention of circular dependencies
- Comprehensive error handling - Detailed error classes for better debugging
- Comprehensive testing - Full test coverage with Poku
- Rich examples - Multiple usage patterns and real-world scenarios
Installation
# NPM
npm install @davidcromianski-dev/ant-di
# PNPM
pnpm add @davidcromianski-dev/ant-diDependencies
Runtime Dependencies
This package has zero runtime dependencies, making it lightweight and avoiding dependency conflicts.
Development Dependencies
The following dependencies are used for development, testing, and building:
- poku - Fast test runner for Node.js
- ts-node - TypeScript execution engine for Node.js
- tsx - TypeScript execution engine with esbuild
- typescript - TypeScript compiler
- vite - Build tool and dev server
- vite-plugin-dts - TypeScript declaration file generation for Vite
Jest Compatibility
This package is fully compatible with Jest and other CommonJS-based testing frameworks. The build process generates both ESM and CommonJS outputs:
- ESM:
dist/index.es.js- For modern bundlers and ES modules - CommonJS:
dist/index.cjs.js- For Jest, Node.js, and CommonJS environments
Using with Jest
To use this package with Jest, simply import from the CommonJS build:
// Jest test file
const { Container } = require('@davidcromianski-dev/ant-di');
describe('Container Tests', () => {
it('should create a container instance', () => {
const container = new Container();
expect(container).toBeInstanceOf(Container);
});
});Quick Start
import { Container } from '@davidcromianski-dev/ant-di';
// Create container
const container = new Container();
// Register a service
container.set('database', () => new DatabaseConnection());
// Get the service
const db = container.get('database');
// Auto-wiring example
class UserService {
constructor(private db: DatabaseConnection) {}
}
container.bind(UserService, [DatabaseConnection]);
const userService = container.get(UserService);Container
The container is the main class of the package. It is used to store the services and parameters of the application.
Creating a Container
import { Container } from '@davidcromianski-dev/ant-di';
// Empty container
const container = new Container();
// Container with initial values
const container = new Container({
'app.name': 'My Application',
'app.version': '1.0.0',
});Basic Operations
Registering Services
To register services in the container, you can use either the set method
(recommended) or offsetSet method:
// Simple value
container.set('app.name', 'My Application');
// Factory function (traditional method)
container.set('database', () => new DatabaseConnection());
// Factory function (direct method)
container.set('database', () => new DatabaseConnection(), true);
// Class constructor
container.set('logger', Logger);
// Legacy method (still supported)
container.offsetSet('app.name', 'My Application');[!NOTE] The
setmethod is the recommended way to register services. TheoffsetSetmethod is maintained for backward compatibility.The third parameter
factory(default: false) allows you to directly register a function as a factory without callingcontainer.factory()first. Whenfactory=true, the function will be executed each time the service is requested.
Getting Services
To get services from the container, you can use the get method:
// Get by string key
const appName = container.get('app.name');
// Get by class constructor (auto-wiring)
const logger = container.get(Logger);Container Management
The container provides methods for managing its lifecycle:
// Clear all registered services
container.clear();
// Dispose of the container (calls clear internally)
container.dispose();[!TIP] Use
clear()when you want to reset the container to its initial state, anddispose()when you're completely done with the container instance.
[!NOTE] If the service is a factory, it will be executed every time it is requested.
Version Compatibility
Version 3.0.0+ (Current)
The following methods are the recommended way to interact with the container:
// Service registration
container.set('key', value);
container.set('key', factory, true);
// Service retrieval
container.get('key');
container.get(Constructor);
// Service management
container.has('key');
container.unset('key');Version < 3.0.0 (Legacy)
The following methods are deprecated but still supported for backward compatibility:
// Deprecated methods (still work, but not recommended)
container.offsetSet('key', value);
container.offsetGet('key');
container.offsetExists('key');
container.offsetUnset('key');[!IMPORTANT] Migration Guide: All deprecated methods now internally call their modern equivalents:
offsetSet()→set()offsetGet()→get()offsetExists()→has()offsetUnset()→unset()Your existing code will continue to work, but consider migrating to the new methods for better maintainability.
[!CAUTION] If the service is not in the container, an exception will be thrown.
Checking Service Existence
To check if a service is registered in the container, you can use the has
method:
const exists = container.has('service');Removing Services
To remove services from the container, you can use the unset method:
container.unset('service');Auto-wiring
Ant DI supports manual dependency injection for TypeScript classes. You can register dependencies manually.
Manual Dependency Registration
class UserService {
constructor(
private db: DatabaseConnection,
private logger: Logger,
) {}
}
// Register dependencies manually
container.bind(UserService, [DatabaseConnection, Logger]);
// Get instance with auto-wired dependencies
const userService = container.get(UserService);Dependency Binding by Name
For cases where you need to bind dependencies using class names as strings (useful for dynamic registration or avoiding circular import issues):
// Alternative binding method using class name
container.bind('UserService', [DatabaseConnection, Logger]);
// Both binding methods achieve the same result
const userService = container.get(UserService);[!TIP] The
bind()method accepts both constructor functions and class name strings, making it flexible for various use cases including dynamic class loading or avoiding potential circular import issues in complex applications.
Circular Dependency Detection
class ServiceA {
constructor(public serviceB: ServiceB) {}
}
class ServiceB {
constructor(public serviceA: ServiceA) {}
}
// This will throw a circular dependency error
container.bind(ServiceA, [ServiceB]);
container.bind(ServiceB, [ServiceA]);Factory and Protection
Registering Factories
To register factories in the container, you can use multiple methods:
Method 1: Using the factory method (traditional)
const factory = (c: Container) => new Service();
container.factory(factory);
container.set('service', factory);Method 2: Using set with factory parameter (recommended)
const factory = (c: Container) => new Service();
container.set('service', factory, true); // true = register as factory[!TIP] All three methods are equivalent. The
setmethod withfactory=trueis the recommended approach as it provides a more direct way to register factories without needing to callfactory()first.
[!TIP] Useful for services that need to be created every time they are requested.
Protecting Services
To protect services in the container, you can use the protect method:
const callable = (c: Container) => new Service();
container.protect(callable);
container.set('service', callable);[!TIP] By default, Ant DI assumes that any callable added to the container is a factory for a service, and it will invoke it when the key is accessed. The
protect()method overrides this behavior, allowing you to store the callable itself.
Frozen Keys
Keys become frozen after first resolution of implicit factories:
// This creates an implicit factory
container.set('service', (c: Container) => new Service());
// First access - works fine
const service = container.get('service');
// Second access - throws error (key is frozen)
container.set('service', 'new value'); // Error!Getting Raw Values
To get raw values from the container, you can use the raw method:
const rawValue = container.raw('service');[!TIP] Useful when you need access to the underlying value (such as a closure or callable) itself, rather than the result of its execution.
Getting All Keys
To get all keys registered in the container, you can use the keys method:
const keys = container.keys();Service Providers
Service providers allow you to organize the registration of services in the container.
import { Container, IServiceProvider } from '@davidcromianski-dev/ant-di';
class DatabaseServiceProvider implements IServiceProvider {
register(container: Container) {
container.set('db.host', 'localhost');
container.set('db.port', 5432);
const connectionFactory = (c: Container) => ({
host: c.get('db.host'),
port: c.get('db.port'),
connect: () => `Connected to ${c.get('db.host')}:${c.get('db.port')}`,
});
container.factory(connectionFactory);
container.set('db.connection', connectionFactory);
}
}
// Register the service provider
container.register(new DatabaseServiceProvider());
// Use the services
const connection = container.get('db.connection');
console.log(connection.connect());Singleton Behavior & Instance Caching
Ant DI automatically caches class instances to ensure singleton behavior:
class DatabaseService {
constructor() {
console.log('DatabaseService created');
}
}
// Register the class
container.bind(DatabaseService, []);
// First access - creates instance
const db1 = container.get(DatabaseService); // "DatabaseService created"
// Second access - returns cached instance
const db2 = container.get(DatabaseService); // No log (cached)
console.log(db1 === db2); // true - same instance[!TIP] Class instances are cached by their constructor function, ensuring that the same class always returns the same instance across the application.
ValueObject Management
Ant DI provides advanced ValueObject management for isolated dependency handling:
import { Container, ValueObject } from '@davidcromianski-dev/ant-di';
class DatabaseService {
constructor(public config: any) {}
}
class UserService {
constructor(public db: DatabaseService) {}
}
const container = new Container();
// Create ValueObject with dependencies
const dbValueObject = new ValueObject(
container,
'DatabaseService',
DatabaseService,
);
dbValueObject.addDependency('dbConfig');
// Register configuration
container.set('dbConfig', { host: 'localhost', port: 5432 });
// Register service with ValueObject
container.set('DatabaseService', DatabaseService, false, ['dbConfig']);
// Get ValueObject from container
const valueObject = container.getValueObject('DatabaseService');
// ValueObject management methods
console.log('Has dependency:', valueObject?.hasDependency('dbConfig'));
console.log('Dependencies:', valueObject?.getDependencyKeys());
console.log('Is resolvable:', valueObject?.isResolvable());
// Create instance with dependency injection
const dbInstance = valueObject?.getValue();
console.log('Database instance:', dbInstance instanceof DatabaseService);ValueObject Features
- Dependency Management: Add, remove, and validate dependencies
- Circular Dependency Detection: Automatic detection and prevention
- Instance Creation: Singleton and factory patterns
- Cloning: Create copies of ValueObjects
- Validation: Check if dependencies are resolvable
// Advanced ValueObject usage
const valueObject = new ValueObject(container, 'UserService', UserService);
// Add multiple dependencies
valueObject
.addDependency('DatabaseService')
.addDependency('Logger')
.asSingleton();
// Validate dependencies
if (valueObject.isResolvable()) {
const instance = valueObject.getValue();
console.log('Service created:', instance);
}
// Clone ValueObject
const cloned = valueObject.clone();
console.log('Cloned dependencies:', cloned.getDependencyKeys());
// Clear all dependencies
valueObject.clearDependencies();
console.log(
'Dependencies cleared:',
valueObject.getDependencyKeys().length === 0,
);Proxy Access
The container supports proxy access for convenient property-style access:
// Set value
container.appName = 'My App';
// Get value
console.log(container.appName); // "My App"Testing
The project uses Poku for testing. All tests
are located in the tests/ directory.
Running Tests
# Run all tests
npm test
# Run tests in watch mode
npm run test:watch
# Run tests with coverage
npm run test:coverageTest Structure
Tests are organized into logical groups:
- Basic Operations - Core container functionality
- Factory Operations - Factory and protection features
- Auto-wiring - Dependency injection and resolution
- Frozen Keys - Key freezing behavior
- Proxy Access - Proxy functionality
- Constructor Initialization - Container initialization
- Service Providers - Service provider registration
Example Test
import { describe, it, assert } from 'poku';
import { Container } from '../src';
describe('Container', () => {
describe('Basic Operations', () => {
it('should set and get a value', () => {
const container = new Container();
container.set('key', 'value');
const value = container.get('key');
assert.equal(value, 'value');
});
});
});Examples
Comprehensive examples are available in the examples/ directory:
Basic Usage
npx ts-node examples/basic-usage.tsDemonstrates simple value storage, factory functions, and basic container operations.
Dependency Injection
npx ts-node examples/dependency-injection.tsShows auto-wiring, manual dependency registration, and circular dependency detection.
Factories and Protection
npx ts-node examples/factories-and-protection.tsExplains factory functions, protected callables, and frozen key behavior.
Service Providers
npx ts-node examples/service-providers.tsDemonstrates modular service registration using service providers.
Advanced Patterns
npx ts-node examples/advanced-patterns.tsReal-world scenarios including event systems and complex dependency graphs.
Run All Examples
npx ts-node examples/index.tsAPI Reference
Container Class
Constructor
new Container(values?: Record<string, ValueOrFactoryOrCallable<any>>)Methods
Core Container Operations
set<T>(key: string, value: ValueOrFactoryOrCallable<T>, factory?: boolean): void- Recommended method to register a value, factory, or callable in the container. The optionalfactoryparameter (default: false) allows direct factory registration when set to true.get<T>(key: string | Constructor<T>): T- Recommended method to retrieve a service by key or class constructor with auto-wiringhas(key: string): boolean- Recommended method to check if a key exists in the containerunset(key: string): void- Recommended method to remove a key from the container
Legacy Methods (Deprecated since 3.0.0)
offsetSet<T>(key: string, value: ValueOrFactoryOrCallable<T>, factory?: boolean): void- Deprecated. Useset()instead.offsetGet<T>(key: string | Constructor<T>): T- Deprecated. Useget()instead.offsetExists(key: string): boolean- Deprecated. Usehas()instead.offsetUnset(key: string): void- Deprecated. Useunset()instead.
Factory and Protection
factory<T>(factory: Factory<T>): Factory<T>- Mark a factory to always create new instances (prevents singleton caching)protect(callable: Callable): Callable- Protect a callable from being treated as a factory
Utility Methods
raw<T>(key: string): T- Get the raw value without executing factories or callableskeys(): string[]- Get all registered keys in the containerclear(): void- Clear all registered services and reset the container to its initial statedispose(): void- Dispose of the container and clean up resources
Service Providers
register(provider: IServiceProvider, values?: Record<string, ValueOrFactoryOrCallable<any>>): Container- Register a service provider with optional additional values
Dependency Injection
bind(target: Function | string, dependencies: any[]): void- Bind dependencies for a class constructor or class name stringgetValueObject(key: string): ValueObjectInterface | undefined- Get a ValueObject for a given key if it exists
ValueObject Management
addDependency(dependency: DependencyKey): this- Add a dependency to the ValueObjectremoveDependency(dependency: DependencyKey): this- Remove a specific dependencyhasDependency(dependency: DependencyKey): boolean- Check if a dependency existsvalidateDependencies(): boolean- Validate all dependencies are availableresolveDependencies(): any[]- Resolve all dependencies with circular dependency detectionisResolvable(): boolean- Check if the ValueObject can be resolvedgetDependencyKeys(): DependencyKey[]- Get all dependency keysclearDependencies(): this- Clear all dependenciesclone(): ValueObjectInterface- Create a copy of the ValueObject
Contributing
- Fork the repository
- Create a feature branch
- Make your changes
- Add tests for new functionality
- Run the test suite
- Submit a pull request
License
This project is licensed under the MIT License. For more details, see the LICENSE file.
