@adimm/x-injection 3.0.1 → 3.0.2

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
@@ -15,6 +15,8 @@
15
15
 
16
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
17
 
18
+ > **TL;DR** — Mark classes with `@Injectable()`, group them into `ProviderModule`s with explicit `imports`/`exports`, and call `module.get(MyService)`. Dependencies are wired automatically, scoped correctly, and fully testable without touching production code.
19
+
18
20
  ## Table of Contents
19
21
 
20
22
  - [Table of Contents](#table-of-contents)
@@ -23,8 +25,6 @@
23
25
  - [Problem 2: Tight Coupling and Testing Difficulty](#problem-2-tight-coupling-and-testing-difficulty)
24
26
  - [Problem 3: Lack of Encapsulation](#problem-3-lack-of-encapsulation)
25
27
  - [Problem 4: Lifecycle Management Complexity](#problem-4-lifecycle-management-complexity)
26
- - [Overview](#overview)
27
- - [Features](#features)
28
28
  - [Installation](#installation)
29
29
  - [Quick Start](#quick-start)
30
30
  - [Core Concepts](#core-concepts)
@@ -32,10 +32,6 @@
32
32
  - [Modules](#modules)
33
33
  - [Blueprints](#blueprints)
34
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
35
  - [Provider Tokens](#provider-tokens)
40
36
  - [1. Class Token](#1-class-token)
41
37
  - [2. Class Token with Substitution](#2-class-token-with-substitution)
@@ -77,10 +73,13 @@
77
73
  - [Blueprint Cloning](#blueprint-cloning)
78
74
  - [Provider Substitution](#provider-substitution)
79
75
  - [Mocking Services](#mocking-services)
76
+ - [OOP-Style Modules with ProviderModuleClass](#oop-style-modules-with-providermoduleclass)
77
+ - [Basic OOP Module](#basic-oop-module)
78
+ - [When to Use OOP vs Functional](#when-to-use-oop-vs-functional)
80
79
  - [Advanced Module API](#advanced-module-api)
81
80
  - [Query Methods](#query-methods)
82
- - [Multiple Provider Binding](#multiple-provider-binding)
83
81
  - [Batch Resolution with getMany()](#batch-resolution-with-getmany)
82
+ - [Hierarchical Dependency Injection](#hierarchical-dependency-injection)
84
83
  - [Resources](#resources)
85
84
  - [Contributing](#contributing)
86
85
  - [Credits](#credits)
@@ -97,15 +96,15 @@ Modern applications face several dependency management challenges. Let's examine
97
96
  ```ts
98
97
  // Manually creating and wiring dependencies
99
98
  class DatabaseService {
100
- constructor(private config: ConfigService) {}
99
+ constructor(private readonly config: ConfigService) {}
101
100
  }
102
101
 
103
102
  class UserRepository {
104
- constructor(private db: DatabaseService) {}
103
+ constructor(private readonly db: DatabaseService) {}
105
104
  }
106
105
 
107
106
  class AuthService {
108
- constructor(private userRepo: UserRepository) {}
107
+ constructor(private readonly userRepo: UserRepository) {}
109
108
  }
110
109
 
111
110
  // Manual instantiation nightmare
@@ -123,17 +122,17 @@ const authService = new AuthService(userRepo);
123
122
  ```ts
124
123
  @Injectable()
125
124
  class DatabaseService {
126
- constructor(private config: ConfigService) {}
125
+ constructor(private readonly config: ConfigService) {}
127
126
  }
128
127
 
129
128
  @Injectable()
130
129
  class UserRepository {
131
- constructor(private db: DatabaseService) {}
130
+ constructor(private readonly db: DatabaseService) {}
132
131
  }
133
132
 
134
133
  @Injectable()
135
134
  class AuthService {
136
- constructor(private userRepo: UserRepository) {}
135
+ constructor(private readonly userRepo: UserRepository) {}
137
136
  }
138
137
 
139
138
  const AuthModule = ProviderModule.create({
@@ -169,7 +168,7 @@ class PaymentService {
169
168
  ```ts
170
169
  @Injectable()
171
170
  class PaymentService {
172
- constructor(private paymentGateway: PaymentGateway) {}
171
+ constructor(private readonly paymentGateway: PaymentGateway) {}
173
172
 
174
173
  async charge(amount: number) {
175
174
  return this.paymentGateway.charge(amount);
@@ -185,6 +184,9 @@ const ProductionModule = ProviderModule.create({
185
184
  const TestModule = ProviderModule.create({
186
185
  providers: [{ provide: PaymentGateway, useClass: MockPaymentGateway }, PaymentService],
187
186
  });
187
+
188
+ const testService = TestModule.get(PaymentService);
189
+ // testService.charge() → logs "Mock charge: $100" instead of hitting Stripe
188
190
  ```
189
191
 
190
192
  ### Problem 3: Lack of Encapsulation
@@ -228,7 +230,7 @@ const ApiModule = ProviderModule.create({
228
230
  const queryBuilder = ApiModule.get(QueryBuilder);
229
231
 
230
232
  // ❌ Error - CacheService not exported (properly encapsulated!)
231
- const cache = ApiModule.get(CacheService);
233
+ const cache = ApiModule.get(CacheService); // throws InjectionProviderModuleMissingProviderError
232
234
  ```
233
235
 
234
236
  ### Problem 4: Lifecycle Management Complexity
@@ -282,23 +284,11 @@ const AppModule = ProviderModule.create({
282
284
  },
283
285
  });
284
286
 
285
- // Lifecycle automatically managed
286
- await AppModule.dispose(); // Everything cleaned up properly
287
+ // Lifecycle automatically managed — onDispose runs db.disconnect() automatically
288
+ await AppModule.dispose(); // No forgotten cleanup, no resource leaks
287
289
  ```
288
290
 
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
291
+ **xInjection** is a [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 solves the pain points above through:
302
292
 
303
293
  - **Modular Architecture** - NestJS-style import/export system for clean dependency boundaries
304
294
  - **Isolated Containers** - Each module manages its own InversifyJS container
@@ -347,7 +337,7 @@ class UserService {
347
337
 
348
338
  @Injectable()
349
339
  class AuthService {
350
- constructor(private userService: UserService) {}
340
+ constructor(private readonly userService: UserService) {}
351
341
 
352
342
  login(userId: string) {
353
343
  const user = this.userService.getUser(userId);
@@ -392,8 +382,8 @@ class RequestContext {
392
382
  @Injectable()
393
383
  class ApiService {
394
384
  constructor(
395
- private logger: LoggerService,
396
- private context: RequestContext
385
+ private readonly logger: LoggerService,
386
+ private readonly context: RequestContext
397
387
  ) {}
398
388
 
399
389
  async fetchData() {
@@ -434,8 +424,8 @@ class InternalCacheService {
434
424
  @Injectable()
435
425
  class UserRepository {
436
426
  constructor(
437
- private db: DatabaseService,
438
- private cache: InternalCacheService
427
+ private readonly db: DatabaseService,
428
+ private readonly cache: InternalCacheService
439
429
  ) {}
440
430
 
441
431
  findById(id: string) {
@@ -463,34 +453,6 @@ const userRepo = ApiModule.get(UserRepository);
463
453
  // const cache = ApiModule.get(InternalCacheService);
464
454
  ```
465
455
 
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
456
  ### Blueprints
495
457
 
496
458
  Blueprints allow you to define module configurations without instantiating them, enabling lazy loading and template reuse.
@@ -533,246 +495,24 @@ const ConfigModuleMock = ConfigModuleBp.clone().updateDefinition({
533
495
  - **Deferred Instantiation** - Only create modules when needed
534
496
  - **Reusable Templates** - Define once, use in multiple places
535
497
  - **Testing** - Clone and modify for test scenarios
536
- - **Scoped Singletons** - Each importer gets its own module instance
498
+ - **Scoped Singletons** - Each importing module gets its own independent module instance converted from the blueprint
537
499
 
538
500
  > [!TIP]
539
501
  > Use blueprints when you need the same module configuration in multiple places, or when you want to delay module creation until runtime.
540
502
 
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
- });
503
+ > [!IMPORTANT]
504
+ > When a blueprint is imported into multiple modules, each importing module receives its **own separate instance** of that blueprint — converted to a full module independently. This means that providers declared as `Singleton` inside a blueprint are only singletons **relative to the module that imported them**, not globally. If `ModuleA` and `ModuleB` both import `ConfigModuleBp`, they each get their own `ConfigService` singleton — the two instances are completely independent of each other.
578
505
 
579
- // Now all modules have access to LoggerService
580
- const AnyModule = ProviderModule.create({
581
- id: 'AnyModule',
582
- });
506
+ ### AppModule
583
507
 
584
- const logger = AnyModule.get(LoggerService); // Works!
585
- ```
508
+ `AppModule` is the built-in global root container. Any module or blueprint created with `isGlobal: true` is automatically imported into it, making its exported providers available to every other module without an explicit `imports` declaration.
586
509
 
587
510
  > [!WARNING]
588
511
  >
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.
512
+ > - `id: 'AppModule'` is reserved you cannot create your own module with that name
513
+ > - You cannot import `AppModule` into other modules
514
+ >
515
+ > See [Global Modules](#global-modules) for full usage and best practices.
776
516
 
777
517
  ## Provider Tokens
778
518
 
@@ -907,58 +647,6 @@ const connection = DatabaseModule.get<DatabaseConnection>('DATABASE_CONNECTION')
907
647
  console.log(connection.url); // 'postgres://localhost:5432/mydb'
908
648
  ```
909
649
 
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
650
  > [!TIP]
963
651
  > Use factory tokens when:
964
652
  >
@@ -1106,43 +794,6 @@ const s2 = MyModule.get(MyService);
1106
794
  console.log(s1 === s2); // false
1107
795
  ```
1108
796
 
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
797
  > [!IMPORTANT]
1147
798
  > Request scope is useful for scenarios like:
1148
799
  >
@@ -1337,7 +988,7 @@ console.log(ModuleB.hasProvider(ServiceC)); // true
1337
988
 
1338
989
  ### Global Modules
1339
990
 
1340
- Mark modules as global to auto-import into `AppModule`, making them available everywhere.
991
+ Declare a single `AppBootstrapModule` blueprint with `isGlobal: true` at your app's entry point. It is automatically imported into `AppModule`, making its exported providers available to every module without any explicit `imports`.
1341
992
 
1342
993
  ```ts
1343
994
  @Injectable()
@@ -1347,56 +998,29 @@ class LoggerService {
1347
998
  }
1348
999
  }
1349
1000
 
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
1001
  @Injectable()
1372
1002
  class ConfigService {
1373
1003
  apiUrl = 'https://api.example.com';
1374
1004
  }
1375
1005
 
1376
- // Blueprint with global flag
1377
- const ConfigModuleBp = ProviderModule.blueprint({
1378
- id: 'ConfigModule',
1006
+ // Declare once at app entry point
1007
+ ProviderModule.blueprint({
1008
+ id: 'AppBootstrapModule',
1379
1009
  isGlobal: true,
1380
- providers: [ConfigService],
1381
- exports: [ConfigService],
1010
+ providers: [LoggerService, ConfigService],
1011
+ exports: [LoggerService, ConfigService],
1382
1012
  });
1383
1013
 
1384
- // Automatically imports into AppModule
1385
- console.log(AppModule.isImportingModule('ConfigModule')); // true
1386
- console.log(AppModule.hasProvider(ConfigService)); // true
1014
+ // Automatically imported into AppModule
1015
+ console.log(AppModule.isImportingModule('AppBootstrapModule')); // true
1387
1016
 
1388
- // Available in all modules
1389
- const AnyModule = ProviderModule.create({ id: 'AnyModule' });
1390
- const config = AnyModule.get(ConfigService); // Works!
1017
+ // Every module can resolve these without importing anything
1018
+ const FeatureModule = ProviderModule.create({ id: 'FeatureModule' });
1019
+ const logger = FeatureModule.get(LoggerService); // Works!
1391
1020
  ```
1392
1021
 
1393
1022
  > [!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
1023
+ > Even if not enforced, it is recommended to keep `isGlobal: true` to a **single** `AppBootstrapModule`. Multiple global modules create hidden implicit dependencies that are hard to trace. If a module is not truly app-wide, import it explicitly instead.
1400
1024
 
1401
1025
  ## Dependency Injection
1402
1026
 
@@ -1423,8 +1047,8 @@ class LoggerService {
1423
1047
  class UserRepository {
1424
1048
  // Dependencies automatically injected via constructor
1425
1049
  constructor(
1426
- private db: DatabaseService,
1427
- private logger: LoggerService
1050
+ private readonly db: DatabaseService,
1051
+ private readonly logger: LoggerService
1428
1052
  ) {}
1429
1053
 
1430
1054
  findAll() {
@@ -1452,9 +1076,9 @@ import { Inject, Injectable } from '@adimm/x-injection';
1452
1076
  @Injectable()
1453
1077
  class ApiService {
1454
1078
  constructor(
1455
- @Inject('API_KEY') private apiKey: string,
1456
- @Inject('API_URL') private apiUrl: string,
1457
- @Inject('MAX_RETRIES') private maxRetries: number
1079
+ @Inject('API_KEY') private readonly apiKey: string,
1080
+ @Inject('API_URL') private readonly apiUrl: string,
1081
+ @Inject('MAX_RETRIES') private readonly maxRetries: number
1458
1082
  ) {}
1459
1083
 
1460
1084
  makeRequest() {
@@ -1494,7 +1118,7 @@ class StripePaymentGateway extends PaymentGateway {
1494
1118
 
1495
1119
  @Injectable()
1496
1120
  class PaymentService {
1497
- constructor(@Inject(PaymentGateway) private gateway: PaymentGateway) {}
1121
+ constructor(@Inject(PaymentGateway) private readonly gateway: PaymentGateway) {}
1498
1122
 
1499
1123
  async processPayment(amount: number) {
1500
1124
  await this.gateway.charge(amount);
@@ -1541,7 +1165,7 @@ abstract class Notifier {
1541
1165
 
1542
1166
  @Injectable()
1543
1167
  class NotificationService {
1544
- constructor(@MultiInject(Notifier) private notifiers: Notifier[]) {}
1168
+ constructor(@MultiInject(Notifier) private readonly notifiers: Notifier[]) {}
1545
1169
 
1546
1170
  notifyAll() {
1547
1171
  this.notifiers.forEach((notifier) => notifier.notify());
@@ -1579,7 +1203,7 @@ const MyModule = ProviderModule.create({
1579
1203
  });
1580
1204
 
1581
1205
  // Get all providers bound to 'Handler'
1582
- const handlers = MyModule.get('Handler', false, true); // Third param = asList
1206
+ const handlers = MyModule.get('Handler', false, true); // (token, isOptional=false, asList=true)
1583
1207
  console.log(handlers); // ['Handler1', 'Handler2', 'Handler3']
1584
1208
  ```
1585
1209
 
@@ -1597,7 +1221,7 @@ class ServiceA {
1597
1221
  class ServiceB {
1598
1222
  constructor(
1599
1223
  private serviceA: ServiceA,
1600
- @Inject('OPTIONAL_CONFIG') private config?: any
1224
+ @Inject('OPTIONAL_CONFIG') private readonly config?: any
1601
1225
  ) {}
1602
1226
  }
1603
1227
 
@@ -1769,70 +1393,6 @@ try {
1769
1393
  }
1770
1394
  ```
1771
1395
 
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
1396
  > [!IMPORTANT]
1837
1397
  > Lifecycle hook execution order:
1838
1398
  >
@@ -1921,6 +1481,17 @@ const db = MonitoredModule.get(DatabaseService);
1921
1481
  **Tracking Module Composition:**
1922
1482
 
1923
1483
  ```ts
1484
+ @Injectable()
1485
+ class ServiceA {}
1486
+ @Injectable()
1487
+ class ServiceB {}
1488
+
1489
+ const DatabaseModule = ProviderModule.create({
1490
+ id: 'DatabaseModule',
1491
+ providers: [ServiceA],
1492
+ exports: [ServiceA],
1493
+ });
1494
+
1924
1495
  const RootModule = ProviderModule.create({
1925
1496
  id: 'RootModule',
1926
1497
  });
@@ -1978,53 +1549,6 @@ DebugModule.update.addProvider(ServiceA);
1978
1549
  DebugModule.update.removeProvider(ServiceA);
1979
1550
  ```
1980
1551
 
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
1552
  > [!WARNING]
2029
1553
  >
2030
1554
  > - Always call `unsubscribe()` to prevent memory leaks
@@ -2584,11 +2108,75 @@ const featureService = FeatureModuleTest.get(FeatureService);
2584
2108
  > Testing strategies:
2585
2109
  >
2586
2110
  > - Use `blueprint.clone()` to create test variations without modifying originals
2587
- > - Use `useValue` for simple mocks with jest.fn()
2111
+ > - Use `useValue` for simple mocks
2588
2112
  > - Use `useClass` for class-based mocks with behavior
2589
2113
  > - Use `useFactory` for complex mock setup
2590
2114
  > - Test module isolation by mocking all external dependencies
2591
- > - Verify mock calls with jest expectations
2115
+ > - Verify mock calls with your test framework
2116
+
2117
+ ## OOP-Style Modules with ProviderModuleClass
2118
+
2119
+ 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.
2120
+
2121
+ ### Basic OOP Module
2122
+
2123
+ ```ts
2124
+ import { Injectable, ProviderModuleClass } from '@adimm/x-injection';
2125
+
2126
+ @Injectable()
2127
+ class UserService {
2128
+ get(id: string) {
2129
+ return { id, name: 'John Doe' };
2130
+ }
2131
+ }
2132
+
2133
+ @Injectable()
2134
+ class AuthService {
2135
+ constructor(private readonly userService: UserService) {}
2136
+
2137
+ login(userId: string) {
2138
+ const user = this.userService.get(userId);
2139
+ return `Logged in as ${user.name}`;
2140
+ }
2141
+ }
2142
+
2143
+ class AuthModule extends ProviderModuleClass {
2144
+ constructor() {
2145
+ super({
2146
+ id: 'AuthModule',
2147
+ providers: [UserService, AuthService],
2148
+ exports: [AuthService],
2149
+ });
2150
+ }
2151
+
2152
+ authenticateUser(userId: string): string {
2153
+ return this.module.get(AuthService).login(userId);
2154
+ }
2155
+
2156
+ // Custom method named 'get' - no conflict with module.get()!
2157
+ get(): string {
2158
+ return 'custom-get-value';
2159
+ }
2160
+ }
2161
+
2162
+ const authModule = new AuthModule();
2163
+ console.log(authModule.authenticateUser('123')); // "Logged in as John Doe"
2164
+ console.log(authModule.get()); // "custom-get-value"
2165
+
2166
+ // DI container always accessible via .module
2167
+ authModule.module.update.addProvider(NewService);
2168
+ ```
2169
+
2170
+ > [!IMPORTANT]
2171
+ > All `ProviderModule` methods are available through the `.module` property to prevent naming conflicts with your custom methods.
2172
+
2173
+ ### When to Use OOP vs Functional
2174
+
2175
+ **OOP-style (`extends ProviderModuleClass`):** when you need custom business logic methods, computed getters, initialization state, or want to prevent naming conflicts with the DI API.
2176
+
2177
+ **Functional-style (`ProviderModule.create()`):** when you only need a provider container with no extra behavior — simpler and more concise.
2178
+
2179
+ Both styles are fully compatible and can be mixed in the same application.
2592
2180
 
2593
2181
  ## Advanced Module API
2594
2182
 
@@ -2645,74 +2233,6 @@ const AppModule = ProviderModule.create({
2645
2233
  console.log(AppModule.isImportingModule(MODULE_ID)); // true
2646
2234
  ```
2647
2235
 
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
2236
  ### Batch Resolution with getMany()
2717
2237
 
2718
2238
  Resolve multiple providers in a single call.
@@ -2805,6 +2325,84 @@ const [database, cache, optionalLogger, allPlugins, config] = MyModule.getMany(
2805
2325
  > - **Simple**: Just pass the token directly
2806
2326
  > - **With options**: Use object with `provider`, `isOptional`, and/or `asList`
2807
2327
 
2328
+ ## Hierarchical Dependency Injection
2329
+
2330
+ When a module resolves a provider via `module.get()`, xInjection walks up a well-defined lookup chain until it finds a binding or throws.
2331
+
2332
+ **Resolution order (highest to lowest priority):**
2333
+
2334
+ 1. **Own container** — providers declared directly in this module
2335
+ 2. **Imported modules** — exported providers from each module in the `imports` array (in order)
2336
+ 3. **AppModule** — globally available providers (from `isGlobal: true` modules)
2337
+
2338
+ ```
2339
+ module.get(SomeService)
2340
+
2341
+
2342
+ ┌──────────────────────┐
2343
+ │ Own container │ ← providers: [SomeService, ...]
2344
+ │ (highest priority) │
2345
+ └──────────┬───────────┘
2346
+ │ not found
2347
+
2348
+ ┌──────────────────────┐
2349
+ │ Imported modules │ ← imports: [DatabaseModule, ConfigModule]
2350
+ │ (exported only) │ DatabaseModule.exports: [DatabaseService]
2351
+ └──────────┬───────────┘ ConfigModule.exports: [ConfigService]
2352
+ │ not found
2353
+
2354
+ ┌──────────────────────┐
2355
+ │ AppModule │ ← AppBootstrapModule { isGlobal: true }
2356
+ │ (lowest priority) │ exports: [LoggerService, ...]
2357
+ └──────────┬───────────┘
2358
+ │ not found
2359
+
2360
+ InjectionProviderModuleMissingProviderError
2361
+ ```
2362
+
2363
+ Here is the same lookup chain rendered as a graph:
2364
+
2365
+ ```mermaid
2366
+ flowchart TD
2367
+ A["module.get(SomeService)"] --> B
2368
+
2369
+ subgraph own["① Own container"]
2370
+ B{{"Bound here?"}}
2371
+ end
2372
+
2373
+ B -- Yes --> Z(["✅ Return instance"])
2374
+ B -- No --> C
2375
+
2376
+ subgraph imports["② Imported modules (exported providers only)"]
2377
+ C{{"Exported by<br/>DatabaseModule?"}}
2378
+ C -- No --> D{{"Exported by<br/>ConfigModule?"}}
2379
+ end
2380
+
2381
+ C -- Yes --> Z
2382
+ D -- Yes --> Z
2383
+ D -- No --> E
2384
+
2385
+ subgraph global["③ AppModule (via AppBootstrapModule)"]
2386
+ E{{"Bound in<br/>AppModule?"}}
2387
+ end
2388
+
2389
+ E -- Yes --> Z
2390
+ E -- No --> F(["❌ MissingProviderError"])
2391
+ ```
2392
+
2393
+ **Resolution in practice** — given the modules set up in [Import/Export Pattern](#importexport-pattern) and [Global Modules](#global-modules):
2394
+
2395
+ ```ts
2396
+ ApiModule.get(ApiService); // ✅ ① own container
2397
+ ApiModule.get(ConfigService); // ✅ ② ConfigModule export
2398
+ ApiModule.get(DatabaseService); // ✅ ② DatabaseModule export
2399
+ ApiModule.get(LoggerService); // ✅ ③ AppModule (via AppBootstrapModule)
2400
+ ApiModule.get(InternalCache); // ❌ not exported → MissingProviderError
2401
+ ```
2402
+
2403
+ > [!TIP]
2404
+ > A provider that isn't in a module's `exports` is completely invisible to any importer — it's a private implementation detail. Think of `exports` as the public API of a module.
2405
+
2808
2406
  ## Resources
2809
2407
 
2810
2408
  📚 **[Full API Documentation](https://adimarianmutu.github.io/x-injection/index.html)** - Complete TypeDoc reference
package/dist/index.cjs CHANGED
@@ -17,9 +17,9 @@ var e, t = Object.defineProperty, i = Object.getOwnPropertyDescriptor, o = Objec
17
17
  InjectFromBase: () => L,
18
18
  Injectable: () => q,
19
19
  InjectionError: () => m,
20
- InjectionProviderModuleDisposedError: () => g,
20
+ InjectionProviderModuleDisposedError: () => I,
21
21
  InjectionProviderModuleError: () => M,
22
- InjectionProviderModuleMissingIdentifierError: () => I,
22
+ InjectionProviderModuleMissingIdentifierError: () => g,
23
23
  InjectionProviderModuleMissingProviderError: () => b,
24
24
  InjectionProviderModuleUnknownProviderError: () => y,
25
25
  InjectionScope: () => u,
@@ -120,7 +120,7 @@ var h, f, v = require("@inversifyjs/core"), m = class e extends Error {
120
120
  constructor(e, t) {
121
121
  super(e, `The [${h.providerTokenToString(t)}] provider is of an unknown type!`);
122
122
  }
123
- }, g = class e extends M {
123
+ }, I = class e extends M {
124
124
  static {
125
125
  n(this, "InjectionProviderModuleDisposedError");
126
126
  }
@@ -128,7 +128,7 @@ var h, f, v = require("@inversifyjs/core"), m = class e extends Error {
128
128
  constructor(e) {
129
129
  super(e, "Has been disposed!");
130
130
  }
131
- }, I = class e extends M {
131
+ }, g = class e extends M {
132
132
  static {
133
133
  n(this, "InjectionProviderModuleMissingIdentifierError");
134
134
  }
@@ -142,7 +142,7 @@ var h, f, v = require("@inversifyjs/core"), m = class e extends Error {
142
142
  }
143
143
  name=e.name;
144
144
  constructor(e, t) {
145
- super(e, `The [${h.providerTokenToString(t)}] provider is not bound to this (or any imported) module container, and was not found either in the 'AppModule'!`);
145
+ super(e, `The [${h.providerTokenToString(t)}] provider not found. It's not in this module, any imported ones, or the root 'AppModule'.`);
146
146
  }
147
147
  }, P = require("inversify"), w = new (require("inversify").Container)({
148
148
  defaultScope: "Singleton"
@@ -306,7 +306,7 @@ var h, f, v = require("@inversifyjs/core"), m = class e extends Error {
306
306
  return this.providerModule.moduleContainer;
307
307
  }
308
308
  get subscribe() {
309
- if (null === this.event$) throw new g(this.providerModule);
309
+ if (null === this.event$) throw new I(this.providerModule);
310
310
  return this.event$.subscribe.bind(this.event$);
311
311
  }
312
312
  moduleDef;
@@ -455,7 +455,7 @@ var h, f, v = require("@inversifyjs/core"), m = class e extends Error {
455
455
  i ? i.push(t) : this.middlewaresMap.set(e, [ t ]);
456
456
  }
457
457
  applyMiddlewares(e, ...t) {
458
- if (null === this.middlewaresMap) throw new g(this.providerModule);
458
+ if (null === this.middlewaresMap) throw new I(this.providerModule);
459
459
  const i = this.middlewaresMap.get(e);
460
460
  if (!i) return t[0];
461
461
  switch (e) {
@@ -635,10 +635,10 @@ var h, f, v = require("@inversifyjs/core"), m = class e extends Error {
635
635
  return this.id.toString();
636
636
  }
637
637
  throwIfIdIsMissing() {
638
- if (!this.options.id || 0 === this.options.id.toString().trim().length) throw new I(this);
638
+ if (!this.options.id || 0 === this.options.id.toString().trim().length) throw new g(this);
639
639
  }
640
640
  throwIfDisposed() {
641
- if (this.isDisposed) throw new g(this);
641
+ if (this.isDisposed) throw new I(this);
642
642
  }
643
643
  }, j = class {
644
644
  static {
package/dist/index.js CHANGED
@@ -99,7 +99,7 @@ var p = class e extends Error {
99
99
  }
100
100
  name=e.name;
101
101
  constructor(e, t) {
102
- super(e, `The [${I.providerTokenToString(t)}] provider is not bound to this (or any imported) module container, and was not found either in the 'AppModule'!`);
102
+ super(e, `The [${I.providerTokenToString(t)}] provider not found. It's not in this module, any imported ones, or the root 'AppModule'.`);
103
103
  }
104
104
  };
105
105
 
package/package.json CHANGED
@@ -5,7 +5,7 @@
5
5
  "url": "https://github.com/AdiMarianMutu/x-injection"
6
6
  },
7
7
  "description": "Powerful IoC library built on-top of InversifyJS inspired by NestJS's DI.",
8
- "version": "3.0.1",
8
+ "version": "3.0.2",
9
9
  "author": "Adi-Marian Mutu",
10
10
  "homepage": "https://github.com/AdiMarianMutu/x-injection#readme",
11
11
  "bugs": "https://github.com/AdiMarianMutu/x-injection/issues",
@@ -39,16 +39,16 @@
39
39
  "v:bump-major": "npm version major -m \"chore: update lib major version %s\""
40
40
  },
41
41
  "dependencies": {
42
- "inversify": "^7.5.2",
42
+ "inversify": "^7.11.0",
43
43
  "reflect-metadata": "^0.2.2"
44
44
  },
45
45
  "devDependencies": {
46
46
  "@ianvs/prettier-plugin-sort-imports": "^4.4.2",
47
47
  "@swc/core": "^1.11.24",
48
- "eslint": "^8.57.1",
49
48
  "@tsconfig/node22": "^22.0.2",
50
49
  "@types/jest": "^30.0.0",
51
50
  "@typescript-eslint/eslint-plugin": "^8.34.1",
51
+ "eslint": "^8.57.1",
52
52
  "eslint-config-prettier": "^10.1.5",
53
53
  "eslint-plugin-import": "^2.31.0",
54
54
  "eslint-plugin-prettier": "^5.3.1",