decorator-dependency-injection 1.0.4 → 1.0.6
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 +286 -1
- package/eslint.config.js +6 -2
- package/index.d.ts +128 -18
- package/index.js +188 -27
- package/package.json +1 -1
- package/src/Container.js +43 -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,113 @@ import {clearContainer} from 'decorator-dependency-injection';
|
|
|
194
353
|
clearContainer(); // Removes all registered singletons, factories, and mocks
|
|
195
354
|
```
|
|
196
355
|
|
|
356
|
+
### Resolving Dependencies Without Decorators
|
|
357
|
+
|
|
358
|
+
The `resolve` function allows non-class code (plain functions, modules, callbacks, etc.) to retrieve instances from the DI container:
|
|
359
|
+
|
|
360
|
+
```javascript
|
|
361
|
+
import {Singleton, Factory, resolve} from 'decorator-dependency-injection';
|
|
362
|
+
|
|
363
|
+
@Singleton()
|
|
364
|
+
class UserService {
|
|
365
|
+
getUser(id) {
|
|
366
|
+
return { id, name: 'John' }
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
@Factory()
|
|
371
|
+
class Logger {
|
|
372
|
+
constructor(prefix) {
|
|
373
|
+
this.prefix = prefix
|
|
374
|
+
}
|
|
375
|
+
log(msg) {
|
|
376
|
+
console.log(`[${this.prefix}] ${msg}`)
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// Use in plain functions
|
|
381
|
+
function handleRequest(req) {
|
|
382
|
+
const userService = resolve(UserService)
|
|
383
|
+
return userService.getUser(req.userId)
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// Use with factory parameters
|
|
387
|
+
function createLogger(moduleName) {
|
|
388
|
+
return resolve(Logger, moduleName)
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// Use with named registrations
|
|
392
|
+
const db = resolve('databaseConnection')
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
This is useful when:
|
|
396
|
+
- Integrating with frameworks that don't support decorators
|
|
397
|
+
- Writing utility functions that need DI access
|
|
398
|
+
- Bridging between decorator-based and non-decorator code
|
|
399
|
+
- Testing or debugging the container directly
|
|
400
|
+
|
|
401
|
+
### Validation Helpers
|
|
402
|
+
|
|
403
|
+
The library provides utilities to validate registrations at runtime, which is useful for catching configuration
|
|
404
|
+
errors early:
|
|
405
|
+
|
|
406
|
+
#### `isRegistered(clazzOrName)`
|
|
407
|
+
|
|
408
|
+
Check if a class or name is registered:
|
|
409
|
+
|
|
410
|
+
```javascript
|
|
411
|
+
import {Singleton, isRegistered} from 'decorator-dependency-injection';
|
|
412
|
+
|
|
413
|
+
@Singleton()
|
|
414
|
+
class MyService {}
|
|
415
|
+
|
|
416
|
+
console.log(isRegistered(MyService)); // true
|
|
417
|
+
console.log(isRegistered('unknownName')); // false
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
#### `validateRegistrations(...tokens)`
|
|
421
|
+
|
|
422
|
+
Validate multiple registrations at once. Throws an error with helpful details if any are missing:
|
|
423
|
+
|
|
424
|
+
```javascript
|
|
425
|
+
import {validateRegistrations} from 'decorator-dependency-injection';
|
|
426
|
+
|
|
427
|
+
// At application startup - fail fast if dependencies are missing
|
|
428
|
+
try {
|
|
429
|
+
validateRegistrations(UserService, AuthService, 'databaseConnection');
|
|
430
|
+
} catch (err) {
|
|
431
|
+
// Error: Missing registrations: [UserService, databaseConnection].
|
|
432
|
+
// Ensure these classes are decorated with @Singleton() or @Factory() before use.
|
|
433
|
+
}
|
|
434
|
+
```
|
|
435
|
+
|
|
436
|
+
This is particularly useful in:
|
|
437
|
+
- Application bootstrap to catch missing dependencies before runtime failures
|
|
438
|
+
- Test setup to ensure mocks are properly configured
|
|
439
|
+
- Module initialization to validate external dependencies
|
|
440
|
+
|
|
441
|
+
### Debug Mode
|
|
442
|
+
|
|
443
|
+
Enable debug logging to understand the injection lifecycle:
|
|
444
|
+
|
|
445
|
+
```javascript
|
|
446
|
+
import {setDebug} from 'decorator-dependency-injection';
|
|
447
|
+
|
|
448
|
+
setDebug(true);
|
|
449
|
+
|
|
450
|
+
// Now logs will appear when:
|
|
451
|
+
// - Classes are registered: [DI] Registered singleton: UserService
|
|
452
|
+
// - Instances are created: [DI] Creating singleton: UserService
|
|
453
|
+
// - Cached singletons are returned: [DI] Returning cached singleton: UserService
|
|
454
|
+
// - Mocks are registered: [DI] Mocked UserService with MockUserService
|
|
455
|
+
```
|
|
456
|
+
|
|
457
|
+
This is helpful for:
|
|
458
|
+
- Debugging injection order issues
|
|
459
|
+
- Understanding when instances are created (eager vs lazy)
|
|
460
|
+
- Troubleshooting circular dependencies
|
|
461
|
+
- Verifying test mocks are applied correctly
|
|
462
|
+
|
|
197
463
|
You can also use the ```@Mock``` decorator as a proxy instead of a full mock. Any method calls not implemented in the
|
|
198
464
|
mock will be passed to the real dependency.
|
|
199
465
|
|
|
@@ -276,6 +542,23 @@ const container = getContainer();
|
|
|
276
542
|
console.log(container.has(MyService)); // Check if a class is registered
|
|
277
543
|
```
|
|
278
544
|
|
|
545
|
+
## TypeScript Support
|
|
546
|
+
|
|
547
|
+
The library includes TypeScript definitions with helpful type aliases:
|
|
548
|
+
|
|
549
|
+
```typescript
|
|
550
|
+
import {Constructor, InjectionToken} from 'decorator-dependency-injection';
|
|
551
|
+
|
|
552
|
+
// Constructor<T> - a class constructor that creates instances of T
|
|
553
|
+
const MyClass: Constructor<MyService> = MyService;
|
|
554
|
+
|
|
555
|
+
// InjectionToken<T> - either a class or a string name
|
|
556
|
+
const token1: InjectionToken<MyService> = MyService;
|
|
557
|
+
const token2: InjectionToken = 'myServiceName';
|
|
558
|
+
```
|
|
559
|
+
|
|
560
|
+
All decorator functions and utilities are fully typed with generics for better autocomplete and type safety.
|
|
561
|
+
|
|
279
562
|
## Running the tests
|
|
280
563
|
|
|
281
564
|
To run the tests, run the following command in the project root.
|
|
@@ -290,4 +573,6 @@ npm test
|
|
|
290
573
|
- 1.0.1 - Automated release with GitHub Actions
|
|
291
574
|
- 1.0.2 - Added proxy option to @Mock decorator
|
|
292
575
|
- 1.0.3 - Added @InjectLazy decorator
|
|
293
|
-
- 1.0.4 - Added Container abstraction, clearContainer(), TypeScript definitions, improved proxy support
|
|
576
|
+
- 1.0.4 - Added Container abstraction, clearContainer(), TypeScript definitions, improved proxy support
|
|
577
|
+
- 1.0.5 - Added private field and accessor support for @Inject and @InjectLazy, debug mode, validation helpers
|
|
578
|
+
- 1.0.6 - Added resolve() function for non-decorator code
|
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,57 @@ 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
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Resolve and return an instance by class or name.
|
|
65
|
+
* This allows non-decorator code to retrieve instances from the container.
|
|
66
|
+
*/
|
|
67
|
+
resolve<T>(clazzOrName: InjectionToken<T>, ...params: any[]): T
|
|
44
68
|
|
|
45
69
|
/**
|
|
46
70
|
* Get or create an instance based on the context.
|
|
47
71
|
*/
|
|
48
|
-
getInstance(instanceContext: InstanceContext, params: any[]):
|
|
72
|
+
getInstance<T>(instanceContext: InstanceContext, params: any[]): T
|
|
49
73
|
|
|
50
74
|
/**
|
|
51
75
|
* Register a mock for an existing class.
|
|
52
76
|
*/
|
|
53
|
-
registerMock(
|
|
54
|
-
targetClazzOrName:
|
|
55
|
-
mockClazz:
|
|
77
|
+
registerMock<T>(
|
|
78
|
+
targetClazzOrName: InjectionToken<T>,
|
|
79
|
+
mockClazz: Constructor<T>,
|
|
56
80
|
useProxy?: boolean
|
|
57
81
|
): void
|
|
58
82
|
|
|
59
83
|
/**
|
|
60
84
|
* Reset a specific mock to its original class.
|
|
61
85
|
*/
|
|
62
|
-
resetMock(clazzOrName:
|
|
86
|
+
resetMock<T>(clazzOrName: InjectionToken<T>): void
|
|
63
87
|
|
|
64
88
|
/**
|
|
65
89
|
* Reset all mocks to their original classes.
|
|
@@ -85,33 +109,72 @@ export declare function Singleton(name?: string): ClassDecorator
|
|
|
85
109
|
export declare function Factory(name?: string): ClassDecorator
|
|
86
110
|
|
|
87
111
|
/**
|
|
88
|
-
*
|
|
112
|
+
* Decorator return type that works for both fields and accessors.
|
|
113
|
+
* For fields, returns a function that provides the initial value.
|
|
114
|
+
* For accessors, returns an object with get/set/init.
|
|
115
|
+
*/
|
|
116
|
+
export type FieldOrAccessorDecorator = (
|
|
117
|
+
target: undefined,
|
|
118
|
+
context: ClassFieldDecoratorContext | ClassAccessorDecoratorContext
|
|
119
|
+
) => void | ((initialValue: any) => any) | ClassAccessorDecoratorResult<any, any>
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Inject a singleton or factory instance into a class field or accessor.
|
|
123
|
+
*
|
|
124
|
+
* Supports:
|
|
125
|
+
* - Public fields: `@Inject(MyClass) myField`
|
|
126
|
+
* - Private fields: `@Inject(MyClass) #myField`
|
|
127
|
+
* - Public accessors: `@Inject(MyClass) accessor myField`
|
|
128
|
+
* - Private accessors: `@Inject(MyClass) accessor #myField`
|
|
129
|
+
*
|
|
89
130
|
* @param clazzOrName The class or name to inject
|
|
90
131
|
* @param params Optional parameters to pass to the constructor
|
|
132
|
+
*
|
|
133
|
+
* @example
|
|
134
|
+
* class MyService {
|
|
135
|
+
* @Inject(Database) db
|
|
136
|
+
* @Inject(Logger) #logger // private field
|
|
137
|
+
* @Inject(Cache) accessor cache // accessor (recommended for lazy-like behavior)
|
|
138
|
+
* }
|
|
91
139
|
*/
|
|
92
140
|
export declare function Inject<T>(
|
|
93
|
-
clazzOrName:
|
|
141
|
+
clazzOrName: InjectionToken<T>,
|
|
94
142
|
...params: any[]
|
|
95
|
-
):
|
|
143
|
+
): FieldOrAccessorDecorator
|
|
96
144
|
|
|
97
145
|
/**
|
|
98
|
-
* Inject a singleton or factory instance lazily into a class field.
|
|
146
|
+
* Inject a singleton or factory instance lazily into a class field or accessor.
|
|
99
147
|
* The instance is created on first access.
|
|
148
|
+
*
|
|
149
|
+
* Supports:
|
|
150
|
+
* - Public fields: `@InjectLazy(MyClass) myField` (true lazy)
|
|
151
|
+
* - Private fields: `@InjectLazy(MyClass) #myField` (not truly lazy - use accessor instead)
|
|
152
|
+
* - Public accessors: `@InjectLazy(MyClass) accessor myField` (true lazy)
|
|
153
|
+
* - Private accessors: `@InjectLazy(MyClass) accessor #myField` (true lazy, recommended)
|
|
154
|
+
*
|
|
155
|
+
* Note: For true lazy injection with private members, use the accessor syntax:
|
|
156
|
+
* `@InjectLazy(MyClass) accessor #myField`
|
|
157
|
+
*
|
|
100
158
|
* @param clazzOrName The class or name to inject
|
|
101
159
|
* @param params Optional parameters to pass to the constructor
|
|
160
|
+
*
|
|
161
|
+
* @example
|
|
162
|
+
* class MyService {
|
|
163
|
+
* @InjectLazy(ExpensiveService) accessor #expensiveService
|
|
164
|
+
* }
|
|
102
165
|
*/
|
|
103
166
|
export declare function InjectLazy<T>(
|
|
104
|
-
clazzOrName:
|
|
167
|
+
clazzOrName: InjectionToken<T>,
|
|
105
168
|
...params: any[]
|
|
106
|
-
):
|
|
169
|
+
): FieldOrAccessorDecorator
|
|
107
170
|
|
|
108
171
|
/**
|
|
109
172
|
* Mark a class as a mock for another class.
|
|
110
173
|
* @param mockedClazzOrName The class or name to mock
|
|
111
174
|
* @param proxy If true, unmocked methods delegate to the original
|
|
112
175
|
*/
|
|
113
|
-
export declare function Mock(
|
|
114
|
-
mockedClazzOrName:
|
|
176
|
+
export declare function Mock<T>(
|
|
177
|
+
mockedClazzOrName: InjectionToken<T>,
|
|
115
178
|
proxy?: boolean
|
|
116
179
|
): ClassDecorator
|
|
117
180
|
|
|
@@ -124,7 +187,7 @@ export declare function resetMocks(): void
|
|
|
124
187
|
* Reset a specific mock to its original class.
|
|
125
188
|
* @param clazzOrName The class or name to reset
|
|
126
189
|
*/
|
|
127
|
-
export declare function resetMock(clazzOrName:
|
|
190
|
+
export declare function resetMock<T>(clazzOrName: InjectionToken<T>): void
|
|
128
191
|
|
|
129
192
|
/**
|
|
130
193
|
* Clear all registered instances and mocks from the container.
|
|
@@ -136,6 +199,53 @@ export declare function clearContainer(): void
|
|
|
136
199
|
*/
|
|
137
200
|
export declare function getContainer(): Container
|
|
138
201
|
|
|
202
|
+
/**
|
|
203
|
+
* Enable or disable debug logging for dependency injection.
|
|
204
|
+
* When enabled, logs when instances are registered, created, and mocked.
|
|
205
|
+
* @param enabled Whether to enable debug mode
|
|
206
|
+
*/
|
|
207
|
+
export declare function setDebug(enabled: boolean): void
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Check if a class or name is registered in the default container.
|
|
211
|
+
* Useful for validation before injection.
|
|
212
|
+
* @param clazzOrName The class or name to check
|
|
213
|
+
* @returns true if registered, false otherwise
|
|
214
|
+
*/
|
|
215
|
+
export declare function isRegistered<T>(clazzOrName: InjectionToken<T>): boolean
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Validate that all provided injection tokens are registered.
|
|
219
|
+
* Throws an error with details about missing registrations.
|
|
220
|
+
* Useful for fail-fast validation at application startup.
|
|
221
|
+
* @param tokens Array of classes or names to validate
|
|
222
|
+
* @throws Error if any token is not registered
|
|
223
|
+
*/
|
|
224
|
+
export declare function validateRegistrations<T extends InjectionToken[]>(...tokens: T): void
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Resolve and return an instance by class or name.
|
|
228
|
+
* This allows non-decorator code (plain functions, modules, etc.) to retrieve
|
|
229
|
+
* instances from the DI container.
|
|
230
|
+
*
|
|
231
|
+
* @param clazzOrName The class or name to resolve
|
|
232
|
+
* @param params Optional parameters to pass to the constructor
|
|
233
|
+
* @returns The resolved instance
|
|
234
|
+
* @throws Error if the class or name is not registered
|
|
235
|
+
*
|
|
236
|
+
* @example
|
|
237
|
+
* // In a plain function:
|
|
238
|
+
* function handleRequest(req: Request) {
|
|
239
|
+
* const userService = resolve(UserService)
|
|
240
|
+
* return userService.getUser(req.userId)
|
|
241
|
+
* }
|
|
242
|
+
*
|
|
243
|
+
* @example
|
|
244
|
+
* // With a named registration:
|
|
245
|
+
* const db = resolve<Database>('database')
|
|
246
|
+
*/
|
|
247
|
+
export declare function resolve<T>(clazzOrName: InjectionToken<T>, ...params: any[]): T
|
|
248
|
+
|
|
139
249
|
/**
|
|
140
250
|
* Create a proxy that delegates to the mock first, then falls back to the original.
|
|
141
251
|
* 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,84 @@ 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
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Resolve and return an instance by class or name.
|
|
318
|
+
* This allows non-decorator code (plain functions, modules, etc.) to retrieve
|
|
319
|
+
* instances from the DI container.
|
|
320
|
+
*
|
|
321
|
+
* @template T
|
|
322
|
+
* @param {string|Function} clazzOrName The class or name to resolve
|
|
323
|
+
* @param {...*} params Parameters to pass to the constructor
|
|
324
|
+
* @returns {T} The resolved instance
|
|
325
|
+
* @throws {Error} If the class or name is not registered
|
|
326
|
+
* @example
|
|
327
|
+
* // In a plain function:
|
|
328
|
+
* function handleRequest(req) {
|
|
329
|
+
* const userService = resolve(UserService)
|
|
330
|
+
* return userService.getUser(req.userId)
|
|
331
|
+
* }
|
|
332
|
+
* @example
|
|
333
|
+
* // With a named registration:
|
|
334
|
+
* const db = resolve('database')
|
|
335
|
+
* @example
|
|
336
|
+
* // With factory parameters:
|
|
337
|
+
* const logger = resolve(Logger, 'my-module')
|
|
338
|
+
*/
|
|
339
|
+
export function resolve(clazzOrName, ...params) {
|
|
340
|
+
return defaultContainer.resolve(clazzOrName, ...params)
|
|
341
|
+
}
|
|
342
|
+
|
|
182
343
|
// Export Container class for advanced use cases (e.g., isolated containers)
|
|
183
344
|
export {Container}
|
|
184
345
|
|
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
|
/**
|
|
@@ -82,6 +106,20 @@ export class Container {
|
|
|
82
106
|
return this.#instances.has(clazzOrName)
|
|
83
107
|
}
|
|
84
108
|
|
|
109
|
+
/**
|
|
110
|
+
* Resolve and return an instance by class or name.
|
|
111
|
+
* This allows non-decorator code to retrieve instances from the container.
|
|
112
|
+
* @template T
|
|
113
|
+
* @param {string|Function} clazzOrName The class or name to resolve
|
|
114
|
+
* @param {...*} params Parameters to pass to the constructor
|
|
115
|
+
* @returns {T} The resolved instance
|
|
116
|
+
* @throws {Error} If the class or name is not registered
|
|
117
|
+
*/
|
|
118
|
+
resolve(clazzOrName, ...params) {
|
|
119
|
+
const instanceContext = this.getContext(clazzOrName)
|
|
120
|
+
return this.getInstance(instanceContext, params)
|
|
121
|
+
}
|
|
122
|
+
|
|
85
123
|
/**
|
|
86
124
|
* Get or create an instance based on the context.
|
|
87
125
|
* @param {InstanceContext} instanceContext The instance context
|
|
@@ -90,11 +128,13 @@ export class Container {
|
|
|
90
128
|
*/
|
|
91
129
|
getInstance(instanceContext, params) {
|
|
92
130
|
if (instanceContext.type === 'singleton' && !instanceContext.originalClazz && instanceContext.instance) {
|
|
131
|
+
this.#log(`Returning cached singleton: ${instanceContext.clazz.name}`)
|
|
93
132
|
return instanceContext.instance
|
|
94
133
|
}
|
|
95
134
|
|
|
96
135
|
let instance
|
|
97
136
|
try {
|
|
137
|
+
this.#log(`Creating ${instanceContext.type}: ${instanceContext.clazz.name}`)
|
|
98
138
|
instance = new instanceContext.clazz(...params)
|
|
99
139
|
} catch (err) {
|
|
100
140
|
if (err instanceof RangeError) {
|
|
@@ -132,6 +172,8 @@ export class Container {
|
|
|
132
172
|
instanceContext.originalClazz = instanceContext.clazz
|
|
133
173
|
instanceContext.proxy = useProxy
|
|
134
174
|
instanceContext.clazz = mockClazz
|
|
175
|
+
const targetName = typeof targetClazzOrName === 'string' ? targetClazzOrName : targetClazzOrName.name
|
|
176
|
+
this.#log(`Mocked ${targetName} with ${mockClazz.name}${useProxy ? ' (proxy)' : ''}`)
|
|
135
177
|
}
|
|
136
178
|
|
|
137
179
|
/**
|
|
@@ -140,8 +182,7 @@ export class Container {
|
|
|
140
182
|
* @throws {Error} If the class or name is not registered
|
|
141
183
|
*/
|
|
142
184
|
resetMock(clazzOrName) {
|
|
143
|
-
|
|
144
|
-
this.#restoreOriginal(this.#instances.get(key), clazzOrName)
|
|
185
|
+
this.#restoreOriginal(this.#instances.get(clazzOrName), clazzOrName)
|
|
145
186
|
}
|
|
146
187
|
|
|
147
188
|
/**
|