decorator-dependency-injection 1.0.6 → 1.0.7

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.d.ts CHANGED
@@ -29,10 +29,43 @@ export interface InstanceContext {
29
29
  proxy?: boolean
30
30
  }
31
31
 
32
+ /**
33
+ * Registration info returned by list()
34
+ */
35
+ export interface RegistrationInfo {
36
+ /** The registration key (class or string name) */
37
+ key: string | Constructor
38
+ /** Human-readable name */
39
+ name: string
40
+ /** Registration type */
41
+ type: 'singleton' | 'factory'
42
+ /** Whether this registration is mocked */
43
+ isMocked: boolean
44
+ /** Whether a cached instance exists */
45
+ hasInstance: boolean
46
+ }
47
+
32
48
  /**
33
49
  * A dependency injection container that manages singleton and factory instances.
34
50
  */
35
51
  export declare class Container {
52
+ /**
53
+ * Custom string tag for better debugging.
54
+ * Shows as [object Container] in console.
55
+ */
56
+ readonly [Symbol.toStringTag]: 'Container'
57
+
58
+ /**
59
+ * Make the container iterable.
60
+ * Yields registration info for each registered class.
61
+ */
62
+ [Symbol.iterator](): IterableIterator<RegistrationInfo>
63
+
64
+ /**
65
+ * Get the number of registrations in the container.
66
+ */
67
+ readonly size: number
68
+
36
69
  /**
37
70
  * Enable or disable debug logging.
38
71
  * When enabled, logs when instances are created.
@@ -60,6 +93,22 @@ export declare class Container {
60
93
  */
61
94
  has<T>(clazzOrName: InjectionToken<T>): boolean
62
95
 
96
+ /**
97
+ * Check if a class or name has a mock registered.
98
+ */
99
+ isMocked<T>(clazzOrName: InjectionToken<T>): boolean
100
+
101
+ /**
102
+ * Unregister a class or name from the container.
103
+ * @returns true if the registration was removed, false if it wasn't registered
104
+ */
105
+ unregister<T>(clazzOrName: InjectionToken<T>): boolean
106
+
107
+ /**
108
+ * List all registrations in the container.
109
+ */
110
+ list(): RegistrationInfo[]
111
+
63
112
  /**
64
113
  * Resolve and return an instance by class or name.
65
114
  * This allows non-decorator code to retrieve instances from the container.
@@ -74,26 +123,49 @@ export declare class Container {
74
123
  /**
75
124
  * Register a mock for an existing class.
76
125
  */
77
- registerMock<T>(
78
- targetClazzOrName: InjectionToken<T>,
79
- mockClazz: Constructor<T>,
80
- useProxy?: boolean
81
- ): void
126
+ registerMock<T>(targetClazzOrName: InjectionToken<T>, mockClazz: Constructor<Partial<T>>, useProxy?: boolean): void
82
127
 
83
128
  /**
84
- * Reset a specific mock to its original class.
129
+ * Get the mock instance for a mocked class.
130
+ * @throws Error if the class is not mocked
131
+ */
132
+ getMockInstance<T>(clazzOrName: InjectionToken<T>, ...params: any[]): T
133
+
134
+ /**
135
+ * Remove a specific mock and restore the original class.
136
+ * This completely removes the mock - it does NOT clear mock call history.
137
+ */
138
+ removeMock<T>(clazzOrName: InjectionToken<T>): void
139
+
140
+ /**
141
+ * Remove all mocks and restore original classes.
142
+ * This completely removes all mocks - it does NOT clear mock call history.
143
+ */
144
+ removeAllMocks(): void
145
+
146
+ /**
147
+ * @deprecated Use removeMock() instead. This will be removed in a future version.
148
+ * WARNING: This removes the mock, it does NOT clear mock call history.
85
149
  */
86
150
  resetMock<T>(clazzOrName: InjectionToken<T>): void
87
151
 
88
152
  /**
89
- * Reset all mocks to their original classes.
153
+ * @deprecated Use removeAllMocks() instead. This will be removed in a future version.
154
+ * WARNING: This removes all mocks, it does NOT clear mock call history.
90
155
  */
91
156
  resetAllMocks(): void
92
157
 
158
+ /**
159
+ * Reset singleton instances without removing registrations.
160
+ * Mock registrations are preserved by default.
161
+ */
162
+ resetSingletons(options?: { preserveMocks?: boolean }): void
163
+
93
164
  /**
94
165
  * Clear all registered instances and mocks.
166
+ * @param options.preserveRegistrations If true, keeps all registrations but clears cached instances.
95
167
  */
96
- clear(): void
168
+ clear(options?: { preserveRegistrations?: boolean }): void
97
169
  }
