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/README.md +323 -345
- package/index.d.ts +202 -13
- package/index.js +273 -21
- package/package.json +18 -5
- package/src/Container.js +262 -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,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' &&
|
|
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
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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
|
-
*
|
|
167
|
-
*
|
|
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
|
-
|
|
281
|
+
removeMock(clazzOrName) {
|
|
171
282
|
this.#restoreOriginal(this.#instances.get(clazzOrName), clazzOrName)
|
|
172
283
|
}
|
|
173
284
|
|
|
174
285
|
/**
|
|
175
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
188
|
-
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)
|
|
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
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
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
|
-
|
|
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
|
}
|