@adimm/x-injection 2.1.2 → 3.0.0

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,683 @@
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
+ ## Table of Contents
17
+
18
+ - [Table of Contents](#table-of-contents)
19
+ - [Overview](#overview)
20
+ - [Features](#features)
21
+ - [Installation](#installation)
22
+ - [Quick Start](#quick-start)
23
+ - [OOP-Style Modules](#oop-style-modules)
24
+ - [Basic OOP Module](#basic-oop-module)
25
+ - [Advanced OOP Patterns](#advanced-oop-patterns)
26
+ - [When to Use OOP vs Functional](#when-to-use-oop-vs-functional)
27
+ - [Core Concepts](#core-concepts)
28
+ - [ProviderModule](#providermodule)
29
+ - [AppModule](#appmodule)
30
+ - [Blueprints](#blueprints)
31
+ - [Provider Tokens](#provider-tokens)
32
+ - [Injection Scopes](#injection-scopes)
33
+ - [Singleton (Default)](#singleton-default)
34
+ - [Transient](#transient)
35
+ - [Request](#request)
36
+ - [Module System](#module-system)
37
+ - [Import/Export Pattern](#importexport-pattern)
38
+ - [Re-exporting Modules](#re-exporting-modules)
39
+ - [Dynamic Module Updates](#dynamic-module-updates)
40
+ - [Global Modules](#global-modules)
41
+ - [Advanced Features](#advanced-features)
42
+ - [Events](#events)
43
+ - [Middlewares](#middlewares)
44
+ - [Testing](#testing)
45
+ - [Resources](#resources)
46
+ - [Contributing](#contributing)
47
+ - [Credits](#credits)
48
+ - [License](#license)
49
+
50
+ ## Overview
51
+
52
+ **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.
53
+
54
+ ## Features
55
+
56
+ - **Modular Architecture** - NestJS-style import/export system for clean dependency boundaries
57
+ - **Isolated Containers** - Each module manages its own InversifyJS container
58
+ - **Flexible Scopes** - Singleton, Transient, and Request-scoped providers
59
+ - **Lazy Loading** - Blueprint pattern for deferred module instantiation
60
+ - **Lifecycle Hooks** - `onReady`, `onReset`, `onDispose` for module lifecycle management
61
+ - **Events & Middlewares** - Deep customization through event subscriptions and middleware chains
62
+ - **Framework Agnostic** - Works in Node.js and browser environments
63
+ - **TypeScript First** - Full type safety with decorator support
64
+
65
+ ## Installation
66
+
67
+ ```bash
68
+ npm install @adimm/x-injection reflect-metadata
69
+ ```
70
+
71
+ **TypeScript Configuration** (`tsconfig.json`):
72
+
73
+ ```json
74
+ {
75
+ "compilerOptions": {
76
+ "experimentalDecorators": true,
77
+ "emitDecoratorMetadata": true
78
+ }
79
+ }
80
+ ```
81
+
82
+ Import `reflect-metadata` at your application's entry point:
83
+
84
+ ```ts
85
+ import 'reflect-metadata';
86
+ ```
87
+
88
+ ## Quick Start
89
+
90
+ ```ts
91
+ import { Injectable, ProviderModule } from '@adimm/x-injection';
92
+
93
+ @Injectable()
94
+ class UserService {
95
+ getUser(id: string) {
96
+ return { id, name: 'John Doe' };
97
+ }
98
+ }
99
+
100
+ @Injectable()
101
+ class AuthService {
102
+ constructor(private userService: UserService) {}
103
+
104
+ login(userId: string) {
105
+ const user = this.userService.getUser(userId);
106
+ return `Logged in as ${user.name}`;
107
+ }
108
+ }
109
+
110
+ const AuthModule = ProviderModule.create({
111
+ id: 'AuthModule',
112
+ providers: [UserService, AuthService],
113
+ exports: [AuthService],
114
+ });
115
+
116
+ const authService = AuthModule.get(AuthService);
117
+ console.log(authService.login('123')); // "Logged in as John Doe"
118
+ ```
119
+
120
+ ## OOP-Style Modules
121
+
122
+ 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.
123
+
124
+ ### Basic OOP Module
125
+
126
+ ```ts
127
+ import { Injectable, ProviderModuleClass } from '@adimm/x-injection';
128
+
129
+ @Injectable()
130
+ class UserService {
131
+ get(id: string) {
132
+ return { id, name: 'John Doe' };
133
+ }
134
+ }
135
+
136
+ @Injectable()
137
+ class AuthService {
138
+ constructor(private userService: UserService) {}
139
+
140
+ login(userId: string) {
141
+ const user = this.userService.get(userId);
142
+ return `Logged in as ${user.name}`;
143
+ }
144
+ }
145
+
146
+ // OOP-style module extending ProviderModuleClass
147
+ class AuthModule extends ProviderModuleClass {
148
+ constructor() {
149
+ super({
150
+ id: 'AuthModule',
151
+ providers: [UserService, AuthService],
152
+ exports: [AuthService],
153
+ });
154
+ }
155
+
156
+ authenticateUser(userId: string): string {
157
+ const authService = this.module.get(AuthService);
158
+ return authService.login(userId);
159
+ }
160
+
161
+ getUserById(userId: string) {
162
+ const userService = this.module.get(UserService);
163
+ return userService.get(userId);
164
+ }
165
+ }
166
+
167
+ // Instantiate and use
168
+ const authModule = new AuthModule();
169
+ console.log(authModule.authenticateUser('123')); // "Logged in as John Doe"
170
+
171
+ // All ProviderModule methods are available through the `.module` property
172
+ const authService = authModule.module.get(AuthService);
173
+ authModule.module.update.addProvider(NewService);
174
+ ```
175
+
176
+ ### Advanced OOP Patterns
177
+
178
+ **Module with Initialization Logic:**
179
+
180
+ ```ts
181
+ class DatabaseModule extends ProviderModuleClass {
182
+ private isConnected = false;
183
+
184
+ constructor() {
185
+ super({
186
+ id: 'DatabaseModule',
187
+ providers: [DatabaseService, ConnectionPool],
188
+ exports: [DatabaseService],
189
+ onReady: async (module) => {
190
+ console.log('DatabaseModule ready!');
191
+ },
192
+ });
193
+ }
194
+
195
+ async connect(): Promise<void> {
196
+ const dbService = this.module.get(DatabaseService);
197
+ await dbService.connect();
198
+ this.isConnected = true;
199
+ }
200
+
201
+ getConnectionStatus(): boolean {
202
+ return this.isConnected;
203
+ }
204
+ }
205
+
206
+ const dbModule = new DatabaseModule();
207
+ await dbModule.connect();
208
+ console.log(dbModule.getConnectionStatus()); // true
209
+ ```
210
+
211
+ **Module with Computed Properties:**
212
+
213
+ ```ts
214
+ class ApiModule extends ProviderModuleClass {
215
+ constructor() {
216
+ super({
217
+ id: 'ApiModule',
218
+ imports: [ConfigModule, LoggerModule],
219
+ providers: [ApiService, HttpClient],
220
+ exports: [ApiService],
221
+ });
222
+ }
223
+
224
+ // Computed property - lazy evaluation
225
+ get apiService(): ApiService {
226
+ return this.module.get(ApiService);
227
+ }
228
+
229
+ get httpClient(): HttpClient {
230
+ return this.module.get(HttpClient);
231
+ }
232
+
233
+ // Business logic using multiple services
234
+ async makeAuthenticatedRequest(url: string, token: string) {
235
+ const client = this.httpClient;
236
+ return client.request(url, {
237
+ headers: { Authorization: `Bearer ${token}` },
238
+ });
239
+ }
240
+ }
241
+
242
+ const apiModule = new ApiModule();
243
+ const response = await apiModule.makeAuthenticatedRequest('/users', 'token');
244
+ ```
245
+
246
+ **Module Composition:**
247
+
248
+ ```ts
249
+ class BaseModule extends ProviderModuleClass {
250
+ protected logAction(action: string): void {
251
+ const logger = this.module.get(LoggerService);
252
+ logger.log(`[${String(this.module.id)}] ${action}`);
253
+ }
254
+ }
255
+
256
+ class UserModule extends BaseModule {
257
+ constructor() {
258
+ super({
259
+ id: 'UserModule',
260
+ providers: [UserService, UserRepository],
261
+ exports: [UserService],
262
+ });
263
+ }
264
+
265
+ createUser(name: string) {
266
+ this.logAction(`Creating user: ${name}`);
267
+ const userService = this.module.get(UserService);
268
+ return userService.create(name);
269
+ }
270
+
271
+ deleteUser(id: string) {
272
+ this.logAction(`Deleting user: ${id}`);
273
+ const userService = this.module.get(UserService);
274
+ return userService.delete(id);
275
+ }
276
+ }
277
+ ```
278
+
279
+ ### When to Use OOP vs Functional
280
+
281
+ **Use OOP-style (`extends ProviderModuleClass`) when:**
282
+
283
+ - You need custom business logic methods on the module itself
284
+ - You prefer class-based architecture
285
+ - You want computed properties or getters for providers
286
+ - You need initialization logic or state management in the module
287
+ - You're building a complex module with multiple related operations
288
+
289
+ **Use Functional-style (`ProviderModule.create()`) when:**
290
+
291
+ - You only need dependency injection without custom logic
292
+ - You prefer functional composition
293
+ - You want simpler, more concise code
294
+ - You're creating straightforward provider containers
295
+
296
+ **Key Point:** Both styles are fully compatible and can be mixed within the same application. `ProviderModuleClass` uses composition (contains a `ProviderModule` as `this.module`), preventing method name conflicts while providing identical DI functionality.
297
+
298
+ ## Core Concepts
299
+
300
+ ### ProviderModule
301
+
302
+ The fundamental building block of xInjection. Similar to NestJS modules, each `ProviderModule` encapsulates related providers with explicit control over what's exposed.
303
+
304
+ ```ts
305
+ const DatabaseModule = ProviderModule.create({
306
+ id: 'DatabaseModule',
307
+ imports: [ConfigModule], // Modules to import
308
+ providers: [DatabaseService], // Services to register
309
+ exports: [DatabaseService], // What to expose to importers
310
+ });
311
+ ```
312
+
313
+ **Key Methods:**
314
+
315
+ - `Module.get(token)` - Resolve a provider instance
316
+ - `Module.update.addProvider()` - Dynamically add providers
317
+ - `Module.update.addImport()` - Import other modules at runtime
318
+ - `Module.dispose()` - Clean up module resources
319
+
320
+ [Full API Documentation →](https://adimarianmutu.github.io/x-injection/classes/IProviderModule.html)
321
+
322
+ ### AppModule
323
+
324
+ The global root module, automatically available in every application. Global modules are auto-imported into `AppModule`.
325
+
326
+ ```ts
327
+ import { AppModule } from '@adimm/x-injection';
328
+
329
+ // Add global providers
330
+ AppModule.update.addProvider(LoggerService);
331
+
332
+ // Access from any module
333
+ const anyModule = ProviderModule.create({ id: 'AnyModule' });
334
+ const logger = anyModule.get(LoggerService);
335
+ ```
336
+
337
+ ### Blueprints
338
+
339
+ Blueprints allow you to define module configurations without instantiating them, enabling lazy loading and template reuse.
340
+
341
+ ```ts
342
+ // Define blueprint
343
+ const DatabaseModuleBp = ProviderModule.blueprint({
344
+ id: 'DatabaseModule',
345
+ providers: [DatabaseService],
346
+ exports: [DatabaseService],
347
+ });
348
+
349
+ // Import blueprint (auto-converts to module)
350
+ const AppModule = ProviderModule.create({
351
+ id: 'AppModule',
352
+ imports: [DatabaseModuleBp],
353
+ });
354
+
355
+ // Or create module from blueprint later
356
+ const DatabaseModule = ProviderModule.create(DatabaseModuleBp);
357
+ ```
358
+
359
+ **Benefits:**
360
+
361
+ - Deferred instantiation for better startup performance
362
+ - Reusable module templates across your application
363
+ - Scoped singletons per importing module
364
+
365
+ ### Provider Tokens
366
+
367
+ xInjection supports four types of provider tokens:
368
+
369
+ **1. Class Token** (simplest):
370
+
371
+ ```ts
372
+ @Injectable()
373
+ class ApiService {}
374
+
375
+ providers: [ApiService];
376
+ ```
377
+
378
+ **2. Class Token with Substitution**:
379
+
380
+ ```ts
381
+ providers: [{ provide: ApiService, useClass: MockApiService }];
382
+ ```
383
+
384
+ **3. Value Token** (constants):
385
+
386
+ ```ts
387
+ providers: [{ provide: 'API_KEY', useValue: 'secret-key-123' }];
388
+ ```
389
+
390
+ **4. Factory Token** (dynamic):
391
+
392
+ ```ts
393
+ providers: [
394
+ {
395
+ provide: 'DATABASE_CONNECTION',
396
+ useFactory: (config: ConfigService) => createConnection(config.dbUrl),
397
+ inject: [ConfigService],
398
+ },
399
+ ];
400
+ ```
401
+
402
+ ## Injection Scopes
403
+
404
+ Control provider lifecycle with three scope types (priority order: token > decorator > module default):
405
+
406
+ ### Singleton (Default)
407
+
408
+ Cached after first resolution - same instance every time:
409
+
410
+ ```ts
411
+ @Injectable() // Singleton by default
412
+ class DatabaseService {}
413
+
414
+ Module.get(DatabaseService) === Module.get(DatabaseService); // true
415
+ ```
416
+
417
+ ### Transient
418
+
419
+ New instance on every resolution:
420
+
421
+ ```ts
422
+ @Injectable(InjectionScope.Transient)
423
+ class RequestLogger {}
424
+
425
+ Module.get(RequestLogger) === Module.get(RequestLogger); // false
426
+ ```
427
+
428
+ ### Request
429
+
430
+ Single instance per resolution tree (useful for request-scoped data):
431
+
432
+ ```ts
433
+ @Injectable(InjectionScope.Request)
434
+ class RequestContext {}
435
+
436
+ @Injectable(InjectionScope.Transient)
437
+ class Controller {
438
+ constructor(
439
+ public ctx1: RequestContext,
440
+ public ctx2: RequestContext
441
+ ) {}
442
+ }
443
+
444
+ const controller = Module.get(Controller);
445
+ controller.ctx1 === controller.ctx2; // true (same resolution)
446
+
447
+ const controller2 = Module.get(Controller);
448
+ controller.ctx1 === controller2.ctx1; // false (different resolution)
449
+ ```
450
+
451
+ **Setting Scopes:**
452
+
453
+ ```ts
454
+ // 1. In provider token (highest priority)
455
+ providers: [{ provide: Service, useClass: Service, scope: InjectionScope.Transient }];
456
+
457
+ // 2. In @Injectable decorator
458
+ @Injectable(InjectionScope.Request)
459
+ class Service {}
460
+
461
+ // 3. Module default (lowest priority)
462
+ ProviderModule.create({
463
+ id: 'MyModule',
464
+ defaultScope: InjectionScope.Transient,
465
+ });
466
+ ```
467
+
468
+ ## Module System
469
+
470
+ ### Import/Export Pattern
471
+
472
+ Modules explicitly control dependency boundaries through imports and exports:
473
+
474
+ ```ts
475
+ const DatabaseModule = ProviderModule.create({
476
+ id: 'DatabaseModule',
477
+ providers: [DatabaseService, InternalCacheService],
478
+ exports: [DatabaseService], // Only DatabaseService is accessible
479
+ });
480
+
481
+ const ApiModule = ProviderModule.create({
482
+ id: 'ApiModule',
483
+ imports: [DatabaseModule], // Gets access to DatabaseService
484
+ providers: [ApiService],
485
+ });
486
+
487
+ // ✅ Works
488
+ const dbService = ApiModule.get(DatabaseService);
489
+
490
+ // ❌ Error - InternalCacheService not exported
491
+ const cache = ApiModule.get(InternalCacheService);
492
+ ```
493
+
494
+ ### Re-exporting Modules
495
+
496
+ Modules can re-export imported modules to create aggregation modules:
497
+
498
+ ```ts
499
+ const CoreModule = ProviderModule.create({
500
+ id: 'CoreModule',
501
+ imports: [DatabaseModule, ConfigModule],
502
+ exports: [DatabaseModule, ConfigModule], // Re-export both
503
+ });
504
+
505
+ // Consumers get both DatabaseModule and ConfigModule
506
+ const AppModule = ProviderModule.create({
507
+ imports: [CoreModule],
508
+ });
509
+ ```
510
+
511
+ ### Dynamic Module Updates
512
+
513
+ Modules support runtime modifications (use sparingly for performance):
514
+
515
+ ```ts
516
+ const module = ProviderModule.create({ id: 'DynamicModule' });
517
+
518
+ // Add providers dynamically
519
+ module.update.addProvider(NewService);
520
+ module.update.addProvider(AnotherService, true); // true = also export
521
+
522
+ // Add imports dynamically
523
+ module.update.addImport(DatabaseModule, true); // true = also export
524
+ ```
525
+
526
+ **Important:** Dynamic imports propagate automatically - if `ModuleA` imports `ModuleB`, and `ModuleB` dynamically imports `ModuleC` (with export), `ModuleA` automatically gets access to `ModuleC`'s exports.
527
+
528
+ ### Global Modules
529
+
530
+ Mark modules as global to auto-import into `AppModule`:
531
+
532
+ ```ts
533
+ const LoggerModule = ProviderModule.create({
534
+ id: 'LoggerModule',
535
+ isGlobal: true,
536
+ providers: [LoggerService],
537
+ exports: [LoggerService],
538
+ });
539
+
540
+ // LoggerService now available in all modules without explicit import
541
+ ```
542
+
543
+ ## Advanced Features
544
+
545
+ > [!WARNING]
546
+ > These features provide deep customization but can add complexity. Use them only when necessary.
547
+
548
+ ### Events
549
+
550
+ Subscribe to module lifecycle events for monitoring and debugging:
551
+
552
+ ```ts
553
+ import { DefinitionEventType } from '@adimm/x-injection';
554
+
555
+ const module = ProviderModule.create({
556
+ id: 'MyModule',
557
+ providers: [MyService],
558
+ });
559
+
560
+ const unsubscribe = module.update.subscribe(({ type, change }) => {
561
+ if (type === DefinitionEventType.GetProvider) {
562
+ console.log('Provider resolved:', change);
563
+ }
564
+ if (type === DefinitionEventType.Import) {
565
+ console.log('Module imported:', change);
566
+ }
567
+ });
568
+
569
+ // Clean up when done
570
+ unsubscribe();
571
+ ```
572
+
573
+ **Available Events:** `GetProvider`, `Import`, `Export`, `AddProvider`, `RemoveProvider`, `ExportModule` - [Full list ](https://adimarianmutu.github.io/x-injection/enums/DefinitionEventType.html)
574
+
575
+ > [!WARNING]
576
+ > Always unsubscribe to prevent memory leaks. Events fire **after** middlewares.
577
+
578
+ ### Middlewares
579
+
580
+ Intercept and transform provider resolution before values are returned:
581
+
582
+ ```ts
583
+ import { MiddlewareType } from '@adimm/x-injection';
584
+
585
+ const module = ProviderModule.create({
586
+ id: 'MyModule',
587
+ providers: [PaymentService],
588
+ });
589
+
590
+ // Transform resolved values
591
+ module.middlewares.add(MiddlewareType.BeforeGet, (provider, token, inject) => {
592
+ // Pass through if not interested
593
+ if (!(provider instanceof PaymentService)) return true;
594
+
595
+ // Use inject() to avoid infinite loops
596
+ const logger = inject(LoggerService);
597
+ logger.log('Payment service accessed');
598
+
599
+ // Transform the value
600
+ return {
601
+ timestamp: Date.now(),
602
+ value: provider,
603
+ };
604
+ });
605
+
606
+ const payment = module.get(PaymentService);
607
+ // { timestamp: 1234567890, value: PaymentService }
608
+ ```
609
+
610
+ **Control export access:**
611
+
612
+ ```ts
613
+ module.middlewares.add(MiddlewareType.OnExportAccess, (importerModule, exportToken) => {
614
+ // Restrict access based on importer
615
+ if (importerModule.id === 'UntrustedModule' && exportToken === SensitiveService) {
616
+ return false; // Deny access
617
+ }
618
+ return true; // Allow
619
+ });
620
+ ```
621
+
622
+ **Available Middlewares:** `BeforeGet`, `BeforeAddProvider`, `BeforeAddImport`, `OnExportAccess` - [Full list →](https://adimarianmutu.github.io/x-injection/enums/MiddlewareType.html)
623
+
624
+ > [!CAUTION]
625
+ >
626
+ > - Returning `false` aborts the chain (no value returned)
627
+ > - Returning `true` passes value unchanged
628
+ > - Middlewares execute in registration order
629
+ > - Always handle errors in middleware chains
630
+
631
+ ## Testing
632
+
633
+ Create mock modules easily using blueprint cloning:
634
+
635
+ ```ts
636
+ // Production module
637
+ const ApiModuleBp = ProviderModule.blueprint({
638
+ id: 'ApiModule',
639
+ providers: [UserService, ApiService],
640
+ exports: [ApiService],
641
+ });
642
+
643
+ // Test module - clone and override
644
+ const ApiModuleMock = ApiModuleBp.clone().updateDefinition({
645
+ id: 'ApiModuleMock',
646
+ providers: [
647
+ { provide: UserService, useClass: MockUserService },
648
+ {
649
+ provide: ApiService,
650
+ useValue: {
651
+ sendRequest: jest.fn().mockResolvedValue({ data: 'test' }),
652
+ },
653
+ },
654
+ ],
655
+ });
656
+
657
+ // Use in tests
658
+ const testModule = ProviderModule.create({
659
+ imports: [ApiModuleMock],
660
+ });
661
+ ```
662
+
663
+ ## Resources
664
+
665
+ 📚 **[Full API Documentation](https://adimarianmutu.github.io/x-injection/index.html)** - Complete TypeDoc reference
666
+
667
+ ⚛️ **[React Integration](https://github.com/AdiMarianMutu/x-injection-reactjs)** - Official React hooks and providers
668
+
669
+ 💡 **[GitHub Issues](https://github.com/AdiMarianMutu/x-injection/issues)** - Bug reports and feature requests
670
+
671
+ ## Contributing
672
+
673
+ Contributions welcome! Please ensure code follows the project style guidelines.
674
+
675
+ ## Credits
676
+
677
+ **Author:** [Adi-Marian Mutu](https://www.linkedin.com/in/mutu-adi-marian/)
678
+ **Built on:** [InversifyJS](https://github.com/inversify/monorepo)
679
+ **Logo:** [Alexandru Turica](https://www.linkedin.com/in/alexandru-turica-82215522b/)
680
+
681
+ ## License
682
+
683
+ MIT © Adi-Marian Mutu