@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 +210 -612
- package/dist/index.cjs +9 -9
- package/dist/index.js +1 -1
- package/package.json +3 -3
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(); //
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
580
|
-
const AnyModule = ProviderModule.create({
|
|
581
|
-
id: 'AnyModule',
|
|
582
|
-
});
|
|
506
|
+
### AppModule
|
|
583
507
|
|
|
584
|
-
|
|
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
|
-
> -
|
|
590
|
-
> -
|
|
591
|
-
>
|
|
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
|
-
|
|
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
|
-
//
|
|
1377
|
-
|
|
1378
|
-
id: '
|
|
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
|
|
1385
|
-
console.log(AppModule.isImportingModule('
|
|
1386
|
-
console.log(AppModule.hasProvider(ConfigService)); // true
|
|
1014
|
+
// Automatically imported into AppModule
|
|
1015
|
+
console.log(AppModule.isImportingModule('AppBootstrapModule')); // true
|
|
1387
1016
|
|
|
1388
|
-
//
|
|
1389
|
-
const
|
|
1390
|
-
const
|
|
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
|
-
>
|
|
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); //
|
|
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
|
|
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
|
|
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: () =>
|
|
20
|
+
InjectionProviderModuleDisposedError: () => I,
|
|
21
21
|
InjectionProviderModuleError: () => M,
|
|
22
|
-
InjectionProviderModuleMissingIdentifierError: () =>
|
|
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
|
-
},
|
|
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
|
-
},
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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.
|
|
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",
|