@adimm/x-injection 2.1.2 → 3.0.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/README.md CHANGED
@@ -1,860 +1,2837 @@
1
- <p align="center">
2
- <img width="260px" height="auto" alt="xInjection Logo" src="https://raw.githubusercontent.com/AdiMarianMutu/x-injection/main/assets/logo.png"><br /><a href="https://www.npmjs.com/package/@adimm/x-injection" target="__blank"><img src="https://badgen.net/npm/v/@adimm/x-injection"></a>
3
- <a href="https://app.codecov.io/gh/AdiMarianMutu/x-injection" target="__blank"><img src="https://badgen.net/codecov/c/github/AdiMarianMutu/x-injection"></a>
4
- <img src="https://badgen.net/npm/license/@adimm/x-injection">
5
- </p>
6
-
7
- <p align="center">
8
- <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>
9
- <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>
10
- <br>
11
- <img src="https://badgen.net/bundlephobia/minzip/@adimm/x-injection">
12
- <a href="https://www.npmjs.com/package/@adimm/x-injection" target="__blank"><img src="https://badgen.net/npm/dm/@adimm/x-injection"></a>
13
-
14
- </p>
15
-
16
- ## Table of Contents
17
-
18
- - [Table of Contents](#table-of-contents)
19
- - [Overview](#overview)
20
- - [Features](#features)
21
- - [Installation](#installation)
22
- - [TypeScript Configuration](#typescript-configuration)
23
- - [Getting Started](#getting-started)
24
- - [Quick Start](#quick-start)
25
- - [Glossary](#glossary)
26
- - [ProviderModule](#providermodule)
27
- - [AppModule](#appmodule)
28
- - [Blueprint](#blueprint)
29
- - [Definition](#definition)
30
- - [Conventions](#conventions)
31
- - [ProviderModule](#providermodule-1)
32
- - [Blueprints](#blueprints)
33
- - [ProviderToken](#providertoken)
34
- - [AppModule](#appmodule-1)
35
- - [ProviderModule API](#providermodule-api)
36
- - [Injection Scope](#injection-scope)
37
- - [Singleton](#singleton)
38
- - [Transient](#transient)
39
- - [Request](#request)
40
- - [Provider Tokens](#provider-tokens)
41
- - [Provider Modules](#provider-modules)
42
- - [Blueprints](#blueprints-1)
43
- - [Import Behavior](#import-behavior)
44
- - [isGlobal](#isglobal)
45
- - [Definitions](#definitions)
46
- - [Advanced Usage](#advanced-usage)
47
- - [Events](#events)
48
- - [Middlewares](#middlewares)
49
- - [Internals](#internals)
50
- - [ProviderModule](#providermodule-2)
51
- - [MiddlewaresManager](#middlewaresmanager)
52
- - [ModuleContainer](#modulecontainer)
53
- - [ImportedModuleContainer](#importedmodulecontainer)
54
- - [DynamicModuleDefinition](#dynamicmoduledefinition)
55
- - [ProviderModuleBlueprint](#providermoduleblueprint)
56
- - [Set of Helpers](#set-of-helpers)
57
- - [Unit Tests](#unit-tests)
58
- - [Documentation](#documentation)
59
- - [ReactJS Implementation](#reactjs-implementation)
60
- - [Contributing](#contributing)
61
- - [Credits](#credits)
62
-
63
- ## Overview
64
-
65
- **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/IProviderModule.html)** classes, allowing for clean **separation** of concerns and **scalable** architecture.
66
-
67
- 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.
68
-
69
- ## Features
70
-
71
- - **NestJS-inspired module system:** Import and export providers between modules.
72
- - **Granular dependency encapsulation:** Each module manages its own container.
73
- - **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.
74
- - **Lifecycle hooks:** [onReady](https://adimarianmutu.github.io/x-injection/interfaces/ProviderModuleOptions.html#onready), [onReset](https://adimarianmutu.github.io/x-injection/interfaces/ProviderModuleOptions.html#onreset) and [onDispose](https://adimarianmutu.github.io/x-injection/interfaces/ProviderModuleOptions.html#ondispose) for _module_ initialization and cleanup.
75
- - **Middlewares:** Tap into the low-level implementation without any effort by just adding new `middlewares`.
76
- - **Events:** Subscribe to internal events for maximum control.
77
- - **Blueprints:** Plan ahead your `modules` without eagerly instantiating them.
78
- - **Fully Agnostic:** It doesn't rely on any framework, just on [InversifyJS](https://inversify.io/) as it uses it under-the-hood to build the containers. It works the same both client side and server side.
79
-
80
- ## Installation
81
-
82
- First, ensure you have [`reflect-metadata`](https://www.npmjs.com/package/reflect-metadata) installed:
83
-
84
- ```sh
85
- npm i reflect-metadata
86
- ```
87
-
88
- > [!NOTE]
89
- >
90
- > You may have to add `import 'reflect-metadata'` at the entry point of your application.
91
-
92
- Then install `xInjection`:
93
-
94
- ```sh
95
- npm i @adimm/x-injection
96
- ```
97
-
98
- ### TypeScript Configuration
99
-
100
- Add the following options to your `tsconfig.json` to enable decorator metadata:
101
-
102
- ```json
103
- {
104
- "compilerOptions": {
105
- "experimentalDecorators": true,
106
- "emitDecoratorMetadata": true
107
- }
108
- }
109
- ```
110
-
111
- ## Getting Started
112
-
113
- ### Quick Start
114
-
115
- ```ts
116
- import { Injectable, ProviderModule } from '@adimm/x-injection';
117
-
118
- @Injectable()
119
- class HelloService {
120
- sayHello() {
121
- return 'Hello, world!';
122
- }
123
- }
124
-
125
- const HelloModule = ProviderModule.create({
126
- id: 'HelloModule',
127
- providers: [HelloService],
128
- exports: [HelloService],
129
- });
130
-
131
- const helloService = HelloModule.get(HelloService);
132
-
133
- console.log(helloService.sayHello());
134
- // => 'Hello, world!'
135
- ```
136
-
137
- ### Glossary
138
-
139
- #### ProviderModule
140
-
141
- The core class of `xInjection`, if you ever worked with [NestJS](https://nestjs.com/) _(or [Angular](https://angular.dev/))_, you'll find it very familiar.
142
-
143
- ```ts
144
- const GarageModule = ProviderModule.create({ id: 'GarageModule', imports: [], providers: [], exports: [] });
145
- ```
146
-
147
- #### AppModule
148
-
149
- It is a special instance of the `ProviderModule` class which acts as the `root` of your `modules` graph, all _global_ modules will be automatically imported into the `AppModule` and shared across all your modules.
150
-
151
- #### Blueprint
152
-
153
- Another core class which most probably you'll end using a lot too, to keep it short, it allows you to plan ahead the `modules` without instantiating them.
154
-
155
- ```ts
156
- const CarModuleBlueprint = ProviderModule.blueprint({ id: 'CarModule', imports: [], providers: [], exports: [] });
157
- ```
158
-
159
- #### Definition
160
-
161
- It is used to refer to the three main blocks of a module:
162
-
163
- - [imports](https://adimarianmutu.github.io/x-injection/interfaces/ProviderModuleOptions.html#imports)
164
- - [providers](https://adimarianmutu.github.io/x-injection/interfaces/ProviderModuleOptions.html#providers)
165
- - [exports](https://adimarianmutu.github.io/x-injection/interfaces/ProviderModuleOptions.html#exports)
166
-
167
- ### Conventions
168
-
169
- The library has some opinionated _naming_ conventions which you should adopt too
170
-
171
- #### ProviderModule
172
-
173
- All variables holding an _instance_ of a `ProviderModule` should be written in [PascalCase](https://www.wikidata.org/wiki/Q9761807) and _suffixed_ with `Module`, like this:
174
-
175
- ```ts
176
- const DatabaseModule = ProviderModule.create({...});
177
- const UserModule = ProviderModule.create({...});
178
- const CarPartsModule = ProviderModule.create({...});
179
- ```
180
-
181
- The `id` property of the `ProviderModule.options` should be the same as the `module` variable name.
182
-
183
- ```ts
184
- const DatabaseModule = ProviderModule.create({ id: 'DatabaseModule' });
185
- const UserModule = ProviderModule.create({ id: 'UserModule' });
186
- const CarPartsModule = ProviderModule.create({ id: 'CarPartsModule' });
187
- ```
188
-
189
- If you are exporting a `module` from a designated file, then you should name that file as following:
190
-
191
- ```
192
- database.module.ts
193
- user.module.ts
194
- car-parts.module.ts
195
- ```
196
-
197
- > [!TIP]
198
- >
199
- > If you install/use the [Material Icon Theme](https://marketplace.visualstudio.com/items?itemName=PKief.material-icon-theme) VS Code extension, you'll see the `*.module.ts` files with a specific icon.
200
-
201
- #### Blueprints
202
-
203
- All variables holding an _instance_ of a `ProviderModuleBlueprint` should be written in [PascalCase](https://www.wikidata.org/wiki/Q9761807) too and _suffixed_ with `ModuleBp`, like this:
204
-
205
- ```ts
206
- const DatabaseModuleBp = ProviderModule.blueprint({...});
207
- const UserModuleBp = ProviderModule.blueprint({...});
208
- const CarPartsModuleBp = ProviderModule.blueprint({...});
209
- ```
210
-
211
- The `id` property of the `ProviderModuleBlueprint.options` should **not** end with `Bp` because when you'll import that `blueprint` into a `module`, the exact provided `id` will be used!
212
-
213
- ```ts
214
- const DatabaseModuleBp = ProviderModule.create({ id: 'DatabaseModule' });
215
- const UserModuleBp = ProviderModule.create({ id: 'UserModule' });
216
- const CarPartsModuleBp = ProviderModule.create({ id: 'CarPartsModule' });
217
- ```
218
-
219
- If you are exporting a `blueprint` from a designated file, then you should name that file as following:
220
-
221
- ```
222
- database.module.bp.ts
223
- user.module.bp.ts
224
- car-parts.module.bp.ts
225
- ```
226
-
227
- #### ProviderToken
228
-
229
- All variables holding an _object_ representing a [ProviderToken](https://adimarianmutu.github.io/x-injection/types/ProviderToken.html) should be written in [SCREAMING_SNAKE_CASE](https://en.wikipedia.org/wiki/Snake_case) and _suffixed_ with `_PROVIDER`, like this:
230
-
231
- ```ts
232
- const USER_SERVICE_PROVIDER = UserService;
233
- ```
234
-
235
- If you are exporting a `provider token` from a designated file, then you should name that file as following:
236
-
237
- ```
238
- user-service.provider.ts
239
- ```
240
-
241
- ### AppModule
242
-
243
- As explained above, it is the `root` module of your application, it is always available and eagerly bootstrapped.
244
-
245
- Usually you'll not interact much with it as any `module` which is defined as _global_ will be automatically imported into it, therefore having its `exports` definition available across all your modules _out-of-the-box_. However, you can use it like any `ProviderModule` instance.
246
-
247
- > [!WARNING]
248
- >
249
- > Importing the `AppModule` into any `module` will throw an error!
250
-
251
- You have 2 options to access it:
252
-
253
- ```ts
254
- import { AppModule } from '@adimm/x-injection';
255
- ```
256
-
257
- or
258
-
259
- ```ts
260
- import { ProviderModule } from '@adimm/x-injection';
261
-
262
- ProviderModule.APP_MODULE_REF;
263
-
264
- // This option is mostly used internally, but you can 100% safely use it as well.
265
- ```
266
-
267
- Providing global services to the `AppModule`:
268
-
269
- ```ts
270
- @Injectable()
271
- class UserService {}
272
-
273
- AppModule.update.addProvider(UserService);
274
- ```
275
-
276
- > [!NOTE]
277
- >
278
- > All `providers` scope is set to [Singleton](https://adimarianmutu.github.io/x-injection/enums/InjectionScope.html#singleton) by default if not provided.
279
-
280
- Yes, that's it, now you have access to the `UserService` anywhere in your app across your `modules`, meaning that you can now do:
281
-
282
- ```ts
283
- const UnrelatedModule = ProviderModule.create({ id: 'UnrelatedModule' });
284
-
285
- const userService = UnrelatedModule.get(UserService);
286
- // returns the `userService` singleton instance.
287
- ```
288
-
289
- ## ProviderModule API
290
-
291
- You can see all the available `properties` and `methods` of the `ProviderModule` [here](https://adimarianmutu.github.io/x-injection/classes/IProviderModule.html).
292
-
293
- ## Injection Scope
294
-
295
- 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.
296
- 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.
297
-
298
- 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):
299
- ```ts
300
- const USER_PROVIDER: ProviderToken<UserService> = {
301
- scope: InjectionScope.Request,
302
- provide: UserService,
303
- useClass: UserService,
304
- };
305
- ```
306
- 2. Within the [@Injectable](https://adimarianmutu.github.io/x-injection/functions/Injectable.html) decorator:
307
- ```ts
308
- @Injectable(InjectionScope.Transient)
309
- class Transaction {}
310
- ```
311
- 3. By providing the [defaultScope](https://adimarianmutu.github.io/x-injection/interfaces/ProviderModuleOptions.html#defaultscope) property when initializing a `ProviderModule`:
312
- ```ts
313
- const RainModuleDef = new ProviderModuleDef({
314
- id: 'RainModule',
315
- defaultScope: InjectionScope.Transient,
316
- });
317
- ```
318
-
319
- > [!NOTE]
320
- >
321
- > _Imported modules/providers retain their original `InjectionScope`!_
322
-
323
- ### Singleton
324
-
325
- The [Singleton](https://adimarianmutu.github.io/x-injection/enums/InjectionScope.html#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.
326
-
327
- Example:
328
-
329
- ```ts
330
- expect(MyModule.get(MyProvider)).toBe(MyModule.get(MyProvider));
331
- // true
332
- ```
333
-
334
- ### Transient
335
-
336
- The [Transient](https://adimarianmutu.github.io/x-injection/enums/InjectionScope.html#transient) injection scope means that a _new_ instance of the dependency will be used whenever a resolution occurs.
337
-
338
- Example:
339
-
340
- ```ts
341
- expect(MyModule.get(MyProvider)).toBe(MyModule.get(MyProvider));
342
- // false
343
- ```
344
-
345
- ### Request
346
-
347
- The [Request](https://adimarianmutu.github.io/x-injection/enums/InjectionScope.html#request) injection scope means that the _same_ instance will be used when a resolution happens in the _same_ request scope.
348
-
349
- Example:
350
-
351
- ```ts
352
- @Injectable(InjectionScope.Transient)
353
- class Book {
354
- author: string;
355
- }
356
-
357
- @Injectable(InjectionScope.Request)
358
- class Metro2033 extends Book {
359
- override author = 'Dmitry Alekseyevich Glukhovsky';
360
- }
361
-
362
- @Injectable(InjectionScope.Transient)
363
- class Library {
364
- constructor(
365
- public readonly metro2033: Metro2033,
366
- public readonly metro2033_reference: Metro2033
367
- ) {}
368
- }
369
-
370
- const winstonLibrary = MyModule.get(Library);
371
- const londonLibrary = MyModule.get(Library);
372
-
373
- expect(winstonLibrary.metro2033).toBe(winstonLibrary.metro2033_reference);
374
- expect(londonLibrary.metro2033).toBe(londonLibrary.metro2033_reference);
375
- // true
376
-
377
- expect(winstonLibrary.metro2033).toBe(londonLibrary.metro2033);
378
- // false
379
- ```
380
-
381
- ## Provider Tokens
382
-
383
- A [ProviderToken](https://adimarianmutu.github.io/x-injection/types/ProviderToken.html) is another core block of `xInjection` _(and also many other IoC/DI libs)_ which is used to define a `token` which can then be used to resolve a `provider`.
384
-
385
- `xInjection` offers _4_ types of tokens:
386
-
387
- - [ProviderIdentifier](https://adimarianmutu.github.io/x-injection/types/ProviderIdentifier.html)
388
- - It allows you to bind a `value` to a specific _transparent_ token, like a `Class`, `Function`, `symbol` or `string`:
389
- ```ts
390
- const API_SERVICE_PROVIDER = ApiService;
391
- // or
392
- const CONSTANT_SECRET_PROVIDER = 'Shh';
393
- ```
394
- - [ProviderClassToken](https://adimarianmutu.github.io/x-injection/types/ProviderClassToken.html)
395
-
396
- - It can be used define the token _and_ the provider:
397
-
398
- ```ts
399
- const HUMAN_SERVICE_PROVIDER = { provide: HumanService, useClass: FemaleService };
400
-
401
- // This will bind `HumanService` as the `token` and will resolve `FemaleService` from the container.
402
- ```
403
-
404
- - [ProviderValueToken](https://adimarianmutu.github.io/x-injection/types/ProviderValueToken.html)
405
-
406
- - It can be used to easily bind _constant_ values, it can be anything, but once resolved it'll be cached and re-used upon further resolutions
407
-
408
- ```ts
409
- const THEY_DONT_KNOW_PROVIDER = { provide: CONSTANT_SECRET_PROVIDER, useValue: `They'll never know` };
410
- const THEY_MAY_KNOW_PROVIDER = { provide: CONSTANT_SECRET_PROVIDER, useValue: 'Maybe they know?' };
411
-
412
- // As you can see we now have 2 different ProviderTokens which use the same `provide` key.
413
- // This means that resolving the `CONSTANT_SECRET_PROVIDER` will return an array of strings.
414
- ```
415
-
416
- - [ProviderFactoryToken](https://adimarianmutu.github.io/x-injection/types/ProviderFactoryToken.html)
417
-
418
- - It can be used to bind a `factory` which is intended for more complex scenarios:
419
-
420
- ```ts
421
- const MAKE_PIZZA_PROVIDER = {
422
- provide: 'MAKE_PIZZA',
423
- useFactory: async (apiService: ApiService, pizzaService: PizzaService) => {
424
- const typeOfPizza = await apiService.getTypeOfPizza();
425
-
426
- if (typeOfPizza === 'margherita') return pizzaService.make.margherita;
427
- if (typeOfPizza === 'quattro_stagioni') return pizzaService.make.quattroStagioni;
428
- // and so on
429
- },
430
- // optional
431
- inject: [API_SERVICE_PROVIDER, PizzaService],
432
- };
433
- ```
434
-
435
- These are all the available `ProviderToken` you can use.
436
-
437
- > [!NOTE]
438
- >
439
- > In `NestJS` and `Angular` you can't use a `ProviderToken` to _get_ a value, `xInjection` allows this pattern, but you must understand that what it actually does, is to use the _value_ from the `provide` property.
440
-
441
- ## Provider Modules
442
-
443
- As you already saw till here, everything relies around the `ProviderModule` class, so let's dive a little more deep into understanding it.
444
-
445
- The most straight forward way to _create/instantiate_ a new `module` is:
446
-
447
- ```ts
448
- const MyModule = ProviderModule.create({
449
- id: 'MyModule',
450
- imports: [AnotherModule, SecondModule, ThirdModule],
451
- providers: [
452
- { provide: CONSTANT_SECRET_PROVIDER, useValue: 'ultra secret' },
453
- PizzaService,
454
- { provide: HumanService, useClass: FemaleService },
455
- ],
456
- exports: [SecondModule, ThirdModule, PizzaService],
457
- });
458
- ```
459
-
460
- From what we can see, the `MyModule` is importing into it 3 more modules, each of them may export one or more _(maybe nothing, that's valid too)_ providers, or even other `modules`.
461
- Because we imported them into the `MyModule`, now we have access to any providers they may have chosen to export, and the same is true also for _their exported_ modules.
462
-
463
- Then, we've chosen to _re-export_ from the `MyModule` the `SecondModule` and `ThirdModule`, meaning that if a different `module` imports `MyModule`, it'll automatically get access to those 2 modules as well. And in the end we also exported our own `PizzaService`, while the remaining other 2 providers, `CONSTANT_SECRET_PROVIDER` and `HumanService` can't be accessed when importing `MyModule`.
464
-
465
- This is the _core_ feature of `xInjection` _(and `Angular`/`NestJS` DI system)_, being able to encapsulate the providers, so nothing can spill out without our explicit consent.
466
-
467
- ---
468
-
469
- We could also achieve the above by using the `ProviderModule` API like this:
470
-
471
- ```ts
472
- MyModule.update.addImport(AnotherModule);
473
- MyModule.update.addImport(SecondModule, true); // `true` means "also add to the `exports` definition"
474
- MyModule.update.addImport(ThirdModule, true);
475
-
476
- MyModule.update.addProvider({ provide: CONSTANT_SECRET_PROVIDER, useValue: 'ultra secret' });
477
- MyModule.update.addProvider(PizzaService, true);
478
- MyModule.update.addProvider({ provide: HumanService, useClass: FemaleService });
479
- ```
480
-
481
- Now you may probably ask yourself `If we import with the 'addImport' method a new module into an already imported module, will we have access to the providers of that newly imported module?`
482
-
483
- The ansuwer is `yes`, we do have access thanks to the _dynamic_ nature of the `ProviderModule` class. Meaning that doing the following will work as expected:
484
-
485
- ```ts
486
- const InnerModule = ProviderModule.create({
487
- id: 'InnerModule',
488
- providers: [FirstService],
489
- exports: [FirstService],
490
- });
491
-
492
- const OuterModule = ProviderModule.create({
493
- id: 'OuterModule',
494
- imports: [InnerModule],
495
- });
496
-
497
- const UnknownModule = ProviderModule.create({
498
- id: 'UnknownModule',
499
- providers: [SecondService],
500
- exports: [SecondService],
501
- });
502
-
503
- InnerModule.update.addImport(UnknownModule, true); // Don't forget to provide `true` to the `addToExports` optional parameter!
504
-
505
- const secondService = OuterModule.get(SecondService);
506
- ```
507
-
508
- The `OuterModule` has now access to the `UnknownModule` exports because it has been _dynamically_ imported _(later at run-time)_ into the `InnerModule` _(which has been imported into `OuterModule` during the `bootstrap` phase)_
509
-
510
- Basically what happens is that when a `module` is imported, it takes care of _notify_ the `host` module if its _definiton_ changed.
511
-
512
- > [!WARNING]
513
- >
514
- > This is a very powerful feature which comes in with some costs, _most of the time negligible_, but if you have an app which has thousand and thousand of `modules` doing this type of _dynamic_ behavior, you may incur in some performance issues which will require proper design to keep under control.
515
- >
516
- > _Most of the times the best solution is to leverage the nature of `blueprints`._
517
-
518
- ---
519
-
520
- Sometimes you may actually want to _lazy_ import a `module` from a _file_, this can be done very easily with `xInjection`:
521
-
522
- ```ts
523
- (async () => {
524
- await MyModule.update.addImportLazy(async () => (await import('./lazy.module')).LazyModule);
525
-
526
- MyModule.isImportingModule('LazyModule');
527
- // => true
528
- })();
529
- ```
530
-
531
- > [!TIP]
532
- >
533
- > This design pattern is _extremely_ powerful and useful when you may have a lot of `modules` initializing during the app bootstrap process as you can defer their initialization, or even never load them if the user never needs those specific `modules` _(this is mostly applicable on the client-side rather than the server-side)_
534
-
535
- Keep reading to understand how you can defer initialization of the `modules` by using `blueprints`.
536
-
537
- ## Blueprints
538
-
539
- The [ProviderModuleBlueprint](https://adimarianmutu.github.io/x-injection/classes/ProviderModuleBlueprint.html) `class` main purpose is to encapsulate the `definitions` of a `Module`, when you do `ProviderModule.blueprint({...})` you are _not_ actually creating an instance of the `ProviderModule` class, but an instance of the `ProviderModuleBlueprint` class.
540
-
541
- > To better understand the above concept; imagine the `blueprint` as being a _dormant_ _(static)_ `module` which is not fully awake _(dynamic)_ till it is actually _imported_ into a `module`.
542
-
543
- ### Import Behavior
544
-
545
- Whenever you _import_ a `blueprint` into a `module`, it'll automatically be "transformed" to a `ProviderModule` instance by the engine, this step is crucial as a `blueprint` per se does not contain a _container_, just its _definitions_.
546
-
547
- > [!NOTE]
548
- >
549
- > Therefore it is important to understand the _injection_ `scope` of an imported `blueprint`; we previously learned that when we import a `blueprint` into a `module` it automatically creates an instance of the `ProviderModule` from it, this means that all the `singleton` providers of the `blueprint` definition are now _scoped singleton_, where _scoped_ means _singleton in relation to their imported module_.
550
-
551
- ### isGlobal
552
-
553
- When you initialize a `blueprint` with the [isGlobal](https://adimarianmutu.github.io/x-injection/interfaces/ProviderModuleOptions.html#isglobal) property set to `true`, the out-of-the-box behavior is to _automatically_ import the `blueprint` into the `AppModule`. You can disable this behavior by setting the [autoImportIntoAppModuleWhenGlobal](https://adimarianmutu.github.io/x-injectioninterfaces/ModuleBlueprintOptions.html#autoimportintoappmodulewhenglobal) property to `false`
554
-
555
- ```ts
556
- const GlobalModuleBp = ProviderModule.blueprint({..., isGlobal: true }, { autoImportIntoAppModuleWhenGlobal: false });
557
- ```
558
-
559
- Now you can decide when to import it into the `AppModule` by doing `AppModule.addImport(GlobalModuleBp)`.
560
-
561
- ---
562
-
563
- I highly recommend to take advantage of the `blueprints` nature in order to plan-ahead your `modules`;
564
-
565
- Why?
566
-
567
- - To _define module configurations upfront_ without incurring the cost of immediate initialization _(even if negligible)_.
568
- - To reuse module _definitions across_ different parts of your application while maintaining isolated instances. _(when possible/applicable)_
569
- - To _compose modules flexibly_, allowing you to adjust module dependencies dynamically before instantiation.
570
-
571
- ### Definitions
572
-
573
- After you have provided the _initial_ `definitons` of a `blueprint`, you can always modify them with the [updateDefinition](https://adimarianmutu.github.io/x-injection/classes/ProviderModuleBlueprint.html#updatedefinition) `method`.
574
-
575
- > [!NOTE]
576
- >
577
- > Updating the `definitions` of a `blueprint` after has been _imported_ into a `module` will **_not_** propagate those changes to the `module` where it has been imported.
578
-
579
- ---
580
-
581
- This means that we can actually _leverage_ the `blueprints` nature to _defer_ the actual initialization of a `module` by doing so:
582
-
583
- ```ts
584
- const UserModuleBp = ProviderModule.blueprint({
585
- id: 'UserModule',
586
- ...
587
- });
588
-
589
- // Later in your code
590
-
591
- const UserModule = ProviderModule.create(UserModuleBp);
592
- ```
593
-
594
- The `UserModule` will be created only when _necessary_ and it'll use the same exact definitons which are available into the `UserModuleBp` at the time of the `create` invokation.
595
-
596
- ## Advanced Usage
597
-
598
- > [!WARNING]
599
- >
600
- > This section covers advanced features which may add additional complexity _(or even bugs)_ to your application if you misuse them, use these features only if truly needed and after evaluating the _pros_ and _cons_ of each.
601
-
602
- ### Events
603
-
604
- Each `module` will emit specific events through its life-cycle and you can intercept them by using the `Module.update.subscribe` method.
605
-
606
- > [!TIP]
607
- >
608
- > [Here](https://adimarianmutu.github.io/x-injection/enums/DefinitionEventType.html) you can see all the available `events`
609
-
610
- If you'd need to intercept a `get` request, you can achieve that by doing:
611
-
612
- ```ts
613
- const CarModule = ProviderModule.create({
614
- id: 'CarModule',
615
- providers: [CarService],
616
- });
617
-
618
- CarModule.update.subscribe(({ type, change }) => {
619
- // We are interested only in the `GetProvider` event.
620
- if (type !== DefinitionEventType.GetProvider) return;
621
-
622
- // As our `CarModule` has only one provider, it is safe to assume
623
- // that the `change` will always be the `CarService` instance.
624
- const carService = change as CarService;
625
-
626
- console.log('CarService: ', carService);
627
- });
628
-
629
- const carService = CarModule.get(CarService);
630
- // logs => CarService: <instance_of_car_service_here>
631
- ```
632
-
633
- > [!WARNING]
634
- >
635
- > After subscribing to a `ProviderModule` signal emission, you should make sure to also `unsubscribe` if you don't need anymore to intercept the changes, not doing
636
- > so may cause memory leaks if you have lots of `subscriptions` which do heavy computations!
637
-
638
- The `subscribe` method will _always_ return a `method` having the signature `() => void`, when you invoke it, it'll close the pipe which intercepts the signal emitted by the `module`:
639
-
640
- ```ts
641
- const unsubscribe = CarModule.update.subscribe(({ type, change }) => {
642
- /* heavy computation here */
643
- });
644
-
645
- // later in your code
646
-
647
- unsubscribe();
648
- ```
649
-
650
- > [!NOTE]
651
- >
652
- > Events are _always_ invoked _after_ middlewares
653
-
654
- ### Middlewares
655
-
656
- Using middlewares is not encouraged as it allows you to tap into very deep low-level code which can cause unexpected bugs if not implemented carefully, however, `middlewares` are the perfect choice if you want to extend/alter the standard behavior of `module` as it allows you to decide what should happen with a resolved value _before_ it is returned to the `consumer`.
657
-
658
- > [!TIP]
659
- >
660
- > [Here](https://adimarianmutu.github.io/x-injection/enums/MiddlewareType.html) you can see all the available `middlewares`
661
-
662
- Let's say that you want to wrap all the returned values of a specific `module` within an object having this signature `{ timestamp: number; value: any }`. By using the `GetProvider` event will not do the trick because it _doesn't_ allow you to alter/change the actual returned value to the `consumer`, you can indeed alter the _content_ via reference, but not the _actual_ result.
663
-
664
- So the easiest way to achieve that is by using the `BeforeGet` middleware as shown below:
665
-
666
- ```ts
667
- const TransactionModule = ProviderModule.create(TransactionModuleBp);
668
-
669
- TransactionModule.middlewares.add(MiddlewareType.BeforeGet, (provider, providerToken, inject) => {
670
- // We are interested only in the `providers` instances which are from the `Payment` class
671
- if (!(provider instanceof Payment)) return true;
672
- // or
673
- if (providerToken !== 'LAST_TRANSACTION') return true;
674
-
675
- // DON'T do this as you'll encounter an infinite loop
676
- const transactionService = TransactionModule.get(TransactionService);
677
- // If you have to inject into the middleware `context` from the `module`
678
- // use the `inject` parameter
679
- const transactionService = inject(TransactionService);
680
-
681
- return {
682
- timestamp: transactionService.getTimestamp(),
683
- value: provider,
684
- };
685
- });
686
-
687
- const transaction = TransactionModule.get('LAST_TRANSACTION');
688
- // transaction => { timestamp: 1363952948, value: <Payment_instance> }
689
- ```
690
-
691
- One more example is to add a `middleware` in order to _dynamically_ control which `modules` can import a specific `module` by using the [OnExportAccess](https://adimarianmutu.github.io/x-injection/enums/MiddlewareType.html#onexportaccess) flag.
692
-
693
- ```ts
694
- const UnauthorizedBranchBankModule = ProviderModule.create({ id: 'UnauthorizedBranchBankModule' });
695
- const SensitiveBankDataModule = ProviderModule.create({
696
- id: 'SensitiveBankDataModule',
697
- providers: [SensitiveBankDataService, NonSensitiveBankDataService],
698
- exports: [SensitiveBankDataService, NonSensitiveBankDataService],
699
- });
700
-
701
- SensitiveBankDataModule.middlewares.add(MiddlewareType.OnExportAccess, (importerModule, currentExport) => {
702
- // We want to deny access to our `SensitiveBankDataService` from the `exports` definition if the importer module is `UnauthorizedBranchBankModule`
703
- if (importerModule.toString() === 'UnauthorizedBranchBankModule' && currentExport === SensitiveBankDataService)
704
- return false;
705
-
706
- // Remaining module are able to import all our `export` definition
707
- // The `UnauthorizedBranchBankModule` is unable to import the `SensitiveBankDataService`
708
- return true;
709
- });
710
- ```
711
-
712
- > [!CAUTION]
713
- >
714
- > Returning `false` in a `middleware` will abort the chain, meaning that for the above example, no value would be returned.
715
- > If you have to explicitly return a `false` boolean value, you may have to wrap your provider value as an workaround. _(`null` is accepted as a return value)_
716
- >
717
- > Meanwhile returning `true` means _"return the value without changing it"_.
718
- >
719
- > In the future this behavior may change, so if your business logic relies a lot on `middlewares` make sure to stay up-to-date with the latest changes.
720
-
721
- It is also worth mentioning that you can apply _multiple_ middlewares by just invoking the `middlewares.add` method multiple times, they are executed in the same exact order as you applied them, meaning that the first invokation to `middlewares.add` will actually be the `root` of the chain.
722
-
723
- If no error is thrown down the chain, all the registered middleware `callback` will be supplied with the necessary values.
724
-
725
- > [!WARNING]
726
- >
727
- > It is the _developer_ responsability to catch any error down the `chain`!
728
-
729
- ### Internals
730
-
731
- If you are not interested in understanding how `xInjection` works under the hood, you can skip this section 😌
732
-
733
- #### ProviderModule
734
-
735
- It is the head of everything, a `ProviderModule` is actually composed by several classes, each with its own purpose.
736
-
737
- > [!TIP]
738
- >
739
- > You can get access to _all_ the internal instances by doing `new ProviderModule({...})` instead of `ProviderModule.create({...})`
740
-
741
- #### MiddlewaresManager
742
-
743
- It is the `class` which takes care of managing the registered `middlewares`, check it out [here](https://adimarianmutu.github.io/x-injection/classes/MiddlewaresManager.html).
744
-
745
- Not much to say about it as its main role is to _register_ and _build_ the `middleware` chain.
746
-
747
- #### ModuleContainer
748
-
749
- It is the `class` which takes care of managing the `inversify` container, check it out [here](https://adimarianmutu.github.io/x-injection/classes/ModuleContainer.html).
750
-
751
- Its main purpose is to initialize the module raw _([InversifyJS Container](https://inversify.io/docs/api/container/))_ `class` and to _bind_ the providers to it.
752
-
753
- #### ImportedModuleContainer
754
-
755
- It is the `class` which takes care of managing the _imported_ modules, check it out [here](https://adimarianmutu.github.io/x-injection/classes/ImportedModuleContainer.html).
756
-
757
- Because `modules` can be imported into other modules, therefore creating a _complex_ `graph` of modules, the purpose of this class is to keep track and sync the changes of the `exports` definition of the _imported_ module.
758
-
759
- The `ProviderModule` API is simple yet very powerful, you may not realize that doing `addImport` will cause _(based on how deep is the imported module)_ a chain reaction which the `ImportedModuleContainer` must keep track of in order to make sure that the _consumer_ `module` which imported the _consumed_ `module` has access only to the `providers`/`modules` explicitly exported by the _consumed_ `module`.
760
-
761
- Therefore it is encouraged to keep things mostly static, as each `addProvider`, `addImport`, `removeImport` and so on have a penality cost on your application performance. This cost in most cases is negligible, however it highly depends on how the _developer_ uses the feature `xInjection` offers.
762
-
763
- > "With great power comes great responsibility."
764
-
765
- #### DynamicModuleDefinition
766
-
767
- It is the `class` which takes care of managing the _updates_ and _event_ emissions of the `module`, check it out [here](https://adimarianmutu.github.io/x-injection/classes/DynamicModuleDefinition.html).
768
-
769
- This class is actually the "parent" of the `ImportedModuleContainer` instances, its purpose is to _build_ the _initial_ definition graph, and while doing so it also instantiate _for each_ imported module a new `ImportedModuleContainer`.
770
-
771
- It also take care of managing the `events` bubbling by checking cirular references and so on.
772
-
773
- #### ProviderModuleBlueprint
774
-
775
- It's the "metadata" counterpart of the `ProviderModule` class, as its only purpose is to carry the definitions. Check it out [here](https://adimarianmutu.github.io/x-injection/classes/ProviderModuleBlueprint.html).
776
-
777
- #### Set of Helpers
778
-
779
- The library does also export a set of useful helpers in the case you may need it:
780
-
781
- ```ts
782
- import { ProviderModuleHelpers, ProviderTokenHelpers } from '@adimm/x-injection';
783
- ```
784
-
785
- ---
786
-
787
- This covers pretty much everything about how `xInjection` is built and how it works.
788
-
789
- ## Unit Tests
790
-
791
- It is very easy to create mock modules so you can use them in your unit tests.
792
-
793
- ```ts
794
- class ApiService {
795
- constructor(private readonly userService: UserService) {}
796
-
797
- async sendRequest<T>(location: LocationParams): Promise<T> {
798
- // Pseudo Implementation
799
- return this.sendToLocation(user, location);
800
- }
801
-
802
- private async sendToLocation(user: User, location: any): Promise<any> {}
803
- }
804
-
805
- const ApiModuleBp = new ProviderModule.blueprint({
806
- id: 'ApiModule',
807
- providers: [UserService, ApiService],
808
- });
809
-
810
- // Clone returns a `deep` clone and wraps all the `methods` to break their reference!
811
- const ApiModuleBpMocked = ApiModuleBp.clone().updateDefinition({
812
- id: 'ApiModuleMocked',
813
- providers: [
814
- {
815
- provide: UserService,
816
- useClass: UserService_Mock,
817
- },
818
- {
819
- provide: ApiService,
820
- useValue: {
821
- sendRequest: async (location) => {
822
- console.log(location);
823
- },
824
- },
825
- },
826
- ],
827
- });
828
- ```
829
-
830
- Now what you have to do is just to provide the `ApiModuleBpMocked` instead of the `ApiModuleBp` 😎
831
-
832
- ## Documentation
833
-
834
- Comprehensive, auto-generated documentation is available at:
835
-
836
- 👉 [https://adimarianmutu.github.io/x-injection/index.html](https://adimarianmutu.github.io/x-injection/index.html)
837
-
838
- ## ReactJS Implementation
839
-
840
- You want to use it within a [ReactJS](https://react.dev/) project? Don't worry, the library does already have an official implementation for React ⚛️
841
-
842
- For more details check out the [GitHub Repository](https://github.com/AdiMarianMutu/x-injection-reactjs).
843
-
844
- ## Contributing
845
-
846
- Pull requests are warmly welcomed! 😃
847
-
848
- Please ensure your contributions adhere to the project's code style. See the repository for more details.
849
-
850
- ## Credits
851
-
852
- - [Adi-Marian Mutu](https://www.linkedin.com/in/mutu-adi-marian/) - Author of `xInjection`
853
- - [InversifyJS](https://github.com/inversify/monorepo) - Base lib
854
- - [Alexandru Turica](https://www.linkedin.com/in/alexandru-turica-82215522b/) - Official Logo
855
-
856
- ---
857
-
858
- > [!NOTE]
859
- >
860
- > **For questions, feature requests, or bug reports, feel free to open an [issue](https://github.com/AdiMarianMutu/x-injection/issues) on GitHub.**
1
+ <p align="center">
2
+ <img width="260px" height="auto" alt="xInjection Logo" src="https://raw.githubusercontent.com/AdiMarianMutu/x-injection/main/assets/logo.png"><br /><a href="https://www.npmjs.com/package/@adimm/x-injection" target="__blank"><img src="https://badgen.net/npm/v/@adimm/x-injection"></a>
3
+ <a href="https://app.codecov.io/gh/AdiMarianMutu/x-injection" target="__blank"><img src="https://badgen.net/codecov/c/github/AdiMarianMutu/x-injection"></a>
4
+ <img src="https://badgen.net/npm/license/@adimm/x-injection">
5
+ </p>
6
+
7
+ <p align="center">
8
+ <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>
9
+ <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>
10
+ <br>
11
+ <img src="https://badgen.net/bundlephobia/minzip/@adimm/x-injection">
12
+ <a href="https://www.npmjs.com/package/@adimm/x-injection" target="__blank"><img src="https://badgen.net/npm/dm/@adimm/x-injection"></a>
13
+
14
+ </p>
15
+
16
+ **A powerful, modular dependency injection library for TypeScript** — Built on [InversifyJS](https://github.com/inversify/InversifyJS), inspired by [NestJS](https://github.com/nestjs/nest)'s elegant module architecture.
17
+
18
+ ## Table of Contents
19
+
20
+ - [Table of Contents](#table-of-contents)
21
+ - [What Problems Does This Solve?](#what-problems-does-this-solve)
22
+ - [Problem 1: Manual Dependency Wiring](#problem-1-manual-dependency-wiring)
23
+ - [Problem 2: Tight Coupling and Testing Difficulty](#problem-2-tight-coupling-and-testing-difficulty)
24
+ - [Problem 3: Lack of Encapsulation](#problem-3-lack-of-encapsulation)
25
+ - [Problem 4: Lifecycle Management Complexity](#problem-4-lifecycle-management-complexity)
26
+ - [Overview](#overview)
27
+ - [Features](#features)
28
+ - [Installation](#installation)
29
+ - [Quick Start](#quick-start)
30
+ - [Core Concepts](#core-concepts)
31
+ - [Services with @Injectable](#services-with-injectable)
32
+ - [Modules](#modules)
33
+ - [Blueprints](#blueprints)
34
+ - [AppModule](#appmodule)
35
+ - [OOP-Style Modules with ProviderModuleClass](#oop-style-modules-with-providermoduleclass)
36
+ - [Basic OOP Module](#basic-oop-module)
37
+ - [Advanced OOP Patterns](#advanced-oop-patterns)
38
+ - [When to Use OOP vs Functional](#when-to-use-oop-vs-functional)
39
+ - [Provider Tokens](#provider-tokens)
40
+ - [1. Class Token](#1-class-token)
41
+ - [2. Class Token with Substitution](#2-class-token-with-substitution)
42
+ - [3. Value Token](#3-value-token)
43
+ - [4. Factory Token](#4-factory-token)
44
+ - [Injection Scopes](#injection-scopes)
45
+ - [Singleton (Default)](#singleton-default)
46
+ - [Transient](#transient)
47
+ - [Request](#request)
48
+ - [Scope Priority Order](#scope-priority-order)
49
+ - [Module System](#module-system)
50
+ - [Import/Export Pattern](#importexport-pattern)
51
+ - [Re-exporting Modules](#re-exporting-modules)
52
+ - [Dynamic Module Updates](#dynamic-module-updates)
53
+ - [Global Modules](#global-modules)
54
+ - [Dependency Injection](#dependency-injection)
55
+ - [Constructor Injection](#constructor-injection)
56
+ - [@Inject Decorator](#inject-decorator)
57
+ - [@MultiInject Decorator](#multiinject-decorator)
58
+ - [Optional Dependencies](#optional-dependencies)
59
+ - [Lifecycle Hooks](#lifecycle-hooks)
60
+ - [onReady Hook](#onready-hook)
61
+ - [onReset Hook](#onreset-hook)
62
+ - [onDispose Hook](#ondispose-hook)
63
+ - [Events System](#events-system)
64
+ - [Subscribing to Events](#subscribing-to-events)
65
+ - [Available Event Types](#available-event-types)
66
+ - [Event Use Cases](#event-use-cases)
67
+ - [Middlewares](#middlewares)
68
+ - [BeforeGet Middleware](#beforeget-middleware)
69
+ - [BeforeAddProvider Middleware](#beforeaddprovider-middleware)
70
+ - [BeforeAddImport Middleware](#beforeaddimport-middleware)
71
+ - [OnExportAccess Middleware](#onexportaccess-middleware)
72
+ - [BeforeRemoveImport Middleware](#beforeremoveimport-middleware)
73
+ - [BeforeRemoveProvider Middleware](#beforeremoveprovider-middleware)
74
+ - [BeforeRemoveExport Middleware](#beforeremoveexport-middleware)
75
+ - [All Available Middleware Types](#all-available-middleware-types)
76
+ - [Testing](#testing)
77
+ - [Blueprint Cloning](#blueprint-cloning)
78
+ - [Provider Substitution](#provider-substitution)
79
+ - [Mocking Services](#mocking-services)
80
+ - [Advanced Module API](#advanced-module-api)
81
+ - [Query Methods](#query-methods)
82
+ - [Multiple Provider Binding](#multiple-provider-binding)
83
+ - [Batch Resolution with getMany()](#batch-resolution-with-getmany)
84
+ - [Resources](#resources)
85
+ - [Contributing](#contributing)
86
+ - [Credits](#credits)
87
+ - [License](#license)
88
+
89
+ ## What Problems Does This Solve?
90
+
91
+ Modern applications face several dependency management challenges. Let's examine these problems and how xInjection solves them.
92
+
93
+ ### Problem 1: Manual Dependency Wiring
94
+
95
+ **Without xInjection:**
96
+
97
+ ```ts
98
+ // Manually creating and wiring dependencies
99
+ class DatabaseService {
100
+ constructor(private config: ConfigService) {}
101
+ }
102
+
103
+ class UserRepository {
104
+ constructor(private db: DatabaseService) {}
105
+ }
106
+
107
+ class AuthService {
108
+ constructor(private userRepo: UserRepository) {}
109
+ }
110
+
111
+ // Manual instantiation nightmare
112
+ const config = new ConfigService();
113
+ const database = new DatabaseService(config);
114
+ const userRepo = new UserRepository(database);
115
+ const authService = new AuthService(userRepo);
116
+
117
+ // Every file needs to repeat this setup
118
+ // Changes to constructors require updating all instantiation sites
119
+ ```
120
+
121
+ **With xInjection:**
122
+
123
+ ```ts
124
+ @Injectable()
125
+ class DatabaseService {
126
+ constructor(private config: ConfigService) {}
127
+ }
128
+
129
+ @Injectable()
130
+ class UserRepository {
131
+ constructor(private db: DatabaseService) {}
132
+ }
133
+
134
+ @Injectable()
135
+ class AuthService {
136
+ constructor(private userRepo: UserRepository) {}
137
+ }
138
+
139
+ const AuthModule = ProviderModule.create({
140
+ id: 'AuthModule',
141
+ providers: [ConfigService, DatabaseService, UserRepository, AuthService],
142
+ });
143
+
144
+ // Automatic dependency resolution
145
+ const authService = AuthModule.get(AuthService);
146
+ // All dependencies automatically injected!
147
+ ```
148
+
149
+ ### Problem 2: Tight Coupling and Testing Difficulty
150
+
151
+ **Without xInjection:**
152
+
153
+ ```ts
154
+ class PaymentService {
155
+ // Hardcoded dependency - impossible to mock
156
+ private stripe = new StripeClient('api-key');
157
+
158
+ async charge(amount: number) {
159
+ return this.stripe.charge(amount);
160
+ }
161
+ }
162
+
163
+ // Testing requires hitting the real Stripe API
164
+ // No way to inject a mock without changing production code
165
+ ```
166
+
167
+ **With xInjection:**
168
+
169
+ ```ts
170
+ @Injectable()
171
+ class PaymentService {
172
+ constructor(private paymentGateway: PaymentGateway) {}
173
+
174
+ async charge(amount: number) {
175
+ return this.paymentGateway.charge(amount);
176
+ }
177
+ }
178
+
179
+ // Production: Use real Stripe
180
+ const ProductionModule = ProviderModule.create({
181
+ providers: [{ provide: PaymentGateway, useClass: StripePaymentGateway }, PaymentService],
182
+ });
183
+
184
+ // Testing: Use mock (no production code changes needed!)
185
+ const TestModule = ProviderModule.create({
186
+ providers: [{ provide: PaymentGateway, useClass: MockPaymentGateway }, PaymentService],
187
+ });
188
+ ```
189
+
190
+ ### Problem 3: Lack of Encapsulation
191
+
192
+ **Without xInjection:**
193
+
194
+ ```ts
195
+ // Internal implementation details exposed
196
+ class CacheService {
197
+ // Should be private but other modules need access
198
+ public internalCache = new Map();
199
+ }
200
+
201
+ class DatabaseModule {
202
+ // Everything is public - no control over what gets used
203
+ public connection = createConnection();
204
+ public cache = new CacheService();
205
+ public queryBuilder = new QueryBuilder();
206
+ }
207
+
208
+ // Other modules can access internals they shouldn't
209
+ const cache = databaseModule.internalCache; // Bad!
210
+ ```
211
+
212
+ **With xInjection:**
213
+
214
+ ```ts
215
+ const DatabaseModule = ProviderModule.create({
216
+ id: 'DatabaseModule',
217
+ providers: [ConnectionPool, CacheService, QueryBuilder],
218
+ exports: [QueryBuilder], // Only expose public API
219
+ });
220
+
221
+ // Other modules can only access QueryBuilder
222
+ // ConnectionPool and CacheService remain internal
223
+ const ApiModule = ProviderModule.create({
224
+ imports: [DatabaseModule],
225
+ });
226
+
227
+ // ✅ Works - QueryBuilder is exported
228
+ const queryBuilder = ApiModule.get(QueryBuilder);
229
+
230
+ // ❌ Error - CacheService not exported (properly encapsulated!)
231
+ const cache = ApiModule.get(CacheService);
232
+ ```
233
+
234
+ ### Problem 4: Lifecycle Management Complexity
235
+
236
+ **Without xInjection:**
237
+
238
+ ```ts
239
+ class AppServices {
240
+ database: DatabaseService;
241
+ cache: CacheService;
242
+
243
+ async initialize() {
244
+ this.database = new DatabaseService();
245
+ await this.database.connect();
246
+
247
+ this.cache = new CacheService();
248
+ await this.cache.initialize();
249
+
250
+ // Manually track initialization order and cleanup
251
+ }
252
+
253
+ async cleanup() {
254
+ // Must remember to clean up in reverse order
255
+ await this.cache.dispose();
256
+ await this.database.disconnect();
257
+ }
258
+ }
259
+
260
+ // Easy to forget cleanup, leading to resource leaks
261
+ ```
262
+
263
+ **With xInjection:**
264
+
265
+ ```ts
266
+ const AppModule = ProviderModule.create({
267
+ id: 'AppModule',
268
+ providers: [DatabaseService, CacheService],
269
+ onReady: async (module) => {
270
+ // Initialization logic - called immediately after module creation
271
+ const db = module.get(DatabaseService);
272
+ await db.connect();
273
+ },
274
+ onDispose: () => {
275
+ return {
276
+ before: async (mod) => {
277
+ // Automatic cleanup in proper order
278
+ const db = mod.get(DatabaseService);
279
+ await db.disconnect();
280
+ },
281
+ };
282
+ },
283
+ });
284
+
285
+ // Lifecycle automatically managed
286
+ await AppModule.dispose(); // Everything cleaned up properly
287
+ ```
288
+
289
+ xInjection transforms these pain points into elegant, maintainable code through:
290
+
291
+ - **Automatic dependency resolution** - No manual wiring
292
+ - **Inversion of Control** - Easy testing and flexibility
293
+ - **Encapsulation** - Fine-grained control over module boundaries
294
+ - **Lifecycle hooks** - Proper initialization and cleanup
295
+ - **Modular architecture** - Scalable application structure
296
+
297
+ ## Overview
298
+
299
+ **xInjection** is a powerful [Dependency Injection](https://en.wikipedia.org/wiki/Dependency_injection) library built on [InversifyJS](https://github.com/inversify/InversifyJS), inspired by [NestJS](https://github.com/nestjs/nest)'s modular architecture. It provides fine-grained control over dependency encapsulation through a module-based system where each module manages its own container with explicit import/export boundaries.
300
+
301
+ ## Features
302
+
303
+ - **Modular Architecture** - NestJS-style import/export system for clean dependency boundaries
304
+ - **Isolated Containers** - Each module manages its own InversifyJS container
305
+ - **Flexible Scopes** - Singleton, Transient, and Request-scoped providers
306
+ - **Lazy Loading** - Blueprint pattern for deferred module instantiation
307
+ - **Lifecycle Hooks** - `onReady`, `onReset`, `onDispose` for module lifecycle management
308
+ - **Events & Middlewares** - Deep customization through event subscriptions and middleware chains
309
+ - **OOP Support** - `ProviderModuleClass` for class-based module architecture
310
+ - **Framework Agnostic** - Works in Node.js and browser environments
311
+ - **TypeScript First** - Full type safety with decorator support
312
+
313
+ ## Installation
314
+
315
+ ```bash
316
+ npm install @adimm/x-injection reflect-metadata
317
+ ```
318
+
319
+ **TypeScript Configuration** (`tsconfig.json`):
320
+
321
+ ```json
322
+ {
323
+ "compilerOptions": {
324
+ "experimentalDecorators": true,
325
+ "emitDecoratorMetadata": true
326
+ }
327
+ }
328
+ ```
329
+
330
+ Import `reflect-metadata` at your application's entry point:
331
+
332
+ ```ts
333
+ import 'reflect-metadata';
334
+ ```
335
+
336
+ ## Quick Start
337
+
338
+ ```ts
339
+ import { Injectable, ProviderModule } from '@adimm/x-injection';
340
+
341
+ @Injectable()
342
+ class UserService {
343
+ getUser(id: string) {
344
+ return { id, name: 'John Doe' };
345
+ }
346
+ }
347
+
348
+ @Injectable()
349
+ class AuthService {
350
+ constructor(private userService: UserService) {}
351
+
352
+ login(userId: string) {
353
+ const user = this.userService.getUser(userId);
354
+ return `Logged in as ${user.name}`;
355
+ }
356
+ }
357
+
358
+ const AuthModule = ProviderModule.create({
359
+ id: 'AuthModule',
360
+ providers: [UserService, AuthService],
361
+ exports: [AuthService],
362
+ });
363
+
364
+ const authService = AuthModule.get(AuthService);
365
+ console.log(authService.login('123')); // "Logged in as John Doe"
366
+ ```
367
+
368
+ ## Core Concepts
369
+
370
+ ### Services with @Injectable
371
+
372
+ The `@Injectable()` decorator marks a class as available for dependency injection. It enables automatic constructor parameter resolution.
373
+
374
+ ```ts
375
+ import { Injectable, InjectionScope } from '@adimm/x-injection';
376
+
377
+ // Basic injectable service (Singleton by default)
378
+ @Injectable()
379
+ class LoggerService {
380
+ log(message: string) {
381
+ console.log(`[LOG] ${message}`);
382
+ }
383
+ }
384
+
385
+ // Injectable with scope specification
386
+ @Injectable(InjectionScope.Request)
387
+ class RequestContext {
388
+ requestId = Math.random();
389
+ }
390
+
391
+ // Complex service with dependencies
392
+ @Injectable()
393
+ class ApiService {
394
+ constructor(
395
+ private logger: LoggerService,
396
+ private context: RequestContext
397
+ ) {}
398
+
399
+ async fetchData() {
400
+ this.logger.log(`Fetching data for request ${this.context.requestId}`);
401
+ return { data: 'example' };
402
+ }
403
+ }
404
+ ```
405
+
406
+ > [!IMPORTANT]
407
+ > The `@Injectable()` decorator is **required** for any class that:
408
+ >
409
+ > - Has constructor dependencies
410
+ > - Needs to be resolved from a module container
411
+ > - Should be managed by the dependency injection system
412
+
413
+ ### Modules
414
+
415
+ Modules are the fundamental building blocks of xInjection. Each module encapsulates providers with explicit control over imports and exports.
416
+
417
+ ```ts
418
+ import { ProviderModule } from '@adimm/x-injection';
419
+
420
+ // Define services
421
+ @Injectable()
422
+ class DatabaseService {
423
+ query(sql: string) {
424
+ return [{ id: 1, name: 'Result' }];
425
+ }
426
+ }
427
+
428
+ @Injectable()
429
+ class InternalCacheService {
430
+ // Internal-only service
431
+ private cache = new Map();
432
+ }
433
+
434
+ @Injectable()
435
+ class UserRepository {
436
+ constructor(
437
+ private db: DatabaseService,
438
+ private cache: InternalCacheService
439
+ ) {}
440
+
441
+ findById(id: string) {
442
+ return this.db.query(`SELECT * FROM users WHERE id = ${id}`);
443
+ }
444
+ }
445
+
446
+ // Create module with encapsulation
447
+ const DatabaseModule = ProviderModule.create({
448
+ id: 'DatabaseModule',
449
+ providers: [DatabaseService, InternalCacheService, UserRepository],
450
+ exports: [UserRepository], // Only UserRepository accessible to importers
451
+ });
452
+
453
+ // Use the module
454
+ const ApiModule = ProviderModule.create({
455
+ id: 'ApiModule',
456
+ imports: [DatabaseModule],
457
+ });
458
+
459
+ // ✅ Works - UserRepository is exported
460
+ const userRepo = ApiModule.get(UserRepository);
461
+
462
+ // ❌ Throws error - InternalCacheService not exported
463
+ // const cache = ApiModule.get(InternalCacheService);
464
+ ```
465
+
466
+ **Key Module Methods:**
467
+
468
+ ```ts
469
+ const MyModule = ProviderModule.create({ id: 'MyModule' });
470
+
471
+ // Resolution
472
+ MyModule.get(ServiceClass); // Get provider instance
473
+ MyModule.getMany(Service1, Service2); // Get multiple providers
474
+
475
+ // Queries
476
+ MyModule.hasProvider(ServiceClass); // Check if provider exists
477
+ MyModule.isImportingModule('ModuleId'); // Check if importing module
478
+ MyModule.isExportingProvider(ServiceClass); // Check if exporting provider
479
+ MyModule.isExportingModule('ModuleId'); // Check if exporting module
480
+
481
+ // Dynamic updates
482
+ MyModule.update.addProvider(NewService); // Add provider
483
+ MyModule.update.addImport(OtherModule); // Add import
484
+ MyModule.update.removeProvider(ServiceClass); // Remove provider
485
+ MyModule.update.removeImport(OtherModule); // Remove import
486
+ MyModule.update.removeFromExports(Service); // Remove from exports
487
+
488
+ // Lifecycle
489
+ await MyModule.reset(); // Reset module state
490
+ await MyModule.dispose(); // Clean up and dispose
491
+ MyModule.isDisposed; // Check disposal state
492
+ ```
493
+
494
+ ### Blueprints
495
+
496
+ Blueprints allow you to define module configurations without instantiating them, enabling lazy loading and template reuse.
497
+
498
+ ```ts
499
+ import { ProviderModule } from '@adimm/x-injection';
500
+
501
+ @Injectable()
502
+ class ConfigService {
503
+ getConfig() {
504
+ return { apiUrl: 'https://api.example.com' };
505
+ }
506
+ }
507
+
508
+ // Define blueprint (not instantiated yet)
509
+ const ConfigModuleBp = ProviderModule.blueprint({
510
+ id: 'ConfigModule',
511
+ providers: [ConfigService],
512
+ exports: [ConfigService],
513
+ });
514
+
515
+ // Use blueprint in imports (auto-converts to module)
516
+ const ApiModule = ProviderModule.create({
517
+ id: 'ApiModule',
518
+ imports: [ConfigModuleBp], // Instantiated automatically when needed
519
+ });
520
+
521
+ // Or create module from blueprint explicitly
522
+ const ConfigModule = ProviderModule.create(ConfigModuleBp);
523
+
524
+ // Clone blueprints for testing
525
+ const ConfigModuleMock = ConfigModuleBp.clone().updateDefinition({
526
+ id: 'ConfigModuleMock',
527
+ providers: [{ provide: ConfigService, useValue: { getConfig: () => ({ apiUrl: 'mock' }) } }],
528
+ });
529
+ ```
530
+
531
+ **Benefits of Blueprints:**
532
+
533
+ - **Deferred Instantiation** - Only create modules when needed
534
+ - **Reusable Templates** - Define once, use in multiple places
535
+ - **Testing** - Clone and modify for test scenarios
536
+ - **Scoped Singletons** - Each importer gets its own module instance
537
+
538
+ > [!TIP]
539
+ > Use blueprints when you need the same module configuration in multiple places, or when you want to delay module creation until runtime.
540
+
541
+ ### AppModule
542
+
543
+ The `AppModule` is a special global root module that's automatically available throughout your application. Global modules are auto-imported into `AppModule`.
544
+
545
+ ```ts
546
+ import { AppModule, ProviderModule } from '@adimm/x-injection';
547
+
548
+ @Injectable()
549
+ class LoggerService {
550
+ log(message: string) {
551
+ console.log(message);
552
+ }
553
+ }
554
+
555
+ // Add global providers to AppModule
556
+ AppModule.update.addProvider(LoggerService);
557
+
558
+ // Access from any module without explicit import
559
+ const FeatureModule = ProviderModule.create({
560
+ id: 'FeatureModule',
561
+ // No need to import AppModule
562
+ });
563
+
564
+ const logger = FeatureModule.get(LoggerService);
565
+ logger.log('Hello from FeatureModule!');
566
+ ```
567
+
568
+ **Global Module Pattern:**
569
+
570
+ ```ts
571
+ // Create a global module (auto-imports into AppModule)
572
+ const LoggerModule = ProviderModule.create({
573
+ id: 'LoggerModule',
574
+ isGlobal: true,
575
+ providers: [LoggerService],
576
+ exports: [LoggerService],
577
+ });
578
+
579
+ // Now all modules have access to LoggerService
580
+ const AnyModule = ProviderModule.create({
581
+ id: 'AnyModule',
582
+ });
583
+
584
+ const logger = AnyModule.get(LoggerService); // Works!
585
+ ```
586
+
587
+ > [!WARNING]
588
+ >
589
+ > - Cannot create a module with `id: 'AppModule'` - this is reserved
590
+ > - Cannot import `AppModule` into other modules
591
+ > - Use global modules sparingly to avoid implicit dependencies
592
+
593
+ ## OOP-Style Modules with ProviderModuleClass
594
+
595
+ For developers who prefer class-based architecture, xInjection provides `ProviderModuleClass` - a composition-based wrapper that prevents naming conflicts between your custom methods and the DI container methods.
596
+
597
+ ### Basic OOP Module
598
+
599
+ ```ts
600
+ import { Injectable, ProviderModuleClass } from '@adimm/x-injection';
601
+
602
+ @Injectable()
603
+ class UserService {
604
+ get(id: string) {
605
+ return { id, name: 'John Doe' };
606
+ }
607
+ }
608
+
609
+ @Injectable()
610
+ class AuthService {
611
+ constructor(private userService: UserService) {}
612
+
613
+ login(userId: string) {
614
+ const user = this.userService.get(userId);
615
+ return `Logged in as ${user.name}`;
616
+ }
617
+ }
618
+
619
+ // OOP-style module extending ProviderModuleClass
620
+ class AuthModule extends ProviderModuleClass {
621
+ constructor() {
622
+ super({
623
+ id: 'AuthModule',
624
+ providers: [UserService, AuthService],
625
+ exports: [AuthService],
626
+ });
627
+ }
628
+
629
+ // Custom business logic methods
630
+ authenticateUser(userId: string): string {
631
+ const authService = this.module.get(AuthService);
632
+ return authService.login(userId);
633
+ }
634
+
635
+ getUserById(userId: string) {
636
+ const userService = this.module.get(UserService);
637
+ return userService.get(userId);
638
+ }
639
+
640
+ // Custom method named 'get' - no conflict!
641
+ get(): string {
642
+ return 'custom-get-value';
643
+ }
644
+ }
645
+
646
+ // Instantiate and use
647
+ const authModule = new AuthModule();
648
+
649
+ // Use custom methods
650
+ console.log(authModule.authenticateUser('123')); // "Logged in as John Doe"
651
+ console.log(authModule.get()); // "custom-get-value"
652
+
653
+ // Access DI container through .module property
654
+ const authService = authModule.module.get(AuthService);
655
+ authModule.module.update.addProvider(NewService);
656
+ ```
657
+
658
+ > [!IMPORTANT]
659
+ > All `ProviderModule` methods are available through the `.module` property to prevent naming conflicts with your custom methods.
660
+
661
+ ### Advanced OOP Patterns
662
+
663
+ **Module with Initialization Logic:**
664
+
665
+ ```ts
666
+ @Injectable()
667
+ class DatabaseService {
668
+ private connected = false;
669
+
670
+ async connect(): Promise<void> {
671
+ // Connection logic
672
+ this.connected = true;
673
+ }
674
+
675
+ isConnected(): boolean {
676
+ return this.connected;
677
+ }
678
+ }
679
+
680
+ class DatabaseModule extends ProviderModuleClass {
681
+ private isModuleConnected = false;
682
+
683
+ constructor() {
684
+ super({
685
+ id: 'DatabaseModule',
686
+ providers: [DatabaseService],
687
+ exports: [DatabaseService],
688
+ onReady: async (module) => {
689
+ console.log('DatabaseModule ready!');
690
+ },
691
+ });
692
+ }
693
+
694
+ async connect(): Promise<void> {
695
+ const dbService = this.module.get(DatabaseService);
696
+ await dbService.connect();
697
+ this.isModuleConnected = true;
698
+ }
699
+
700
+ getConnectionStatus(): boolean {
701
+ return this.isModuleConnected;
702
+ }
703
+ }
704
+
705
+ const dbModule = new DatabaseModule();
706
+ await dbModule.connect();
707
+ console.log(dbModule.getConnectionStatus()); // true
708
+ ```
709
+
710
+ **Module with Computed Properties:**
711
+
712
+ ```ts
713
+ @Injectable()
714
+ class ApiService {
715
+ makeRequest() {
716
+ return 'response';
717
+ }
718
+ }
719
+
720
+ @Injectable()
721
+ class HttpClient {
722
+ get(url: string) {
723
+ return `GET ${url}`;
724
+ }
725
+ }
726
+
727
+ class ApiModule extends ProviderModuleClass {
728
+ constructor() {
729
+ super({
730
+ id: 'ApiModule',
731
+ providers: [ApiService, HttpClient],
732
+ exports: [ApiService],
733
+ });
734
+ }
735
+
736
+ // Computed properties - lazy evaluation
737
+ get apiService(): ApiService {
738
+ return this.module.get(ApiService);
739
+ }
740
+
741
+ get httpClient(): HttpClient {
742
+ return this.module.get(HttpClient);
743
+ }
744
+
745
+ // Business logic using multiple services
746
+ async makeAuthenticatedRequest(url: string, token: string) {
747
+ const client = this.httpClient;
748
+ return client.get(url) + ` with token ${token}`;
749
+ }
750
+ }
751
+
752
+ const apiModule = new ApiModule();
753
+ const response = await apiModule.makeAuthenticatedRequest('/users', 'token123');
754
+ ```
755
+
756
+ ### When to Use OOP vs Functional
757
+
758
+ **Use OOP-style (`extends ProviderModuleClass`) when:**
759
+
760
+ - You need custom business logic methods on the module itself
761
+ - You prefer class-based architecture and inheritance patterns
762
+ - You want computed properties or getters for providers
763
+ - You need initialization logic or state management in the module
764
+ - You're building a complex module with multiple related operations
765
+ - You want to prevent naming conflicts (e.g., custom `get()` method)
766
+
767
+ **Use Functional-style (`ProviderModule.create()`) when:**
768
+
769
+ - You only need dependency injection without custom logic
770
+ - You prefer functional composition and simplicity
771
+ - You want more concise code
772
+ - You're creating straightforward provider containers
773
+ - You don't need module-level state or behavior
774
+
775
+ **Key Point:** Both styles are fully compatible and can be mixed within the same application. `ProviderModuleClass` uses composition (contains a `ProviderModule` as `this.module`), providing identical DI functionality while preventing method name conflicts.
776
+
777
+ ## Provider Tokens
778
+
779
+ xInjection supports four types of provider tokens, each serving different use cases.
780
+
781
+ ### 1. Class Token
782
+
783
+ The simplest form - just provide the class directly.
784
+
785
+ ```ts
786
+ @Injectable()
787
+ class UserService {
788
+ getUsers() {
789
+ return [{ id: '1', name: 'Alice' }];
790
+ }
791
+ }
792
+
793
+ const MyModule = ProviderModule.create({
794
+ id: 'MyModule',
795
+ providers: [UserService], // Class token
796
+ });
797
+
798
+ const userService = MyModule.get(UserService);
799
+ ```
800
+
801
+ ### 2. Class Token with Substitution
802
+
803
+ Use one class as the token but instantiate a different class. Perfect for polymorphism and testing.
804
+
805
+ ```ts
806
+ @Injectable()
807
+ abstract class PaymentGateway {
808
+ abstract charge(amount: number): Promise<void>;
809
+ }
810
+
811
+ @Injectable()
812
+ class StripePaymentGateway extends PaymentGateway {
813
+ async charge(amount: number) {
814
+ console.log(`Charging $${amount} via Stripe`);
815
+ }
816
+ }
817
+
818
+ @Injectable()
819
+ class MockPaymentGateway extends PaymentGateway {
820
+ async charge(amount: number) {
821
+ console.log(`Mock charge: $${amount}`);
822
+ }
823
+ }
824
+
825
+ // Production
826
+ const ProductionModule = ProviderModule.create({
827
+ id: 'ProductionModule',
828
+ providers: [{ provide: PaymentGateway, useClass: StripePaymentGateway }],
829
+ });
830
+
831
+ // Testing
832
+ const TestModule = ProviderModule.create({
833
+ id: 'TestModule',
834
+ providers: [{ provide: PaymentGateway, useClass: MockPaymentGateway }],
835
+ });
836
+
837
+ const prodGateway = ProductionModule.get(PaymentGateway); // StripePaymentGateway
838
+ const testGateway = TestModule.get(PaymentGateway); // MockPaymentGateway
839
+ ```
840
+
841
+ ### 3. Value Token
842
+
843
+ Provide constant values or pre-instantiated objects.
844
+
845
+ ```ts
846
+ // Configuration values
847
+ const ConfigModule = ProviderModule.create({
848
+ id: 'ConfigModule',
849
+ providers: [
850
+ { provide: 'API_KEY', useValue: 'secret-key-123' },
851
+ { provide: 'API_URL', useValue: 'https://api.example.com' },
852
+ { provide: 'MAX_RETRIES', useValue: 3 },
853
+ ],
854
+ exports: ['API_KEY', 'API_URL', 'MAX_RETRIES'],
855
+ });
856
+
857
+ const apiKey = ConfigModule.get('API_KEY'); // 'secret-key-123'
858
+ const apiUrl = ConfigModule.get('API_URL'); // 'https://api.example.com'
859
+ const maxRetries = ConfigModule.get('MAX_RETRIES'); // 3
860
+
861
+ // Pre-instantiated objects
862
+ const existingLogger = new Logger();
863
+ const LoggerModule = ProviderModule.create({
864
+ id: 'LoggerModule',
865
+ providers: [{ provide: Logger, useValue: existingLogger }],
866
+ });
867
+ ```
868
+
869
+ ### 4. Factory Token
870
+
871
+ Use a factory function to create providers dynamically. The `inject` parameter specifies dependencies.
872
+
873
+ ```ts
874
+ @Injectable()
875
+ class ConfigService {
876
+ dbUrl = 'postgres://localhost:5432/mydb';
877
+ dbPort = 5432;
878
+ }
879
+
880
+ interface DatabaseConnection {
881
+ url: string;
882
+ port: number;
883
+ connected: boolean;
884
+ }
885
+
886
+ const DatabaseModule = ProviderModule.create({
887
+ id: 'DatabaseModule',
888
+ providers: [
889
+ ConfigService,
890
+ {
891
+ provide: 'DATABASE_CONNECTION',
892
+ useFactory: (config: ConfigService) => {
893
+ // Factory receives injected dependencies
894
+ return {
895
+ url: config.dbUrl,
896
+ port: config.dbPort,
897
+ connected: true,
898
+ };
899
+ },
900
+ inject: [ConfigService], // Dependencies to inject into factory
901
+ },
902
+ ],
903
+ exports: ['DATABASE_CONNECTION'],
904
+ });
905
+
906
+ const connection = DatabaseModule.get<DatabaseConnection>('DATABASE_CONNECTION');
907
+ console.log(connection.url); // 'postgres://localhost:5432/mydb'
908
+ ```
909
+
910
+ **Complex Factory Example with Multiple Dependencies:**
911
+
912
+ ```ts
913
+ @Injectable()
914
+ class LoggerService {
915
+ log(message: string) {
916
+ console.log(message);
917
+ }
918
+ }
919
+
920
+ @Injectable()
921
+ class MetricsService {
922
+ track(event: string) {
923
+ console.log(`Tracking: ${event}`);
924
+ }
925
+ }
926
+
927
+ interface ApiClient {
928
+ logger: LoggerService;
929
+ metrics: MetricsService;
930
+ baseUrl: string;
931
+ makeRequest(endpoint: string): void;
932
+ }
933
+
934
+ const ApiModule = ProviderModule.create({
935
+ id: 'ApiModule',
936
+ providers: [
937
+ LoggerService,
938
+ MetricsService,
939
+ { provide: 'BASE_URL', useValue: 'https://api.example.com' },
940
+ {
941
+ provide: 'API_CLIENT',
942
+ useFactory: (logger: LoggerService, metrics: MetricsService, baseUrl: string): ApiClient => {
943
+ return {
944
+ logger,
945
+ metrics,
946
+ baseUrl,
947
+ makeRequest(endpoint: string) {
948
+ this.logger.log(`Making request to ${this.baseUrl}${endpoint}`);
949
+ this.metrics.track('api_request');
950
+ },
951
+ };
952
+ },
953
+ inject: [LoggerService, MetricsService, 'BASE_URL'],
954
+ },
955
+ ],
956
+ });
957
+
958
+ const apiClient = ApiModule.get<ApiClient>('API_CLIENT');
959
+ apiClient.makeRequest('/users');
960
+ ```
961
+
962
+ > [!TIP]
963
+ > Use factory tokens when:
964
+ >
965
+ > - Provider creation requires complex logic
966
+ > - You need to inject dependencies into the factory
967
+ > - You're creating providers that depend on runtime configuration
968
+ > - You need to create multiple instances with different configurations
969
+
970
+ ## Injection Scopes
971
+
972
+ Control provider lifecycle with three scope types. Scope priority order: **token scope > decorator scope > module default scope**.
973
+
974
+ ### Singleton (Default)
975
+
976
+ Cached after first resolution - same instance returned every time.
977
+
978
+ ```ts
979
+ @Injectable() // Singleton by default
980
+ class DatabaseService {
981
+ connectionId = Math.random();
982
+ }
983
+
984
+ const MyModule = ProviderModule.create({
985
+ id: 'MyModule',
986
+ providers: [DatabaseService],
987
+ });
988
+
989
+ const db1 = MyModule.get(DatabaseService);
990
+ const db2 = MyModule.get(DatabaseService);
991
+
992
+ console.log(db1 === db2); // true
993
+ console.log(db1.connectionId === db2.connectionId); // true
994
+ ```
995
+
996
+ ### Transient
997
+
998
+ New instance created on every resolution.
999
+
1000
+ ```ts
1001
+ @Injectable(InjectionScope.Transient)
1002
+ class RequestLogger {
1003
+ requestId = Math.random();
1004
+ }
1005
+
1006
+ const MyModule = ProviderModule.create({
1007
+ id: 'MyModule',
1008
+ providers: [RequestLogger],
1009
+ });
1010
+
1011
+ const logger1 = MyModule.get(RequestLogger);
1012
+ const logger2 = MyModule.get(RequestLogger);
1013
+
1014
+ console.log(logger1 === logger2); // false
1015
+ console.log(logger1.requestId === logger2.requestId); // false
1016
+ ```
1017
+
1018
+ ### Request
1019
+
1020
+ Single instance per resolution tree. All dependencies resolved in the same `get()` call share the same instance.
1021
+
1022
+ ```ts
1023
+ @Injectable(InjectionScope.Request)
1024
+ class RequestContext {
1025
+ requestId = Math.random();
1026
+ }
1027
+
1028
+ @Injectable(InjectionScope.Transient)
1029
+ class ServiceA {
1030
+ constructor(public ctx: RequestContext) {}
1031
+ }
1032
+
1033
+ @Injectable(InjectionScope.Transient)
1034
+ class ServiceB {
1035
+ constructor(public ctx: RequestContext) {}
1036
+ }
1037
+
1038
+ @Injectable(InjectionScope.Transient)
1039
+ class Controller {
1040
+ constructor(
1041
+ public serviceA: ServiceA,
1042
+ public serviceB: ServiceB
1043
+ ) {}
1044
+ }
1045
+
1046
+ const MyModule = ProviderModule.create({
1047
+ id: 'MyModule',
1048
+ providers: [RequestContext, ServiceA, ServiceB, Controller],
1049
+ });
1050
+
1051
+ // First resolution tree
1052
+ const controller1 = MyModule.get(Controller);
1053
+ console.log(controller1.serviceA.ctx === controller1.serviceB.ctx); // true
1054
+ // ServiceA and ServiceB share the same RequestContext
1055
+
1056
+ // Second resolution tree
1057
+ const controller2 = MyModule.get(Controller);
1058
+ console.log(controller2.serviceA.ctx === controller2.serviceB.ctx); // true
1059
+ // New resolution, both services get a new shared RequestContext
1060
+
1061
+ // Different resolution trees get different contexts
1062
+ console.log(controller1.serviceA.ctx === controller2.serviceA.ctx); // false
1063
+ ```
1064
+
1065
+ **Visual Representation:**
1066
+
1067
+ ```
1068
+ First module.get(Controller):
1069
+ Controller (new) ──┬──> ServiceA (new) ──┐
1070
+ │ ├──> RequestContext (SAME instance)
1071
+ └──> ServiceB (new) ──┘
1072
+
1073
+ Second module.get(Controller):
1074
+ Controller (new) ──┬──> ServiceA (new) ──┐
1075
+ │ ├──> RequestContext (NEW instance)
1076
+ └──> ServiceB (new) ──┘
1077
+ ```
1078
+
1079
+ ### Scope Priority Order
1080
+
1081
+ Scopes are resolved in the following priority order (highest to lowest):
1082
+
1083
+ 1. **Token scope** (highest priority)
1084
+ 2. **Decorator scope**
1085
+ 3. **Module default scope** (lowest priority)
1086
+
1087
+ ```ts
1088
+ @Injectable(InjectionScope.Singleton) // Priority 2
1089
+ class MyService {}
1090
+
1091
+ const MyModule = ProviderModule.create({
1092
+ id: 'MyModule',
1093
+ defaultScope: InjectionScope.Singleton, // Priority 3 (lowest)
1094
+ providers: [
1095
+ {
1096
+ provide: MyService,
1097
+ useClass: MyService,
1098
+ scope: InjectionScope.Transient, // Priority 1 (highest) - WINS!
1099
+ },
1100
+ ],
1101
+ });
1102
+
1103
+ // Token scope wins: new instance every time
1104
+ const s1 = MyModule.get(MyService);
1105
+ const s2 = MyModule.get(MyService);
1106
+ console.log(s1 === s2); // false
1107
+ ```
1108
+
1109
+ **Examples of Each Priority:**
1110
+
1111
+ ```ts
1112
+ // Priority 1: Token scope
1113
+ const Module1 = ProviderModule.create({
1114
+ id: 'Module1',
1115
+ defaultScope: InjectionScope.Singleton,
1116
+ providers: [
1117
+ {
1118
+ provide: MyService,
1119
+ useClass: MyService,
1120
+ scope: InjectionScope.Transient, // Token wins
1121
+ },
1122
+ ],
1123
+ });
1124
+
1125
+ // Priority 2: Decorator scope (no token scope)
1126
+ @Injectable(InjectionScope.Request)
1127
+ class DecoratedService {}
1128
+
1129
+ const Module2 = ProviderModule.create({
1130
+ id: 'Module2',
1131
+ defaultScope: InjectionScope.Singleton,
1132
+ providers: [DecoratedService], // Decorator wins
1133
+ });
1134
+
1135
+ // Priority 3: Module default (no token or decorator scope)
1136
+ @Injectable() // No scope specified
1137
+ class PlainService {}
1138
+
1139
+ const Module3 = ProviderModule.create({
1140
+ id: 'Module3',
1141
+ defaultScope: InjectionScope.Transient, // Module default wins
1142
+ providers: [PlainService],
1143
+ });
1144
+ ```
1145
+
1146
+ > [!IMPORTANT]
1147
+ > Request scope is useful for scenarios like:
1148
+ >
1149
+ > - HTTP request tracking (same request ID across all services in one request)
1150
+ > - Transaction contexts (same database transaction across all repositories)
1151
+ > - User context (same user data across all services in one operation)
1152
+
1153
+ ## Module System
1154
+
1155
+ ### Import/Export Pattern
1156
+
1157
+ Modules explicitly control dependency boundaries through imports and exports, providing encapsulation and clear interfaces.
1158
+
1159
+ ```ts
1160
+ @Injectable()
1161
+ class DatabaseService {
1162
+ query(sql: string) {
1163
+ return [{ result: 'data' }];
1164
+ }
1165
+ }
1166
+
1167
+ @Injectable()
1168
+ class InternalCacheService {
1169
+ // Private to DatabaseModule
1170
+ cache = new Map();
1171
+ }
1172
+
1173
+ const DatabaseModule = ProviderModule.create({
1174
+ id: 'DatabaseModule',
1175
+ providers: [DatabaseService, InternalCacheService],
1176
+ exports: [DatabaseService], // Only DatabaseService is accessible
1177
+ });
1178
+
1179
+ const ApiModule = ProviderModule.create({
1180
+ id: 'ApiModule',
1181
+ imports: [DatabaseModule],
1182
+ providers: [ApiService],
1183
+ });
1184
+
1185
+ // ✅ Works - DatabaseService is exported
1186
+ const dbService = ApiModule.get(DatabaseService);
1187
+
1188
+ // ❌ Error - InternalCacheService not exported
1189
+ // const cache = ApiModule.get(InternalCacheService);
1190
+ ```
1191
+
1192
+ **Nested Imports:**
1193
+
1194
+ ```ts
1195
+ const LayerA = ProviderModule.create({
1196
+ id: 'LayerA',
1197
+ providers: [ServiceA],
1198
+ exports: [ServiceA],
1199
+ });
1200
+
1201
+ const LayerB = ProviderModule.create({
1202
+ id: 'LayerB',
1203
+ imports: [LayerA],
1204
+ providers: [ServiceB],
1205
+ exports: [ServiceB, LayerA], // Re-export LayerA
1206
+ });
1207
+
1208
+ const LayerC = ProviderModule.create({
1209
+ id: 'LayerC',
1210
+ imports: [LayerB],
1211
+ });
1212
+
1213
+ // ✅ Works - ServiceA accessible through LayerB's re-export
1214
+ const serviceA = LayerC.get(ServiceA);
1215
+
1216
+ // ✅ Works - ServiceB exported by LayerB
1217
+ const serviceB = LayerC.get(ServiceB);
1218
+ ```
1219
+
1220
+ ### Re-exporting Modules
1221
+
1222
+ Modules can re-export imported modules to create aggregation modules.
1223
+
1224
+ ```ts
1225
+ const DatabaseModule = ProviderModule.create({
1226
+ id: 'DatabaseModule',
1227
+ providers: [DatabaseService],
1228
+ exports: [DatabaseService],
1229
+ });
1230
+
1231
+ const ConfigModule = ProviderModule.create({
1232
+ id: 'ConfigModule',
1233
+ providers: [ConfigService],
1234
+ exports: [ConfigService],
1235
+ });
1236
+
1237
+ const LoggerModule = ProviderModule.create({
1238
+ id: 'LoggerModule',
1239
+ providers: [LoggerService],
1240
+ exports: [LoggerService],
1241
+ });
1242
+
1243
+ // CoreModule aggregates common modules
1244
+ const CoreModule = ProviderModule.create({
1245
+ id: 'CoreModule',
1246
+ imports: [DatabaseModule, ConfigModule, LoggerModule],
1247
+ exports: [DatabaseModule, ConfigModule, LoggerModule], // Re-export all
1248
+ });
1249
+
1250
+ // Consumers import CoreModule and get all three modules
1251
+ const FeatureModule = ProviderModule.create({
1252
+ id: 'FeatureModule',
1253
+ imports: [CoreModule], // Just import one module
1254
+ });
1255
+
1256
+ // Access all re-exported providers
1257
+ const db = FeatureModule.get(DatabaseService);
1258
+ const config = FeatureModule.get(ConfigService);
1259
+ const logger = FeatureModule.get(LoggerService);
1260
+ ```
1261
+
1262
+ > [!TIP]
1263
+ > Create "barrel" or "core" modules that re-export commonly used modules to simplify imports throughout your application.
1264
+
1265
+ ### Dynamic Module Updates
1266
+
1267
+ Modules support runtime modifications for flexibility. Use sparingly as it can impact performance.
1268
+
1269
+ ```ts
1270
+ const DynamicModule = ProviderModule.create({
1271
+ id: 'DynamicModule',
1272
+ providers: [ServiceA],
1273
+ });
1274
+
1275
+ // Add providers dynamically
1276
+ DynamicModule.update.addProvider(ServiceB);
1277
+ DynamicModule.update.addProvider(ServiceC, true); // true = also export
1278
+
1279
+ // Add imports dynamically
1280
+ const DatabaseModule = ProviderModule.create({
1281
+ id: 'DatabaseModule',
1282
+ providers: [DatabaseService],
1283
+ exports: [DatabaseService],
1284
+ });
1285
+
1286
+ DynamicModule.update.addImport(DatabaseModule, true); // true = also export
1287
+
1288
+ // Check what's available
1289
+ console.log(DynamicModule.hasProvider(ServiceB)); // true
1290
+ console.log(DynamicModule.isImportingModule('DatabaseModule')); // true
1291
+ console.log(DynamicModule.isExportingProvider(ServiceC)); // true
1292
+
1293
+ // Remove providers and imports
1294
+ DynamicModule.update.removeProvider(ServiceB);
1295
+ DynamicModule.update.removeImport(DatabaseModule);
1296
+ DynamicModule.update.removeFromExports(ServiceC);
1297
+ ```
1298
+
1299
+ **Dynamic Import Propagation:**
1300
+
1301
+ ```ts
1302
+ const ModuleA = ProviderModule.create({
1303
+ id: 'ModuleA',
1304
+ providers: [ServiceA],
1305
+ exports: [ServiceA],
1306
+ });
1307
+
1308
+ const ModuleB = ProviderModule.create({
1309
+ id: 'ModuleB',
1310
+ imports: [ModuleA],
1311
+ exports: [ModuleA],
1312
+ });
1313
+
1314
+ const ModuleC = ProviderModule.create({
1315
+ id: 'ModuleC',
1316
+ providers: [ServiceC],
1317
+ exports: [ServiceC],
1318
+ });
1319
+
1320
+ // Initially, ModuleB doesn't have ServiceC
1321
+ console.log(ModuleB.hasProvider(ServiceC)); // false
1322
+
1323
+ // Dynamically import ModuleC into ModuleA and export it
1324
+ ModuleA.update.addImport(ModuleC, true);
1325
+
1326
+ // Now ModuleB automatically has ServiceC (import propagation!)
1327
+ console.log(ModuleB.hasProvider(ServiceC)); // true
1328
+ ```
1329
+
1330
+ > [!WARNING]
1331
+ > Dynamic module updates:
1332
+ >
1333
+ > - Can impact performance if used frequently
1334
+ > - Should be used primarily for testing or plugin systems
1335
+ > - May make dependency graphs harder to understand
1336
+ > - Are propagated automatically to importing modules
1337
+
1338
+ ### Global Modules
1339
+
1340
+ Mark modules as global to auto-import into `AppModule`, making them available everywhere.
1341
+
1342
+ ```ts
1343
+ @Injectable()
1344
+ class LoggerService {
1345
+ log(message: string) {
1346
+ console.log(`[LOG] ${message}`);
1347
+ }
1348
+ }
1349
+
1350
+ // Create global module
1351
+ const LoggerModule = ProviderModule.create({
1352
+ id: 'LoggerModule',
1353
+ isGlobal: true, // Auto-imports into AppModule
1354
+ providers: [LoggerService],
1355
+ exports: [LoggerService],
1356
+ });
1357
+
1358
+ // Now any module can access LoggerService without explicit import
1359
+ const FeatureModule = ProviderModule.create({
1360
+ id: 'FeatureModule',
1361
+ // No imports needed!
1362
+ });
1363
+
1364
+ const logger = FeatureModule.get(LoggerService); // Works!
1365
+ logger.log('Hello from FeatureModule');
1366
+ ```
1367
+
1368
+ **Global Module with Blueprint:**
1369
+
1370
+ ```ts
1371
+ @Injectable()
1372
+ class ConfigService {
1373
+ apiUrl = 'https://api.example.com';
1374
+ }
1375
+
1376
+ // Blueprint with global flag
1377
+ const ConfigModuleBp = ProviderModule.blueprint({
1378
+ id: 'ConfigModule',
1379
+ isGlobal: true,
1380
+ providers: [ConfigService],
1381
+ exports: [ConfigService],
1382
+ });
1383
+
1384
+ // Automatically imports into AppModule
1385
+ console.log(AppModule.isImportingModule('ConfigModule')); // true
1386
+ console.log(AppModule.hasProvider(ConfigService)); // true
1387
+
1388
+ // Available in all modules
1389
+ const AnyModule = ProviderModule.create({ id: 'AnyModule' });
1390
+ const config = AnyModule.get(ConfigService); // Works!
1391
+ ```
1392
+
1393
+ > [!CAUTION]
1394
+ > Use global modules sparingly:
1395
+ >
1396
+ > - They create implicit dependencies that can make code harder to understand
1397
+ > - They reduce encapsulation and explicit dependency graphs
1398
+ > - Best used for true cross-cutting concerns (logging, configuration, telemetry)
1399
+ > - Prefer explicit imports when possible for better maintainability
1400
+
1401
+ ## Dependency Injection
1402
+
1403
+ ### Constructor Injection
1404
+
1405
+ The primary way to inject dependencies. TypeScript metadata handles it automatically with `@Injectable()`.
1406
+
1407
+ ```ts
1408
+ @Injectable()
1409
+ class DatabaseService {
1410
+ query(sql: string) {
1411
+ return [{ data: 'result' }];
1412
+ }
1413
+ }
1414
+
1415
+ @Injectable()
1416
+ class LoggerService {
1417
+ log(message: string) {
1418
+ console.log(message);
1419
+ }
1420
+ }
1421
+
1422
+ @Injectable()
1423
+ class UserRepository {
1424
+ // Dependencies automatically injected via constructor
1425
+ constructor(
1426
+ private db: DatabaseService,
1427
+ private logger: LoggerService
1428
+ ) {}
1429
+
1430
+ findAll() {
1431
+ this.logger.log('Finding all users');
1432
+ return this.db.query('SELECT * FROM users');
1433
+ }
1434
+ }
1435
+
1436
+ const UserModule = ProviderModule.create({
1437
+ id: 'UserModule',
1438
+ providers: [DatabaseService, LoggerService, UserRepository],
1439
+ });
1440
+
1441
+ // UserRepository automatically receives DatabaseService and LoggerService
1442
+ const userRepo = UserModule.get(UserRepository);
1443
+ ```
1444
+
1445
+ ### @Inject Decorator
1446
+
1447
+ Use `@Inject` for explicit injection when automatic resolution doesn't work (e.g., string tokens, interfaces).
1448
+
1449
+ ```ts
1450
+ import { Inject, Injectable } from '@adimm/x-injection';
1451
+
1452
+ @Injectable()
1453
+ class ApiService {
1454
+ constructor(
1455
+ @Inject('API_KEY') private apiKey: string,
1456
+ @Inject('API_URL') private apiUrl: string,
1457
+ @Inject('MAX_RETRIES') private maxRetries: number
1458
+ ) {}
1459
+
1460
+ makeRequest() {
1461
+ console.log(`Calling ${this.apiUrl} with key ${this.apiKey}`);
1462
+ console.log(`Max retries: ${this.maxRetries}`);
1463
+ }
1464
+ }
1465
+
1466
+ const ApiModule = ProviderModule.create({
1467
+ id: 'ApiModule',
1468
+ providers: [
1469
+ { provide: 'API_KEY', useValue: 'secret-123' },
1470
+ { provide: 'API_URL', useValue: 'https://api.example.com' },
1471
+ { provide: 'MAX_RETRIES', useValue: 3 },
1472
+ ApiService,
1473
+ ],
1474
+ });
1475
+
1476
+ const apiService = ApiModule.get(ApiService);
1477
+ apiService.makeRequest();
1478
+ ```
1479
+
1480
+ **Injecting Abstract Classes:**
1481
+
1482
+ ```ts
1483
+ @Injectable()
1484
+ abstract class PaymentGateway {
1485
+ abstract charge(amount: number): Promise<void>;
1486
+ }
1487
+
1488
+ @Injectable()
1489
+ class StripePaymentGateway extends PaymentGateway {
1490
+ async charge(amount: number) {
1491
+ console.log(`Stripe: Charging $${amount}`);
1492
+ }
1493
+ }
1494
+
1495
+ @Injectable()
1496
+ class PaymentService {
1497
+ constructor(@Inject(PaymentGateway) private gateway: PaymentGateway) {}
1498
+
1499
+ async processPayment(amount: number) {
1500
+ await this.gateway.charge(amount);
1501
+ }
1502
+ }
1503
+
1504
+ const PaymentModule = ProviderModule.create({
1505
+ id: 'PaymentModule',
1506
+ providers: [{ provide: PaymentGateway, useClass: StripePaymentGateway }, PaymentService],
1507
+ });
1508
+ ```
1509
+
1510
+ ### @MultiInject Decorator
1511
+
1512
+ Inject multiple providers bound to the same token as an array.
1513
+
1514
+ ```ts
1515
+ import { Injectable, MultiInject } from '@adimm/x-injection';
1516
+
1517
+ @Injectable()
1518
+ class EmailNotifier {
1519
+ notify() {
1520
+ console.log('Email notification sent');
1521
+ }
1522
+ }
1523
+
1524
+ @Injectable()
1525
+ class SmsNotifier {
1526
+ notify() {
1527
+ console.log('SMS notification sent');
1528
+ }
1529
+ }
1530
+
1531
+ @Injectable()
1532
+ class PushNotifier {
1533
+ notify() {
1534
+ console.log('Push notification sent');
1535
+ }
1536
+ }
1537
+
1538
+ abstract class Notifier {
1539
+ abstract notify(): void;
1540
+ }
1541
+
1542
+ @Injectable()
1543
+ class NotificationService {
1544
+ constructor(@MultiInject(Notifier) private notifiers: Notifier[]) {}
1545
+
1546
+ notifyAll() {
1547
+ this.notifiers.forEach((notifier) => notifier.notify());
1548
+ }
1549
+ }
1550
+
1551
+ const NotificationModule = ProviderModule.create({
1552
+ id: 'NotificationModule',
1553
+ providers: [
1554
+ { provide: Notifier, useClass: EmailNotifier },
1555
+ { provide: Notifier, useClass: SmsNotifier },
1556
+ { provide: Notifier, useClass: PushNotifier },
1557
+ NotificationService,
1558
+ ],
1559
+ });
1560
+
1561
+ const service = NotificationModule.get(NotificationService);
1562
+ service.notifyAll();
1563
+ // Output:
1564
+ // Email notification sent
1565
+ // SMS notification sent
1566
+ // Push notification sent
1567
+ ```
1568
+
1569
+ **Alternative with module.get():**
1570
+
1571
+ ```ts
1572
+ const MyModule = ProviderModule.create({
1573
+ id: 'MyModule',
1574
+ providers: [
1575
+ { provide: 'Handler', useValue: 'Handler1' },
1576
+ { provide: 'Handler', useValue: 'Handler2' },
1577
+ { provide: 'Handler', useValue: 'Handler3' },
1578
+ ],
1579
+ });
1580
+
1581
+ // Get all providers bound to 'Handler'
1582
+ const handlers = MyModule.get('Handler', false, true); // Third param = asList
1583
+ console.log(handlers); // ['Handler1', 'Handler2', 'Handler3']
1584
+ ```
1585
+
1586
+ ### Optional Dependencies
1587
+
1588
+ Use the `isOptional` flag to handle missing dependencies gracefully.
1589
+
1590
+ ```ts
1591
+ @Injectable()
1592
+ class ServiceA {
1593
+ value = 'A';
1594
+ }
1595
+
1596
+ @Injectable()
1597
+ class ServiceB {
1598
+ constructor(
1599
+ private serviceA: ServiceA,
1600
+ @Inject('OPTIONAL_CONFIG') private config?: any
1601
+ ) {}
1602
+ }
1603
+
1604
+ const MyModule = ProviderModule.create({
1605
+ id: 'MyModule',
1606
+ providers: [ServiceA, ServiceB],
1607
+ });
1608
+
1609
+ // Get with optional flag
1610
+ const optionalService = MyModule.get('NOT_EXISTS', true); // isOptional = true
1611
+ console.log(optionalService); // undefined (no error thrown)
1612
+
1613
+ // Without optional flag (throws error)
1614
+ try {
1615
+ const service = MyModule.get('NOT_EXISTS'); // Throws!
1616
+ } catch (error) {
1617
+ console.error('Provider not found');
1618
+ }
1619
+ ```
1620
+
1621
+ > [!TIP]
1622
+ > Use `@Inject` when:
1623
+ >
1624
+ > - Injecting string tokens or symbols
1625
+ > - Injecting abstract classes
1626
+ > - TypeScript's automatic injection doesn't work (interfaces, etc.)
1627
+ >
1628
+ > Use `@MultiInject` when:
1629
+ >
1630
+ > - You want to collect all providers bound to a single token
1631
+ > - Implementing plugin systems
1632
+ > - Working with strategy patterns
1633
+
1634
+ ## Lifecycle Hooks
1635
+
1636
+ Lifecycle hooks allow you to execute code at specific points in a module's lifecycle.
1637
+
1638
+ ### onReady Hook
1639
+
1640
+ Invoked immediately after module creation. Perfect for initialization logic.
1641
+
1642
+ ```ts
1643
+ @Injectable()
1644
+ class DatabaseService {
1645
+ connected = false;
1646
+
1647
+ async connect() {
1648
+ console.log('Connecting to database...');
1649
+ this.connected = true;
1650
+ }
1651
+ }
1652
+
1653
+ const DatabaseModule = ProviderModule.create({
1654
+ id: 'DatabaseModule',
1655
+ providers: [DatabaseService],
1656
+ onReady: async (module) => {
1657
+ console.log('DatabaseModule is ready!');
1658
+
1659
+ // Initialize services
1660
+ const db = module.get(DatabaseService);
1661
+ await db.connect();
1662
+
1663
+ console.log('Database connected:', db.connected);
1664
+ },
1665
+ });
1666
+
1667
+ // Output:
1668
+ // DatabaseModule is ready!
1669
+ // Connecting to database...
1670
+ // Database connected: true
1671
+ ```
1672
+
1673
+ ### onReset Hook
1674
+
1675
+ Invoked when `module.reset()` is called. Provides `before` and `after` callbacks for cleanup and reinitialization.
1676
+
1677
+ ```ts
1678
+ @Injectable()
1679
+ class CacheService {
1680
+ cache = new Map();
1681
+
1682
+ clear() {
1683
+ this.cache.clear();
1684
+ }
1685
+ }
1686
+
1687
+ const CacheModule = ProviderModule.create({
1688
+ id: 'CacheModule',
1689
+ providers: [CacheService],
1690
+ onReset: () => {
1691
+ return {
1692
+ before: async (mod) => {
1693
+ console.log('Before reset - clearing cache');
1694
+ const cache = mod.get(CacheService);
1695
+ cache.clear();
1696
+ },
1697
+ after: async () => {
1698
+ console.log('After reset - cache reinitialized');
1699
+ },
1700
+ };
1701
+ },
1702
+ });
1703
+
1704
+ // Trigger reset
1705
+ await CacheModule.reset();
1706
+ // Output:
1707
+ // Before reset - clearing cache
1708
+ // After reset - cache reinitialized
1709
+ ```
1710
+
1711
+ ### onDispose Hook
1712
+
1713
+ Invoked when `module.dispose()` is called. Perfect for cleanup tasks like closing connections.
1714
+
1715
+ ```ts
1716
+ @Injectable()
1717
+ class DatabaseService {
1718
+ connected = true;
1719
+
1720
+ async disconnect() {
1721
+ console.log('Disconnecting from database...');
1722
+ this.connected = false;
1723
+ }
1724
+ }
1725
+
1726
+ @Injectable()
1727
+ class FileService {
1728
+ async closeFiles() {
1729
+ console.log('Closing open files...');
1730
+ }
1731
+ }
1732
+
1733
+ const AppModule = ProviderModule.create({
1734
+ id: 'AppModule',
1735
+ providers: [DatabaseService, FileService],
1736
+ onDispose: () => {
1737
+ return {
1738
+ before: async (mod) => {
1739
+ console.log('Cleanup started');
1740
+ const db = mod.get(DatabaseService);
1741
+ const files = mod.get(FileService);
1742
+
1743
+ await db.disconnect();
1744
+ await files.closeFiles();
1745
+ },
1746
+ after: async () => {
1747
+ console.log('Cleanup completed');
1748
+ },
1749
+ };
1750
+ },
1751
+ });
1752
+
1753
+ // Dispose module
1754
+ await AppModule.dispose();
1755
+ // Output:
1756
+ // Cleanup started
1757
+ // Disconnecting from database...
1758
+ // Closing open files...
1759
+ // Cleanup completed
1760
+
1761
+ // Module is now disposed
1762
+ console.log(AppModule.isDisposed); // true
1763
+
1764
+ // Subsequent operations throw error
1765
+ try {
1766
+ AppModule.get(DatabaseService);
1767
+ } catch (error) {
1768
+ console.error('Cannot access disposed module');
1769
+ }
1770
+ ```
1771
+
1772
+ **Complete Lifecycle Example:**
1773
+
1774
+ ```ts
1775
+ @Injectable()
1776
+ class ResourceService {
1777
+ initialized = false;
1778
+
1779
+ async initialize() {
1780
+ console.log('Initializing resource...');
1781
+ this.initialized = true;
1782
+ }
1783
+
1784
+ async cleanup() {
1785
+ console.log('Cleaning up resource...');
1786
+ this.initialized = false;
1787
+ }
1788
+ }
1789
+
1790
+ const ResourceModule = ProviderModule.create({
1791
+ id: 'ResourceModule',
1792
+ providers: [ResourceService],
1793
+
1794
+ onReady: async (module) => {
1795
+ console.log('[READY] Module created');
1796
+ const service = module.get(ResourceService);
1797
+ await service.initialize();
1798
+ },
1799
+
1800
+ onReset: () => {
1801
+ console.log('[RESET] Resetting module');
1802
+ return {
1803
+ before: async (mod) => {
1804
+ console.log('[RESET:BEFORE] Cleaning up before reset');
1805
+ const service = mod.get(ResourceService);
1806
+ await service.cleanup();
1807
+ },
1808
+ after: async () => {
1809
+ console.log('[RESET:AFTER] Reinitializing after reset');
1810
+ const service = mod.get(ResourceService);
1811
+ await service.initialize();
1812
+ },
1813
+ };
1814
+ },
1815
+
1816
+ onDispose: () => {
1817
+ console.log('[DISPOSE] Disposing module');
1818
+ return {
1819
+ before: async (mod) => {
1820
+ console.log('[DISPOSE:BEFORE] Final cleanup');
1821
+ const service = mod.get(ResourceService);
1822
+ await service.cleanup();
1823
+ },
1824
+ after: async () => {
1825
+ console.log('[DISPOSE:AFTER] Module fully disposed');
1826
+ },
1827
+ };
1828
+ },
1829
+ });
1830
+
1831
+ // Usage
1832
+ await ResourceModule.reset();
1833
+ await ResourceModule.dispose();
1834
+ ```
1835
+
1836
+ > [!IMPORTANT]
1837
+ > Lifecycle hook execution order:
1838
+ >
1839
+ > 1. **onReady** - Immediately after module creation
1840
+ > 2. **onReset** (before) → module reset → **onReset** (after)
1841
+ > 3. **onDispose** (before) → module disposal → **onDispose** (after)
1842
+
1843
+ > [!WARNING]
1844
+ > After calling `dispose()`:
1845
+ >
1846
+ > - All module operations will throw errors
1847
+ > - The module cannot be reused
1848
+ > - Internal resources are cleaned up
1849
+ > - Use for application shutdown or when modules are truly finished
1850
+
1851
+ ## Events System
1852
+
1853
+ The events system allows you to observe and react to module changes in real-time.
1854
+
1855
+ ### Subscribing to Events
1856
+
1857
+ ```ts
1858
+ import { DefinitionEventType } from '@adimm/x-injection';
1859
+
1860
+ const MyModule = ProviderModule.create({
1861
+ id: 'MyModule',
1862
+ providers: [ServiceA],
1863
+ });
1864
+
1865
+ // Subscribe to all events
1866
+ const unsubscribe = MyModule.update.subscribe(({ type, change }) => {
1867
+ console.log(`Event: ${DefinitionEventType[type]}`, change);
1868
+ });
1869
+
1870
+ // Trigger events
1871
+ MyModule.update.addProvider(ServiceB); // Event: Provider
1872
+ MyModule.update.addImport(OtherModule); // Event: Import
1873
+ const service = MyModule.get(ServiceA); // Event: GetProvider
1874
+
1875
+ // Clean up
1876
+ unsubscribe();
1877
+ ```
1878
+
1879
+ ### Available Event Types
1880
+
1881
+ ```ts
1882
+ enum DefinitionEventType {
1883
+ Noop, // No operation
1884
+ Import, // Module/blueprint added
1885
+ Provider, // Provider added
1886
+ GetProvider, // Provider resolved
1887
+ Export, // Export added
1888
+ ExportModule, // Module added to exports
1889
+ ExportProvider, // Provider added to exports
1890
+ ImportRemoved, // Module removed
1891
+ ProviderRemoved, // Provider removed
1892
+ ExportRemoved, // Export removed
1893
+ ExportModuleRemoved, // Module removed from exports
1894
+ ExportProviderRemoved, // Provider removed from exports
1895
+ }
1896
+ ```
1897
+
1898
+ ### Event Use Cases
1899
+
1900
+ **Monitoring Provider Resolution:**
1901
+
1902
+ ```ts
1903
+ const MonitoredModule = ProviderModule.create({
1904
+ id: 'MonitoredModule',
1905
+ providers: [DatabaseService, CacheService],
1906
+ });
1907
+
1908
+ MonitoredModule.update.subscribe(({ type, change }) => {
1909
+ if (type === DefinitionEventType.GetProvider) {
1910
+ console.log('Provider accessed:', change.constructor.name);
1911
+ console.log('Access time:', new Date().toISOString());
1912
+ }
1913
+ });
1914
+
1915
+ // Logs access
1916
+ const db = MonitoredModule.get(DatabaseService);
1917
+ // Output: Provider accessed: DatabaseService
1918
+ // Access time: 2024-01-15T10:30:00.000Z
1919
+ ```
1920
+
1921
+ **Tracking Module Composition:**
1922
+
1923
+ ```ts
1924
+ const RootModule = ProviderModule.create({
1925
+ id: 'RootModule',
1926
+ });
1927
+
1928
+ const compositionLog: string[] = [];
1929
+
1930
+ RootModule.update.subscribe(({ type, change }) => {
1931
+ switch (type) {
1932
+ case DefinitionEventType.Import:
1933
+ compositionLog.push(`Imported: ${change.id}`);
1934
+ break;
1935
+ case DefinitionEventType.Provider:
1936
+ const providerName = typeof change === 'function' ? change.name : change.provide;
1937
+ compositionLog.push(`Added provider: ${providerName}`);
1938
+ break;
1939
+ case DefinitionEventType.Export:
1940
+ compositionLog.push(`Exported: ${JSON.stringify(change)}`);
1941
+ break;
1942
+ }
1943
+ });
1944
+
1945
+ RootModule.update.addImport(DatabaseModule);
1946
+ RootModule.update.addProvider(ServiceA);
1947
+ RootModule.update.addProvider(ServiceB, true);
1948
+
1949
+ console.log(compositionLog);
1950
+ // [
1951
+ // 'Imported: DatabaseModule',
1952
+ // 'Added provider: ServiceA',
1953
+ // 'Added provider: ServiceB',
1954
+ // 'Exported: ServiceB'
1955
+ // ]
1956
+ ```
1957
+
1958
+ **Debugging Dynamic Changes:**
1959
+
1960
+ ```ts
1961
+ const DebugModule = ProviderModule.create({
1962
+ id: 'DebugModule',
1963
+ });
1964
+
1965
+ DebugModule.update.subscribe(({ type, change }) => {
1966
+ const eventName = DefinitionEventType[type];
1967
+
1968
+ if (type === DefinitionEventType.ImportRemoved) {
1969
+ console.warn(`⚠️ Module removed: ${change.id}`);
1970
+ } else if (type === DefinitionEventType.ProviderRemoved) {
1971
+ console.warn(`⚠️ Provider removed:`, change);
1972
+ } else {
1973
+ console.log(`✅ ${eventName}:`, change);
1974
+ }
1975
+ });
1976
+
1977
+ DebugModule.update.addProvider(ServiceA);
1978
+ DebugModule.update.removeProvider(ServiceA);
1979
+ ```
1980
+
1981
+ **Building a Module Activity Logger:**
1982
+
1983
+ ```ts
1984
+ class ModuleActivityLogger {
1985
+ private events: Array<{ timestamp: number; type: string; change: any }> = [];
1986
+
1987
+ constructor(module: ProviderModule) {
1988
+ module.update.subscribe(({ type, change }) => {
1989
+ this.events.push({
1990
+ timestamp: Date.now(),
1991
+ type: DefinitionEventType[type],
1992
+ change,
1993
+ });
1994
+ });
1995
+ }
1996
+
1997
+ getReport() {
1998
+ return {
1999
+ totalEvents: this.events.length,
2000
+ events: this.events,
2001
+ summary: this.events.reduce(
2002
+ (acc, event) => {
2003
+ acc[event.type] = (acc[event.type] || 0) + 1;
2004
+ return acc;
2005
+ },
2006
+ {} as Record<string, number>
2007
+ ),
2008
+ };
2009
+ }
2010
+ }
2011
+
2012
+ const TrackedModule = ProviderModule.create({ id: 'TrackedModule' });
2013
+ const logger = new ModuleActivityLogger(TrackedModule);
2014
+
2015
+ TrackedModule.update.addProvider(ServiceA);
2016
+ TrackedModule.update.addProvider(ServiceB);
2017
+ TrackedModule.get(ServiceA);
2018
+ TrackedModule.get(ServiceB);
2019
+
2020
+ console.log(logger.getReport());
2021
+ // {
2022
+ // totalEvents: 4,
2023
+ // events: [...],
2024
+ // summary: { Provider: 2, GetProvider: 2 }
2025
+ // }
2026
+ ```
2027
+
2028
+ > [!WARNING]
2029
+ >
2030
+ > - Always call `unsubscribe()` to prevent memory leaks
2031
+ > - Events fire **after** middlewares have executed
2032
+ > - Event handlers are synchronous - avoid heavy operations
2033
+ > - High-frequency events (like `GetProvider`) can impact performance
2034
+
2035
+ ## Middlewares
2036
+
2037
+ Middlewares intercept and transform module operations before they complete. They provide powerful customization capabilities.
2038
+
2039
+ ### BeforeGet Middleware
2040
+
2041
+ Transform provider values before they're returned to consumers.
2042
+
2043
+ ```ts
2044
+ import { MiddlewareType } from '@adimm/x-injection';
2045
+
2046
+ @Injectable()
2047
+ class UserService {
2048
+ getUser() {
2049
+ return { id: 1, name: 'Alice' };
2050
+ }
2051
+ }
2052
+
2053
+ const MyModule = ProviderModule.create({
2054
+ id: 'MyModule',
2055
+ providers: [UserService],
2056
+ });
2057
+
2058
+ // Wrap resolved providers with metadata
2059
+ MyModule.middlewares.add(MiddlewareType.BeforeGet, (provider, token, inject) => {
2060
+ // Return true to pass through unchanged
2061
+ if (!(provider instanceof UserService)) return true;
2062
+
2063
+ // Transform the value
2064
+ return {
2065
+ timestamp: Date.now(),
2066
+ instance: provider,
2067
+ metadata: { cached: false },
2068
+ };
2069
+ });
2070
+
2071
+ const result = MyModule.get(UserService);
2072
+ console.log(result);
2073
+ // {
2074
+ // timestamp: 1705320000000,
2075
+ // instance: UserService { ... },
2076
+ // metadata: { cached: false }
2077
+ // }
2078
+ ```
2079
+
2080
+ **Conditional Transformation:**
2081
+
2082
+ ```ts
2083
+ @Injectable()
2084
+ class ServiceA {}
2085
+
2086
+ @Injectable()
2087
+ class ServiceB {}
2088
+
2089
+ MyModule.middlewares.add(MiddlewareType.BeforeGet, (provider, token) => {
2090
+ // Only transform ServiceA
2091
+ if (provider instanceof ServiceA) {
2092
+ return { wrapped: provider, type: 'A' };
2093
+ }
2094
+
2095
+ // Pass through everything else unchanged
2096
+ return true;
2097
+ });
2098
+
2099
+ const serviceA = MyModule.get(ServiceA); // { wrapped: ServiceA, type: 'A' }
2100
+ const serviceB = MyModule.get(ServiceB); // ServiceB (unchanged)
2101
+ ```
2102
+
2103
+ **Using inject() to avoid infinite loops:**
2104
+
2105
+ ```ts
2106
+ @Injectable()
2107
+ class LoggerService {
2108
+ log(message: string) {
2109
+ console.log(message);
2110
+ }
2111
+ }
2112
+
2113
+ @Injectable()
2114
+ class PaymentService {}
2115
+
2116
+ MyModule.middlewares.add(MiddlewareType.BeforeGet, (provider, token, inject) => {
2117
+ if (!(provider instanceof PaymentService)) return true;
2118
+
2119
+ // Use inject() instead of module.get() to avoid infinite loop
2120
+ const logger = inject(LoggerService);
2121
+ logger.log('Payment service accessed');
2122
+
2123
+ return provider; // Or transform it
2124
+ });
2125
+ ```
2126
+
2127
+ ### BeforeAddProvider Middleware
2128
+
2129
+ Block specific providers:
2130
+
2131
+ ```ts
2132
+ MyModule.middlewares.add(MiddlewareType.BeforeAddProvider, (provider) => {
2133
+ // Block ServiceB from being added
2134
+ if ((provider as any).name === 'ServiceB') {
2135
+ return false; // Abort
2136
+ }
2137
+ return true; // Allow
2138
+ });
2139
+
2140
+ MyModule.update.addProvider(ServiceA);
2141
+ MyModule.update.addProvider(ServiceB); // Silently rejected
2142
+ MyModule.update.addProvider(ServiceC);
2143
+
2144
+ console.log(MyModule.hasProvider(ServiceA)); // true
2145
+ console.log(MyModule.hasProvider(ServiceB)); // false
2146
+ console.log(MyModule.hasProvider(ServiceC)); // true
2147
+ ```
2148
+
2149
+ ### BeforeAddImport Middleware
2150
+
2151
+ Intercept modules before they're imported.
2152
+
2153
+ ```ts
2154
+ const Module1 = ProviderModule.create({ id: 'Module1' });
2155
+ const Module2 = ProviderModule.create({ id: 'Module2' });
2156
+ const RestrictedModule = ProviderModule.create({ id: 'RestrictedModule' });
2157
+
2158
+ const MainModule = ProviderModule.create({ id: 'MainModule' });
2159
+
2160
+ // Block specific modules
2161
+ MainModule.middlewares.add(MiddlewareType.BeforeAddImport, (module) => {
2162
+ if (module.id === 'RestrictedModule') {
2163
+ console.warn(`❌ Cannot import ${module.id}`);
2164
+ return false; // Block
2165
+ }
2166
+ return true; // Allow
2167
+ });
2168
+
2169
+ MainModule.update.addImport(Module1); // ✅ Allowed
2170
+ MainModule.update.addImport(Module2); // ✅ Allowed
2171
+ MainModule.update.addImport(RestrictedModule); // ❌ Blocked
2172
+
2173
+ console.log(MainModule.isImportingModule('Module1')); // true
2174
+ console.log(MainModule.isImportingModule('RestrictedModule')); // false
2175
+ ```
2176
+
2177
+ **Auto-add providers to imported modules:**
2178
+
2179
+ ```ts
2180
+ MyModule.middlewares.add(MiddlewareType.BeforeAddImport, (importedModule) => {
2181
+ // Add logger to every imported module
2182
+ importedModule.update.addProvider(LoggerService, true);
2183
+ return importedModule; // Return modified module
2184
+ });
2185
+
2186
+ MyModule.update.addImport(FeatureModule);
2187
+ // FeatureModule now has LoggerService
2188
+ ```
2189
+
2190
+ ### OnExportAccess Middleware
2191
+
2192
+ Control which importing modules can access exports.
2193
+
2194
+ ```ts
2195
+ @Injectable()
2196
+ class SensitiveService {}
2197
+
2198
+ @Injectable()
2199
+ class PublicService {}
2200
+
2201
+ const SecureModule = ProviderModule.create({
2202
+ id: 'SecureModule',
2203
+ providers: [SensitiveService, PublicService],
2204
+ exports: [SensitiveService, PublicService],
2205
+ });
2206
+
2207
+ // Restrict access based on importer
2208
+ SecureModule.middlewares.add(MiddlewareType.OnExportAccess, (importerModule, exportToken) => {
2209
+ // Block untrusted modules from accessing SensitiveService
2210
+ if (importerModule.id === 'UntrustedModule' && exportToken === SensitiveService) {
2211
+ console.warn(`❌ ${importerModule.id} denied access to SensitiveService`);
2212
+ return false; // Deny
2213
+ }
2214
+ return true; // Allow
2215
+ });
2216
+
2217
+ const TrustedModule = ProviderModule.create({
2218
+ id: 'TrustedModule',
2219
+ imports: [SecureModule],
2220
+ });
2221
+
2222
+ const UntrustedModule = ProviderModule.create({
2223
+ id: 'UntrustedModule',
2224
+ imports: [SecureModule],
2225
+ });
2226
+
2227
+ // Trusted module can access both
2228
+ console.log(TrustedModule.hasProvider(SensitiveService)); // true
2229
+ console.log(TrustedModule.hasProvider(PublicService)); // true
2230
+
2231
+ // Untrusted module blocked from SensitiveService
2232
+ console.log(UntrustedModule.hasProvider(SensitiveService)); // false
2233
+ console.log(UntrustedModule.hasProvider(PublicService)); // true
2234
+ ```
2235
+
2236
+ **Complete access control:**
2237
+
2238
+ ```ts
2239
+ SecureModule.middlewares.add(MiddlewareType.OnExportAccess, (importer, exportToken) => {
2240
+ const allowlist = ['TrustedModule1', 'TrustedModule2'];
2241
+
2242
+ if (!allowlist.includes(String(importer.id))) {
2243
+ console.warn(`Access denied for ${importer.id}`);
2244
+ return false;
2245
+ }
2246
+
2247
+ return true;
2248
+ });
2249
+ ```
2250
+
2251
+ ### BeforeRemoveImport Middleware
2252
+
2253
+ Prevent specific modules from being removed.
2254
+
2255
+ ```ts
2256
+ const PermanentModule = ProviderModule.create({ id: 'PermanentModule' });
2257
+ const TemporaryModule = ProviderModule.create({ id: 'TemporaryModule' });
2258
+
2259
+ const MainModule = ProviderModule.create({ id: 'MainModule' });
2260
+
2261
+ // Protect PermanentModule
2262
+ MainModule.middlewares.add(MiddlewareType.BeforeRemoveImport, (module) => {
2263
+ if (module.id === 'PermanentModule') {
2264
+ console.warn(`⚠️ Cannot remove ${module.id}`);
2265
+ return false; // Block removal
2266
+ }
2267
+ return true; // Allow removal
2268
+ });
2269
+
2270
+ MainModule.update.addImport(PermanentModule);
2271
+ MainModule.update.addImport(TemporaryModule);
2272
+
2273
+ // Try to remove
2274
+ MainModule.update.removeImport(PermanentModule); // ❌ Blocked
2275
+ MainModule.update.removeImport(TemporaryModule); // ✅ Removed
2276
+
2277
+ console.log(MainModule.isImportingModule('PermanentModule')); // true
2278
+ console.log(MainModule.isImportingModule('TemporaryModule')); // false
2279
+ ```
2280
+
2281
+ ### BeforeRemoveProvider Middleware
2282
+
2283
+ Prevent specific providers from being removed.
2284
+
2285
+ ```ts
2286
+ MyModule.middlewares.add(MiddlewareType.BeforeRemoveProvider, (provider) => {
2287
+ // Block removal of critical services
2288
+ if (provider === DatabaseService) {
2289
+ console.warn('⚠️ Cannot remove DatabaseService');
2290
+ return false;
2291
+ }
2292
+ return true;
2293
+ });
2294
+
2295
+ MyModule.update.addProvider(DatabaseService);
2296
+ MyModule.update.addProvider(CacheService);
2297
+
2298
+ MyModule.update.removeProvider(DatabaseService); // ❌ Blocked
2299
+ MyModule.update.removeProvider(CacheService); // ✅ Removed
2300
+
2301
+ console.log(MyModule.hasProvider(DatabaseService)); // true
2302
+ console.log(MyModule.hasProvider(CacheService)); // false
2303
+ ```
2304
+
2305
+ ### BeforeRemoveExport Middleware
2306
+
2307
+ Prevent specific exports from being removed.
2308
+
2309
+ ```ts
2310
+ import { ProviderModuleHelpers } from '@adimm/x-injection';
2311
+
2312
+ const MyModule = ProviderModule.create({
2313
+ id: 'MyModule',
2314
+ providers: [ServiceA, ServiceB],
2315
+ exports: [ServiceA, ServiceB],
2316
+ });
2317
+
2318
+ MyModule.middlewares.add(MiddlewareType.BeforeRemoveExport, (exportDef) => {
2319
+ // Check if it's a module or provider
2320
+ if (ProviderModuleHelpers.isModule(exportDef)) {
2321
+ // Block module removal
2322
+ return exportDef.id !== 'ProtectedModule';
2323
+ } else {
2324
+ // Block ServiceA removal
2325
+ return exportDef !== ServiceA;
2326
+ }
2327
+ });
2328
+
2329
+ MyModule.update.removeFromExports(ServiceA); // ❌ Blocked
2330
+ MyModule.update.removeFromExports(ServiceB); // ✅ Removed
2331
+
2332
+ console.log(MyModule.isExportingProvider(ServiceA)); // true
2333
+ console.log(MyModule.isExportingProvider(ServiceB)); // false
2334
+ ```
2335
+
2336
+ ### All Available Middleware Types
2337
+
2338
+ ```ts
2339
+ enum MiddlewareType {
2340
+ BeforeAddImport, // Before importing a module
2341
+ BeforeAddProvider, // Before adding a provider
2342
+ BeforeGet, // Before returning provider to consumer
2343
+ BeforeRemoveImport, // Before removing an import
2344
+ BeforeRemoveProvider, // Before removing a provider
2345
+ BeforeRemoveExport, // Before removing an export
2346
+ OnExportAccess, // When importer accesses exports
2347
+ }
2348
+ ```
2349
+
2350
+ **Middleware Return Values:**
2351
+
2352
+ - `false` - Abort the operation (block it)
2353
+ - `true` - Pass through unchanged
2354
+ - Modified value - Transform and continue
2355
+ - For `BeforeGet`: Can return any value (transformation)
2356
+
2357
+ > [!CAUTION]
2358
+ > Middleware best practices:
2359
+ >
2360
+ > - Returning `false` aborts the chain (no value returned)
2361
+ > - Middlewares execute in registration order
2362
+ > - Always handle errors in middleware chains
2363
+ > - Use `inject()` parameter in BeforeGet to avoid infinite loops
2364
+ > - Be careful with performance - middlewares run on every operation
2365
+ > - Events fire **after** middlewares complete
2366
+
2367
+ ## Testing
2368
+
2369
+ xInjection makes testing easy through blueprint cloning and provider substitution.
2370
+
2371
+ ### Blueprint Cloning
2372
+
2373
+ Clone blueprints to create test-specific configurations without affecting production code.
2374
+
2375
+ ```ts
2376
+ // Production blueprint
2377
+ const DatabaseModuleBp = ProviderModule.blueprint({
2378
+ id: 'DatabaseModule',
2379
+ providers: [DatabaseService, ConnectionPool],
2380
+ exports: [DatabaseService],
2381
+ });
2382
+
2383
+ // Test blueprint - clone and modify
2384
+ const DatabaseModuleMock = DatabaseModuleBp.clone().updateDefinition({
2385
+ id: 'DatabaseModuleMock',
2386
+ providers: [
2387
+ { provide: DatabaseService, useClass: MockDatabaseService },
2388
+ { provide: ConnectionPool, useClass: MockConnectionPool },
2389
+ ],
2390
+ });
2391
+
2392
+ // Use in tests
2393
+ const TestModule = ProviderModule.create({
2394
+ id: 'TestModule',
2395
+ imports: [DatabaseModuleMock],
2396
+ });
2397
+
2398
+ const db = TestModule.get(DatabaseService); // MockDatabaseService
2399
+ ```
2400
+
2401
+ **Deep Blueprint Cloning:**
2402
+
2403
+ ```ts
2404
+ const OriginalBp = ProviderModule.blueprint({
2405
+ id: 'Original',
2406
+ providers: [ServiceA, ServiceB, ServiceC],
2407
+ exports: [ServiceA, ServiceB],
2408
+ onReady: (module) => console.log('Original ready'),
2409
+ });
2410
+
2411
+ // Clone and completely override
2412
+ const ClonedBp = OriginalBp.clone().updateDefinition({
2413
+ id: 'Cloned',
2414
+ providers: [MockServiceA, MockServiceB], // Different providers
2415
+ exports: [MockServiceA], // Different exports
2416
+ onReady: undefined, // Remove lifecycle hooks
2417
+ });
2418
+
2419
+ // Original blueprint unchanged
2420
+ console.log(OriginalBp.providers?.length); // 3
2421
+ console.log(ClonedBp.providers?.length); // 2
2422
+ ```
2423
+
2424
+ ### Provider Substitution
2425
+
2426
+ Replace real services with mocks for testing.
2427
+
2428
+ ```ts
2429
+ // Production services
2430
+ @Injectable()
2431
+ class ApiService {
2432
+ async fetchData() {
2433
+ return fetch('https://api.example.com/data').then((r) => r.json());
2434
+ }
2435
+ }
2436
+
2437
+ @Injectable()
2438
+ class UserService {
2439
+ constructor(private api: ApiService) {}
2440
+
2441
+ async getUsers() {
2442
+ return this.api.fetchData();
2443
+ }
2444
+ }
2445
+
2446
+ // Mock service
2447
+ class MockApiService {
2448
+ async fetchData() {
2449
+ return { users: [{ id: 1, name: 'Mock User' }] };
2450
+ }
2451
+ }
2452
+
2453
+ // Production module
2454
+ const ProductionModule = ProviderModule.create({
2455
+ id: 'ProductionModule',
2456
+ providers: [ApiService, UserService],
2457
+ });
2458
+
2459
+ // Test module with substitution
2460
+ const TestModule = ProviderModule.create({
2461
+ id: 'TestModule',
2462
+ providers: [
2463
+ { provide: ApiService, useClass: MockApiService },
2464
+ UserService, // Uses MockApiService automatically
2465
+ ],
2466
+ });
2467
+
2468
+ const userService = TestModule.get(UserService);
2469
+ const users = await userService.getUsers();
2470
+ console.log(users); // Mock data
2471
+ ```
2472
+
2473
+ ### Mocking Services
2474
+
2475
+ **Using useValue for simple mocks:**
2476
+
2477
+ ```ts
2478
+ const mockPaymentGateway = {
2479
+ charge: jest.fn().mockResolvedValue({ success: true }),
2480
+ refund: jest.fn().mockResolvedValue({ success: true }),
2481
+ };
2482
+
2483
+ const TestModule = ProviderModule.create({
2484
+ id: 'TestModule',
2485
+ providers: [{ provide: PaymentGateway, useValue: mockPaymentGateway }, PaymentService],
2486
+ });
2487
+
2488
+ const paymentService = TestModule.get(PaymentService);
2489
+ await paymentService.processPayment(100);
2490
+
2491
+ expect(mockPaymentGateway.charge).toHaveBeenCalledWith(100);
2492
+ ```
2493
+
2494
+ **Using useFactory for complex mocks:**
2495
+
2496
+ ```ts
2497
+ const TestModule = ProviderModule.create({
2498
+ id: 'TestModule',
2499
+ providers: [
2500
+ {
2501
+ provide: 'DATABASE_CONNECTION',
2502
+ useFactory: () => {
2503
+ return {
2504
+ query: jest.fn().mockResolvedValue([{ id: 1, name: 'Test' }]),
2505
+ connect: jest.fn().mockResolvedValue(true),
2506
+ disconnect: jest.fn().mockResolvedValue(true),
2507
+ };
2508
+ },
2509
+ },
2510
+ ],
2511
+ });
2512
+
2513
+ const db = TestModule.get('DATABASE_CONNECTION');
2514
+ const results = await db.query('SELECT * FROM users');
2515
+ expect(results).toEqual([{ id: 1, name: 'Test' }]);
2516
+ ```
2517
+
2518
+ **Complete Testing Example:**
2519
+
2520
+ ```ts
2521
+ // Production code
2522
+ @Injectable()
2523
+ class EmailService {
2524
+ async sendEmail(to: string, subject: string, body: string) {
2525
+ // Real email sending logic
2526
+ console.log(`Sending email to ${to}`);
2527
+ }
2528
+ }
2529
+
2530
+ @Injectable()
2531
+ class UserNotificationService {
2532
+ constructor(private emailService: EmailService) {}
2533
+
2534
+ async notifyUser(userId: string, message: string) {
2535
+ await this.emailService.sendEmail(`user${userId}@example.com`, 'Notification', message);
2536
+ }
2537
+ }
2538
+
2539
+ // Test code
2540
+ describe('UserNotificationService', () => {
2541
+ it('should send email notification', async () => {
2542
+ const mockEmailService = {
2543
+ sendEmail: jest.fn().mockResolvedValue(undefined),
2544
+ };
2545
+
2546
+ const TestModule = ProviderModule.create({
2547
+ id: 'TestModule',
2548
+ providers: [{ provide: EmailService, useValue: mockEmailService }, UserNotificationService],
2549
+ });
2550
+
2551
+ const notificationService = TestModule.get(UserNotificationService);
2552
+ await notificationService.notifyUser('123', 'Test message');
2553
+
2554
+ expect(mockEmailService.sendEmail).toHaveBeenCalledWith('user123@example.com', 'Notification', 'Test message');
2555
+ });
2556
+ });
2557
+ ```
2558
+
2559
+ **Testing with Multiple Module Layers:**
2560
+
2561
+ ```ts
2562
+ // Create mock blueprint
2563
+ const MockDataModuleBp = ProviderModule.blueprint({
2564
+ id: 'MockDataModule',
2565
+ providers: [
2566
+ { provide: DatabaseService, useClass: MockDatabaseService },
2567
+ { provide: CacheService, useClass: MockCacheService },
2568
+ ],
2569
+ exports: [DatabaseService, CacheService],
2570
+ });
2571
+
2572
+ // Use mock in feature module tests
2573
+ const FeatureModuleTest = ProviderModule.create({
2574
+ id: 'FeatureModuleTest',
2575
+ imports: [MockDataModuleBp],
2576
+ providers: [FeatureService],
2577
+ });
2578
+
2579
+ const featureService = FeatureModuleTest.get(FeatureService);
2580
+ // FeatureService receives mock dependencies
2581
+ ```
2582
+
2583
+ > [!TIP]
2584
+ > Testing strategies:
2585
+ >
2586
+ > - Use `blueprint.clone()` to create test variations without modifying originals
2587
+ > - Use `useValue` for simple mocks with jest.fn()
2588
+ > - Use `useClass` for class-based mocks with behavior
2589
+ > - Use `useFactory` for complex mock setup
2590
+ > - Test module isolation by mocking all external dependencies
2591
+ > - Verify mock calls with jest expectations
2592
+
2593
+ ## Advanced Module API
2594
+
2595
+ ### Query Methods
2596
+
2597
+ Check module state and relationships.
2598
+
2599
+ ```ts
2600
+ const MyModule = ProviderModule.create({
2601
+ id: 'MyModule',
2602
+ imports: [DatabaseModule, ConfigModule],
2603
+ providers: [ServiceA, ServiceB],
2604
+ exports: [ServiceA, DatabaseModule],
2605
+ });
2606
+
2607
+ // Provider queries
2608
+ MyModule.hasProvider(ServiceA); // true
2609
+ MyModule.hasProvider(ServiceC); // false
2610
+ MyModule.hasProvider(DatabaseService); // true (from import)
2611
+
2612
+ // Import queries
2613
+ MyModule.isImportingModule('DatabaseModule'); // true
2614
+ MyModule.isImportingModule(ConfigModule); // true (by reference)
2615
+ MyModule.isImportingModule('NonExistent'); // false
2616
+
2617
+ // Export queries
2618
+ MyModule.isExportingProvider(ServiceA); // true
2619
+ MyModule.isExportingProvider(ServiceB); // false
2620
+ MyModule.isExportingModule('DatabaseModule'); // true
2621
+ MyModule.isExportingModule(ConfigModule); // false
2622
+
2623
+ // State queries
2624
+ MyModule.isDisposed; // false
2625
+ MyModule.id; // 'MyModule'
2626
+ ```
2627
+
2628
+ **Using Symbol Identifiers:**
2629
+
2630
+ ```ts
2631
+ const MODULE_ID = Symbol('FeatureModule');
2632
+
2633
+ const FeatureModule = ProviderModule.create({
2634
+ id: MODULE_ID,
2635
+ providers: [FeatureService],
2636
+ exports: [FeatureService],
2637
+ });
2638
+
2639
+ const AppModule = ProviderModule.create({
2640
+ id: 'AppModule',
2641
+ imports: [FeatureModule],
2642
+ });
2643
+
2644
+ // Query using Symbol
2645
+ console.log(AppModule.isImportingModule(MODULE_ID)); // true
2646
+ ```
2647
+
2648
+ ### Multiple Provider Binding
2649
+
2650
+ Bind multiple providers to the same token and retrieve them as a list.
2651
+
2652
+ ```ts
2653
+ @Injectable()
2654
+ abstract class Plugin {
2655
+ abstract execute(): void;
2656
+ }
2657
+
2658
+ @Injectable()
2659
+ class PluginA extends Plugin {
2660
+ execute() {
2661
+ console.log('Plugin A executing');
2662
+ }
2663
+ }
2664
+
2665
+ @Injectable()
2666
+ class PluginB extends Plugin {
2667
+ execute() {
2668
+ console.log('Plugin B executing');
2669
+ }
2670
+ }
2671
+
2672
+ @Injectable()
2673
+ class PluginC extends Plugin {
2674
+ execute() {
2675
+ console.log('Plugin C executing');
2676
+ }
2677
+ }
2678
+
2679
+ const PluginModule = ProviderModule.create({
2680
+ id: 'PluginModule',
2681
+ providers: [
2682
+ { provide: Plugin, useClass: PluginA },
2683
+ { provide: Plugin, useClass: PluginB },
2684
+ { provide: Plugin, useClass: PluginC },
2685
+ ],
2686
+ });
2687
+
2688
+ // Get all plugins as array (third parameter = asList)
2689
+ const plugins = PluginModule.get(Plugin, false, true);
2690
+ console.log(plugins.length); // 3
2691
+
2692
+ // Execute all plugins
2693
+ plugins.forEach((plugin) => plugin.execute());
2694
+ // Output:
2695
+ // Plugin A executing
2696
+ // Plugin B executing
2697
+ // Plugin C executing
2698
+ ```
2699
+
2700
+ **String Token Example:**
2701
+
2702
+ ```ts
2703
+ const MyModule = ProviderModule.create({
2704
+ id: 'MyModule',
2705
+ providers: [
2706
+ { provide: 'Handler', useValue: 'Handler1' },
2707
+ { provide: 'Handler', useValue: 'Handler2' },
2708
+ { provide: 'Handler', useValue: 'Handler3' },
2709
+ ],
2710
+ });
2711
+
2712
+ const handlers = MyModule.get('Handler', false, true);
2713
+ console.log(handlers); // ['Handler1', 'Handler2', 'Handler3']
2714
+ ```
2715
+
2716
+ ### Batch Resolution with getMany()
2717
+
2718
+ Resolve multiple providers in a single call.
2719
+
2720
+ ```ts
2721
+ @Injectable()
2722
+ class ServiceA {
2723
+ name = 'A';
2724
+ }
2725
+
2726
+ @Injectable()
2727
+ class ServiceB {
2728
+ name = 'B';
2729
+ }
2730
+
2731
+ @Injectable()
2732
+ class ServiceC {
2733
+ name = 'C';
2734
+ }
2735
+
2736
+ const MyModule = ProviderModule.create({
2737
+ id: 'MyModule',
2738
+ providers: [
2739
+ ServiceA,
2740
+ ServiceB,
2741
+ ServiceC,
2742
+ { provide: 'CONFIG_A', useValue: 'config-a' },
2743
+ { provide: 'CONFIG_B', useValue: 'config-b' },
2744
+ ],
2745
+ });
2746
+
2747
+ // Simple getMany
2748
+ const [serviceA, serviceB, configA] = MyModule.getMany(ServiceA, ServiceB, 'CONFIG_A');
2749
+
2750
+ console.log(serviceA.name); // 'A'
2751
+ console.log(serviceB.name); // 'B'
2752
+ console.log(configA); // 'config-a'
2753
+ ```
2754
+
2755
+ **With Options:**
2756
+
2757
+ ```ts
2758
+ // Optional providers
2759
+ const [serviceA, missing, serviceC] = MyModule.getMany(
2760
+ ServiceA,
2761
+ { provider: 'NON_EXISTENT', isOptional: true },
2762
+ ServiceC
2763
+ );
2764
+
2765
+ console.log(serviceA); // ServiceA instance
2766
+ console.log(missing); // undefined (no error)
2767
+ console.log(serviceC); // ServiceC instance
2768
+
2769
+ // Get as list (multiple bindings)
2770
+ const HandlerModule = ProviderModule.create({
2771
+ id: 'HandlerModule',
2772
+ providers: [
2773
+ { provide: 'Handler', useValue: 'H1' },
2774
+ { provide: 'Handler', useValue: 'H2' },
2775
+ { provide: 'Handler', useValue: 'H3' },
2776
+ ],
2777
+ });
2778
+
2779
+ const [handlers] = HandlerModule.getMany({
2780
+ provider: 'Handler',
2781
+ asList: true,
2782
+ });
2783
+
2784
+ console.log(handlers); // ['H1', 'H2', 'H3']
2785
+ ```
2786
+
2787
+ **Complex Example:**
2788
+
2789
+ ```ts
2790
+ const [database, cache, optionalLogger, allPlugins, config] = MyModule.getMany(
2791
+ DatabaseService,
2792
+ CacheService,
2793
+ { provider: LoggerService, isOptional: true },
2794
+ { provider: Plugin, asList: true },
2795
+ 'APP_CONFIG'
2796
+ );
2797
+
2798
+ // All providers resolved in one call
2799
+ // optionalLogger is undefined if not available
2800
+ // allPlugins is an array of all Plugin bindings
2801
+ ```
2802
+
2803
+ > [!IMPORTANT] > `getMany()` parameter types:
2804
+ >
2805
+ > - **Simple**: Just pass the token directly
2806
+ > - **With options**: Use object with `provider`, `isOptional`, and/or `asList`
2807
+
2808
+ ## Resources
2809
+
2810
+ 📚 **[Full API Documentation](https://adimarianmutu.github.io/x-injection/index.html)** - Complete TypeDoc reference
2811
+
2812
+ ⚛️ **[React Integration](https://github.com/AdiMarianMutu/x-injection-reactjs)** - Official React hooks and providers
2813
+
2814
+ 💡 **[GitHub Issues](https://github.com/AdiMarianMutu/x-injection/issues)** - Bug reports and feature requests
2815
+
2816
+ 🌟 **[GitHub Repository](https://github.com/AdiMarianMutu/x-injection)** - Source code and examples
2817
+
2818
+ ## Contributing
2819
+
2820
+ Contributions are welcome! Please ensure code follows the project style guidelines and includes appropriate tests.
2821
+
2822
+ 1. Fork the repository
2823
+ 2. Create a feature branch
2824
+ 3. Make your changes with tests
2825
+ 4. Submit a pull request
2826
+
2827
+ ## Credits
2828
+
2829
+ **Author:** [Adi-Marian Mutu](https://www.linkedin.com/in/mutu-adi-marian/)
2830
+
2831
+ **Built on:** [InversifyJS](https://github.com/inversify/monorepo)
2832
+
2833
+ **Logo:** [Alexandru Turica](https://www.linkedin.com/in/alexandru-turica-82215522b/)
2834
+
2835
+ ## License
2836
+
2837
+ MIT © Adi-Marian Mutu