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 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, not directly
70
- "no-unused-vars": ["warn", { "argsIgnorePattern": "^_", "varsIgnorePattern": "^_" }]
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: new (...args: any[]) => any, name?: string): void
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: new (...args: any[]) => any, name?: string): void
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: string | (new (...args: any[]) => any)): InstanceContext
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: string | (new (...args: any[]) => any)): boolean
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[]): 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: string | (new (...args: any[]) => any),
55
- mockClazz: new (...args: any[]) => any,
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: string | (new (...args: any[]) => any)): void
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
- * Inject a singleton or factory instance into a class field.
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: string | (new (...args: any[]) => T),
141
+ clazzOrName: InjectionToken<T>,
94
142
  ...params: any[]
95
- ): PropertyDecorator
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: string | (new (...args: any[]) => T),
167
+ clazzOrName: InjectionToken<T>,
105
168
  ...params: any[]
106
- ): PropertyDecorator
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: string | (new (...args: any[]) => any),
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: string | (new (...args: any[]) => any)): void
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
- * @throws {Error} If the injection target is not a field
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
- if (context.kind !== 'field') {
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
- * @throws {Error} If the injection target is not a field
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 !== 'field') {
105
- throw new Error('Invalid injection target')
106
- }
107
- context.addInitializer(function () {
108
- Object.defineProperty(this, context.name, {
109
- get() {
110
- if (!cache.has(this)) {
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 cache.get(this)
116
- },
117
- set() {
118
- throw new Error(`Cannot assign value to lazy-injected field "${context.name}"`)
119
- },
120
- configurable: true,
121
- enumerable: true
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "decorator-dependency-injection",
3
- "version": "1.0.4",
3
+ "version": "1.0.6",
4
4
  "description": "A simple library for dependency injection using decorators",
5
5
  "author": "Ravi Gairola <mallox@pyxzl.net>",
6
6
  "license": "Apache-2.0",
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
- const key = typeof clazzOrName === 'string' ? clazzOrName : clazzOrName
144
- this.#restoreOriginal(this.#instances.get(key), clazzOrName)
185
+ this.#restoreOriginal(this.#instances.get(clazzOrName), clazzOrName)
145
186
  }
146
187
 
147
188
  /**