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/README.md +318 -386
- package/index.d.ts +173 -13
- package/index.js +246 -21
- package/package.json +18 -5
- package/src/Container.js +248 -26
- package/src/proxy.js +15 -15
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
|
-
|
|
89
|
-
|
|
112
|
+
const context = this.#instances.get(clazzOrName)
|
|
113
|
+
if (context) {
|
|
114
|
+
return context
|
|
90
115
|
}
|
|
91
|
-
const available =
|
|
116
|
+
const available = [...this.#instances.keys()]
|
|
92
117
|
.map(k => typeof k === 'string' ? k : k.name)
|
|
93
118
|
.join(', ')
|
|
94
|
-
|
|
95
|
-
|
|
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' &&
|
|
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
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
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
|
-
*
|
|
181
|
-
*
|
|
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
|
-
|
|
281
|
+
removeMock(clazzOrName) {
|
|
185
282
|
this.#restoreOriginal(this.#instances.get(clazzOrName), clazzOrName)
|
|
186
283
|
}
|
|
187
284
|
|
|
188
285
|
/**
|
|
189
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
202
|
-
this
|
|
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
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
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
|
-
|
|
13
|
-
|
|
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
|
-
|
|
20
|
-
|
|
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
|
-
|
|
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
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
}
|