decorator-dependency-injection 1.0.4 → 1.0.5
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 +240 -1
- package/eslint.config.js +6 -2
- package/index.d.ts +99 -18
- package/index.js +161 -27
- package/package.json +1 -1
- package/src/Container.js +29 -2
package/README.md
CHANGED
|
@@ -110,6 +110,165 @@ class Consumer {
|
|
|
110
110
|
}
|
|
111
111
|
```
|
|
112
112
|
|
|
113
|
+
### Private Field Injection
|
|
114
|
+
|
|
115
|
+
Both `@Inject` and `@InjectLazy` support private fields using the `#` syntax:
|
|
116
|
+
|
|
117
|
+
```javascript
|
|
118
|
+
import {Singleton, Inject} from 'decorator-dependency-injection';
|
|
119
|
+
|
|
120
|
+
@Singleton()
|
|
121
|
+
class Database {
|
|
122
|
+
query(sql) { /* ... */ }
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
class UserService {
|
|
126
|
+
@Inject(Database) #db // truly private - not accessible from outside
|
|
127
|
+
|
|
128
|
+
getUser(id) {
|
|
129
|
+
return this.#db.query(`SELECT * FROM users WHERE id = ${id}`)
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const service = new UserService()
|
|
134
|
+
service.#db // SyntaxError: Private field '#db' must be declared
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
### The `accessor` Keyword
|
|
138
|
+
|
|
139
|
+
The `accessor` keyword (part of the TC39 decorators proposal) creates an auto-accessor - a private backing field with
|
|
140
|
+
automatic getter/setter. This is particularly useful for **lazy injection with private fields**.
|
|
141
|
+
|
|
142
|
+
```javascript
|
|
143
|
+
class Example {
|
|
144
|
+
accessor myField = 'value'
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Roughly equivalent to:
|
|
148
|
+
class Example {
|
|
149
|
+
#myField = 'value'
|
|
150
|
+
get myField() { return this.#myField }
|
|
151
|
+
set myField(v) { this.#myField = v }
|
|
152
|
+
}
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
#### Using `accessor` with Injection
|
|
156
|
+
|
|
157
|
+
```javascript
|
|
158
|
+
import {Singleton, Inject, InjectLazy} from 'decorator-dependency-injection';
|
|
159
|
+
|
|
160
|
+
@Singleton()
|
|
161
|
+
class ExpensiveService {
|
|
162
|
+
constructor() {
|
|
163
|
+
console.log('ExpensiveService created')
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
class Consumer {
|
|
168
|
+
// Public accessor - works with both @Inject and @InjectLazy
|
|
169
|
+
@Inject(ExpensiveService) accessor service
|
|
170
|
+
|
|
171
|
+
// Private accessor - recommended for lazy private injection
|
|
172
|
+
@InjectLazy(ExpensiveService) accessor #privateService
|
|
173
|
+
|
|
174
|
+
doWork() {
|
|
175
|
+
// Instance created only when first accessed
|
|
176
|
+
return this.#privateService.process()
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
### Injection Support Matrix
|
|
182
|
+
|
|
183
|
+
| Decorator | Syntax | Lazy? | Notes |
|
|
184
|
+
|-----------|--------|-------|-------|
|
|
185
|
+
| `@Inject` | `@Inject(Dep) field` | No | Standard injection |
|
|
186
|
+
| `@Inject` | `@Inject(Dep) #field` | No | Private field injection |
|
|
187
|
+
| `@Inject` | `@Inject(Dep) accessor field` | No* | Accessor injection |
|
|
188
|
+
| `@Inject` | `@Inject(Dep) accessor #field` | No* | Private accessor injection |
|
|
189
|
+
| `@Inject` | `@Inject(Dep) static field` | No | Static field injection |
|
|
190
|
+
| `@Inject` | `@Inject(Dep) static #field` | No | Static private field |
|
|
191
|
+
| `@Inject` | `@Inject(Dep) static accessor field` | No* | Static accessor |
|
|
192
|
+
| `@Inject` | `@Inject(Dep) static accessor #field` | No* | Static private accessor |
|
|
193
|
+
| `@InjectLazy` | `@InjectLazy(Dep) field` | ✅ Yes | Lazy public field |
|
|
194
|
+
| `@InjectLazy` | `@InjectLazy(Dep) #field` | ⚠️ No | See caveat below |
|
|
195
|
+
| `@InjectLazy` | `@InjectLazy(Dep) accessor field` | ✅ Yes | Lazy accessor |
|
|
196
|
+
| `@InjectLazy` | `@InjectLazy(Dep) accessor #field` | ✅ Yes | **Recommended for lazy private** |
|
|
197
|
+
| `@InjectLazy` | `@InjectLazy(Dep) static field` | ✅ Yes | Lazy static field |
|
|
198
|
+
| `@InjectLazy` | `@InjectLazy(Dep) static #field` | ⚠️ No | Same caveat as instance private |
|
|
199
|
+
| `@InjectLazy` | `@InjectLazy(Dep) static accessor #field` | ✅ Yes | Lazy static private accessor |
|
|
200
|
+
|
|
201
|
+
*`@Inject` with accessors caches on first access, which is similar to lazy behavior.
|
|
202
|
+
|
|
203
|
+
#### Caveat: `@InjectLazy` with Private Fields
|
|
204
|
+
|
|
205
|
+
Due to JavaScript limitations, `@InjectLazy` on private fields (`#field`) **cannot be truly lazy**. The instance is
|
|
206
|
+
created at construction time (or class definition time for static fields), not on first access. This is because
|
|
207
|
+
`Object.defineProperty()` cannot create getters on private fields.
|
|
208
|
+
|
|
209
|
+
This applies to both instance and static private fields.
|
|
210
|
+
|
|
211
|
+
**Recommendation:** For true lazy injection with private members, use the `accessor` keyword:
|
|
212
|
+
|
|
213
|
+
```javascript
|
|
214
|
+
// ❌ Not truly lazy (created at construction)
|
|
215
|
+
@InjectLazy(ExpensiveService) #service
|
|
216
|
+
|
|
217
|
+
// ✅ Truly lazy (created on first access)
|
|
218
|
+
@InjectLazy(ExpensiveService) accessor #service
|
|
219
|
+
|
|
220
|
+
// Static fields work the same way:
|
|
221
|
+
// ❌ Not truly lazy (created at class definition)
|
|
222
|
+
@InjectLazy(ExpensiveService) static #service
|
|
223
|
+
|
|
224
|
+
// ✅ Truly lazy
|
|
225
|
+
@InjectLazy(ExpensiveService) static accessor #service
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
### Static Field Injection
|
|
229
|
+
|
|
230
|
+
All injection decorators work with static fields. Static injections are shared across all instances of the class:
|
|
231
|
+
|
|
232
|
+
```javascript
|
|
233
|
+
import {Factory, Singleton, Inject} from 'decorator-dependency-injection';
|
|
234
|
+
|
|
235
|
+
@Singleton()
|
|
236
|
+
class SharedConfig {
|
|
237
|
+
apiUrl = 'https://api.example.com'
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
@Factory()
|
|
241
|
+
class RequestLogger {
|
|
242
|
+
static nextId = 0
|
|
243
|
+
id = ++RequestLogger.nextId
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
class ApiService {
|
|
247
|
+
@Inject(SharedConfig) static config // Shared across all instances
|
|
248
|
+
@Inject(RequestLogger) logger // New instance per ApiService
|
|
249
|
+
|
|
250
|
+
getUrl() {
|
|
251
|
+
return ApiService.config.apiUrl
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const a = new ApiService()
|
|
256
|
+
const b = new ApiService()
|
|
257
|
+
console.log(a.logger.id) // 1
|
|
258
|
+
console.log(b.logger.id) // 2
|
|
259
|
+
console.log(ApiService.config === ApiService.config) // true (singleton)
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
### Additional Supported Features
|
|
263
|
+
|
|
264
|
+
The injection decorators also support:
|
|
265
|
+
|
|
266
|
+
- **Computed property names**: `@Inject(Dep) [dynamicPropertyName]`
|
|
267
|
+
- **Symbol property names**: `@Inject(Dep) [Symbol('key')]`
|
|
268
|
+
- **Inheritance**: Subclasses inherit parent class injections
|
|
269
|
+
- **Multiple decorators**: Combine `@Inject` with other decorators
|
|
270
|
+
- **Nested injection**: Singletons/Factories can have their own injected dependencies
|
|
271
|
+
|
|
113
272
|
## Passing parameters to a dependency
|
|
114
273
|
|
|
115
274
|
You can pass parameters to a dependency by using the ```@Inject``` decorator with a function that returns the
|
|
@@ -194,6 +353,68 @@ import {clearContainer} from 'decorator-dependency-injection';
|
|
|
194
353
|
clearContainer(); // Removes all registered singletons, factories, and mocks
|
|
195
354
|
```
|
|
196
355
|
|
|
356
|
+
### Validation Helpers
|
|
357
|
+
|
|
358
|
+
The library provides utilities to validate registrations at runtime, which is useful for catching configuration
|
|
359
|
+
errors early:
|
|
360
|
+
|
|
361
|
+
#### `isRegistered(clazzOrName)`
|
|
362
|
+
|
|
363
|
+
Check if a class or name is registered:
|
|
364
|
+
|
|
365
|
+
```javascript
|
|
366
|
+
import {Singleton, isRegistered} from 'decorator-dependency-injection';
|
|
367
|
+
|
|
368
|
+
@Singleton()
|
|
369
|
+
class MyService {}
|
|
370
|
+
|
|
371
|
+
console.log(isRegistered(MyService)); // true
|
|
372
|
+
console.log(isRegistered('unknownName')); // false
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
#### `validateRegistrations(...tokens)`
|
|
376
|
+
|
|
377
|
+
Validate multiple registrations at once. Throws an error with helpful details if any are missing:
|
|
378
|
+
|
|
379
|
+
```javascript
|
|
380
|
+
import {validateRegistrations} from 'decorator-dependency-injection';
|
|
381
|
+
|
|
382
|
+
// At application startup - fail fast if dependencies are missing
|
|
383
|
+
try {
|
|
384
|
+
validateRegistrations(UserService, AuthService, 'databaseConnection');
|
|
385
|
+
} catch (err) {
|
|
386
|
+
// Error: Missing registrations: [UserService, databaseConnection].
|
|
387
|
+
// Ensure these classes are decorated with @Singleton() or @Factory() before use.
|
|
388
|
+
}
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
This is particularly useful in:
|
|
392
|
+
- Application bootstrap to catch missing dependencies before runtime failures
|
|
393
|
+
- Test setup to ensure mocks are properly configured
|
|
394
|
+
- Module initialization to validate external dependencies
|
|
395
|
+
|
|
396
|
+
### Debug Mode
|
|
397
|
+
|
|
398
|
+
Enable debug logging to understand the injection lifecycle:
|
|
399
|
+
|
|
400
|
+
```javascript
|
|
401
|
+
import {setDebug} from 'decorator-dependency-injection';
|
|
402
|
+
|
|
403
|
+
setDebug(true);
|
|
404
|
+
|
|
405
|
+
// Now logs will appear when:
|
|
406
|
+
// - Classes are registered: [DI] Registered singleton: UserService
|
|
407
|
+
// - Instances are created: [DI] Creating singleton: UserService
|
|
408
|
+
// - Cached singletons are returned: [DI] Returning cached singleton: UserService
|
|
409
|
+
// - Mocks are registered: [DI] Mocked UserService with MockUserService
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
This is helpful for:
|
|
413
|
+
- Debugging injection order issues
|
|
414
|
+
- Understanding when instances are created (eager vs lazy)
|
|
415
|
+
- Troubleshooting circular dependencies
|
|
416
|
+
- Verifying test mocks are applied correctly
|
|
417
|
+
|
|
197
418
|
You can also use the ```@Mock``` decorator as a proxy instead of a full mock. Any method calls not implemented in the
|
|
198
419
|
mock will be passed to the real dependency.
|
|
199
420
|
|
|
@@ -276,6 +497,23 @@ const container = getContainer();
|
|
|
276
497
|
console.log(container.has(MyService)); // Check if a class is registered
|
|
277
498
|
```
|
|
278
499
|
|
|
500
|
+
## TypeScript Support
|
|
501
|
+
|
|
502
|
+
The library includes TypeScript definitions with helpful type aliases:
|
|
503
|
+
|
|
504
|
+
```typescript
|
|
505
|
+
import {Constructor, InjectionToken} from 'decorator-dependency-injection';
|
|
506
|
+
|
|
507
|
+
// Constructor<T> - a class constructor that creates instances of T
|
|
508
|
+
const MyClass: Constructor<MyService> = MyService;
|
|
509
|
+
|
|
510
|
+
// InjectionToken<T> - either a class or a string name
|
|
511
|
+
const token1: InjectionToken<MyService> = MyService;
|
|
512
|
+
const token2: InjectionToken = 'myServiceName';
|
|
513
|
+
```
|
|
514
|
+
|
|
515
|
+
All decorator functions and utilities are fully typed with generics for better autocomplete and type safety.
|
|
516
|
+
|
|
279
517
|
## Running the tests
|
|
280
518
|
|
|
281
519
|
To run the tests, run the following command in the project root.
|
|
@@ -290,4 +528,5 @@ npm test
|
|
|
290
528
|
- 1.0.1 - Automated release with GitHub Actions
|
|
291
529
|
- 1.0.2 - Added proxy option to @Mock decorator
|
|
292
530
|
- 1.0.3 - Added @InjectLazy decorator
|
|
293
|
-
- 1.0.4 - Added Container abstraction, clearContainer(), TypeScript definitions, improved proxy support
|
|
531
|
+
- 1.0.4 - Added Container abstraction, clearContainer(), TypeScript definitions, improved proxy support
|
|
532
|
+
- 1.0.5 - Added private field and accessor support for @Inject and @InjectLazy, debug mode, validation helpers
|
package/eslint.config.js
CHANGED
|
@@ -66,8 +66,12 @@ export default defineConfig([
|
|
|
66
66
|
}
|
|
67
67
|
},
|
|
68
68
|
rules: {
|
|
69
|
-
// In tests, decorated classes are used by the decorator system
|
|
70
|
-
|
|
69
|
+
// In tests, decorated classes are often "used" by the decorator system (side effects)
|
|
70
|
+
// rather than being referenced directly. Also allow underscore-prefixed vars.
|
|
71
|
+
"no-unused-vars": ["warn", {
|
|
72
|
+
"argsIgnorePattern": "^_",
|
|
73
|
+
"varsIgnorePattern": "^_|Mock|Service|Factory|Singleton|Consumer|Injection|Lazy|^[A-Z]$|^[A-Z][0-9]$"
|
|
74
|
+
}]
|
|
71
75
|
}
|
|
72
76
|
}
|
|
73
77
|
]);
|
package/index.d.ts
CHANGED
|
@@ -2,6 +2,17 @@
|
|
|
2
2
|
* Type definitions for decorator-dependency-injection
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* A class constructor type.
|
|
7
|
+
* @template T The instance type
|
|
8
|
+
*/
|
|
9
|
+
export type Constructor<T = any> = new (...args: any[]) => T
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Valid injection target: either a class constructor or a string name.
|
|
13
|
+
*/
|
|
14
|
+
export type InjectionToken<T = any> = string | Constructor<T>
|
|
15
|
+
|
|
5
16
|
/**
|
|
6
17
|
* Context for registered instances in the container
|
|
7
18
|
*/
|
|
@@ -22,44 +33,51 @@ export interface InstanceContext {
|
|
|
22
33
|
* A dependency injection container that manages singleton and factory instances.
|
|
23
34
|
*/
|
|
24
35
|
export declare class Container {
|
|
36
|
+
/**
|
|
37
|
+
* Enable or disable debug logging.
|
|
38
|
+
* When enabled, logs when instances are created.
|
|
39
|
+
*/
|
|
40
|
+
setDebug(enabled: boolean): void
|
|
41
|
+
|
|
25
42
|
/**
|
|
26
43
|
* Register a class as a singleton.
|
|
27
44
|
*/
|
|
28
|
-
registerSingleton(clazz:
|
|
45
|
+
registerSingleton<T>(clazz: Constructor<T>, name?: string): void
|
|
29
46
|
|
|
30
47
|
/**
|
|
31
48
|
* Register a class as a factory.
|
|
32
49
|
*/
|
|
33
|
-
registerFactory(clazz:
|
|
50
|
+
registerFactory<T>(clazz: Constructor<T>, name?: string): void
|
|
34
51
|
|
|
35
52
|
/**
|
|
36
53
|
* Get the context for a given class or name.
|
|
54
|
+
* @throws Error if the class/name is not registered
|
|
37
55
|
*/
|
|
38
|
-
getContext(clazzOrName:
|
|
56
|
+
getContext<T>(clazzOrName: InjectionToken<T>): InstanceContext
|
|
39
57
|
|
|
40
58
|
/**
|
|
41
59
|
* Check if a class or name is registered.
|
|
42
60
|
*/
|
|
43
|
-
has(clazzOrName:
|
|
61
|
+
has<T>(clazzOrName: InjectionToken<T>): boolean
|
|
44
62
|
|
|
45
63
|
/**
|
|
46
64
|
* Get or create an instance based on the context.
|
|
47
65
|
*/
|
|
48
|
-
getInstance(instanceContext: InstanceContext, params: any[]):
|
|
66
|
+
getInstance<T>(instanceContext: InstanceContext, params: any[]): T
|
|
49
67
|
|
|
50
68
|
/**
|
|
51
69
|
* Register a mock for an existing class.
|
|
52
70
|
*/
|
|
53
|
-
registerMock(
|
|
54
|
-
targetClazzOrName:
|
|
55
|
-
mockClazz:
|
|
71
|
+
registerMock<T>(
|
|
72
|
+
targetClazzOrName: InjectionToken<T>,
|
|
73
|
+
mockClazz: Constructor<T>,
|
|
56
74
|
useProxy?: boolean
|
|
57
75
|
): void
|
|
58
76
|
|
|
59
77
|
/**
|
|
60
78
|
* Reset a specific mock to its original class.
|
|
61
79
|
*/
|
|
62
|
-
resetMock(clazzOrName:
|
|
80
|
+
resetMock<T>(clazzOrName: InjectionToken<T>): void
|
|
63
81
|
|
|
64
82
|
/**
|
|
65
83
|
* Reset all mocks to their original classes.
|
|
@@ -85,33 +103,72 @@ export declare function Singleton(name?: string): ClassDecorator
|
|
|
85
103
|
export declare function Factory(name?: string): ClassDecorator
|
|
86
104
|
|
|
87
105
|
/**
|
|
88
|
-
*
|
|
106
|
+
* Decorator return type that works for both fields and accessors.
|
|
107
|
+
* For fields, returns a function that provides the initial value.
|
|
108
|
+
* For accessors, returns an object with get/set/init.
|
|
109
|
+
*/
|
|
110
|
+
export type FieldOrAccessorDecorator = (
|
|
111
|
+
target: undefined,
|
|
112
|
+
context: ClassFieldDecoratorContext | ClassAccessorDecoratorContext
|
|
113
|
+
) => void | ((initialValue: any) => any) | ClassAccessorDecoratorResult<any, any>
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Inject a singleton or factory instance into a class field or accessor.
|
|
117
|
+
*
|
|
118
|
+
* Supports:
|
|
119
|
+
* - Public fields: `@Inject(MyClass) myField`
|
|
120
|
+
* - Private fields: `@Inject(MyClass) #myField`
|
|
121
|
+
* - Public accessors: `@Inject(MyClass) accessor myField`
|
|
122
|
+
* - Private accessors: `@Inject(MyClass) accessor #myField`
|
|
123
|
+
*
|
|
89
124
|
* @param clazzOrName The class or name to inject
|
|
90
125
|
* @param params Optional parameters to pass to the constructor
|
|
126
|
+
*
|
|
127
|
+
* @example
|
|
128
|
+
* class MyService {
|
|
129
|
+
* @Inject(Database) db
|
|
130
|
+
* @Inject(Logger) #logger // private field
|
|
131
|
+
* @Inject(Cache) accessor cache // accessor (recommended for lazy-like behavior)
|
|
132
|
+
* }
|
|
91
133
|
*/
|
|
92
134
|
export declare function Inject<T>(
|
|
93
|
-
clazzOrName:
|
|
135
|
+
clazzOrName: InjectionToken<T>,
|
|
94
136
|
...params: any[]
|
|
95
|
-
):
|
|
137
|
+
): FieldOrAccessorDecorator
|
|
96
138
|
|
|
97
139
|
/**
|
|
98
|
-
* Inject a singleton or factory instance lazily into a class field.
|
|
140
|
+
* Inject a singleton or factory instance lazily into a class field or accessor.
|
|
99
141
|
* The instance is created on first access.
|
|
142
|
+
*
|
|
143
|
+
* Supports:
|
|
144
|
+
* - Public fields: `@InjectLazy(MyClass) myField` (true lazy)
|
|
145
|
+
* - Private fields: `@InjectLazy(MyClass) #myField` (not truly lazy - use accessor instead)
|
|
146
|
+
* - Public accessors: `@InjectLazy(MyClass) accessor myField` (true lazy)
|
|
147
|
+
* - Private accessors: `@InjectLazy(MyClass) accessor #myField` (true lazy, recommended)
|
|
148
|
+
*
|
|
149
|
+
* Note: For true lazy injection with private members, use the accessor syntax:
|
|
150
|
+
* `@InjectLazy(MyClass) accessor #myField`
|
|
151
|
+
*
|
|
100
152
|
* @param clazzOrName The class or name to inject
|
|
101
153
|
* @param params Optional parameters to pass to the constructor
|
|
154
|
+
*
|
|
155
|
+
* @example
|
|
156
|
+
* class MyService {
|
|
157
|
+
* @InjectLazy(ExpensiveService) accessor #expensiveService
|
|
158
|
+
* }
|
|
102
159
|
*/
|
|
103
160
|
export declare function InjectLazy<T>(
|
|
104
|
-
clazzOrName:
|
|
161
|
+
clazzOrName: InjectionToken<T>,
|
|
105
162
|
...params: any[]
|
|
106
|
-
):
|
|
163
|
+
): FieldOrAccessorDecorator
|
|
107
164
|
|
|
108
165
|
/**
|
|
109
166
|
* Mark a class as a mock for another class.
|
|
110
167
|
* @param mockedClazzOrName The class or name to mock
|
|
111
168
|
* @param proxy If true, unmocked methods delegate to the original
|
|
112
169
|
*/
|
|
113
|
-
export declare function Mock(
|
|
114
|
-
mockedClazzOrName:
|
|
170
|
+
export declare function Mock<T>(
|
|
171
|
+
mockedClazzOrName: InjectionToken<T>,
|
|
115
172
|
proxy?: boolean
|
|
116
173
|
): ClassDecorator
|
|
117
174
|
|
|
@@ -124,7 +181,7 @@ export declare function resetMocks(): void
|
|
|
124
181
|
* Reset a specific mock to its original class.
|
|
125
182
|
* @param clazzOrName The class or name to reset
|
|
126
183
|
*/
|
|
127
|
-
export declare function resetMock(clazzOrName:
|
|
184
|
+
export declare function resetMock<T>(clazzOrName: InjectionToken<T>): void
|
|
128
185
|
|
|
129
186
|
/**
|
|
130
187
|
* Clear all registered instances and mocks from the container.
|
|
@@ -136,6 +193,30 @@ export declare function clearContainer(): void
|
|
|
136
193
|
*/
|
|
137
194
|
export declare function getContainer(): Container
|
|
138
195
|
|
|
196
|
+
/**
|
|
197
|
+
* Enable or disable debug logging for dependency injection.
|
|
198
|
+
* When enabled, logs when instances are registered, created, and mocked.
|
|
199
|
+
* @param enabled Whether to enable debug mode
|
|
200
|
+
*/
|
|
201
|
+
export declare function setDebug(enabled: boolean): void
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Check if a class or name is registered in the default container.
|
|
205
|
+
* Useful for validation before injection.
|
|
206
|
+
* @param clazzOrName The class or name to check
|
|
207
|
+
* @returns true if registered, false otherwise
|
|
208
|
+
*/
|
|
209
|
+
export declare function isRegistered<T>(clazzOrName: InjectionToken<T>): boolean
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Validate that all provided injection tokens are registered.
|
|
213
|
+
* Throws an error with details about missing registrations.
|
|
214
|
+
* Useful for fail-fast validation at application startup.
|
|
215
|
+
* @param tokens Array of classes or names to validate
|
|
216
|
+
* @throws Error if any token is not registered
|
|
217
|
+
*/
|
|
218
|
+
export declare function validateRegistrations<T extends InjectionToken[]>(...tokens: T): void
|
|
219
|
+
|
|
139
220
|
/**
|
|
140
221
|
* Create a proxy that delegates to the mock first, then falls back to the original.
|
|
141
222
|
* This is an internal utility but exported for advanced use cases.
|
package/index.js
CHANGED
|
@@ -11,6 +11,34 @@ import {Container} from './src/Container.js'
|
|
|
11
11
|
/** @type {Container} The default global container */
|
|
12
12
|
const defaultContainer = new Container()
|
|
13
13
|
|
|
14
|
+
/**
|
|
15
|
+
* Creates a lazy accessor descriptor with WeakMap-based caching.
|
|
16
|
+
* @param {WeakMap} cache - WeakMap for per-instance caching
|
|
17
|
+
* @param {Function} getValue - Factory function to create the value
|
|
18
|
+
* @param {string} name - The accessor name for error messages
|
|
19
|
+
* @returns {{init: Function, get: Function, set: Function}} Accessor descriptor
|
|
20
|
+
* @private
|
|
21
|
+
*/
|
|
22
|
+
function createLazyAccessor(cache, getValue, name) {
|
|
23
|
+
return {
|
|
24
|
+
init(initialValue) {
|
|
25
|
+
if (initialValue) {
|
|
26
|
+
throw new Error(`Cannot assign value to injected accessor "${name}"`)
|
|
27
|
+
}
|
|
28
|
+
return undefined
|
|
29
|
+
},
|
|
30
|
+
get() {
|
|
31
|
+
if (!cache.has(this)) {
|
|
32
|
+
cache.set(this, getValue())
|
|
33
|
+
}
|
|
34
|
+
return cache.get(this)
|
|
35
|
+
},
|
|
36
|
+
set() {
|
|
37
|
+
throw new Error(`Cannot assign value to injected accessor "${name}"`)
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
14
42
|
/**
|
|
15
43
|
* Register a class as a singleton. If a name is provided, it will be used as the key in the singleton map.
|
|
16
44
|
* Singleton instances only ever have one instance created via the @Inject decorator.
|
|
@@ -63,26 +91,44 @@ export function Factory(name) {
|
|
|
63
91
|
* Inject a singleton or factory instance into a class field. You can also provide parameters to the constructor.
|
|
64
92
|
* If the instance is a singleton, it will only be created once with the first set of parameters it encounters.
|
|
65
93
|
*
|
|
94
|
+
* Supports:
|
|
95
|
+
* - Public fields: @Inject(MyClass) myField
|
|
96
|
+
* - Private fields: @Inject(MyClass) #myField
|
|
97
|
+
* - Accessors: @Inject(MyClass) accessor myField
|
|
98
|
+
* - Private accessors: @Inject(MyClass) accessor #myField
|
|
99
|
+
*
|
|
66
100
|
* @param {string|Function} clazzOrName The singleton or factory class or name
|
|
67
101
|
* @param {...*} params Parameters to pass to the constructor. Recommended to use only with factories.
|
|
68
102
|
* @returns {(function(*, {kind: string, name: string}): function(): Object)}
|
|
69
103
|
* @example @Inject(MySingleton) mySingleton
|
|
70
104
|
* @example @Inject("myCustomName") myFactory
|
|
71
|
-
* @
|
|
105
|
+
* @example @Inject(MyService) #privateService
|
|
106
|
+
* @example @Inject(MyService) accessor myService
|
|
107
|
+
* @throws {Error} If the injection target is not a field or accessor
|
|
72
108
|
* @throws {Error} If the injected field is assigned a value
|
|
73
109
|
*/
|
|
74
110
|
export function Inject(clazzOrName, ...params) {
|
|
75
111
|
return function (_, context) {
|
|
76
|
-
|
|
77
|
-
throw new Error('Invalid injection target')
|
|
78
|
-
}
|
|
79
|
-
return function (initialValue) {
|
|
80
|
-
if (initialValue) {
|
|
81
|
-
throw new Error('Cannot assign value to injected field')
|
|
82
|
-
}
|
|
112
|
+
const getValue = () => {
|
|
83
113
|
const instanceContext = defaultContainer.getContext(clazzOrName)
|
|
84
114
|
return defaultContainer.getInstance(instanceContext, params)
|
|
85
115
|
}
|
|
116
|
+
|
|
117
|
+
if (context.kind === 'field') {
|
|
118
|
+
return function (initialValue) {
|
|
119
|
+
if (initialValue) {
|
|
120
|
+
throw new Error(`Cannot assign value to injected field "${context.name}"`)
|
|
121
|
+
}
|
|
122
|
+
return getValue()
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (context.kind === 'accessor') {
|
|
127
|
+
const cache = new WeakMap()
|
|
128
|
+
return createLazyAccessor(cache, getValue, context.name)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
throw new Error('Invalid injection target: @Inject can only be used on fields or accessors')
|
|
86
132
|
}
|
|
87
133
|
}
|
|
88
134
|
|
|
@@ -90,37 +136,74 @@ export function Inject(clazzOrName, ...params) {
|
|
|
90
136
|
* Inject a singleton or factory instance lazily into a class field. You can also provide parameters to the constructor.
|
|
91
137
|
* If the instance is a singleton, it will only be created once with the first set of parameters it encounters.
|
|
92
138
|
*
|
|
139
|
+
* The lazy injection defers instantiation until the field is first accessed. This is useful for:
|
|
140
|
+
* - Breaking circular dependencies
|
|
141
|
+
* - Deferring expensive initializations
|
|
142
|
+
*
|
|
143
|
+
* Supports:
|
|
144
|
+
* - Public fields: @InjectLazy(MyClass) myField
|
|
145
|
+
* - Private fields: @InjectLazy(MyClass) #myField
|
|
146
|
+
* - Accessors: @InjectLazy(MyClass) accessor myField
|
|
147
|
+
* - Private accessors: @InjectLazy(MyClass) accessor #myField
|
|
148
|
+
*
|
|
149
|
+
* Note: For private fields, the lazy behavior is achieved through the field initializer
|
|
150
|
+
* returning a getter-based proxy. For accessors, it's achieved through the accessor's
|
|
151
|
+
* get/set methods directly.
|
|
152
|
+
*
|
|
93
153
|
* @param {string|Function} clazzOrName The singleton or factory class or name
|
|
94
154
|
* @param {...*} params Parameters to pass to the constructor. Recommended to use only with factories.
|
|
95
155
|
* @returns {(function(*, {kind: string, name: string, addInitializer: Function}): void)}
|
|
96
156
|
* @example @InjectLazy(MySingleton) mySingleton
|
|
97
157
|
* @example @InjectLazy("myCustomName") myFactory
|
|
98
|
-
* @
|
|
158
|
+
* @example @InjectLazy(MyService) #privateService
|
|
159
|
+
* @throws {Error} If the injection target is not a field or accessor
|
|
99
160
|
* @throws {Error} If the injected field is assigned a value
|
|
100
161
|
*/
|
|
101
162
|
export function InjectLazy(clazzOrName, ...params) {
|
|
102
163
|
const cache = new WeakMap()
|
|
164
|
+
|
|
165
|
+
const getValue = () => {
|
|
166
|
+
const instanceContext = defaultContainer.getContext(clazzOrName)
|
|
167
|
+
return defaultContainer.getInstance(instanceContext, params)
|
|
168
|
+
}
|
|
169
|
+
|
|
103
170
|
return (_, context) => {
|
|
104
|
-
if (context.kind
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
const instanceContext = defaultContainer.getContext(clazzOrName)
|
|
112
|
-
const value = defaultContainer.getInstance(instanceContext, params)
|
|
113
|
-
cache.set(this, value)
|
|
171
|
+
if (context.kind === 'field') {
|
|
172
|
+
// For private fields, we cannot use Object.defineProperty to create a lazy getter.
|
|
173
|
+
// Instead, we eagerly create the value. For true lazy behavior, use accessor syntax.
|
|
174
|
+
if (context.private) {
|
|
175
|
+
return function (initialValue) {
|
|
176
|
+
if (initialValue) {
|
|
177
|
+
throw new Error(`Cannot assign value to lazy-injected field "${context.name}"`)
|
|
114
178
|
}
|
|
115
|
-
return
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
179
|
+
return getValue()
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// For public fields, use Object.defineProperty for true lazy behavior
|
|
184
|
+
context.addInitializer(function () {
|
|
185
|
+
Object.defineProperty(this, context.name, {
|
|
186
|
+
get() {
|
|
187
|
+
if (!cache.has(this)) {
|
|
188
|
+
cache.set(this, getValue())
|
|
189
|
+
}
|
|
190
|
+
return cache.get(this)
|
|
191
|
+
},
|
|
192
|
+
set() {
|
|
193
|
+
throw new Error(`Cannot assign value to lazy-injected field "${context.name}"`)
|
|
194
|
+
},
|
|
195
|
+
configurable: true,
|
|
196
|
+
enumerable: true
|
|
197
|
+
})
|
|
122
198
|
})
|
|
123
|
-
|
|
199
|
+
return
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (context.kind === 'accessor') {
|
|
203
|
+
return createLazyAccessor(cache, getValue, context.name)
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
throw new Error('Invalid injection target: @InjectLazy can only be used on fields or accessors')
|
|
124
207
|
}
|
|
125
208
|
}
|
|
126
209
|
|
|
@@ -179,6 +262,57 @@ export function getContainer() {
|
|
|
179
262
|
return defaultContainer
|
|
180
263
|
}
|
|
181
264
|
|
|
265
|
+
/**
|
|
266
|
+
* Enable or disable debug logging for dependency injection.
|
|
267
|
+
* When enabled, logs when instances are registered, created, and mocked.
|
|
268
|
+
*
|
|
269
|
+
* @param {boolean} enabled Whether to enable debug mode
|
|
270
|
+
* @example
|
|
271
|
+
* setDebug(true)
|
|
272
|
+
* // [DI] Registered singleton: UserService
|
|
273
|
+
* // [DI] Creating singleton: UserService
|
|
274
|
+
*/
|
|
275
|
+
export function setDebug(enabled) {
|
|
276
|
+
defaultContainer.setDebug(enabled)
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Check if a class or name is registered in the default container.
|
|
281
|
+
* Useful for validation before injection.
|
|
282
|
+
*
|
|
283
|
+
* @param {string|Function} clazzOrName The class or name to check
|
|
284
|
+
* @returns {boolean} true if registered, false otherwise
|
|
285
|
+
* @example
|
|
286
|
+
* if (!isRegistered(MyService)) {
|
|
287
|
+
* console.warn('MyService not registered!')
|
|
288
|
+
* }
|
|
289
|
+
*/
|
|
290
|
+
export function isRegistered(clazzOrName) {
|
|
291
|
+
return defaultContainer.has(clazzOrName)
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Validate that all provided injection tokens are registered.
|
|
296
|
+
* Throws an error with details about missing registrations.
|
|
297
|
+
* Useful for fail-fast validation at application startup.
|
|
298
|
+
*
|
|
299
|
+
* @param {...(string|Function)} tokens Classes or names to validate
|
|
300
|
+
* @throws {Error} If any token is not registered
|
|
301
|
+
* @example
|
|
302
|
+
* // At app startup:
|
|
303
|
+
* validateRegistrations(UserService, AuthService, 'databaseConnection')
|
|
304
|
+
*/
|
|
305
|
+
export function validateRegistrations(...tokens) {
|
|
306
|
+
const missing = tokens.filter(token => !defaultContainer.has(token))
|
|
307
|
+
if (missing.length > 0) {
|
|
308
|
+
const names = missing.map(t => typeof t === 'string' ? t : t.name).join(', ')
|
|
309
|
+
throw new Error(
|
|
310
|
+
`Missing registrations: [${names}]. ` +
|
|
311
|
+
`Ensure these classes are decorated with @Singleton() or @Factory() before use.`
|
|
312
|
+
)
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
182
316
|
// Export Container class for advanced use cases (e.g., isolated containers)
|
|
183
317
|
export {Container}
|
|
184
318
|
|
package/package.json
CHANGED
package/src/Container.js
CHANGED
|
@@ -18,6 +18,29 @@ export class Container {
|
|
|
18
18
|
/** @type {Map<string|Function, InstanceContext>} */
|
|
19
19
|
#instances = new Map()
|
|
20
20
|
|
|
21
|
+
/** @type {boolean} Enable debug logging */
|
|
22
|
+
#debug = false
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Enable or disable debug logging.
|
|
26
|
+
* When enabled, logs when instances are created.
|
|
27
|
+
* @param {boolean} enabled Whether to enable debug mode
|
|
28
|
+
*/
|
|
29
|
+
setDebug(enabled) {
|
|
30
|
+
this.#debug = enabled
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Log a debug message if debug mode is enabled.
|
|
35
|
+
* @param {string} message The message to log
|
|
36
|
+
* @private
|
|
37
|
+
*/
|
|
38
|
+
#log(message) {
|
|
39
|
+
if (this.#debug) {
|
|
40
|
+
console.log(`[DI] ${message}`)
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
21
44
|
/**
|
|
22
45
|
* Register a class as a singleton.
|
|
23
46
|
* @param {Function} clazz The class constructor
|
|
@@ -52,6 +75,7 @@ export class Container {
|
|
|
52
75
|
)
|
|
53
76
|
}
|
|
54
77
|
this.#instances.set(key, {clazz, type})
|
|
78
|
+
this.#log(`Registered ${type}: ${name || clazz.name}`)
|
|
55
79
|
}
|
|
56
80
|
|
|
57
81
|
/**
|
|
@@ -90,11 +114,13 @@ export class Container {
|
|
|
90
114
|
*/
|
|
91
115
|
getInstance(instanceContext, params) {
|
|
92
116
|
if (instanceContext.type === 'singleton' && !instanceContext.originalClazz && instanceContext.instance) {
|
|
117
|
+
this.#log(`Returning cached singleton: ${instanceContext.clazz.name}`)
|
|
93
118
|
return instanceContext.instance
|
|
94
119
|
}
|
|
95
120
|
|
|
96
121
|
let instance
|
|
97
122
|
try {
|
|
123
|
+
this.#log(`Creating ${instanceContext.type}: ${instanceContext.clazz.name}`)
|
|
98
124
|
instance = new instanceContext.clazz(...params)
|
|
99
125
|
} catch (err) {
|
|
100
126
|
if (err instanceof RangeError) {
|
|
@@ -132,6 +158,8 @@ export class Container {
|
|
|
132
158
|
instanceContext.originalClazz = instanceContext.clazz
|
|
133
159
|
instanceContext.proxy = useProxy
|
|
134
160
|
instanceContext.clazz = mockClazz
|
|
161
|
+
const targetName = typeof targetClazzOrName === 'string' ? targetClazzOrName : targetClazzOrName.name
|
|
162
|
+
this.#log(`Mocked ${targetName} with ${mockClazz.name}${useProxy ? ' (proxy)' : ''}`)
|
|
135
163
|
}
|
|
136
164
|
|
|
137
165
|
/**
|
|
@@ -140,8 +168,7 @@ export class Container {
|
|
|
140
168
|
* @throws {Error} If the class or name is not registered
|
|
141
169
|
*/
|
|
142
170
|
resetMock(clazzOrName) {
|
|
143
|
-
|
|
144
|
-
this.#restoreOriginal(this.#instances.get(key), clazzOrName)
|
|
171
|
+
this.#restoreOriginal(this.#instances.get(clazzOrName), clazzOrName)
|
|
145
172
|
}
|
|
146
173
|
|
|
147
174
|
/**
|