@adimm/x-injection v1.1.6
Table of Contents
- Table of Contents
- Overview
- Features
- Installation
- Getting Started
- Provider Modules
- Advanced Usage
- Unit Tests
- Documentation
- ReactJS Implementation
- Contributing
Overview
xInjection is a robust Inversion of Control (IoC) library that extends InversifyJS with a modular, NestJS-inspired Dependency Injection (DI) system. It enables you to encapsulate dependencies with fine-grained control using ProviderModule classes, allowing for clean separation of concerns and scalable architecture.
Each ProviderModule manages its own container, supporting easy decoupling and explicit control over which providers are exported and imported across modules. The global AppModule is always available, ensuring a seamless foundation for your application's DI needs.
Features
- NestJS-inspired module system: Import and export providers between modules.
- Granular dependency encapsulation: Each module manages its own container.
- Flexible provider scopes: Singleton, Request, and Transient lifecycles.
- Lifecycle hooks: onReady and onDispose for module initialization and cleanup.
- Advanced container access: Directly interact with the underlying InversifyJS containers if needed.
Installation
First, ensure you have reflect-metadata installed:
npm i reflect-metadataThen install xInjection:
npm i @adimm/x-injectionTypeScript Configuration
Add the following options to your tsconfig.json to enable decorator metadata:
{
"compilerOptions": {
"experimentalDecorators": true,
"emitDecoratorMetadata": true
}
}Getting Started
Bootstrapping the AppModule
In your application's entry point, import and register the global AppModule:
import { AppModule } from '@adimm/x-injection';
AppModule.register({});Note: You must call
AppModule.register()even if you have no global providers. Passing an empty object{}is valid.
Registering Global Providers
To make services available throughout your application, register them as global providers:
import { AppModule, Injectable } from '@adimm/x-injection';
@Injectable()
class LoggerService {}
@Injectable()
class ConfigService {
constructor(private readonly logger: LoggerService) {}
}
AppModule.register({
providers: [LoggerService, ConfigService],
});Now, LoggerService and ConfigService can be injected anywhere in your app, including inside all ProviderModules.
Registering Global Modules
You can also import entire modules into the AppModule like so:
const SECRET_TOKEN_PROVIDER = { provide: 'SECRET_TOKEN', useValue: '123' };
const SECRET_TOKEN_2_PROVIDER = { provide: 'SECRET_TOKEN_2', useValue: 123 };
const ConfigModule = new ProviderModule({
identifier: Symbol('ConfigModule'),
markAsGlobal: true,
providers: [SECRET_TOKEN_PROVIDER, SECRET_TOKEN_2_PROVIDER],
exports: [SECRET_TOKEN_PROVIDER, SECRET_TOKEN_2_PROVIDER],
});
AppModule.register({
imports: [ConfigModule],
});Note: All modules which are imported into the
AppModulemust have the markAsGlobal option set totrue, otherwise the InjectionProviderModuleGlobalMarkError exception will be thrown!Note2: An InjectionProviderModuleGlobalMarkError exception will be thrown also when importing into the
AppModulea module which does not have the markAsGlobal flag option!
Injection Scope
There are mainly 3 first-class ways to set the InjectionScope of a provider, and each one has an order priority. The below list shows them in order of priority (highest to lowest), meaning that if 2 (or more) ways are used, the method with the highest priority will take precedence.
- By providing the scope property to the ProviderToken:
const USER_PROVIDER: ProviderToken<UserService> = { scope: InjectionScope.Request, provide: UserService, useClass: UserService, }; - Within the @Injectable decorator:
@Injectable(InjectionScope.Transient) class Transaction {} - By providing the defaultScope property when initializing a
ProviderModule:const RainModule = new ProviderModule({ identifier: Symbol('RainModule'), defaultScope: InjectionScope.Transient, });
Note: Imported modules/providers retain their original
InjectionScope!
Singleton
The Singleton injection scope means that once a dependency has been resolved from within a module will be cached and further resolutions will use the value from the cache.
Example:
expect(MyModule.get(MyProvider)).toBe(MyModule.get(MyProvider));
// trueTransient
The Transient injection scope means that a new instance of the dependency will be used whenever a resolution occurs.
Example:
expect(MyModule.get(MyProvider)).toBe(MyModule.get(MyProvider));
// falseRequest
The Request injection scope means that the same instance will be used when a resolution happens in the same request scope.
Example:
@Injectable(InjectionScope.Transient)
class Book {
author: string;
}
@Injectable(InjectionScope.Request)
class Metro2033 extends Book {
override author = 'Dmitry Alekseyevich Glukhovsky';
}
@Injectable(InjectionScope.Transient)
class Library {
constructor(
public readonly metro2033: Metro2033,
public readonly metro2033_reference: Metro2033
) {}
}
const winstonLibrary = MyModule.get(Library);
const londonLibrary = MyModule.get(Library);
expect(winstonLibrary.metro2033).toBe(winstonLibrary.metro2033_reference);
expect(londonLibrary.metro2033).toBe(londonLibrary.metro2033_reference);
// true
expect(winstonLibrary.metro2033).toBe(londonLibrary.metro2033);
// falseProvider Modules
You can define modules to encapsulate related providers and manage their scope:
import { Injectable, InjectionScope, ProviderModule } from '@adimm/x-injection';
@Injectable()
export class DatabaseService {
// Implementation...
}
@Injectable()
export class SessionService {
constructor(public readonly userService: UserService) {}
// Implementation...
}
export const DatabaseModule = new ProviderModule({
identifier: Symbol('DatabaseModule'),
// or: identifier: 'DatabaseModule',
providers: [DatabaseService],
exports: [DatabaseService],
onReady: async (module) => {
const databaseService = module.get(DatabaseService);
// Additional initialization...
},
onDispose: async (module) => {
const databaseService = module.get(DatabaseService);
databaseService.closeConnection();
},
});
export const SessionModule = new ProviderModule({
identifier: Symbol('SessionModule'),
defaultScope: InjectionScope.Request,
providers: [SessionService],
exports: [SessionService],
});Register these modules in your AppModule:
AppModule.register({
imports: [DatabaseModule, SessionModule],
});Note: The
AppModule.registermethod can be invoked only once! (You may re-invoke it only after the module has been disposed) Preferably during your application bootstrapping process.
From now on, the AppModule container has the references of the DatabaseService and the SessionService.
Inject multiple dependencies:
const [serviceA, serviceB] = BigModule.getMany(ServiceA, ServiceB);
// or
const [serviceC, serviceD] = BigModule.getMany<[ServiceC, ServiceD]>(SERVICE_TOKEN, 'SERVICE_ID');ProviderModuleDefinition
When you do:
const MyModule = new ProviderModule({...});The MyModule will be eagerly instantiated, therefore creating under-the-hood an unique container for the MyModule instance.
In some scenarios you may need/want to avoid that, you can achieve that by using the ProviderModuleDefinition class. It allows you to just define a blueprint of the ProviderModule without all the overhead of instantiating the actual module.
const GarageModuleDefinition = new ProviderModuleDefinition({ identifier: 'GarageModuleDefinition' });
// You can always edit all the properties of the definition.
GarageModuleDefinition.imports = [...GarageModuleDefinition.imports, PorscheModule, FerrariModuleDefinition];Feed to new ProviderModule
const GarageModule = new ProviderModule(GarageModuleDefinition);Feed to lazyImport
ExistingModule.lazyImport(GarageModuleDefinition);Note: Providing it to the
lazyImportmethod will automatically instantiate a newProviderModuleon-the-fly!
Why not just use the ProviderModuleOptions interface?
That's a very good question! It means that you understood that the ProviderModuleDefinition is actually a class wrapper of the ProviderModuleOptions.
Theoretically you can use a plain object having the ProviderModuleOptions interface, however, the ProviderModuleOptions interface purpose is solely to expose/shape the options with which a module can be instantiated, while the ProviderModuleDefinition purpose is to define the actual ProviderModule blueprint.
Lazy imports and exports
You can also lazy import or export providers/modules, usually you don't need this feature, but there may be some advanced cases where you may want to be able to do so.
The lazy nature defers the actual module resolution, this may help in breaking immediate circular reference chain under some circumstances.
Imports
You can lazily import a module by invoking the lazyImport method at any time in your code.
const GarageModule = new ProviderModule({
identifier: 'GarageModule',
// Eager imports happen at module initialization
imports: [FerrariModule, PorscheModule, ...]
});
// Later in your code
GarageModule.lazyImport(LamborghiniModule, BugattiModule, ...);Exports
You can lazily export a provider or module by providing a callback (it can also be an async callback) as shown below:
const SecureBankBranchModule = new ProviderModule({
identifier: 'SecureBankBranchModule',
providers: [BankBranchService],
exports: [BankBranchService],
});
const BankModule = new ProviderModule({
identifier: 'BankModule',
imports: [SecureBankBranchModule],
exports: [..., (importerModule) => {
// When the module having the identifier `UnknownBankModule` imports the `BankModule`
// it'll not be able to also import the `SecureBankBranchModule` as we are not returning it here.
if (importerModule.toString() === 'UnknownBankModule') return;
// Otherwise we safely export it
return SecureBankBranchModule;
}]
});Advanced Usage
ProviderModuleNaked Interface
Each ProviderModule instance implements the IProviderModule interface for simplicity, but can be cast to IProviderModuleNaked for advanced operations:
const nakedModule = ProviderModuleInstance.toNaked();
// or: nakedModule = ProviderModuleInstance as IproviderModuleNaked;
const inversifyContainer = nakedModule.container;You can also access the global InversifyJS container directly:
import { AppModule, GlobalContainer } from '@adimm/x-injection';
const globalContainer = GlobalContainer || AppModule.toNaked().container;For advanced scenarios, IProviderModuleNaked exposes additional methods (prefixed with __) that wrap InversifyJS APIs, supporting native xInjection provider tokens and more.
Strict Mode
By default the AppModule runs in "strict mode", a built-in mode which enforces an opinionated set of rules aiming to reduce common pitfalls and edge-case bugs.
When invoking the AppModule.register method you can set the _strict property to false in order to permanentely disable those set of built-in rules.
Note: Do not open an
issueif a bug or edge-case is caused by having thestrictproperty disabled!
Why you should not turn it off:
MarkAsGlobal
The markAsGlobal flag property is used to make sure that modules which should be registered directly into the AppModule are indeed provided to the the imports array of the AppModule and the the other way around, if a module is imported into the AppModule without having the markAsGlobal flag property set, it'll throw an error.
This may look redundant, but it may save you (and your team) some hours of debugging in understanding why some providers are able to make their way into other modules. As those providers are now acting as global providers.
Imagine the following scenario:
const ScopedModule = new ProviderModule({
identifier: 'ScopedModule',
providers: [...],
exports: [...],
});
const AnotherScopedModule = new ProviderModule({
identifier: 'AnotherScopedModule',
imports: [ScopedModule],
providers: [...],
exports: [...],
});
const GlobalModule = new ProviderModule({
identifier: 'GlobalModule',
markAsGlobal: true,
imports: [AnotherScopedModule],
});
AppModule.register({
imports: [GlobalModule],
});At first glance you may not spot/understand the issue there, but because the GlobalModule (which is then imported into the AppModule) is directly importing the AnotherScopedModule, it means that all the providers of the AnotherScopedModule and ScopedModule (because AnotherScopedModule also imports ScopedModule) will become accessible through your entire app!
Disabling strict mode removes this safeguard, allowing any module to be imported into the AppModule regardless of markAsGlobal, increasing risk of bugs by exposing yourself to the above example.
Unit Tests
It is very easy to create mock modules so you can use them in your unit tests.
class ApiService {
constructor(private readonly userService: UserService) {}
async sendRequest<T>(location: LocationParams): Promise<T> {
// Pseudo Implementation
return this.sendToLocation(user, location);
}
private async sendToLocation(user: User, location: any): Promise<any> {}
}
const ApiModule = new ProviderModule({
identifier: Symbol('ApiModule'),
providers: [UserService, ApiService],
});
const ApiModuleMocked = new ProviderModule({
identifier: Symbol('ApiModule_MOCK'),
providers: [
{
provide: UserService,
useClass: UserService_Mock,
},
{
provide: ApiService,
useValue: {
sendRequest: async (location) => {
console.log(location);
},
},
},
],
});Now what you have to do is just to provide the ApiModuleMocked instead of the ApiModule š
Documentation
Comprehensive, auto-generated documentation is available at:
š https://adimarianmutu.github.io/x-injection/index.html
ReactJS Implementation
You want to use it within a ReactJS project? Don't worry, the library does already have an official implementation for React āļø
For more details check out the GitHub Repository.
Contributing
Pull requests are warmly welcomed! š
Please ensure your contributions adhere to the project's code style. See the repository for more details.
For questions, feature requests, or bug reports, feel free to open an issue on GitHub!
5 months ago
5 months ago
6 months ago
6 months ago
6 months ago
6 months ago
6 months ago
6 months ago
6 months ago
6 months ago
6 months ago
6 months ago
6 months ago
6 months ago
6 months ago
6 months ago
6 months ago
6 months ago
6 months ago
6 months ago
6 months ago
6 months ago
6 months ago
6 months ago
6 months ago
6 months ago
6 months ago
6 months ago
6 months ago
6 months ago
6 months ago
6 months ago
6 months ago
6 months ago