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 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, 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,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: 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
44
62
 
45
63
  /**
46
64
  * Get or create an instance based on the context.
47
65
  */
48
- getInstance(instanceContext: InstanceContext, params: any[]): 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: string | (new (...args: any[]) => any),
55
- mockClazz: new (...args: any[]) => any,
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: string | (new (...args: any[]) => any)): void
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
- * Inject a singleton or factory instance into a class field.
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: string | (new (...args: any[]) => T),
135
+ clazzOrName: InjectionToken<T>,
94
136
  ...params: any[]
95
- ): PropertyDecorator
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: string | (new (...args: any[]) => T),
161
+ clazzOrName: InjectionToken<T>,
105
162
  ...params: any[]
106
- ): PropertyDecorator
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: string | (new (...args: any[]) => any),
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: string | (new (...args: any[]) => any)): void
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
- * @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,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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "decorator-dependency-injection",
3
- "version": "1.0.4",
3
+ "version": "1.0.5",
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
  /**
@@ -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
- const key = typeof clazzOrName === 'string' ? clazzOrName : clazzOrName
144
- this.#restoreOriginal(this.#instances.get(key), clazzOrName)
171
+ this.#restoreOriginal(this.#instances.get(clazzOrName), clazzOrName)
145
172
  }
146
173
 
147
174
  /**