decorator-dependency-injection 1.0.6 → 1.1.0
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 +369 -369
- package/docs/FRAMEWORK_INTEGRATION.md +808 -0
- package/eslint.config.js +4 -1
- package/index.d.ts +35 -224
- package/index.js +81 -188
- package/package.json +23 -5
- package/src/Container.js +150 -81
- package/src/integrations/middleware.d.ts +40 -0
- package/src/integrations/middleware.js +171 -0
- package/src/proxy.js +15 -15
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "decorator-dependency-injection",
|
|
3
|
-
"version": "1.0
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "1.1.0",
|
|
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,20 +16,38 @@
|
|
|
16
16
|
"dependency-injection",
|
|
17
17
|
"di",
|
|
18
18
|
"decorators",
|
|
19
|
-
"
|
|
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": ">=
|
|
37
|
+
"node": ">=20.0.0"
|
|
25
38
|
},
|
|
26
39
|
"main": "index.js",
|
|
27
40
|
"types": "index.d.ts",
|
|
28
41
|
"type": "module",
|
|
42
|
+
"sideEffects": false,
|
|
29
43
|
"exports": {
|
|
30
44
|
".": {
|
|
31
45
|
"types": "./index.d.ts",
|
|
32
46
|
"import": "./index.js"
|
|
47
|
+
},
|
|
48
|
+
"./middleware": {
|
|
49
|
+
"types": "./src/integrations/middleware.d.ts",
|
|
50
|
+
"import": "./src/integrations/middleware.js"
|
|
33
51
|
}
|
|
34
52
|
},
|
|
35
53
|
"scripts": {
|
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
|
|
|
@@ -18,23 +17,27 @@ export class Container {
|
|
|
18
17
|
/** @type {Map<string|Function, InstanceContext>} */
|
|
19
18
|
#instances = new Map()
|
|
20
19
|
|
|
21
|
-
/** @type {boolean} Enable debug logging */
|
|
22
20
|
#debug = false
|
|
23
21
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
22
|
+
get [Symbol.toStringTag]() {
|
|
23
|
+
return 'Container'
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** @yields {RegistrationInfo} */
|
|
27
|
+
*[Symbol.iterator]() {
|
|
28
|
+
yield* this.list()
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** @returns {number} */
|
|
32
|
+
get size() {
|
|
33
|
+
return this.#instances.size
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** @param {boolean} enabled */
|
|
29
37
|
setDebug(enabled) {
|
|
30
38
|
this.#debug = enabled
|
|
31
39
|
}
|
|
32
40
|
|
|
33
|
-
/**
|
|
34
|
-
* Log a debug message if debug mode is enabled.
|
|
35
|
-
* @param {string} message The message to log
|
|
36
|
-
* @private
|
|
37
|
-
*/
|
|
38
41
|
#log(message) {
|
|
39
42
|
if (this.#debug) {
|
|
40
43
|
console.log(`[DI] ${message}`)
|
|
@@ -42,30 +45,21 @@ export class Container {
|
|
|
42
45
|
}
|
|
43
46
|
|
|
44
47
|
/**
|
|
45
|
-
*
|
|
46
|
-
* @param {
|
|
47
|
-
* @param {string} [name] Optional name key
|
|
48
|
+
* @param {Function} clazz
|
|
49
|
+
* @param {string} [name]
|
|
48
50
|
*/
|
|
49
51
|
registerSingleton(clazz, name) {
|
|
50
52
|
this.#register(clazz, 'singleton', name)
|
|
51
53
|
}
|
|
52
54
|
|
|
53
55
|
/**
|
|
54
|
-
*
|
|
55
|
-
* @param {
|
|
56
|
-
* @param {string} [name] Optional name key
|
|
56
|
+
* @param {Function} clazz
|
|
57
|
+
* @param {string} [name]
|
|
57
58
|
*/
|
|
58
59
|
registerFactory(clazz, name) {
|
|
59
60
|
this.#register(clazz, 'factory', name)
|
|
60
61
|
}
|
|
61
62
|
|
|
62
|
-
/**
|
|
63
|
-
* Internal registration logic.
|
|
64
|
-
* @param {Function} clazz The class constructor
|
|
65
|
-
* @param {'singleton'|'factory'} type The registration type
|
|
66
|
-
* @param {string} [name] Optional name key
|
|
67
|
-
* @private
|
|
68
|
-
*/
|
|
69
63
|
#register(clazz, type, name) {
|
|
70
64
|
const key = name ?? clazz
|
|
71
65
|
if (this.#instances.has(key)) {
|
|
@@ -79,41 +73,77 @@ export class Container {
|
|
|
79
73
|
}
|
|
80
74
|
|
|
81
75
|
/**
|
|
82
|
-
*
|
|
83
|
-
* @param {string|Function} clazzOrName The class or name to look up
|
|
76
|
+
* @param {string|Function} clazzOrName
|
|
84
77
|
* @returns {InstanceContext}
|
|
85
|
-
* @throws {Error} If
|
|
78
|
+
* @throws {Error} If not found
|
|
86
79
|
*/
|
|
87
80
|
getContext(clazzOrName) {
|
|
88
|
-
|
|
89
|
-
|
|
81
|
+
const context = this.#instances.get(clazzOrName)
|
|
82
|
+
if (context) {
|
|
83
|
+
return context
|
|
90
84
|
}
|
|
91
|
-
|
|
85
|
+
|
|
86
|
+
const name = clazzOrName?.name ?? clazzOrName
|
|
87
|
+
const nameStr = String(name)
|
|
88
|
+
const available = [...this.#instances.keys()]
|
|
92
89
|
.map(k => typeof k === 'string' ? k : k.name)
|
|
93
90
|
.join(', ')
|
|
91
|
+
|
|
92
|
+
// Detect if this looks like a mock class from a module mocking system
|
|
93
|
+
const looksLikeMock = /^Mock[A-Z]|mock/i.test(nameStr) ||
|
|
94
|
+
nameStr.includes('Mock') ||
|
|
95
|
+
nameStr.startsWith('vi_') ||
|
|
96
|
+
nameStr.startsWith('jest_')
|
|
97
|
+
|
|
98
|
+
const hint = looksLikeMock
|
|
99
|
+
? `\n\nHint: The class name "${name}" suggests this may be a mock created by a module mocking system. ` +
|
|
100
|
+
`If you're using module mocking (e.g., vi.mock() or jest.mock()), consider using @Mock(OriginalService) instead, ` +
|
|
101
|
+
`which properly registers with the DI container.`
|
|
102
|
+
: ''
|
|
103
|
+
|
|
94
104
|
throw new Error(
|
|
95
|
-
`Cannot find injection source for "${
|
|
96
|
-
`Available: [${available}]`
|
|
105
|
+
`Cannot find injection source for "${name}". Available: [${available}]${hint}`
|
|
97
106
|
)
|
|
98
107
|
}
|
|
99
108
|
|
|
100
|
-
/**
|
|
101
|
-
* Check if a class or name is registered.
|
|
102
|
-
* @param {string|Function} clazzOrName The class or name to check
|
|
103
|
-
* @returns {boolean}
|
|
104
|
-
*/
|
|
109
|
+
/** @param {string|Function} clazzOrName */
|
|
105
110
|
has(clazzOrName) {
|
|
106
111
|
return this.#instances.has(clazzOrName)
|
|
107
112
|
}
|
|
108
113
|
|
|
114
|
+
/** @param {string|Function} clazzOrName */
|
|
115
|
+
isMocked(clazzOrName) {
|
|
116
|
+
return !!this.#instances.get(clazzOrName)?.originalClazz
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* @param {string|Function} clazzOrName
|
|
121
|
+
* @returns {boolean} true if removed
|
|
122
|
+
*/
|
|
123
|
+
unregister(clazzOrName) {
|
|
124
|
+
const removed = this.#instances.delete(clazzOrName)
|
|
125
|
+
if (removed) {
|
|
126
|
+
this.#log(`Unregistered: ${clazzOrName?.name ?? clazzOrName}`)
|
|
127
|
+
}
|
|
128
|
+
return removed
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** @returns {Array<{key: string|Function, name: string, type: 'singleton'|'factory', isMocked: boolean, hasInstance: boolean}>} */
|
|
132
|
+
list() {
|
|
133
|
+
return [...this.#instances.entries()].map(([key, context]) => ({
|
|
134
|
+
key,
|
|
135
|
+
name: typeof key === 'string' ? key : key.name,
|
|
136
|
+
type: context.type,
|
|
137
|
+
isMocked: !!context.originalClazz,
|
|
138
|
+
hasInstance: !!context.instance
|
|
139
|
+
}))
|
|
140
|
+
}
|
|
141
|
+
|
|
109
142
|
/**
|
|
110
|
-
* Resolve and return an instance by class or name.
|
|
111
|
-
* This allows non-decorator code to retrieve instances from the container.
|
|
112
143
|
* @template T
|
|
113
|
-
* @param {string|Function} clazzOrName
|
|
114
|
-
* @param {...*} params
|
|
115
|
-
* @returns {T}
|
|
116
|
-
* @throws {Error} If the class or name is not registered
|
|
144
|
+
* @param {string|Function} clazzOrName
|
|
145
|
+
* @param {...*} params
|
|
146
|
+
* @returns {T}
|
|
117
147
|
*/
|
|
118
148
|
resolve(clazzOrName, ...params) {
|
|
119
149
|
const instanceContext = this.getContext(clazzOrName)
|
|
@@ -121,25 +151,24 @@ export class Container {
|
|
|
121
151
|
}
|
|
122
152
|
|
|
123
153
|
/**
|
|
124
|
-
*
|
|
125
|
-
* @param {
|
|
126
|
-
* @
|
|
127
|
-
* @returns {Object} The instance
|
|
154
|
+
* @param {InstanceContext} instanceContext
|
|
155
|
+
* @param {Array} params
|
|
156
|
+
* @returns {Object}
|
|
128
157
|
*/
|
|
129
158
|
getInstance(instanceContext, params) {
|
|
130
|
-
if (instanceContext.type === 'singleton' &&
|
|
159
|
+
if (instanceContext.type === 'singleton' && instanceContext.instance) {
|
|
131
160
|
this.#log(`Returning cached singleton: ${instanceContext.clazz.name}`)
|
|
132
161
|
return instanceContext.instance
|
|
133
162
|
}
|
|
134
163
|
|
|
164
|
+
this.#log(`Creating ${instanceContext.type}: ${instanceContext.clazz.name}`)
|
|
135
165
|
let instance
|
|
136
166
|
try {
|
|
137
|
-
this.#log(`Creating ${instanceContext.type}: ${instanceContext.clazz.name}`)
|
|
138
167
|
instance = new instanceContext.clazz(...params)
|
|
139
168
|
} catch (err) {
|
|
140
169
|
if (err instanceof RangeError) {
|
|
141
170
|
throw new Error(
|
|
142
|
-
`Circular dependency detected for ${instanceContext.clazz.name
|
|
171
|
+
`Circular dependency detected for ${instanceContext.clazz.name ?? instanceContext.clazz}. ` +
|
|
143
172
|
`Use @InjectLazy to break the cycle.`
|
|
144
173
|
)
|
|
145
174
|
}
|
|
@@ -159,67 +188,107 @@ export class Container {
|
|
|
159
188
|
}
|
|
160
189
|
|
|
161
190
|
/**
|
|
162
|
-
*
|
|
163
|
-
* @param {
|
|
164
|
-
* @param {
|
|
165
|
-
* @param {boolean} [useProxy=false] Whether to proxy unmocked methods to original
|
|
191
|
+
* @param {string|Function} targetClazzOrName
|
|
192
|
+
* @param {Function} mockClazz
|
|
193
|
+
* @param {boolean} [useProxy=false]
|
|
166
194
|
*/
|
|
167
195
|
registerMock(targetClazzOrName, mockClazz, useProxy = false) {
|
|
168
196
|
const instanceContext = this.getContext(targetClazzOrName)
|
|
169
197
|
if (instanceContext.originalClazz) {
|
|
170
198
|
throw new Error('Mock already defined, reset before mocking again')
|
|
171
199
|
}
|
|
172
|
-
instanceContext
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
200
|
+
Object.assign(instanceContext, {
|
|
201
|
+
originalClazz: instanceContext.clazz,
|
|
202
|
+
clazz: mockClazz,
|
|
203
|
+
proxy: useProxy,
|
|
204
|
+
instance: undefined
|
|
205
|
+
})
|
|
206
|
+
this.#log(`Mocked ${targetClazzOrName?.name ?? targetClazzOrName} with ${mockClazz.name}${useProxy ? ' (proxy)' : ''}`)
|
|
177
207
|
}
|
|
178
208
|
|
|
179
209
|
/**
|
|
180
|
-
*
|
|
181
|
-
* @param {string|Function} clazzOrName
|
|
182
|
-
* @throws {Error} If the class or name is not registered
|
|
210
|
+
* Remove mock and restore original. Does NOT clear mock call history.
|
|
211
|
+
* @param {string|Function} clazzOrName
|
|
183
212
|
*/
|
|
184
|
-
|
|
213
|
+
removeMock(clazzOrName) {
|
|
185
214
|
this.#restoreOriginal(this.#instances.get(clazzOrName), clazzOrName)
|
|
186
215
|
}
|
|
187
216
|
|
|
217
|
+
/** Remove all mocks. Does NOT clear mock call history. */
|
|
218
|
+
removeAllMocks() {
|
|
219
|
+
for (const instanceContext of this.#instances.values()) {
|
|
220
|
+
this.#restoreOriginal(instanceContext)
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
188
224
|
/**
|
|
189
|
-
*
|
|
225
|
+
* Clear cached singleton instances. They'll be recreated on next resolve.
|
|
226
|
+
* @param {Object} [options]
|
|
227
|
+
* @param {boolean} [options.preserveMocks=true]
|
|
190
228
|
*/
|
|
191
|
-
|
|
229
|
+
resetSingletons(options = {}) {
|
|
230
|
+
const { preserveMocks = true } = options
|
|
231
|
+
|
|
192
232
|
for (const instanceContext of this.#instances.values()) {
|
|
193
|
-
|
|
233
|
+
delete instanceContext.instance
|
|
234
|
+
if (!preserveMocks && instanceContext.originalClazz) {
|
|
235
|
+
this.#restoreOriginal(instanceContext)
|
|
236
|
+
}
|
|
194
237
|
}
|
|
238
|
+
this.#log(`Reset singletons (preserveMocks: ${preserveMocks})`)
|
|
195
239
|
}
|
|
196
240
|
|
|
197
241
|
/**
|
|
198
|
-
* Clear all
|
|
199
|
-
*
|
|
242
|
+
* Clear all registrations. Use resetSingletons() to keep registrations.
|
|
243
|
+
* @param {Object} [options]
|
|
244
|
+
* @param {boolean} [options.preserveRegistrations=false]
|
|
200
245
|
*/
|
|
201
|
-
clear() {
|
|
202
|
-
|
|
246
|
+
clear(options = {}) {
|
|
247
|
+
const { preserveRegistrations = false } = options
|
|
248
|
+
|
|
249
|
+
if (preserveRegistrations) {
|
|
250
|
+
for (const instanceContext of this.#instances.values()) {
|
|
251
|
+
delete instanceContext.instance
|
|
252
|
+
}
|
|
253
|
+
this.#log('Cleared instances (preserved registrations)')
|
|
254
|
+
} else {
|
|
255
|
+
this.#instances.clear()
|
|
256
|
+
this.#log('Cleared all registrations')
|
|
257
|
+
}
|
|
203
258
|
}
|
|
204
259
|
|
|
205
260
|
/**
|
|
206
|
-
*
|
|
207
|
-
* @param {
|
|
208
|
-
* @param {
|
|
209
|
-
* @
|
|
210
|
-
* @
|
|
261
|
+
* @template T
|
|
262
|
+
* @param {string|Function} clazzOrName
|
|
263
|
+
* @param {...*} params
|
|
264
|
+
* @returns {T}
|
|
265
|
+
* @throws {Error} If not mocked
|
|
211
266
|
*/
|
|
267
|
+
getMockInstance(clazzOrName, ...params) {
|
|
268
|
+
const instanceContext = this.getContext(clazzOrName)
|
|
269
|
+
|
|
270
|
+
if (!instanceContext.originalClazz) {
|
|
271
|
+
const name = clazzOrName?.name ?? clazzOrName
|
|
272
|
+
throw new Error(
|
|
273
|
+
`"${name}" is not mocked. Use @Mock(${name}) to register a mock first.`
|
|
274
|
+
)
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
return this.getInstance(instanceContext, params)
|
|
278
|
+
}
|
|
279
|
+
|
|
212
280
|
#restoreOriginal(instanceContext, clazzOrName) {
|
|
213
281
|
if (!instanceContext) {
|
|
214
282
|
const name = clazzOrName?.name || clazzOrName || 'unknown'
|
|
215
283
|
throw new Error(`Cannot reset mock for "${name}": not registered`)
|
|
216
284
|
}
|
|
217
285
|
if (instanceContext.originalClazz) {
|
|
218
|
-
instanceContext
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
286
|
+
Object.assign(instanceContext, {
|
|
287
|
+
clazz: instanceContext.originalClazz,
|
|
288
|
+
instance: undefined,
|
|
289
|
+
originalClazz: undefined,
|
|
290
|
+
proxy: undefined
|
|
291
|
+
})
|
|
223
292
|
}
|
|
224
293
|
}
|
|
225
294
|
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { Container, InjectionToken } from '../../index'
|
|
2
|
+
|
|
3
|
+
export type ContainerScope = 'request' | 'global'
|
|
4
|
+
|
|
5
|
+
export function getGlobalContainer(): Container
|
|
6
|
+
export function getContainer(): Container
|
|
7
|
+
|
|
8
|
+
export interface ResolveOptions {
|
|
9
|
+
scope?: ContainerScope
|
|
10
|
+
params?: any[]
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function resolve<T>(clazzOrName: InjectionToken<T>, options?: ResolveOptions): T
|
|
14
|
+
|
|
15
|
+
export function runWithContainer<T>(
|
|
16
|
+
container: Container,
|
|
17
|
+
fn: () => T,
|
|
18
|
+
options?: { scope?: ContainerScope }
|
|
19
|
+
): T
|
|
20
|
+
|
|
21
|
+
export interface MiddlewareOptions {
|
|
22
|
+
scope?: ContainerScope
|
|
23
|
+
debug?: boolean
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface ContainerRequest {
|
|
27
|
+
di: Container
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function containerMiddleware(
|
|
31
|
+
options?: MiddlewareOptions
|
|
32
|
+
): (req: any, res: any, next: () => void) => void
|
|
33
|
+
|
|
34
|
+
export function koaContainerMiddleware(
|
|
35
|
+
options?: MiddlewareOptions
|
|
36
|
+
): (ctx: any, next: () => Promise<void>) => Promise<void>
|
|
37
|
+
|
|
38
|
+
export function withContainer<T extends (...args: any[]) => any>(
|
|
39
|
+
options?: MiddlewareOptions
|
|
40
|
+
): (handler: T) => T
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server middleware integration for decorator-dependency-injection.
|
|
3
|
+
*
|
|
4
|
+
* Provides request-scoped containers using AsyncLocalStorage, enabling
|
|
5
|
+
* automatic per-request isolation without manual container management.
|
|
6
|
+
*
|
|
7
|
+
* IMPORTANT: When using this module, `resolve()` behaves differently than
|
|
8
|
+
* the main module's resolve:
|
|
9
|
+
* - Inside a request: Returns instances from the request-scoped container
|
|
10
|
+
* (singletons are isolated per-request, preventing data leaks between users)
|
|
11
|
+
* - Outside a request: Falls back to the global container
|
|
12
|
+
*
|
|
13
|
+
* @module decorator-dependency-injection/middleware
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { AsyncLocalStorage } from 'node:async_hooks'
|
|
17
|
+
import { Container } from '../Container.js'
|
|
18
|
+
import { defaultContainer as mainDefaultContainer } from '../../index.js'
|
|
19
|
+
|
|
20
|
+
/** @type {AsyncLocalStorage<{container: Container, scope: string}>} */
|
|
21
|
+
const requestContext = new AsyncLocalStorage()
|
|
22
|
+
|
|
23
|
+
/** @type {Container} */
|
|
24
|
+
const globalContainer = mainDefaultContainer
|
|
25
|
+
|
|
26
|
+
/** @returns {Container|null} */
|
|
27
|
+
export function getGlobalContainer() {
|
|
28
|
+
return globalContainer
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function getRequestContext() {
|
|
32
|
+
return requestContext.getStore()
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** @returns {Container} */
|
|
36
|
+
export function getContainer() {
|
|
37
|
+
return getRequestContext()?.container ?? globalContainer
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Request-aware resolve. Uses request-scoped container inside requests,
|
|
42
|
+
* falls back to global container outside. Auto-registers from global container.
|
|
43
|
+
*
|
|
44
|
+
* @template T
|
|
45
|
+
* @param {string|Function} clazzOrName
|
|
46
|
+
* @param {Object} [options]
|
|
47
|
+
* @param {'request'|'global'} [options.scope]
|
|
48
|
+
* @param {Array} [options.params]
|
|
49
|
+
* @returns {T}
|
|
50
|
+
*/
|
|
51
|
+
export function resolve(clazzOrName, options = {}) {
|
|
52
|
+
const { scope, params = [] } = options
|
|
53
|
+
const ctx = getRequestContext()
|
|
54
|
+
|
|
55
|
+
if (scope === 'global' || ctx?.scope === 'global') {
|
|
56
|
+
return globalContainer.resolve(clazzOrName, ...params)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (!ctx) {
|
|
60
|
+
if (scope === 'request') {
|
|
61
|
+
console.warn(
|
|
62
|
+
`[DI] resolve() called with scope='request' but no request context exists. ` +
|
|
63
|
+
`Did you forget to use containerMiddleware()? Falling back to global container.`
|
|
64
|
+
)
|
|
65
|
+
}
|
|
66
|
+
return globalContainer.resolve(clazzOrName, ...params)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const requestContainer = ctx.container
|
|
70
|
+
|
|
71
|
+
if (!requestContainer.has(clazzOrName) && globalContainer?.has(clazzOrName)) {
|
|
72
|
+
const globalContext = globalContainer.getContext(clazzOrName)
|
|
73
|
+
if (globalContext.type === 'singleton') {
|
|
74
|
+
requestContainer.registerSingleton(globalContext.clazz,
|
|
75
|
+
typeof clazzOrName === 'string' ? clazzOrName : undefined)
|
|
76
|
+
} else {
|
|
77
|
+
requestContainer.registerFactory(globalContext.clazz,
|
|
78
|
+
typeof clazzOrName === 'string' ? clazzOrName : undefined)
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return requestContainer.resolve(clazzOrName, ...params)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* @template T
|
|
87
|
+
* @param {Container} container
|
|
88
|
+
* @param {function(): T} fn
|
|
89
|
+
* @param {Object} [options]
|
|
90
|
+
* @param {'request'|'global'} [options.scope='request']
|
|
91
|
+
* @returns {T}
|
|
92
|
+
*/
|
|
93
|
+
export function runWithContainer(container, fn, options = {}) {
|
|
94
|
+
const { scope = 'request' } = options
|
|
95
|
+
return requestContext.run({ container, scope }, fn)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* @typedef {Object} MiddlewareOptions
|
|
100
|
+
* @property {'request'|'global'} [scope='request'] - Container scope mode:
|
|
101
|
+
* - 'request': Each request gets its own container with isolated singletons (default, SSR-safe)
|
|
102
|
+
* - 'global': All requests share the global container (use only for stateless services)
|
|
103
|
+
* @property {boolean} [debug=false] - Enable debug logging
|
|
104
|
+
*/
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Express/Connect middleware. scope='request' gives each request isolated singletons (SSR-safe).
|
|
108
|
+
* @param {MiddlewareOptions} [options={}]
|
|
109
|
+
* @returns {function(req, res, next): void}
|
|
110
|
+
*/
|
|
111
|
+
export function containerMiddleware(options = {}) {
|
|
112
|
+
const { scope = 'request', debug = false } = options
|
|
113
|
+
|
|
114
|
+
return (req, res, next) => {
|
|
115
|
+
if (scope === 'global') {
|
|
116
|
+
requestContext.run({ container: globalContainer, scope: 'global' }, () => {
|
|
117
|
+
req.di = globalContainer
|
|
118
|
+
next()
|
|
119
|
+
})
|
|
120
|
+
return
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const container = new Container()
|
|
124
|
+
if (debug) container.setDebug(true)
|
|
125
|
+
req.di = container
|
|
126
|
+
requestContext.run({ container, scope: 'request' }, () => next())
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Koa middleware. See containerMiddleware() for scope behavior.
|
|
132
|
+
* @param {MiddlewareOptions} [options={}]
|
|
133
|
+
* @returns {function(ctx, next): Promise<void>}
|
|
134
|
+
*/
|
|
135
|
+
export function koaContainerMiddleware(options = {}) {
|
|
136
|
+
const { scope = 'request', debug = false } = options
|
|
137
|
+
|
|
138
|
+
return async (ctx, next) => {
|
|
139
|
+
if (scope === 'global') {
|
|
140
|
+
await requestContext.run({ container: globalContainer, scope: 'global' }, async () => {
|
|
141
|
+
ctx.di = globalContainer
|
|
142
|
+
await next()
|
|
143
|
+
})
|
|
144
|
+
return
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const container = new Container()
|
|
148
|
+
if (debug) container.setDebug(true)
|
|
149
|
+
ctx.di = container
|
|
150
|
+
await requestContext.run({ container, scope: 'request' }, () => next())
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Hono/Fastify-style handler wrapper. See containerMiddleware() for scope behavior.
|
|
156
|
+
* @param {MiddlewareOptions} [options={}]
|
|
157
|
+
* @returns {function(handler): function}
|
|
158
|
+
*/
|
|
159
|
+
export function withContainer(options = {}) {
|
|
160
|
+
const { scope = 'request', debug = false } = options
|
|
161
|
+
|
|
162
|
+
return (handler) => (...args) => {
|
|
163
|
+
if (scope === 'global') {
|
|
164
|
+
return requestContext.run({ container: globalContainer, scope: 'global' }, () => handler(...args))
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const container = new Container()
|
|
168
|
+
if (debug) container.setDebug(true)
|
|
169
|
+
return requestContext.run({ container, scope: 'request' }, () => handler(...args))
|
|
170
|
+
}
|
|
171
|
+
}
|
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
|
}
|