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/src/Container.js CHANGED
@@ -4,7 +4,6 @@
4
4
  * @property {Function} clazz - The class constructor for the instance.
5
5
  * @property {Function} [originalClazz] - The original class if this is a mock.
6
6
  * @property {Object} [instance] - The singleton instance, if created.
7
- * @property {Object} [originalInstance] - The original instance if this is a mock.
8
7
  * @property {boolean} [proxy=false] - If true, the mock will proxy to the original class for undefined methods/properties.
9
8
  */
10
9
 
@@ -21,6 +20,31 @@ export class Container {
21
20
  /** @type {boolean} Enable debug logging */
22
21
  #debug = false
23
22
 
23
+ /**
24
+ * Custom string tag for better debugging.
25
+ * Shows as [object Container] in console.
26
+ */
27
+ get [Symbol.toStringTag]() {
28
+ return 'Container'
29
+ }
30
+
31
+ /**
32
+ * Make the container iterable.
33
+ * Yields registration info for each registered class.
34
+ * @yields {RegistrationInfo}
35
+ */
36
+ *[Symbol.iterator]() {
37
+ yield* this.list()
38
+ }
39
+
40
+ /**
41
+ * Get the number of registrations in the container.
42
+ * @returns {number}
43
+ */
44
+ get size() {
45
+ return this.#instances.size
46
+ }
47
+
24
48
  /**
25
49
  * Enable or disable debug logging.
26
50
  * When enabled, logs when instances are created.
@@ -85,16 +109,33 @@ export class Container {
85
109
  * @throws {Error} If the context is not found
86
110
  */
87
111
  getContext(clazzOrName) {
88
- if (this.#instances.has(clazzOrName)) {
89
- return this.#instances.get(clazzOrName)
112
+ const context = this.#instances.get(clazzOrName)
113
+ if (context) {
114
+ return context
90
115
  }
91
- const available = Array.from(this.#instances.keys())
116
+ const available = [...this.#instances.keys()]
92
117
  .map(k => typeof k === 'string' ? k : k.name)
93
118
  .join(', ')
94
- throw new Error(
95
- `Cannot find injection source for "${clazzOrName?.name || clazzOrName}". ` +
119
+
120
+ const name = clazzOrName?.name || clazzOrName
121
+ const nameStr = String(name)
122
+
123
+ // Detect if this looks like a mock class from a module mocking system
124
+ const looksLikeMock = /^Mock[A-Z]|mock/i.test(nameStr) ||
125
+ nameStr.includes('Mock') ||
126
+ nameStr.startsWith('vi_') ||
127
+ nameStr.startsWith('jest_')
128
+
129
+ let errorMessage = `Cannot find injection source for "${name}". ` +
96
130
  `Available: [${available}]`
97
- )
131
+
132
+ if (looksLikeMock) {
133
+ errorMessage += `\n\nHint: The class name "${name}" suggests this may be a mock created by a module mocking system. ` +
134
+ `If you're using module mocking (e.g., vi.mock() or jest.mock()), consider using @Mock(OriginalService) instead, ` +
135
+ `which properly registers with the DI container.`
136
+ }
137
+
138
+ throw new Error(errorMessage)
98
139
  }
99
140
 
100
141
  /**
@@ -106,6 +147,54 @@ export class Container {
106
147
  return this.#instances.has(clazzOrName)
107
148
  }
108
149
 
150
+ /**
151
+ * Check if a class or name has a mock registered.
152
+ * @param {string|Function} clazzOrName The class or name to check
153
+ * @returns {boolean} true if a mock is registered, false otherwise
154
+ */
155
+ isMocked(clazzOrName) {
156
+ return !!this.#instances.get(clazzOrName)?.originalClazz
157
+ }
158
+
159
+ /**
160
+ * Unregister a class or name from the container.
161
+ * This removes the registration entirely, including any mock.
162
+ *
163
+ * @param {string|Function} clazzOrName The class or name to unregister
164
+ * @returns {boolean} true if the registration was removed, false if it wasn't registered
165
+ *
166
+ * @example
167
+ * container.unregister(MyService) // Returns true if was registered
168
+ */
169
+ unregister(clazzOrName) {
170
+ const removed = this.#instances.delete(clazzOrName)
171
+ if (removed) {
172
+ this.#log(`Unregistered: ${clazzOrName?.name ?? clazzOrName}`)
173
+ }
174
+ return removed
175
+ }
176
+
177
+ /**
178
+ * List all registrations in the container.
179
+ * Useful for debugging and introspection.
180
+ *
181
+ * @returns {Array<{key: string|Function, type: 'singleton'|'factory', isMocked: boolean, hasInstance: boolean}>}
182
+ *
183
+ * @example
184
+ * container.list().forEach(reg => {
185
+ * console.log(`${reg.key}: ${reg.type}, mocked: ${reg.isMocked}`)
186
+ * })
187
+ */
188
+ list() {
189
+ return [...this.#instances.entries()].map(([key, context]) => ({
190
+ key,
191
+ name: typeof key === 'string' ? key : key.name,
192
+ type: context.type,
193
+ isMocked: !!context.originalClazz,
194
+ hasInstance: !!context.instance
195
+ }))
196
+ }
197
+
109
198
  /**
110
199
  * Resolve and return an instance by class or name.
111
200
  * This allows non-decorator code to retrieve instances from the container.
@@ -127,7 +216,7 @@ export class Container {
127
216
  * @returns {Object} The instance
128
217
  */
129
218
  getInstance(instanceContext, params) {
130
- if (instanceContext.type === 'singleton' && !instanceContext.originalClazz && instanceContext.instance) {
219
+ if (instanceContext.type === 'singleton' && instanceContext.instance) {
131
220
  this.#log(`Returning cached singleton: ${instanceContext.clazz.name}`)
132
221
  return instanceContext.instance
133
222
  }
@@ -169,37 +258,168 @@ export class Container {
169
258
  if (instanceContext.originalClazz) {
170
259
  throw new Error('Mock already defined, reset before mocking again')
171
260
  }
172
- instanceContext.originalClazz = instanceContext.clazz
173
- instanceContext.proxy = useProxy
174
- instanceContext.clazz = mockClazz
175
- const targetName = typeof targetClazzOrName === 'string' ? targetClazzOrName : targetClazzOrName.name
176
- this.#log(`Mocked ${targetName} with ${mockClazz.name}${useProxy ? ' (proxy)' : ''}`)
261
+ Object.assign(instanceContext, {
262
+ originalClazz: instanceContext.clazz,
263
+ clazz: mockClazz,
264
+ proxy: useProxy,
265
+ instance: undefined // Clear cached instance so the mock takes effect on next resolve
266
+ })
267
+ this.#log(`Mocked ${targetClazzOrName?.name ?? targetClazzOrName} with ${mockClazz.name}${useProxy ? ' (proxy)' : ''}`)
177
268
  }
178
269
 
179
270
  /**
180
- * Reset a specific mock to its original class.
181
- * @param {string|Function} clazzOrName The class or name to reset
271
+ * Remove a specific mock and restore the original class.
272
+ * This completely removes the mock - it does NOT just clear mock call history.
273
+ *
274
+ * @param {string|Function} clazzOrName The class or name to restore
182
275
  * @throws {Error} If the class or name is not registered
276
+ *
277
+ * @example
278
+ * // After this call, resolve(MyService) returns the original class, not the mock
279
+ * container.removeMock(MyService)
183
280
  */
184
- resetMock(clazzOrName) {
281
+ removeMock(clazzOrName) {
185
282
  this.#restoreOriginal(this.#instances.get(clazzOrName), clazzOrName)
186
283
  }
187
284
 
188
285
  /**
189
- * Reset all mocks to their original classes.
286
+ * @deprecated Use removeMock() instead. This method will be removed in a future version.
287
+ *
288
+ * Note: This does NOT reset mock call history like vi.fn().mockClear().
289
+ * It completely removes the mock and restores the original class.
290
+ *
291
+ * @param {string|Function} clazzOrName The class or name to restore
292
+ * @throws {Error} If the class or name is not registered
190
293
  */
191
- resetAllMocks() {
294
+ resetMock(clazzOrName) {
295
+ console.warn(
296
+ '[DI] resetMock() is deprecated and will be removed in a future version. ' +
297
+ 'Use removeMock() instead. Note: This removes the mock entirely, ' +
298
+ 'it does NOT clear mock call history.'
299
+ )
300
+ this.removeMock(clazzOrName)
301
+ }
302
+
303
+ /**
304
+ * Remove all mocks and restore original classes.
305
+ * This completely removes all mocks - it does NOT just clear mock call history.
306
+ */
307
+ removeAllMocks() {
192
308
  for (const instanceContext of this.#instances.values()) {
193
309
  this.#restoreOriginal(instanceContext)
194
310
  }
195
311
  }
196
312
 
313
+ /**
314
+ * @deprecated Use removeAllMocks() instead. This method will be removed in a future version.
315
+ *
316
+ * Note: This does NOT reset mock call history.
317
+ * It completely removes all mocks and restores original classes.
318
+ */
319
+ resetAllMocks() {
320
+ console.warn(
321
+ '[DI] resetAllMocks() is deprecated and will be removed in a future version. ' +
322
+ 'Use removeAllMocks() instead. Note: This removes all mocks entirely, ' +
323
+ 'it does NOT clear mock call history.'
324
+ )
325
+ this.removeAllMocks()
326
+ }
327
+
328
+ /**
329
+ * Reset singleton instances without removing registrations.
330
+ * This clears cached singleton instances so they will be recreated on next resolve.
331
+ * Mock registrations are preserved by default.
332
+ *
333
+ * @param {Object} [options] Options for resetting
334
+ * @param {boolean} [options.preserveMocks=true] If true, keeps mock registrations intact.
335
+ * If false, also removes mocks (same as clear()).
336
+ *
337
+ * @example
338
+ * // Clear singleton instances but keep mocks registered
339
+ * container.resetSingletons()
340
+ *
341
+ * // Clear singleton instances AND remove mocks
342
+ * container.resetSingletons({ preserveMocks: false })
343
+ */
344
+ resetSingletons(options = {}) {
345
+ const { preserveMocks = true } = options
346
+
347
+ for (const instanceContext of this.#instances.values()) {
348
+ // Clear the cached singleton instance
349
+ delete instanceContext.instance
350
+
351
+ // Optionally remove mock registrations
352
+ if (!preserveMocks && instanceContext.originalClazz) {
353
+ this.#restoreOriginal(instanceContext)
354
+ }
355
+ }
356
+ this.#log(`Reset singletons (preserveMocks: ${preserveMocks})`)
357
+ }
358
+
197
359
  /**
198
360
  * Clear all registered instances and mocks.
199
- * Useful for test isolation.
361
+ * Useful for complete test isolation between test suites.
362
+ *
363
+ * Note: By default this removes ALL registrations including @Singleton and @Factory classes.
364
+ * For clearing just singleton instances while keeping registrations, use resetSingletons().
365
+ *
366
+ * @param {Object} [options] Options for clearing
367
+ * @param {boolean} [options.preserveRegistrations=false] If true, keeps all registrations but clears cached instances.
368
+ *
369
+ * @example
370
+ * // Remove everything (full reset)
371
+ * container.clear()
372
+ *
373
+ * // Clear cached instances but keep all registrations (including mocks)
374
+ * container.clear({ preserveRegistrations: true })
375
+ */
376
+ clear(options = {}) {
377
+ const { preserveRegistrations = false } = options
378
+
379
+ if (preserveRegistrations) {
380
+ // Just clear cached instances, keep all registrations
381
+ for (const instanceContext of this.#instances.values()) {
382
+ delete instanceContext.instance
383
+ }
384
+ this.#log('Cleared instances (preserved registrations)')
385
+ } else {
386
+ this.#instances.clear()
387
+ this.#log('Cleared all registrations')
388
+ }
389
+ }
390
+
391
+ /**
392
+ * Get the mock instance for a mocked class.
393
+ * This is useful when you need to configure mock behavior dynamically in tests.
394
+ *
395
+ * @template T
396
+ * @param {string|Function} clazzOrName The original class or name that was mocked
397
+ * @param {...*} params Parameters to pass to the constructor
398
+ * @returns {T} The mock instance
399
+ * @throws {Error} If the class is not registered
400
+ * @throws {Error} If the class is not mocked
401
+ *
402
+ * @example
403
+ * @Mock(UserService)
404
+ * class MockUserService {
405
+ * getUser = vi.fn()
406
+ * }
407
+ *
408
+ * // In test:
409
+ * const mockInstance = container.getMockInstance(UserService)
410
+ * mockInstance.getUser.mockReturnValue({ id: 1, name: 'Test' })
200
411
  */
201
- clear() {
202
- this.#instances.clear()
412
+ getMockInstance(clazzOrName, ...params) {
413
+ const instanceContext = this.getContext(clazzOrName)
414
+
415
+ if (!instanceContext.originalClazz) {
416
+ const name = clazzOrName?.name ?? clazzOrName
417
+ throw new Error(
418
+ `"${name}" is not mocked. Use @Mock(${name}) to register a mock first.`
419
+ )
420
+ }
421
+
422
+ return this.getInstance(instanceContext, params)
203
423
  }
204
424
 
205
425
  /**
@@ -215,11 +435,13 @@ export class Container {
215
435
  throw new Error(`Cannot reset mock for "${name}": not registered`)
216
436
  }
217
437
  if (instanceContext.originalClazz) {
218
- instanceContext.clazz = instanceContext.originalClazz
219
- delete instanceContext.instance
220
- delete instanceContext.originalClazz
221
- delete instanceContext.originalInstance
222
- delete instanceContext.proxy
438
+ Object.assign(instanceContext, {
439
+ clazz: instanceContext.originalClazz,
440
+ instance: undefined,
441
+ originalClazz: undefined,
442
+ originalInstance: undefined,
443
+ proxy: undefined
444
+ })
223
445
  }
224
446
  }
225
447
  }
package/src/proxy.js CHANGED
@@ -9,17 +9,15 @@
9
9
  export function createProxy(mock, original) {
10
10
  return new Proxy(mock, {
11
11
  get(target, prop, receiver) {
12
- if (prop in target) {
13
- return Reflect.get(target, prop, receiver)
14
- }
15
- return Reflect.get(original, prop, original)
12
+ return prop in target
13
+ ? Reflect.get(target, prop, receiver)
14
+ : Reflect.get(original, prop, original)
16
15
  },
17
16
 
18
17
  set(target, prop, value, receiver) {
19
- if (prop in target) {
20
- return Reflect.set(target, prop, value, receiver)
21
- }
22
- return Reflect.set(original, prop, value, original)
18
+ return prop in target
19
+ ? Reflect.set(target, prop, value, receiver)
20
+ : Reflect.set(original, prop, value, original)
23
21
  },
24
22
 
25
23
  has(target, prop) {
@@ -27,16 +25,18 @@ export function createProxy(mock, original) {
27
25
  },
28
26
 
29
27
  ownKeys(target) {
30
- const mockKeys = Reflect.ownKeys(target)
31
- const originalKeys = Reflect.ownKeys(original)
32
- return [...new Set([...mockKeys, ...originalKeys])]
28
+ return [...new Set([...Reflect.ownKeys(target), ...Reflect.ownKeys(original)])]
33
29
  },
34
30
 
35
31
  getOwnPropertyDescriptor(target, prop) {
36
- if (prop in target) {
37
- return Reflect.getOwnPropertyDescriptor(target, prop)
38
- }
39
- return Reflect.getOwnPropertyDescriptor(original, prop)
32
+ return prop in target
33
+ ? Reflect.getOwnPropertyDescriptor(target, prop)
34
+ : Reflect.getOwnPropertyDescriptor(original, prop)
35
+ },
36
+
37
+ getPrototypeOf() {
38
+ // Return original's prototype so instanceof checks work
39
+ return Object.getPrototypeOf(original)
40
40
  }
41
41
  })
42
42
  }