decorator-dependency-injection 1.0.1 → 1.0.3
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/.github/workflows/release.yml +13 -8
- package/README.md +125 -29
- package/index.js +158 -76
- package/package.json +1 -1
- package/test/injection.test.js +164 -7
- package/test/injectionLazy.test.js +249 -0
- package/test/mock.test.js +230 -6
- package/test/proxy.test.js +130 -0
package/test/injection.test.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {Factory, Inject, Singleton} from '../index.js'
|
|
1
|
+
import {Factory, Inject, resetMocks, Singleton} from '../index.js'
|
|
2
2
|
|
|
3
3
|
describe('Injection via fields', () => {
|
|
4
4
|
@Singleton()
|
|
@@ -46,6 +46,12 @@ describe('Injection via fields', () => {
|
|
|
46
46
|
}
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
+
afterEach(() => {
|
|
50
|
+
TestFactory.calls = 0
|
|
51
|
+
TestSingleton.calls = 0
|
|
52
|
+
resetMocks()
|
|
53
|
+
})
|
|
54
|
+
|
|
49
55
|
it('should inject factory', () => {
|
|
50
56
|
class TestInjectionFactory {
|
|
51
57
|
@Inject(TestFactory) testFactory
|
|
@@ -83,7 +89,53 @@ describe('Injection via fields', () => {
|
|
|
83
89
|
new TestInjectionFactoryParams()
|
|
84
90
|
})
|
|
85
91
|
|
|
86
|
-
|
|
92
|
+
it('should cache factory instance on repeated accesses', () => {
|
|
93
|
+
class TestRepeatedFactoryAccess {
|
|
94
|
+
@Inject(TestFactory) testFactory
|
|
95
|
+
|
|
96
|
+
constructor() {
|
|
97
|
+
const instance1 = this.testFactory
|
|
98
|
+
const instance2 = this.testFactory
|
|
99
|
+
expect(instance1).toBe(instance2)
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
new TestRepeatedFactoryAccess()
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
it('should create distinct factory instances for different fields in the same object', () => {
|
|
107
|
+
class TestMultipleFactoryInjection {
|
|
108
|
+
@Inject(TestFactory) testFactory1
|
|
109
|
+
@Inject(TestFactory) testFactory2
|
|
110
|
+
|
|
111
|
+
constructor() {
|
|
112
|
+
// Access both properties to trigger initialization.
|
|
113
|
+
const one = this.testFactory1
|
|
114
|
+
const two = this.testFactory2
|
|
115
|
+
expect(one).not.toBe(two)
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
new TestMultipleFactoryInjection()
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
it('should inject the same singleton instance for different fields in the same object', () => {
|
|
123
|
+
class TestMultipleSingletonInjection {
|
|
124
|
+
@Inject(TestSingleton) testSingleton1
|
|
125
|
+
@Inject(TestSingleton) testSingleton2
|
|
126
|
+
|
|
127
|
+
constructor() {
|
|
128
|
+
// Access both properties to trigger initialization.
|
|
129
|
+
const one = this.testSingleton1
|
|
130
|
+
const two = this.testSingleton2
|
|
131
|
+
expect(one).toBe(two)
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
new TestMultipleSingletonInjection()
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
@Singleton('named')
|
|
87
139
|
class NamedSingleton {
|
|
88
140
|
static calls = 0
|
|
89
141
|
|
|
@@ -94,7 +146,7 @@ describe('Injection via fields', () => {
|
|
|
94
146
|
|
|
95
147
|
it('should inject named singleton', () => {
|
|
96
148
|
class TestInjectionNamedSingleton {
|
|
97
|
-
@Inject(
|
|
149
|
+
@Inject('named') namedSingleton
|
|
98
150
|
|
|
99
151
|
constructor() {
|
|
100
152
|
expect(this.namedSingleton).toBeInstanceOf(NamedSingleton)
|
|
@@ -103,7 +155,7 @@ describe('Injection via fields', () => {
|
|
|
103
155
|
}
|
|
104
156
|
|
|
105
157
|
class TestInjectionNamedSingleton2 {
|
|
106
|
-
@Inject(
|
|
158
|
+
@Inject('named') namedSingleton
|
|
107
159
|
|
|
108
160
|
constructor() {
|
|
109
161
|
expect(this.namedSingleton).toBeInstanceOf(NamedSingleton)
|
|
@@ -115,7 +167,7 @@ describe('Injection via fields', () => {
|
|
|
115
167
|
new TestInjectionNamedSingleton2()
|
|
116
168
|
})
|
|
117
169
|
|
|
118
|
-
@Factory(
|
|
170
|
+
@Factory('named2')
|
|
119
171
|
class NamedFactory {
|
|
120
172
|
static calls = 0
|
|
121
173
|
params
|
|
@@ -128,7 +180,7 @@ describe('Injection via fields', () => {
|
|
|
128
180
|
|
|
129
181
|
it('should inject named factory', () => {
|
|
130
182
|
class TestInjectionNamedFactory {
|
|
131
|
-
@Inject(
|
|
183
|
+
@Inject('named2') namedFactory
|
|
132
184
|
|
|
133
185
|
constructor() {
|
|
134
186
|
expect(this.namedFactory).toBeInstanceOf(NamedFactory)
|
|
@@ -137,7 +189,7 @@ describe('Injection via fields', () => {
|
|
|
137
189
|
}
|
|
138
190
|
|
|
139
191
|
class TestInjectionNamedFactory2 {
|
|
140
|
-
@Inject(
|
|
192
|
+
@Inject('named2') namedFactory
|
|
141
193
|
|
|
142
194
|
constructor() {
|
|
143
195
|
expect(this.namedFactory).toBeInstanceOf(NamedFactory)
|
|
@@ -149,4 +201,109 @@ describe('Injection via fields', () => {
|
|
|
149
201
|
new TestInjectionNamedFactory2()
|
|
150
202
|
expect(result.namedFactory.params).toEqual([])
|
|
151
203
|
})
|
|
204
|
+
|
|
205
|
+
it('should cache named factory instance on repeated accesses', () => {
|
|
206
|
+
class TestRepeatedNamedFactoryAccess {
|
|
207
|
+
@Inject('named2') namedFactory
|
|
208
|
+
|
|
209
|
+
constructor() {
|
|
210
|
+
const instance1 = this.namedFactory
|
|
211
|
+
const instance2 = this.namedFactory
|
|
212
|
+
expect(instance1).toBe(instance2)
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
new TestRepeatedNamedFactoryAccess()
|
|
217
|
+
})
|
|
218
|
+
|
|
219
|
+
it('should throw if @Inject is applied to a method', () => {
|
|
220
|
+
expect(() => {
|
|
221
|
+
// noinspection JSUnusedLocalSymbols
|
|
222
|
+
class BadInjection {
|
|
223
|
+
@Inject('something')
|
|
224
|
+
someMethod() {
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}).toThrow()
|
|
228
|
+
})
|
|
229
|
+
|
|
230
|
+
it('should handle circular dependencies gracefully', () => {
|
|
231
|
+
@Singleton()
|
|
232
|
+
class A {
|
|
233
|
+
@Inject('B') b
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
@Singleton('B')
|
|
237
|
+
class B {
|
|
238
|
+
@Inject(A) a
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
expect(() => new A()).toThrow(/Circular dependency detected.*@InjectLazy/)
|
|
242
|
+
})
|
|
243
|
+
|
|
244
|
+
it('should throw if decorator is used on non-class object', () => {
|
|
245
|
+
expect(() => {
|
|
246
|
+
const obj = {}
|
|
247
|
+
Inject('something')(obj, 'field')
|
|
248
|
+
}).toThrow()
|
|
249
|
+
})
|
|
250
|
+
|
|
251
|
+
it('should throw a helpful error for eager circular dependencies', () => {
|
|
252
|
+
@Factory()
|
|
253
|
+
class A2 {
|
|
254
|
+
@Inject('B2') b
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
@Singleton('B2')
|
|
258
|
+
class B2 {
|
|
259
|
+
@Inject(A2) a
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
expect(() => new A2()).toThrow(/Circular dependency detected.*@InjectLazy/)
|
|
263
|
+
})
|
|
264
|
+
|
|
265
|
+
it('should inject into symbol-named fields', () => {
|
|
266
|
+
const sym = Symbol('sym')
|
|
267
|
+
|
|
268
|
+
@Singleton()
|
|
269
|
+
class S {
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
class Test {
|
|
273
|
+
@Inject(S) [sym]
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const t = new Test()
|
|
277
|
+
expect(t[sym]).toBeInstanceOf(S)
|
|
278
|
+
})
|
|
279
|
+
|
|
280
|
+
it('should not leak injected properties to prototype', () => {
|
|
281
|
+
@Singleton()
|
|
282
|
+
class S {
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
class Test {
|
|
286
|
+
@Inject(S) dep
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// noinspection JSUnusedLocalSymbols
|
|
290
|
+
const t = new Test()
|
|
291
|
+
expect(Object.prototype.hasOwnProperty.call(Test.prototype, 'dep')).toBe(false)
|
|
292
|
+
})
|
|
293
|
+
|
|
294
|
+
it('should handle undefined/null/complex params in factory', () => {
|
|
295
|
+
@Factory()
|
|
296
|
+
class F {
|
|
297
|
+
constructor(...params) {
|
|
298
|
+
this.params = params
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
class Test {
|
|
303
|
+
@Inject(F, undefined, null, {a: 1}) dep
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
const t = new Test()
|
|
307
|
+
expect(t.dep.params).toEqual([undefined, null, {a: 1}])
|
|
308
|
+
})
|
|
152
309
|
})
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import {Factory, InjectLazy, resetMocks, Singleton} from '../index.js'
|
|
2
|
+
|
|
3
|
+
describe('Lazy Injection via fields', () => {
|
|
4
|
+
@Singleton()
|
|
5
|
+
class TestLazySingleton {
|
|
6
|
+
static calls = 0
|
|
7
|
+
|
|
8
|
+
constructor() {
|
|
9
|
+
TestLazySingleton.calls++
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
it('should lazily inject singleton', () => {
|
|
14
|
+
class TestLazySingletonInjection {
|
|
15
|
+
@InjectLazy(TestLazySingleton) testSingleton
|
|
16
|
+
|
|
17
|
+
constructor() {
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const instance = new TestLazySingletonInjection()
|
|
22
|
+
expect(TestLazySingleton.calls).toBe(0) // Not constructed until access
|
|
23
|
+
const first = instance.testSingleton
|
|
24
|
+
expect(first).toBeInstanceOf(TestLazySingleton)
|
|
25
|
+
expect(TestLazySingleton.calls).toBe(1)
|
|
26
|
+
// Repeated access returns the same instance.
|
|
27
|
+
const second = instance.testSingleton
|
|
28
|
+
expect(first).toBe(second)
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
@Factory()
|
|
32
|
+
class TestLazyFactory {
|
|
33
|
+
static calls = 0
|
|
34
|
+
params
|
|
35
|
+
|
|
36
|
+
constructor(...params) {
|
|
37
|
+
TestLazyFactory.calls++
|
|
38
|
+
this.params = params
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
afterEach(() => {
|
|
43
|
+
TestLazyFactory.calls = 0
|
|
44
|
+
TestLazySingleton.calls = 0
|
|
45
|
+
resetMocks()
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
it('should lazily inject factory with caching on first access per field', () => {
|
|
49
|
+
class TestLazyFactoryInjection {
|
|
50
|
+
@InjectLazy(TestLazyFactory) testFactory
|
|
51
|
+
|
|
52
|
+
constructor() {
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const inst = new TestLazyFactoryInjection()
|
|
57
|
+
expect(TestLazyFactory.calls).toBe(0)
|
|
58
|
+
const first = inst.testFactory
|
|
59
|
+
expect(first).toBeInstanceOf(TestLazyFactory)
|
|
60
|
+
expect(TestLazyFactory.calls).toBe(1)
|
|
61
|
+
expect(inst.testFactory).toBe(first)
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
it('should create distinct factory instances for different fields', () => {
|
|
65
|
+
class TestMultipleLazyFactoryInjection {
|
|
66
|
+
@InjectLazy(TestLazyFactory) testFactory1
|
|
67
|
+
@InjectLazy(TestLazyFactory) testFactory2
|
|
68
|
+
|
|
69
|
+
constructor() {
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const instance = new TestMultipleLazyFactoryInjection()
|
|
74
|
+
expect(TestLazyFactory.calls).toBe(0)
|
|
75
|
+
const one = instance.testFactory1
|
|
76
|
+
const two = instance.testFactory2
|
|
77
|
+
expect(one).toBeInstanceOf(TestLazyFactory)
|
|
78
|
+
expect(two).toBeInstanceOf(TestLazyFactory)
|
|
79
|
+
expect(one).not.toBe(two)
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
it('should pass constructor parameters when using lazy injection', () => {
|
|
83
|
+
class TestLazyFactoryParamsInjection {
|
|
84
|
+
@InjectLazy(TestLazyFactory, 'param1', 'param2') testFactory
|
|
85
|
+
|
|
86
|
+
constructor() {
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
TestLazyFactory.calls = 0
|
|
91
|
+
const instance = new TestLazyFactoryParamsInjection()
|
|
92
|
+
expect(TestLazyFactory.calls).toBe(0)
|
|
93
|
+
const factoryInst = instance.testFactory
|
|
94
|
+
expect(factoryInst).toBeInstanceOf(TestLazyFactory)
|
|
95
|
+
expect(factoryInst.params).toEqual(['param1', 'param2'])
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
it('should not initialize dependency if property is never accessed', () => {
|
|
99
|
+
class TestNeverAccess {
|
|
100
|
+
@InjectLazy(TestLazySingleton) testSingleton
|
|
101
|
+
|
|
102
|
+
constructor() {
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
TestLazySingleton.calls = 0
|
|
107
|
+
new TestNeverAccess() // Do not access testSingleton.
|
|
108
|
+
expect(TestLazySingleton.calls).toBe(0)
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
@Singleton('lazyNamedSingleton')
|
|
112
|
+
class NamedLazySingleton {
|
|
113
|
+
static calls = 0
|
|
114
|
+
|
|
115
|
+
constructor() {
|
|
116
|
+
NamedLazySingleton.calls++
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
it('should lazily inject named singleton', () => {
|
|
121
|
+
class TestLazyNamedSingletonInjection {
|
|
122
|
+
@InjectLazy('lazyNamedSingleton') namedSingleton
|
|
123
|
+
|
|
124
|
+
constructor() {
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const instance = new TestLazyNamedSingletonInjection()
|
|
129
|
+
expect(NamedLazySingleton.calls).toBe(0)
|
|
130
|
+
const first = instance.namedSingleton
|
|
131
|
+
expect(first).toBeInstanceOf(NamedLazySingleton)
|
|
132
|
+
expect(NamedLazySingleton.calls).toBe(1)
|
|
133
|
+
expect(instance.namedSingleton).toBe(first)
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
@Factory('lazyNamedFactory')
|
|
137
|
+
class NamedLazyFactory {
|
|
138
|
+
static calls = 0
|
|
139
|
+
params
|
|
140
|
+
|
|
141
|
+
constructor(...params) {
|
|
142
|
+
NamedLazyFactory.calls++
|
|
143
|
+
this.params = params
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
it('should lazily inject named factory', () => {
|
|
148
|
+
class TestLazyNamedFactoryInjection {
|
|
149
|
+
@InjectLazy('lazyNamedFactory') namedFactory
|
|
150
|
+
|
|
151
|
+
constructor() {
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
NamedLazyFactory.calls = 0
|
|
156
|
+
const instance = new TestLazyNamedFactoryInjection()
|
|
157
|
+
expect(NamedLazyFactory.calls).toBe(0)
|
|
158
|
+
const first = instance.namedFactory
|
|
159
|
+
expect(first).toBeInstanceOf(NamedLazyFactory)
|
|
160
|
+
expect(NamedLazyFactory.calls).toBe(1)
|
|
161
|
+
expect(instance.namedFactory).toBe(first)
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
it('should not expose any internal caching artifacts', () => {
|
|
165
|
+
class TestLazyEnum {
|
|
166
|
+
@InjectLazy(TestLazySingleton) lazyProp
|
|
167
|
+
|
|
168
|
+
constructor() {
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const inst = new TestLazyEnum()
|
|
173
|
+
// noinspection JSUnusedLocalSymbols: Force lazy initialization.
|
|
174
|
+
const _ = inst.lazyProp
|
|
175
|
+
const keys = Object.keys(inst)
|
|
176
|
+
expect(keys).toContain('lazyProp')
|
|
177
|
+
expect(keys.length).toBe(1)
|
|
178
|
+
const symbols = Object.getOwnPropertySymbols(inst)
|
|
179
|
+
expect(symbols.length).toBe(0)
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
it('should throw if @InjectLazy is applied to a method', () => {
|
|
183
|
+
expect(() => {
|
|
184
|
+
// noinspection JSUnusedLocalSymbols
|
|
185
|
+
class BadLazy {
|
|
186
|
+
@InjectLazy('something')
|
|
187
|
+
someMethod() {
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}).toThrow()
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
it('should handle circular dependencies (lazy)', () => {
|
|
194
|
+
@Singleton()
|
|
195
|
+
class A {
|
|
196
|
+
@InjectLazy('B') b
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
@Singleton('B')
|
|
200
|
+
class B {
|
|
201
|
+
@InjectLazy(A) a
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
expect(() => new A()).not.toThrow()
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
it('should inject into symbol-named fields (lazy)', () => {
|
|
208
|
+
const sym = Symbol('sym')
|
|
209
|
+
|
|
210
|
+
@Singleton()
|
|
211
|
+
class S {
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
class Test {
|
|
215
|
+
@InjectLazy(S) [sym]
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const t = new Test()
|
|
219
|
+
expect(t[sym]).toBeInstanceOf(S)
|
|
220
|
+
})
|
|
221
|
+
|
|
222
|
+
it('should not leak lazy injected properties to prototype', () => {
|
|
223
|
+
@Singleton()
|
|
224
|
+
class S {
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
class Test {
|
|
228
|
+
@InjectLazy(S) dep
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// noinspection JSUnusedLocalSymbols
|
|
232
|
+
const t = new Test()
|
|
233
|
+
expect(Object.prototype.hasOwnProperty.call(Test.prototype, 'dep')).toBe(false)
|
|
234
|
+
})
|
|
235
|
+
|
|
236
|
+
it('should allow circular dependencies with lazy injection', () => {
|
|
237
|
+
@Singleton()
|
|
238
|
+
class A1 {
|
|
239
|
+
@InjectLazy('B1') b
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
@Factory('B1')
|
|
243
|
+
class B1 {
|
|
244
|
+
@InjectLazy(A1) a
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
expect(() => new A1()).not.toThrow()
|
|
248
|
+
})
|
|
249
|
+
})
|