98
170
 
99
171
  /**
@@ -170,8 +242,33 @@ export declare function InjectLazy<T>(
170
242
 
171
243
  /**
172
244
  * Mark a class as a mock for another class.
245
+ * The mock class can implement only the methods you need (Partial<T>).
246
+ *
173
247
  * @param mockedClazzOrName The class or name to mock
174
- * @param proxy If true, unmocked methods delegate to the original
248
+ * @param proxy If true, unmocked methods delegate to the original implementation
249
+ *
250
+ * @example Basic mocking
251
+ * ```ts
252
+ * @Mock(UserService)
253
+ * class MockUserService {
254
+ * // Only implement methods you need to mock
255
+ * getUser() { return { id: 1, name: 'Test' } }
256
+ * }
257
+ * ```
258
+ *
259
+ * @example With hoisted mock functions (Vitest/Jest)
260
+ * ```ts
261
+ * const mockGetUser = vi.hoisted(() => vi.fn())
262
+ *
263
+ * @Mock(UserService)
264
+ * class MockUserService {
265
+ * getUser = mockGetUser
266
+ * }
267
+ *
268
+ * beforeEach(() => {
269
+ * mockGetUser.mockClear() // Clear call history, not removeMock()
270
+ * })
271
+ * ```
175
272
  */
