decorator-dependency-injection 1.0.7 → 1.1.0

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/index.js CHANGED
@@ -1,34 +1,21 @@
1
- /**
2
- * Decorator Dependency Injection
3
- *
4
- * A simple library for dependency injection using TC39 Stage 3 decorators.
5
- *
6
- * @module decorator-dependency-injection
7
- */
8
-
9
1
  import {Container} from './src/Container.js'
10
2
 
11
- /** @type {Container} The default global container */
3
+ /** @type {Container} */
12
4
  const defaultContainer = new Container()
13
5
 
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
- */
6
+ /** @private */
22
7
  function createLazyAccessor(cache, getValue, name) {
23
8
  return {
24
9
  init(initialValue) {
25
- if (initialValue) {
10
+ if (initialValue !== undefined) {
26
11
  throw new Error(`Cannot assign value to injected accessor "${name}"`)
27
12
  }
28
- return undefined
29
13
  },
30
14
  get() {
31
- return cache.get(this) ?? (cache.set(this, getValue()), cache.get(this))
15
+ if (!cache.has(this)) {
16
+ cache.set(this, getValue())
17
+ }
18
+ return cache.get(this)
32
19
  },
33
20
  set() {
34
21
  throw new Error(`Cannot assign value to injected accessor "${name}"`)
@@ -36,18 +23,7 @@ function createLazyAccessor(cache, getValue, name) {
36
23
  }
37
24
  }
38
25
 
39
- /**
40
- * Register a class as a singleton. If a name is provided, it will be used as the key in the singleton map.
41
- * Singleton instances only ever have one instance created via the @Inject decorator.
42
- *
43
- * @param {string} [name] The name of the singleton. If not provided, the class will be used as the key.
44
- * @returns {(function(Function, {kind: string}): void)}
45
- * @example @Singleton() class MySingleton {}
46
- * @example @Singleton('customName') class MySingleton {}
47
- * @throws {Error} If the injection target is not a class
48
- * @throws {Error} If a singleton or factory with the same name is already defined
49
- * @throws {Error} If the target is not a class constructor
50
- */
26
+ /** @param {string} [name] */
51
27
  export function Singleton(name) {
52
28
  return (clazz, context) => {
53
29
  if (context.kind !== 'class') {
@@ -60,18 +36,7 @@ export function Singleton(name) {
60
36
  }
61
37
  }
62
38
 
63
- /**
64
- * Register a class as a factory. If a name is provided, it will be used as the key in the factory map.
65
- * Factory instances are created via the @Inject decorator. Each call to the factory will create a new instance.
66
- *
67
- * @param {string} [name] The name of the factory. If not provided, the class will be used as the key.
68
- * @returns {(function(Function, {kind: string}): void)}
69
- * @example @Factory() class MyFactory {}
70
- * @example @Factory('customName') class MyFactory {}
71
- * @throws {Error} If the injection target is not a class
72
- * @throws {Error} If a factory or singleton with the same name is already defined
73
- * @throws {Error} If the target is not a class constructor
74
- */
39
+ /** @param {string} [name] */
75
40
  export function Factory(name) {
76
41
  return (clazz, context) => {
77
42
  if (context.kind !== 'class') {
@@ -85,24 +50,8 @@ export function Factory(name) {
85
50
  }
86
51
 
87
52
  /**
88
- * Inject a singleton or factory instance into a class field. You can also provide parameters to the constructor.
89
- * If the instance is a singleton, it will only be created once with the first set of parameters it encounters.
90
- *
91
- * Supports:
92
- * - Public fields: @Inject(MyClass) myField
93
- * - Private fields: @Inject(MyClass) #myField
94
- * - Accessors: @Inject(MyClass) accessor myField
95
- * - Private accessors: @Inject(MyClass) accessor #myField
96
- *
97
- * @param {string|Function} clazzOrName The singleton or factory class or name
98
- * @param {...*} params Parameters to pass to the constructor. Recommended to use only with factories.
99
- * @returns {(function(*, {kind: string, name: string}): function(): Object)}
100
- * @example @Inject(MySingleton) mySingleton
101
- * @example @Inject("myCustomName") myFactory
102
- * @example @Inject(MyService) #privateService
103
- * @example @Inject(MyService) accessor myService
104
- * @throws {Error} If the injection target is not a field or accessor
105
- * @throws {Error} If the injected field is assigned a value
53
+ * @param {string|Function} clazzOrName
54
+ * @param {...*} params
106
55
  */
107
56
  export function Inject(clazzOrName, ...params) {
108
57
  return (_, context) => {
@@ -113,7 +62,7 @@ export function Inject(clazzOrName, ...params) {
113
62
 
114
63
  if (context.kind === 'field') {
115
64
  return (initialValue) => {
116
- if (initialValue) {
65
+ if (initialValue !== undefined) {
117
66
  throw new Error(`Cannot assign value to injected field "${context.name}"`)
118
67
  }
119
68
  return getValue()
@@ -130,31 +79,9 @@ export function Inject(clazzOrName, ...params) {
130
79
  }
131
80
 
132
81
  /**
133
- * Inject a singleton or factory instance lazily into a class field. You can also provide parameters to the constructor.
134
- * If the instance is a singleton, it will only be created once with the first set of parameters it encounters.
135
- *
136
- * The lazy injection defers instantiation until the field is first accessed. This is useful for:
137
- * - Breaking circular dependencies
138
- * - Deferring expensive initializations
139
- *
140
- * Supports:
141
- * - Public fields: @InjectLazy(MyClass) myField
142
- * - Private fields: @InjectLazy(MyClass) #myField
143
- * - Accessors: @InjectLazy(MyClass) accessor myField
144
- * - Private accessors: @InjectLazy(MyClass) accessor #myField
145
- *
146
- * Note: For private fields, the lazy behavior is achieved through the field initializer
147
- * returning a getter-based proxy. For accessors, it's achieved through the accessor's
148
- * get/set methods directly.
149
- *
150
- * @param {string|Function} clazzOrName The singleton or factory class or name
151
- * @param {...*} params Parameters to pass to the constructor. Recommended to use only with factories.
152
- * @returns {(function(*, {kind: string, name: string, addInitializer: Function}): void)}
153
- * @example @InjectLazy(MySingleton) mySingleton
154
- * @example @InjectLazy("myCustomName") myFactory
155
- * @example @InjectLazy(MyService) #privateService
156
- * @throws {Error} If the injection target is not a field or accessor
157
- * @throws {Error} If the injected field is assigned a value
82
+ * Defers instantiation until first access. For private fields, use accessor syntax for true lazy behavior.
83
+ * @param {string|Function} clazzOrName
84
+ * @param {...*} params
158
85
  */
159
86
  export function InjectLazy(clazzOrName, ...params) {
160
87
  const cache = new WeakMap()
@@ -170,7 +97,7 @@ export function InjectLazy(clazzOrName, ...params) {
170
97
  // Instead, we eagerly create the value. For true lazy behavior, use accessor syntax.
171
98
  if (context.private) {
172
99
  return (initialValue) => {
173
- if (initialValue) {
100
+ if (initialValue !== undefined) {
174
101
  throw new Error(`Cannot assign value to lazy-injected field "${context.name}"`)
175
102
  }
176
103
  return getValue()
@@ -205,67 +132,8 @@ export function InjectLazy(clazzOrName, ...params) {
205
132
  }
206
133
 
207
134
  /**
208
- * Mark a class as a mock. This will replace the original class with the mock when injected.
209
- * The mock registration persists until explicitly removed with removeMock() or removeAllMocks().
210
- *
211
- * @param {string|Function} mockedClazzOrName The singleton or factory class or name to be mocked
212
- * @param {boolean} [proxy=false] If true, the mock will proxy to the original class.
213
- * Any methods not defined in the mock will be called on the original class.
214
- * @returns {(function(Function, {kind: string}): void)}
215
- *
216
- * @example Basic mocking
217
- * ```js
218
- * @Mock(MySingleton)
219
- * class MockedSingleton {
220
- * doSomething() { return 'mocked result' }
221
- * }
222
- * ```
223
- *
224
- * @example Proxy mocking (partial mock)
225
- * ```js
226
- * // Only override specific methods, others delegate to original
227
- * @Mock(MySingleton, true)
228
- * class PartialMock {
229
- * onlyThisMethod() { return 'mocked' }
230
- * // All other methods call the original implementation
231
- * }
232
- * ```
233
- *
234
- * @example Testing pattern with hoisted mock functions
235
- * ```js
236
- * // Hoist mock functions for per-test configuration
237
- * const mockMethod = vi.hoisted(() => vi.fn())
238
- *
239
- * @Mock(MyService)
240
- * class MockMyService {
241
- * doSomething = mockMethod
242
- * }
243
- *
244
- * beforeEach(() => {
245
- * // Clear call history - NOT removeMock() which removes the mock entirely
246
- * mockMethod.mockClear()
247
- * })
248
- *
249
- * it('should call the service', () => {
250
- * mockMethod.mockReturnValue('test value')
251
- * // ... your test
252
- * expect(mockMethod).toHaveBeenCalled()
253
- * })
254
- * ```
255
- *
256
- * @example Cleanup in afterEach
257
- * ```js
258
- * afterEach(() => {
259
- * // Option 1: Remove all mocks and restore originals
260
- * removeAllMocks()
261
- *
262
- * // Option 2: Just clear singleton instances, keep mocks registered
263
- * resetSingletons()
264
- * })
265
- * ```
266
- *
267
- * @throws {Error} If the injection target is not a class
268
- * @throws {Error} If the injection source is not found
135
+ * @param {string|Function} mockedClazzOrName
136
+ * @param {boolean} [proxy=false] If true, unmocked methods delegate to the original
269
137
  */
270
138
  export function Mock(mockedClazzOrName, proxy = false) {
271
139
  return (clazz, context) => {
@@ -276,297 +144,97 @@ export function Mock(mockedClazzOrName, proxy = false) {
276
144
  }
277
145
  }
278
146
 
279
- /**
280
- * Remove all mocks and restore original classes.
281
- * This completely removes all mocks - it does NOT clear mock call history.
282
- *
283
- * If you want to clear call history on mock functions without removing the mock,
284
- * call mockClear() on your mock functions instead.
285
- *
286
- * @example
287
- * ```js
288
- * afterEach(() => {
289
- * removeAllMocks() // Restores original classes
290
- * })
291
- * ```
292
- */
147
+ /** Remove all mocks and restore originals. Does NOT clear mock call history. */
293
148
  export function removeAllMocks() {
294
149
  defaultContainer.removeAllMocks()
295
150
  }
296
151
 
297
- /**
298
- * Remove a specific mock and restore the original class.
299
- * This completely removes the mock - it does NOT clear mock call history.
300
- *
301
- * @param {string|Function} clazzOrName The singleton or factory class or name to restore
302
- *
303
- * @example
304
- * ```js
305
- * removeMock(UserService) // Restores original UserService
306
- * ```
307
- */
152
+ /** @param {string|Function} clazzOrName */
308
153
  export function removeMock(clazzOrName) {
309
154
  defaultContainer.removeMock(clazzOrName)
310
155
  }
311
156
 
312
- /**
313
- * @deprecated Use removeAllMocks() instead. This will be removed in a future version.
314
- *
315
- * WARNING: Despite the name, this does NOT reset mock call history like mockClear().
316
- * It completely removes all mocks and restores the original classes.
317
- */
318
- export function resetMocks() {
319
- console.warn(
320
- '[DI] resetMocks() is deprecated. Use removeAllMocks() instead. ' +
321
- 'Note: This removes mocks entirely, NOT clearing call history.'
322
- )
323
- defaultContainer.removeAllMocks()
324
- }
325
-
326
- /**
327
- * @deprecated Use removeMock() instead. This will be removed in a future version.
328
- *
329
- * WARNING: Despite the name, this does NOT reset mock call history like mockClear().
330
- * It completely removes the mock and restores the original class.
331
- *
332
- * @param {string|Function} clazzOrName The singleton or factory class or name to restore
333
- */
334
- export function resetMock(clazzOrName) {
335
- console.warn(
336
- '[DI] resetMock() is deprecated. Use removeMock() instead. ' +
337
- 'Note: This removes the mock entirely, NOT clearing call history.'
338
- )
339
- defaultContainer.removeMock(clazzOrName)
340
- }
341
-
342
- /**
343
- * Reset singleton instances without removing registrations.
344
- * This clears cached singleton instances so they will be recreated on next resolve.
345
- * Mock registrations are preserved by default.
346
- *
347
- * This is ideal for test isolation where you want:
348
- * - Fresh singleton instances per test
349
- * - Mock registrations to remain intact
350
- *
351
- * @param {Object} [options] Options for resetting
352
- * @param {boolean} [options.preserveMocks=true] If true, keeps mock registrations.
353
- * If false, also removes mocks.
354
- *
355
- * @example
356
- * ```js
357
- * beforeEach(() => {
358
- * // Each test gets fresh singleton instances
359
- * // but mocks remain registered
360
- * resetSingletons()
361
- * })
362
- * ```
363
- */
157
+ /** @param {{preserveMocks?: boolean}} [options] */
364
158
  export function resetSingletons(options) {
365
159
  defaultContainer.resetSingletons(options)
366
160
  }
367
161
 
368
- /**
369
- * Clear all registered instances and mocks from the container.
370
- *
371
- * By default, this removes ALL registrations including @Singleton and @Factory classes.
372
- * For just clearing singleton instances without removing any registrations,
373
- * use resetSingletons() instead.
374
- *
375
- * @param {Object} [options] Options for clearing
376
- * @param {boolean} [options.preserveRegistrations=false] If true, keeps all registrations but clears cached instances.
377
- *
378
- * @example
379
- * ```js
380
- * // Complete reset - removes everything
381
- * clearContainer()
382
- *
383
- * // Clear cached instances but keep registrations (including mocks)
384
- * clearContainer({ preserveRegistrations: true })
385
- *
386
- * // Just clear singleton instances (preferred for test isolation)
387
- * resetSingletons()
388
- * ```
389
- */
162
+ /** @param {{preserveRegistrations?: boolean}} [options] */
390
163
  export function clearContainer(options) {
391
164
  defaultContainer.clear(options)
392
165
  }
393
166
 
394
- /**
395
- * Get the default container instance.
396
- * Useful for advanced use cases or testing the container itself.
397
- *
398
- * @returns {Container} The default container
399
- */
167
+ /** @returns {Container} */
400
168
  export function getContainer() {
401
169
  return defaultContainer
402
170
  }
403
171
 
404
- /**
405
- * Enable or disable debug logging for dependency injection.
406
- * When enabled, logs when instances are registered, created, and mocked.
407
- *
408
- * @param {boolean} enabled Whether to enable debug mode
409
- * @example
410
- * setDebug(true)
411
- * // [DI] Registered singleton: UserService
412
- * // [DI] Creating singleton: UserService
413
- */
172
+ /** @param {boolean} enabled */
414
173
  export function setDebug(enabled) {
415
174
  defaultContainer.setDebug(enabled)
416
175
  }
417
176
 
418
177
  /**
419
- * Check if a class or name is registered in the default container.
420
- * Useful for validation before injection.
421
- *
422
- * @param {string|Function} clazzOrName The class or name to check
423
- * @returns {boolean} true if registered, false otherwise
424
- * @example
425
- * if (!isRegistered(MyService)) {
426
- * console.warn('MyService not registered!')
427
- * }
178
+ * @param {string|Function} clazzOrName
179
+ * @returns {boolean}
428
180
  */
429
181
  export function isRegistered(clazzOrName) {
430
182
  return defaultContainer.has(clazzOrName)
431
183
  }
432
184
 
433
- /**
434
- * Validate that all provided injection tokens are registered.
435
- * Throws an error with details about missing registrations.
436
- * Useful for fail-fast validation at application startup.
437
- *
438
- * @param {...(string|Function)} tokens Classes or names to validate
439
- * @throws {Error} If any token is not registered
440
- * @example
441
- * // At app startup:
442
- * validateRegistrations(UserService, AuthService, 'databaseConnection')
443
- */
185
+ /** @param {...(string|Function)} tokens */
444
186
  export function validateRegistrations(...tokens) {
445
187
  const missing = tokens.filter(token => !defaultContainer.has(token))
446
- if (missing.length > 0) {
447
- const names = missing.map(t => typeof t === 'string' ? t : t.name).join(', ')
448
- throw new Error(
449
- `Missing registrations: [${names}]. ` +
450
- `Ensure these classes are decorated with @Singleton() or @Factory() before use.`
451
- )
452
- }
188
+ if (missing.length === 0) return
189
+
190
+ const names = missing.map(t => t?.name ?? t).join(', ')
191
+ throw new Error(
192
+ `Missing registrations: [${names}]. ` +
193
+ `Ensure these classes are decorated with @Singleton() or @Factory() before use.`
194
+ )
453
195
  }
454
196
 
455
197
  /**
456
- * Resolve and return an instance by class or name.
457
- * This allows non-decorator code (plain functions, modules, etc.) to retrieve
458
- * instances from the DI container.
459
- *
460
198
  * @template T
461
- * @param {string|Function} clazzOrName The class or name to resolve
462
- * @param {...*} params Parameters to pass to the constructor
463
- * @returns {T} The resolved instance
464
- * @throws {Error} If the class or name is not registered
465
- * @example
466
- * // In a plain function:
467
- * function handleRequest(req) {
468
- * const userService = resolve(UserService)
469
- * return userService.getUser(req.userId)
470
- * }
471
- * @example
472
- * // With a named registration:
473
- * const db = resolve('database')
474
- * @example
475
- * // With factory parameters:
476
- * const logger = resolve(Logger, 'my-module')
199
+ * @param {string|Function} clazzOrName
200
+ * @param {...*} params
201
+ * @returns {T}
477
202
  */
478
203
  export function resolve(clazzOrName, ...params) {
479
204
  return defaultContainer.resolve(clazzOrName, ...params)
480
205
  }
481
206
 
482
207
  /**
483
- * Get the mock instance for a mocked class.
484
- * This is useful when you need to access or configure mock behavior dynamically in tests.
485
- *
486
- * Unlike resolve(), this explicitly checks that the class is mocked and provides
487
- * better error messages if it's not.
488
- *
489
208
  * @template T
490
- * @param {string|Function} clazzOrName The original class or name that was mocked
491
- * @param {...*} params Parameters to pass to the constructor
492
- * @returns {T} The mock instance
493
- * @throws {Error} If the class is not registered
494
- * @throws {Error} If the class is not mocked
495
- *
496
- * @example
497
- * ```js
498
- * @Mock(UserService)
499
- * class MockUserService {
500
- * getUser = vi.fn()
501
- * }
502
- *
503
- * it('should get user', () => {
504
- * const mock = getMockInstance(UserService)
505
- * mock.getUser.mockReturnValue({ id: 1, name: 'Test' })
506
- *
507
- * // ... test code that uses UserService
508
- *
509
- * expect(mock.getUser).toHaveBeenCalledWith(1)
510
- * })
511
- * ```
209
+ * @param {string|Function} clazzOrName
210
+ * @param {...*} params
211
+ * @returns {T}
512
212
  */
513
213
  export function getMockInstance(clazzOrName, ...params) {
514
214
  return defaultContainer.getMockInstance(clazzOrName, ...params)
515
215
  }
516
216
 
517
217
  /**
518
- * Check if a class or name has a mock registered.
519
- * Useful for conditional test logic or debugging.
520
- *
521
- * @param {string|Function} clazzOrName The class or name to check
522
- * @returns {boolean} true if a mock is registered, false otherwise
523
- *
524
- * @example
525
- * ```js
526
- * if (isMocked(UserService)) {
527
- * console.log('UserService is mocked')
528
- * }
529
- * ```
218
+ * @param {string|Function} clazzOrName
219
+ * @returns {boolean}
530
220
  */
531
221
  export function isMocked(clazzOrName) {
532
222
  return defaultContainer.isMocked(clazzOrName)
533
223
  }
534
224
 
535
225
  /**
536
- * Unregister a class or name from the container.
537
- * This removes the registration entirely, including any mock.
538
- *
539
- * @param {string|Function} clazzOrName The class or name to unregister
540
- * @returns {boolean} true if the registration was removed, false if it wasn't registered
541
- *
542
- * @example
543
- * ```js
544
- * unregister(MyService) // Returns true if was registered
545
- * ```
226
+ * @param {string|Function} clazzOrName
227
+ * @returns {boolean}
546
228
  */
547
229
  export function unregister(clazzOrName) {
548
230
  return defaultContainer.unregister(clazzOrName)
549
231
  }
550
232
 
551
- /**
552
- * List all registrations in the container.
553
- * Useful for debugging and introspection.
554
- *
555
- * @returns {Array<{key: string|Function, name: string, type: 'singleton'|'factory', isMocked: boolean, hasInstance: boolean}>}
556
- *
557
- * @example
558
- * ```js
559
- * listRegistrations().forEach(reg => {
560
- * console.log(`${reg.name}: ${reg.type}, mocked: ${reg.isMocked}`)
561
- * })
562
- * ```
563
- */
233
+ /** @returns {Array<{key: string|Function, name: string, type: 'singleton'|'factory', isMocked: boolean, hasInstance: boolean}>} */
564
234
  export function listRegistrations() {
565
235
  return defaultContainer.list()
566
236
  }
567
237
 
568
- // Export Container class for advanced use cases (e.g., isolated containers)
569
238
  export {Container}
570
-
571
- // Export createProxy for advanced proxy use cases
239
+ export {defaultContainer}
572
240
  export {createProxy} from './src/proxy.js'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "decorator-dependency-injection",
3
- "version": "1.0.7",
3
+ "version": "1.1.0",
4
4
  "description": "Lightweight dependency injection (DI) library using native TC39 Stage 3 decorators. Zero dependencies, built-in mocking, TypeScript support.",
5
5
  "author": "Ravi Gairola <mallox@pyxzl.net>",
6
6
  "license": "Apache-2.0",
@@ -39,10 +39,15 @@
39
39
  "main": "index.js",
40
40
  "types": "index.d.ts",
41
41
  "type": "module",
42
+ "sideEffects": false,
42
43
  "exports": {
43
44
  ".": {
44
45
  "types": "./index.d.ts",
45
46
  "import": "./index.js"
47
+ },
48
+ "./middleware": {
49
+ "types": "./src/integrations/middleware.d.ts",
50
+ "import": "./src/integrations/middleware.js"
46
51
  }
47
52
  },
48
53
  "scripts": {