decorator-dependency-injection 1.0.5 → 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,68 @@ 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
+
198
+ /**
199
+ * Resolve and return an instance by class or name.
200
+ * This allows non-decorator code to retrieve instances from the container.
201
+ * @template T
202
+ * @param {string|Function} clazzOrName The class or name to resolve
203
+ * @param {...*} params Parameters to pass to the constructor
204
+ * @returns {T} The resolved instance
205
+ * @throws {Error} If the class or name is not registered
206
+ */
207
+ resolve(clazzOrName, ...params) {
208
+ const instanceContext = this.getContext(clazzOrName)
209
+ return this.getInstance(instanceContext, params)
210
+ }
211
+
109
212
  /**
110
213
  * Get or create an instance based on the context.
111
214
  * @param {InstanceContext} instanceContext The instance context
@@ -113,7 +216,7 @@ export class Container {
113
216
  * @returns {Object} The instance
114
217
  */
115
218
  getInstance(instanceContext, params) {
116
- if (instanceContext.type === 'singleton' && !instanceContext.originalClazz && instanceContext.instance) {
219
+ if (instanceContext.type === 'singleton' && instanceContext.instance) {
117
220
  this.#log(`Returning cached singleton: ${instanceContext.clazz.name}`)
118
221
  return instanceContext.instance
119
222
  }
@@ -155,37 +258,168 @@ export class Container {
155
258
  if (instanceContext.originalClazz) {
156
259
  throw new Error('Mock already defined, reset before mocking again')
157
260
  }
158
- instanceContext.originalClazz = instanceContext.clazz
159
- instanceContext.proxy = useProxy
160
- instanceContext.clazz = mockClazz
161
- const targetName = typeof targetClazzOrName === 'string' ? targetClazzOrName : targetClazzOrName.name
162
- 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)' : ''}`)
163
268
  }
164
269
 
165
270
  /**
166
- * Reset a specific mock to its original class.
167
- * @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
168
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)
169
280
  */
170
- resetMock(clazzOrName) {
281
+ removeMock(clazzOrName) {
171
282
  this.#restoreOriginal(this.#instances.get(clazzOrName), clazzOrName)
172
283
  }
173
284
 
174
285
  /**
175
- * 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
176
293
  */
177
- 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() {
178
308
  for (const instanceContext of this.#instances.values()) {
179
309
  this.#restoreOriginal(instanceContext)
180
310
  }
181
311
  }
182
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
+
183
359
  /**
184
360
  * Clear all registered instances and mocks.
185
- * 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' })
186
411
  */
187
- clear() {
188
- 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)
189
423
  }
190
424
 
191
425
  /**
@@ -201,11 +435,13 @@ export class Container {
201
435
  throw new Error(`Cannot reset mock for "${name}": not registered`)
202
436
  }
203
437
  if (instanceContext.originalClazz) {
204
- instanceContext.clazz = instanceContext.originalClazz
205
- delete instanceContext.instance
206
- delete instanceContext.originalClazz
207
- delete instanceContext.originalInstance
208
- delete instanceContext.proxy
438
+ Object.assign(instanceContext, {
439
+ clazz: instanceContext.originalClazz,
440
+ instance: undefined,
441
+ originalClazz: undefined,
442
+ originalInstance: undefined,
443
+ proxy: undefined
444
+ })
209
445
  }
210
446
  }
211
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
  }