176
273
  export declare function Mock<T>(
177
274
  mockedClazzOrName: InjectionToken<T>,
@@ -179,20 +276,46 @@ export declare function Mock<T>(
179
276
  ): ClassDecorator
180
277
 
181
278
  /**
182
- * Reset all mocks to their original classes.
279
+ * Remove all mocks and restore original classes.
280
+ * This completely removes all mocks - it does NOT clear mock call history.
281
+ */
282
+ export declare function removeAllMocks(): void
283
+
284
+ /**
285
+ * Remove a specific mock and restore the original class.
286
+ * This completely removes the mock - it does NOT clear mock call history.
287
+ * @param clazzOrName The class or name to restore
288
+ */
289
+ export declare function removeMock<T>(clazzOrName: InjectionToken<T>): void
290
+
291
+ /**
292
+ * @deprecated Use removeAllMocks() instead. This will be removed in a future version.
293
+ * WARNING: This removes all mocks, it does NOT clear mock call history.
183
294
  */
184
295
  export declare function resetMocks(): void
185
296
 
186
297
  /**
187
- * Reset a specific mock to its original class.
188
- * @param clazzOrName The class or name to reset
298
+ * @deprecated Use removeMock() instead. This will be removed in a future version.
299
+ * WARNING: This removes the mock, it does NOT clear mock call history.
300
+ * @param clazzOrName The class or name to restore
189
301
  */
190
302
  export declare function resetMock<T>(clazzOrName: InjectionToken<T>): void
191
303
 
304
+ /**
305
+ * Reset singleton instances without removing registrations.
306
+ * Mock registrations are preserved by default.
307
+ *
308
+ * Ideal for test isolation where you want fresh instances but keep mocks.
309
+ *
310
+ * @param options.preserveMocks If true (default), keeps mock registrations
311
+ */
312
+ export declare function resetSingletons(options?: { preserveMocks?: boolean }): void
313
+
192
314
  /**
193
315
  * Clear all registered instances and mocks from the container.
316
+ * @param options.preserveRegistrations If true, keeps all registrations but clears cached instances.
194
317
  */
195
- export declare function clearContainer(): void
318
+ export declare function clearContainer(options?: { preserveRegistrations?: boolean }): void
196
319
 
197
320
  /**
198
321
  * Get the default container instance.
@@ -214,6 +337,43 @@ export declare function setDebug(enabled: boolean): void
214
337
  */
215
338
  export declare function isRegistered<T>(clazzOrName: InjectionToken<T>): boolean
216
339
 
340
+ /**
341
+ * Check if a class or name has a mock registered.
342
+ * @param clazzOrName The class or name to check
343
+ * @returns true if mocked, false otherwise
344
+ */
345
+ export declare function isMocked<T>(clazzOrName: InjectionToken<T>): boolean
346
+
347
+ /**
348
+ * Get the mock instance for a mocked class.
349
+ * Useful for configuring mock behavior dynamically in tests.
350
+ *
351
+ * @param clazzOrName The original class or name that was mocked
352
+ * @param params Parameters to pass to the constructor
353
+ * @returns The mock instance
354
+ * @throws Error if the class is not mocked
355
+ *
356
+ * @example
357
+ * ```ts
358
+ * const mock = getMockInstance(UserService)
359
+ * mock.getUser.mockReturnValue({ id: 1 })
360
+ * ```
361
+ */
362
+ export declare function getMockInstance<T>(clazzOrName: InjectionToken<T>, ...params: any[]): T
363
+
364
+ /**
365
+ * Unregister a class or name from the container.
366
+ * @param clazzOrName The class or name to unregister
367
+ * @returns true if the registration was removed, false if it wasn't registered
368
+ */
369
+ export declare function unregister<T>(clazzOrName: InjectionToken<T>): boolean
370
+
371
+ /**
372
+ * List all registrations in the container.
373
+ * Useful for debugging and introspection.
374
+ */
375
+ export declare function listRegistrations(): RegistrationInfo[]
376
+
217
377
  /**
218
378
  * Validate that all provided injection tokens are registered.
219
379
  * Throws an error with details about missing registrations.
package/index.js CHANGED
@@ -28,10 +28,7 @@ function createLazyAccessor(cache, getValue, name) {
28
28
  return undefined
29
29
  },
30
30
  get() {
31
- if (!cache.has(this)) {
32
- cache.set(this, getValue())
33
- }
34
- return cache.get(this)
31
+ return cache.get(this) ?? (cache.set(this, getValue()), cache.get(this))
35
32
  },
36
33
  set() {
37
34
  throw new Error(`Cannot assign value to injected accessor "${name}"`)
@@ -52,7 +49,7 @@ function createLazyAccessor(cache, getValue, name) {
52
49
  * @throws {Error} If the target is not a class constructor
53
50
  */
54
51
  export function Singleton(name) {
55
- return function (clazz, context) {
52
+ return (clazz, context) => {
56
53
  if (context.kind !== 'class') {
57
54
  throw new Error('Invalid injection target')
58
55
  }
@@ -76,7 +73,7 @@ export function Singleton(name) {
76
73
  * @throws {Error} If the target is not a class constructor
77
74
  */
78
75
  export function Factory(name) {
79
- return function (clazz, context) {
76
+ return (clazz, context) => {
80
77
  if (context.kind !== 'class') {
81
78
  throw new Error('Invalid injection target')
82
79
  }
@@ -108,14 +105,14 @@ export function Factory(name) {
108
105
  * @throws {Error} If the injected field is assigned a value
109
106
  */
110
107
  export function Inject(clazzOrName, ...params) {
111
- return function (_, context) {
108
+ return (_, context) => {
112
109
  const getValue = () => {
113
110
  const instanceContext = defaultContainer.getContext(clazzOrName)
114
111
  return defaultContainer.getInstance(instanceContext, params)
115
112
  }
116
113
 
117
114
  if (context.kind === 'field') {
118
- return function (initialValue) {
115
+ return (initialValue) => {
119
116
  if (initialValue) {
120
117
  throw new Error(`Cannot assign value to injected field "${context.name}"`)
121
118
  }
@@ -172,7 +169,7 @@ export function InjectLazy(clazzOrName, ...params) {
172
169
  // For private fields, we cannot use Object.defineProperty to create a lazy getter.
173
170
  // Instead, we eagerly create the value. For true lazy behavior, use accessor syntax.
174
171
  if (context.private) {
175
- return function (initialValue) {
172
+ return (initialValue) => {
176
173
  if (initialValue) {
177
174
  throw new Error(`Cannot assign value to lazy-injected field "${context.name}"`)
178
175
  }
@@ -208,19 +205,70 @@ export function InjectLazy(clazzOrName, ...params) {
208
205
  }
209
206
 
210
207
  /**
211
- * Mark a class as a mock. This will replace the class with a mock instance when injected.
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().
212
210
  *
213
211
  * @param {string|Function} mockedClazzOrName The singleton or factory class or name to be mocked
214
212
  * @param {boolean} [proxy=false] If true, the mock will proxy to the original class.
215
213
  * Any methods not defined in the mock will be called on the original class.
216
214
  * @returns {(function(Function, {kind: string}): void)}
217
- * @example @Mock(MySingleton) class MyMock {}
218
- * @example @Mock("myCustomName", true) class MyMock {}
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
+ *
219
267
  * @throws {Error} If the injection target is not a class
220
268
  * @throws {Error} If the injection source is not found
221
269
  */
222
270
  export function Mock(mockedClazzOrName, proxy = false) {
223
- return function (clazz, context) {
271
+ return (clazz, context) => {
224
272
  if (context.kind !== 'class') {
225
273
  throw new Error('Invalid injection target')
226
274
  }
@@ -229,27 +277,118 @@ export function Mock(mockedClazzOrName, proxy = false) {
229
277
  }
230
278
 
231
279
  /**
232
- * Reset all mocks to their original classes.
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
+ */
293
+ export function removeAllMocks() {
294
+ defaultContainer.removeAllMocks()
295
+ }
296
+
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
+ */
308
+ export function removeMock(clazzOrName) {
309
+ defaultContainer.removeMock(clazzOrName)
310
+ }
311
+
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.
233
317
  */
234
318
  export function resetMocks() {
235
- defaultContainer.resetAllMocks()
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()
236
324
  }
237
325
 
238
326
  /**
239
- * Reset a specific mock to its original class.
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.
240
331
  *
241
- * @param {string|Function} clazzOrName The singleton or factory class or name to reset
332
+ * @param {string|Function} clazzOrName The singleton or factory class or name to restore
242
333
  */
243
334
  export function resetMock(clazzOrName) {
244
- defaultContainer.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
+ */
364
+ export function resetSingletons(options) {
365
+ defaultContainer.resetSingletons(options)
245
366
  }
246
367
 
247
368
  /**
248
369
  * Clear all registered instances and mocks from the container.
249
- * Useful for complete test isolation between test suites.
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
+ * ```
250
389
  */
251
- export function clearContainer() {
252
- defaultContainer.clear()
390
+ export function clearContainer(options) {
391
+ defaultContainer.clear(options)
253
392
  }
254
393
 
255
394
  /**
@@ -340,6 +479,92 @@ export function resolve(clazzOrName, ...params) {
340
479
  return defaultContainer.resolve(clazzOrName, ...params)
341
480
  }
342
481
 
482
+ /**
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
+ * @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
+ * ```
512
+ */
513
+ export function getMockInstance(clazzOrName, ...params) {
514
+ return defaultContainer.getMockInstance(clazzOrName, ...params)
515
+ }
516
+
517
+ /**
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
+ * ```
530
+ */
531
+ export function isMocked(clazzOrName) {
532
+ return defaultContainer.isMocked(clazzOrName)
533
+ }
534
+
535
+ /**
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
+ * ```
546
+ */
547
+ export function unregister(clazzOrName) {
548
+ return defaultContainer.unregister(clazzOrName)
549
+ }
550
+
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
+ */
564
+ export function listRegistrations() {
565
+ return defaultContainer.list()
566
+ }
567
+
343
568
  // Export Container class for advanced use cases (e.g., isolated containers)
344
569
  export {Container}
345
570
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "decorator-dependency-injection",
3
- "version": "1.0.6",
4
- "description": "A simple library for dependency injection using decorators",
3
+ "version": "1.0.7",
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",
7
7
  "repository": {
@@ -16,12 +16,25 @@
16
16
  "dependency-injection",
17
17
  "di",
18
18
  "decorators",
19
- "mocking",
19
+ "tc39-decorators",
20
+ "stage-3-decorators",
21
+ "inject",
22
+ "injectable",
20
23
  "singleton",
21
- "factory"
24
+ "factory",
25
+ "ioc",
26
+ "inversion-of-control",
27
+ "container",
28
+ "mocking",
29
+ "mock",
30
+ "testing",
31
+ "jest",
32
+ "vitest",
33
+ "typescript",
34
+ "service-locator"
22
35
  ],
23
36
  "engines": {
24
- "node": ">=18.0.0"
37
+ "node": ">=20.0.0"
25
38
  },
26
39
  "main": "index.js",
27
40
  "types": "index.d.ts",