@adimm/x-injection 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Adi-Marian Mutu
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,319 @@
1
+ <h1 align="center">
2
+ xInjection&nbsp;<a href="https://www.npmjs.com/package/@adimm/x-injection" target="__blank" alt="Release Version"><img src="https://img.shields.io/npm/v/@adimm/x-injection?color=0476bc&label="></a>
3
+ <img src="https://flat.badgen.net/npm/license/@adimm/x-injection" alt="License">
4
+ </h1>
5
+
6
+ <p align="center">
7
+ <a href="https://github.com/AdiMarianMutu/x-injection/actions/workflows/ci.yml?query=branch%3Amain" target="__blank"><img src="https://github.com/AdiMarianMutu/x-injection/actions/workflows/ci.yml/badge.svg?branch=main"></a>
8
+ <a href="https://github.com/AdiMarianMutu/x-injection/actions/workflows/publish.yml" target="__blank"><img src="https://github.com/AdiMarianMutu/x-injection/actions/workflows/publish.yml/badge.svg"></a>
9
+ <br>
10
+ <img src="https://flat.badgen.net/bundlephobia/minzip/@adimm/x-injection">
11
+ <a href="https://www.npmjs.com/package/@adimm/x-injection" target="__blank" alt="Monthly Downloads"><img src="https://flat.badgen.net/npm/dm/@adimm/x-injection"></a>
12
+ </p>
13
+
14
+ ## Table of Contents
15
+
16
+ - [Table of Contents](#table-of-contents)
17
+ - [Overview](#overview)
18
+ - [Features](#features)
19
+ - [Installation](#installation)
20
+ - [TypeScript Configuration](#typescript-configuration)
21
+ - [Getting Started](#getting-started)
22
+ - [Bootstrapping the AppModule](#bootstrapping-the-appmodule)
23
+ - [Registering Global Providers](#registering-global-providers)
24
+ - [Injection Scope](#injection-scope)
25
+ - [Custom Provider Modules](#custom-provider-modules)
26
+ - [Dynamic Exports](#dynamic-exports)
27
+ - [Advanced Usage](#advanced-usage)
28
+ - [Documentation](#documentation)
29
+ - [Contributing](#contributing)
30
+
31
+ ## Overview
32
+
33
+ **xInjection** is a robust Inversion of Control [(IoC)](https://en.wikipedia.org/wiki/Inversion_of_control) library that extends [InversifyJS](https://github.com/inversify/InversifyJS) with a modular, [NestJS](https://github.com/nestjs/nest)-inspired Dependency Injection [(DI)](https://en.wikipedia.org/wiki/Dependency_injection) system. It enables you to **encapsulate** dependencies with fine-grained control using **[ProviderModule](https://adimarianmutu.github.io/x-injection/classes/ProviderModule.html)** classes, allowing for clean **separation** of concerns and **scalable** architecture.
34
+
35
+ Each `ProviderModule` manages its _own_ container, supporting easy **decoupling** and _explicit_ control over which providers are **exported** and **imported** across modules. The global **[AppModule](https://adimarianmutu.github.io/x-injection/variables/AppModule.html)** is always available, ensuring a seamless foundation for your application's DI needs.
36
+
37
+ ## Features
38
+
39
+ - **NestJS-inspired module system:** Import and export providers between modules.
40
+ - **Granular dependency encapsulation:** Each module manages its own container.
41
+ - **Flexible provider scopes:** [Singleton](https://adimarianmutu.github.io/x-injection/enums/InjectionScope.html#singleton), [Request](https://adimarianmutu.github.io/x-injection/enums/InjectionScope.html#request), and [Transient](https://adimarianmutu.github.io/x-injection/enums/InjectionScope.html#transient) lifecycles.
42
+ - **Lifecycle hooks:** [onReady](https://adimarianmutu.github.io/x-injection/interfaces/ProviderModuleOptions.html#onready) and [onDispose](https://adimarianmutu.github.io/x-injection/interfaces/ProviderModuleOptions.html#ondispose) for _module_ initialization and cleanup.
43
+ - **Advanced container access:** Directly interact with the underlying [InversifyJS containers](https://inversify.io/docs/api/container/) if needed.
44
+
45
+ ## Installation
46
+
47
+ First, ensure you have [`reflect-metadata`](https://www.npmjs.com/package/reflect-metadata) installed:
48
+
49
+ ```sh
50
+ npm i reflect-metadata
51
+ ```
52
+
53
+ Then install `xInjection`:
54
+
55
+ ```sh
56
+ npm i @adimm/x-injection
57
+ ```
58
+
59
+ ### TypeScript Configuration
60
+
61
+ Add the following options to your `tsconfig.json` to enable decorator metadata:
62
+
63
+ ```json
64
+ {
65
+ "compilerOptions": {
66
+ "experimentalDecorators": true,
67
+ "emitDecoratorMetadata": true
68
+ }
69
+ }
70
+ ```
71
+
72
+ ## Getting Started
73
+
74
+ ### Bootstrapping the AppModule
75
+
76
+ In your application's entry point, import and register the global `AppModule`:
77
+
78
+ ```ts
79
+ import { AppModule } from '@adimm/x-injection';
80
+
81
+ AppModule.register({});
82
+ ```
83
+
84
+ > **Note:** You must call `AppModule.register()` even if you have no global providers. Passing an empty object `{}` is valid.
85
+
86
+ ### Registering Global Providers
87
+
88
+ To make services available throughout your application, register them as global providers:
89
+
90
+ ```ts
91
+ import { AppModule, Injectable } from '@adimm/x-injection';
92
+
93
+ @Injectable()
94
+ class LoggerService {}
95
+
96
+ @Injectable()
97
+ class ConfigService {
98
+ constructor(private readonly logger: LoggerService) {}
99
+ }
100
+
101
+ AppModule.register({
102
+ providers: [LoggerService, ConfigService],
103
+ });
104
+ ```
105
+
106
+ Now, `LoggerService` and `ConfigService` can be injected anywhere in your app, including inside all `ProviderModules`.
107
+
108
+ ### Injection Scope
109
+
110
+ There are mainly 3 first-class ways to set the [InjectionScope](https://adimarianmutu.github.io/x-injection/enums/InjectionScope.html) of a provider, and each one has an order priority.
111
+ 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.
112
+
113
+ 1. By providing the [scope](https://adimarianmutu.github.io/x-injection/interfaces/ProviderScopeOption.html) property to the [ProviderToken](https://adimarianmutu.github.io/x-injection/types/ProviderToken.html):
114
+ ```ts
115
+ const USER_PROVIDER: ProviderToken<UserService> = {
116
+ scope: InjectionScope.Request,
117
+ provide: UserService,
118
+ useClass: UserService,
119
+ };
120
+ ```
121
+ 2. Within the [@Injectable](https://adimarianmutu.github.io/x-injection/functions/Injectable.html) decorator:
122
+ ```ts
123
+ @Injectable(InjectionScope.Transient)
124
+ class Transaction {}
125
+ ```
126
+ 3. By providing the [defaultScope](https://adimarianmutu.github.io/x-injection/interfaces/ProviderModuleOptions.html#defaultscope) property when initializing a `ProviderModule`:
127
+ ```ts
128
+ const RainModule = new ProviderModule({
129
+ defaultScope: InjectionScope.Transient,
130
+ });
131
+ ```
132
+
133
+ > **Note:** _Imported modules/providers retain their original `InjectionScope`!_
134
+
135
+ ## Custom Provider Modules
136
+
137
+ You can define custom modules to encapsulate related providers and manage their scope:
138
+
139
+ ```ts
140
+ import { Injectable, InjectionScope, ProviderModule } from '@adimm/x-injection';
141
+
142
+ @Injectable()
143
+ export class DatabaseService {
144
+ // Implementation...
145
+ }
146
+
147
+ @Injectable()
148
+ export class SessionService {
149
+ constructor(public readonly userService: UserService) {}
150
+
151
+ // Implementation...
152
+ }
153
+
154
+ export const DatabaseModule = new ProviderModule({
155
+ name: 'DatabaseModule',
156
+ providers: [DatabaseService],
157
+ exports: [DatabaseService],
158
+ onReady: async (module) => {
159
+ const databaseService = module.get(DatabaseService);
160
+
161
+ // Additional initialization...
162
+ },
163
+ onDispose: async (module) => {
164
+ const databaseService = module.get(DatabaseService);
165
+
166
+ databaseService.closeConnection();
167
+ },
168
+ });
169
+
170
+ export const SessionModule = new ProviderModule({
171
+ name: 'SessionModule',
172
+ defaultScope: InjectionScope.Request,
173
+ providers: [SessionService],
174
+ exports: [SessionService],
175
+ });
176
+ ```
177
+
178
+ Register these modules in your `AppModule`:
179
+
180
+ ```ts
181
+ AppModule.register({
182
+ imports: [DatabaseModule, SessionModule],
183
+ });
184
+ ```
185
+
186
+ > **Note:** The `AppModule.register` method can be invoked only _once_! _(You may re-invoke it only after the module has been disposed)_ Preferably during your application bootstrapping process.
187
+
188
+ From now on, the `AppModule` container has the references of the `DatabaseService` and the `SessionService`.
189
+ But it is important to keep in mind of some aspects:
190
+
191
+ - We know that the `DatabaseService` scope is set to `Singleton`.
192
+ - We know that the `SessionService` scope is set to `Request`.
193
+
194
+ This means that if we do:
195
+
196
+ ```ts
197
+ const databaseService_a = AppModule.get(DatabaseService);
198
+ const databaseService_b = AppModule.get(DatabaseService);
199
+
200
+ expect(databaseService_a).toBe(databaseService_b);
201
+ ```
202
+
203
+ Will produce `true`, but, doing:
204
+
205
+ ```ts
206
+ const sessionService_a = AppModule.get(SessionService);
207
+ const sessionService_b = AppModule.get(SessionService);
208
+
209
+ expect(sessionService_a).toBe(sessionService_b);
210
+ ```
211
+
212
+ Will produce `false` because Inversify's [Request](https://inversify.io/docs/fundamentals/binding/#request) scope acts as a `Singleton` scope within the same module context
213
+ and as a `Transient` scope outside the module context.
214
+
215
+ **Inject multiple dependencies:**
216
+
217
+ ```ts
218
+ const [serviceA, serviceB] = BigModule.getMany(ServiceA, ServiceB);
219
+ // or
220
+ const [serviceC, serviceD] = BigModule.getMany<[ServiceC, ServiceD]>(SERVICE_TOKEN, 'SERVICE_ID');
221
+ ```
222
+
223
+ ### Dynamic Exports
224
+
225
+ `xInjection` allows a `ProviderModule` to also dynamically choose _what_ and _when_ to export a provider/module.
226
+
227
+ > **With great power comes great responsibility! 🥸**
228
+
229
+ This is a very powerful feature and for the most use cases, you'll not need to use it.
230
+ However, if you may ever need to use a dynamic export, here is a simple example:
231
+
232
+ ```ts
233
+ class WingsService {}
234
+
235
+ class AnimalService {}
236
+
237
+ class CatService extends AnimalService {
238
+ // This line throws an error at some point.
239
+ constructor(public readonly wings: WingsService) {}
240
+ }
241
+
242
+ class CrowService extends AnimalService {
243
+ constructor(public readonly wings: WingsService) {}
244
+ }
245
+
246
+ const AnimalModule = new ProviderModule({
247
+ name: 'AnimalModule',
248
+ providers: [AnimalService, { provide: WingsService, useClass: WingsService, scope: InjectionScope.Transient }],
249
+ exports: [AnimalService, WingsService],
250
+ dynamicExports: (importerModule, moduleExports) => {
251
+ // If the importer module is `CrowModule`, we'll export the entire
252
+ // `exports` list, because `moduleExports` is actually the `exports` array declared above.
253
+ // Meaning that the `CrowModule` container will also have access to the `WingsService`
254
+ if (importerModule.toNaked().name === 'CrowModule') return moduleExports;
255
+
256
+ // Otherwise it is the `CatModule` and we are not
257
+ // exporting the `WingsService` as cats don't fly, or do they fly? 🧐
258
+ return [AnimalService];
259
+ },
260
+ });
261
+
262
+ const CrowModule = new ProviderModule({
263
+ name: 'CrowModule',
264
+ imports: [AnimalModule],
265
+ providers: [CrowService],
266
+ exports: [CrowService],
267
+ });
268
+
269
+ const CatModule = new ProviderModule({
270
+ name: 'CatModule',
271
+ imports: [AnimalModule],
272
+ providers: [CatService],
273
+ exports: [CatService],
274
+ });
275
+ ```
276
+
277
+ Hopefully the provided example shows how powerful the `dynamicExports` property it is, but, there are some things to keep in mind in order to avoid nasty bugs!
278
+
279
+ 1. You **must always** return only the providers/modules declared into the static `exports` array.
280
+ 2. You **can** return _less_ providers/modules as long as they are still part of the static `exports` array.
281
+ 3. You **cannot** return _more_ providers/modules than the static `exports` array!
282
+
283
+ > When point number `1` and point number `3` are triggered, the library will throw a descriptive error which should help to find the culprit module.
284
+
285
+ ## Advanced Usage
286
+
287
+ Each `ProviderModule` instance implements the `IProviderModule` interface for simplicity, but can be cast to `IProviderModuleNaked` for advanced operations:
288
+
289
+ ```ts
290
+ const nakedModule = ProviderModuleInstance.toNaked();
291
+ // or: nakedModule = ProviderModuleInstance as IproviderModuleNaked;
292
+ const inversifyContainer = nakedModule.container;
293
+ ```
294
+
295
+ You can also access the global `InversifyJS` container directly:
296
+
297
+ ```ts
298
+ import { AppModule, GlobalContainer } from '@adimm/x-injection';
299
+
300
+ const globalContainer = GlobalContainer || AppModule.toNaked().container;
301
+ ```
302
+
303
+ For advanced scenarios, `IProviderModuleNaked` exposes additional methods (prefixed with `__`) that wrap InversifyJS APIs, supporting native `xInjection` provider tokens and more.
304
+
305
+ ## Documentation
306
+
307
+ Comprehensive, auto-generated documentation is available at:
308
+
309
+ 👉 [https://adimarianmutu.github.io/x-injection/index.html](https://adimarianmutu.github.io/x-injection/index.html)
310
+
311
+ ## Contributing
312
+
313
+ Pull requests are warmly welcomed! 😃
314
+
315
+ Please ensure your contributions adhere to the project's code style. See the repository for more details.
316
+
317
+ ---
318
+
319
+ > For questions, feature requests, or bug reports, feel free to open an [issue](https://github.com/AdiMarianMutu/x-injection/issues) on GitHub!