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
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
name:
|
|
1
|
+
name: Release
|
|
2
2
|
|
|
3
3
|
on:
|
|
4
4
|
push:
|
|
@@ -48,12 +48,19 @@ jobs:
|
|
|
48
48
|
TAG_EXISTS=$(git ls-remote --tags origin "v${VERSION}" | wc -l)
|
|
49
49
|
echo "TAG_EXISTS=${TAG_EXISTS}" >> $GITHUB_ENV
|
|
50
50
|
|
|
51
|
+
- name: Get commit messages
|
|
52
|
+
id: commit_messages
|
|
53
|
+
run: |
|
|
54
|
+
COMMITS=$(git log --pretty=format:"%h - %s" $(git describe --tags --abbrev=0 @^)..@ | while read -r hash msg; do echo "[${hash}](https://github.com/${{ github.repository }}/commit/${hash}) - ${msg}"; done)
|
|
55
|
+
echo "COMMITS=${COMMITS}" >> $GITHUB_ENV
|
|
56
|
+
|
|
51
57
|
- name: Create Tag and Release
|
|
52
58
|
uses: actions/github-script@v7
|
|
53
59
|
with:
|
|
54
60
|
script: |
|
|
55
61
|
const version = process.env.VERSION
|
|
56
62
|
const tag = `v${version}`
|
|
63
|
+
const commits = process.env.COMMITS
|
|
57
64
|
const { data: tags } = await github.rest.repos.listTags({
|
|
58
65
|
owner: context.repo.owner,
|
|
59
66
|
repo: context.repo.repo,
|
|
@@ -72,7 +79,7 @@ jobs:
|
|
|
72
79
|
repo: context.repo.repo,
|
|
73
80
|
tag_name: tag,
|
|
74
81
|
name: `Release ${tag}`,
|
|
75
|
-
body: `Automated release of version ${tag}`,
|
|
82
|
+
body: `Automated release of version ${tag}\n\nCommits:\n${commits}`,
|
|
76
83
|
draft: false,
|
|
77
84
|
prerelease: false,
|
|
78
85
|
})
|
|
@@ -101,7 +108,7 @@ jobs:
|
|
|
101
108
|
- name: Check if version exists on npm
|
|
102
109
|
id: check_npm_version
|
|
103
110
|
run: |
|
|
104
|
-
if npm show
|
|
111
|
+
if npm show decorator-dependency-injection@${{ env.VERSION }} > /dev/null 2>&1; then
|
|
105
112
|
echo "VERSION_EXISTS=true" >> $GITHUB_ENV
|
|
106
113
|
else
|
|
107
114
|
echo "VERSION_EXISTS=false" >> $GITHUB_ENV
|
|
@@ -109,14 +116,12 @@ jobs:
|
|
|
109
116
|
|
|
110
117
|
- name: Publish to npm
|
|
111
118
|
if: env.VERSION_EXISTS == 'false'
|
|
112
|
-
run:
|
|
119
|
+
run: |
|
|
120
|
+
npm publish --access public
|
|
121
|
+
echo "::notice::Published new version ${{ env.VERSION }} to npm."
|
|
113
122
|
env:
|
|
114
123
|
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
|
115
124
|
|
|
116
|
-
- name: Notice Publish Status
|
|
117
|
-
if: env.VERSION_EXISTS == 'false'
|
|
118
|
-
run: echo "::notice::Published new version ${{ env.VERSION }} to npm."
|
|
119
|
-
|
|
120
125
|
- name: Notice Publish Status
|
|
121
126
|
if: env.VERSION_EXISTS == 'true'
|
|
122
127
|
run: echo "::notice::Version ${{ env.VERSION }} already exists on npm. No new version published."
|
package/README.md
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
# Decorator Dependency Injection
|
|
2
|
-
[](http://badge.fury.io/js/decorator-dependency-injection)
|
|
3
|
-
[](https://github.com/mallocator/decorator-dependency-injection/actions/workflows/node.js.yml)
|
|
4
2
|
|
|
3
|
+
[](http://badge.fury.io/js/decorator-dependency-injection)
|
|
4
|
+
[](https://github.com/mallocator/decorator-dependency-injection/actions/workflows/release.yml)
|
|
5
5
|
|
|
6
6
|
## Description
|
|
7
7
|
|
|
8
|
-
With [TC39](https://github.com/tc39/proposal-decorators) reaching stage 3
|
|
8
|
+
With the [TC39 proposal-decorators](https://github.com/tc39/proposal-decorators) reaching stage 3, it's time to start
|
|
9
|
+
thinking about how we can use them in our projects. One of the most common patterns in JavaScript is dependency
|
|
10
|
+
injection. This pattern is used to make our code more testable and maintainable. This library provides simple decorators
|
|
11
|
+
to help you inject dependencies into your classes and mock them for testing.
|
|
9
12
|
|
|
10
13
|
## Installation
|
|
11
14
|
|
|
@@ -13,21 +16,26 @@ With [TC39](https://github.com/tc39/proposal-decorators) reaching stage 3 on the
|
|
|
13
16
|
npm install decorator-dependency-injection
|
|
14
17
|
```
|
|
15
18
|
|
|
16
|
-
Until we reach stage 4, you will need to enable the decorators proposal in your project. You can do this by adding the
|
|
19
|
+
Until we reach stage 4, you will need to enable the decorators proposal in your project. You can do this by adding the
|
|
20
|
+
following babel transpiler options to your `.babelrc` file.
|
|
17
21
|
|
|
18
22
|
```json
|
|
19
23
|
{
|
|
20
|
-
"plugins": [
|
|
24
|
+
"plugins": [
|
|
25
|
+
"@babel/plugin-proposal-decorators"
|
|
26
|
+
]
|
|
21
27
|
}
|
|
22
28
|
```
|
|
23
29
|
|
|
24
|
-
To run your project with decorators enabled you will need to use the babel transpiler. You can do this by running the
|
|
30
|
+
To run your project with decorators enabled, you will need to use the babel transpiler. You can do this by running the
|
|
31
|
+
following command in your project root.
|
|
25
32
|
|
|
26
33
|
```bash
|
|
27
34
|
npx babel-node index.js
|
|
28
35
|
```
|
|
29
36
|
|
|
30
|
-
Finally, for running tests with decorators enabled you will need to use the babel-jest package. You can do this by
|
|
37
|
+
Finally, for running tests with decorators enabled, you will need to use the babel-jest package. You can do this by
|
|
38
|
+
adding the following configuration to your `package.json` file.
|
|
31
39
|
|
|
32
40
|
```json
|
|
33
41
|
{
|
|
@@ -43,20 +51,21 @@ Other testing frameworks may require a different configuration.
|
|
|
43
51
|
|
|
44
52
|
For a full example of how to set up a project with decorators, see this project's ```package.json``` file.
|
|
45
53
|
|
|
46
|
-
|
|
47
54
|
## Usage
|
|
48
55
|
|
|
49
|
-
There are
|
|
56
|
+
There are two ways of specifying injectable dependencies: ```@Singleton``` and ```@Factory```:
|
|
50
57
|
|
|
51
58
|
### Singleton
|
|
52
59
|
|
|
53
|
-
The ```@Singleton``` decorator is used to inject a single instance of a dependency into a class. This is useful when you
|
|
60
|
+
The ```@Singleton``` decorator is used to inject a single instance of a dependency into a class. This is useful when you
|
|
61
|
+
want to share the same instance of a class across multiple classes.
|
|
54
62
|
|
|
55
63
|
```javascript
|
|
56
|
-
import {
|
|
64
|
+
import {Singleton} from 'decorator-dependency-injection';
|
|
57
65
|
|
|
58
66
|
@Singleton
|
|
59
|
-
class Dependency {
|
|
67
|
+
class Dependency {
|
|
68
|
+
}
|
|
60
69
|
|
|
61
70
|
class Consumer {
|
|
62
71
|
@Inject(Dependency) dependency // creates an instance only once
|
|
@@ -65,31 +74,54 @@ class Consumer {
|
|
|
65
74
|
|
|
66
75
|
### Factory
|
|
67
76
|
|
|
68
|
-
The ```@Factory``` decorator is used to inject a new instance of a dependency into a class each time it is requested.
|
|
77
|
+
The ```@Factory``` decorator is used to inject a new instance of a dependency into a class each time it is requested.
|
|
78
|
+
This is useful when you want to create a new instance of a class each time it is injected.
|
|
69
79
|
|
|
70
80
|
```javascript
|
|
71
|
-
import {
|
|
81
|
+
import {Factory} from 'decorator-dependency-injection';
|
|
72
82
|
|
|
73
83
|
@Factory
|
|
74
|
-
class Dependency {
|
|
84
|
+
class Dependency {
|
|
85
|
+
}
|
|
75
86
|
|
|
76
87
|
class Consumer {
|
|
77
88
|
@Inject(Dependency) dependency // creates a new instance each time a new Consumer is created
|
|
78
89
|
}
|
|
79
90
|
```
|
|
80
91
|
|
|
92
|
+
### LazyInject
|
|
93
|
+
|
|
94
|
+
```@Inject``` annotated properties are evaluated during instance initialization. That means that all properties should
|
|
95
|
+
be accessible in the constructor. That also means that we're creating an instance no matter if you access the property
|
|
96
|
+
or not. If you want to only create an instance when you access the property, you can use the ```@LazyInject```
|
|
97
|
+
decorator. This will create the instance only when the property is accessed for the first time. Note that this also
|
|
98
|
+
works from the constructor, same as the regular ```@Inject```.
|
|
99
|
+
|
|
100
|
+
```javascript
|
|
101
|
+
import {LazyInject} from 'decorator-dependency-injection';
|
|
102
|
+
|
|
103
|
+
@Singleton
|
|
104
|
+
class Dependency {
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
class Consumer {
|
|
108
|
+
@LazyInject(Dependency) dependency // creates an instance only when the property is accessed
|
|
109
|
+
}
|
|
110
|
+
```
|
|
111
|
+
|
|
81
112
|
## Passing parameters to a dependency
|
|
82
113
|
|
|
83
|
-
You can pass parameters to a dependency by using the ```@Inject``` decorator with a function that returns the
|
|
114
|
+
You can pass parameters to a dependency by using the ```@Inject``` decorator with a function that returns the
|
|
115
|
+
dependency.
|
|
84
116
|
|
|
85
117
|
```javascript
|
|
86
|
-
import {
|
|
118
|
+
import {Factory, Inject} from 'decorator-dependency-injection';
|
|
87
119
|
|
|
88
120
|
@Factory
|
|
89
121
|
class Dependency {
|
|
90
122
|
constructor(param1, param2) {
|
|
91
|
-
this.param1 = param1
|
|
92
|
-
this.param2 = param2
|
|
123
|
+
this.param1 = param1
|
|
124
|
+
this.param2 = param2
|
|
93
125
|
}
|
|
94
126
|
}
|
|
95
127
|
|
|
@@ -98,40 +130,97 @@ class Consumer {
|
|
|
98
130
|
}
|
|
99
131
|
```
|
|
100
132
|
|
|
101
|
-
While this is most useful for Factory dependencies, it can also be used with Singleton dependencies. However, parameters
|
|
133
|
+
While this is most useful for Factory dependencies, it can also be used with Singleton dependencies. However, parameters
|
|
134
|
+
will only be passed to the dependency the first time it is created.
|
|
102
135
|
|
|
103
136
|
## Mocking dependencies for testing
|
|
104
137
|
|
|
105
138
|
You can mock dependencies by using the ```@Mock``` decorator with a function that returns the mock dependency.
|
|
106
139
|
|
|
107
140
|
```javascript
|
|
108
|
-
import {
|
|
141
|
+
import {Factory, Inject, Mock} from 'decorator-dependency-injection'
|
|
109
142
|
|
|
110
143
|
@Factory
|
|
111
144
|
class Dependency {
|
|
112
145
|
method() {
|
|
113
|
-
return 'real'
|
|
146
|
+
return 'real'
|
|
114
147
|
}
|
|
115
148
|
}
|
|
116
149
|
|
|
117
150
|
class Consumer {
|
|
118
151
|
@Inject(Dependency) dependency
|
|
119
152
|
|
|
120
|
-
|
|
121
|
-
|
|
153
|
+
constructor() {
|
|
154
|
+
console.log(this.dependency.method())
|
|
122
155
|
}
|
|
123
156
|
}
|
|
124
157
|
|
|
125
|
-
|
|
158
|
+
// Test Code
|
|
159
|
+
|
|
160
|
+
@Mock(Dependency)
|
|
126
161
|
class MockDependency {
|
|
127
162
|
method() {
|
|
128
|
-
return 'mock'
|
|
163
|
+
return 'mock'
|
|
129
164
|
}
|
|
130
165
|
}
|
|
131
166
|
|
|
132
|
-
const consumer = new Consumer()
|
|
167
|
+
const consumer = new Consumer() // prints 'mock'
|
|
168
|
+
|
|
169
|
+
resetMock(Dependency)
|
|
170
|
+
|
|
171
|
+
const consumer = new Consumer() // prints 'real'
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
### Resetting Mocks
|
|
133
175
|
|
|
134
|
-
|
|
176
|
+
The `resetMock` utility function allows you to remove any active mock for a dependency and restore the original
|
|
177
|
+
implementation. This is useful for cleaning up after tests or switching between real and mock dependencies.
|
|
178
|
+
|
|
179
|
+
```javascript
|
|
180
|
+
import {resetMock} from 'decorator-dependency-injection';
|
|
181
|
+
|
|
182
|
+
resetMock(Dependency); // Restores the original Dependency implementation
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
You can also use the ```@Mock``` decorator as a proxy instead of a full mock. Any method calls not implemented in the
|
|
186
|
+
mock will be passed to the real dependency.
|
|
187
|
+
|
|
188
|
+
```javascript
|
|
189
|
+
import {Factory, Inject, Mock} from 'decorator-dependency-injection'
|
|
190
|
+
|
|
191
|
+
@Factory
|
|
192
|
+
class Dependency {
|
|
193
|
+
method() {
|
|
194
|
+
return 'real'
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
otherMethod() {
|
|
198
|
+
return 'other'
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
class Consumer {
|
|
203
|
+
@Inject(Dependency) dependency
|
|
204
|
+
|
|
205
|
+
constructor() {
|
|
206
|
+
console.log(this.dependency.method(), this.dependency.otherMethod())
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Test Code
|
|
211
|
+
|
|
212
|
+
@Mock(Dependency, true)
|
|
213
|
+
class MockDependency {
|
|
214
|
+
method() {
|
|
215
|
+
return 'mock'
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const consumer = new Consumer() // prints 'mock other'
|
|
220
|
+
|
|
221
|
+
resetMock(Dependency)
|
|
222
|
+
|
|
223
|
+
const consumer = new Consumer() // prints 'real other'
|
|
135
224
|
```
|
|
136
225
|
|
|
137
226
|
For more examples, see the tests in the ```test``` directory.
|
|
@@ -142,4 +231,11 @@ To run the tests, run the following command in the project root.
|
|
|
142
231
|
|
|
143
232
|
```bash
|
|
144
233
|
npm test
|
|
145
|
-
```
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
## Version History
|
|
237
|
+
|
|
238
|
+
- 1.0.0 - Initial release
|
|
239
|
+
- 1.0.1 - Automated release with GitHub Actions
|
|
240
|
+
- 1.0.2 - Added proxy option to @Mock decorator
|
|
241
|
+
- 1.0.3 - Added @LazyInject decorator
|
package/index.js
CHANGED
|
@@ -1,39 +1,41 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @typedef {Object} InstanceContext
|
|
3
|
-
* @property {
|
|
4
|
-
* @property {
|
|
5
|
-
* @property {
|
|
3
|
+
* @property {'singleton'|'factory'} type - The type of the instance.
|
|
4
|
+
* @property {Function} clazz - The class constructor for the instance.
|
|
5
|
+
* @property {Function} [originalClazz] - The original class if this is a mock.
|
|
6
|
+
* @property {Object} [instance] - The singleton instance, if created.
|
|
7
|
+
* @property {Object} [originalInstance] - The original instance if this is a mock.
|
|
8
|
+
* @property {boolean} [proxy=false] - If true, the mock will proxy to the original class for undefined methods/properties.
|
|
6
9
|
*/
|
|
7
10
|
|
|
8
11
|
/** @type {Map<string|Class, InstanceContext>} */
|
|
9
|
-
const
|
|
10
|
-
/** @type {Map<string|Class, InstanceContext>} */
|
|
11
|
-
const factories = new Map()
|
|
12
|
+
const instances = new Map()
|
|
12
13
|
|
|
13
14
|
/**
|
|
14
15
|
* Register a class as a singleton. If a name is provided, it will be used as the key in the singleton map.
|
|
15
16
|
* Singleton instances only ever have one instance created via the @Inject decorator.
|
|
16
17
|
*
|
|
17
18
|
* @param {string} [name] The name of the singleton. If not provided, the class will be used as the key.
|
|
18
|
-
* @return {(function(
|
|
19
|
+
* @return {(function(Function, {kind: string}): void)}
|
|
19
20
|
* @example @Singleton() class MySingleton {}
|
|
20
21
|
* @example @Singleton('customName') class MySingleton {}
|
|
21
22
|
* @throws {Error} If the injection target is not a class
|
|
22
|
-
* @throws {Error} If a singleton with the same name is already defined
|
|
23
|
-
* @throws {Error} If
|
|
23
|
+
* @throws {Error} If a singleton or factory with the same name is already defined
|
|
24
|
+
* @throws {Error} If the target is not a class constructor
|
|
24
25
|
*/
|
|
25
26
|
export function Singleton(name) {
|
|
26
27
|
return function (clazz, context) {
|
|
27
|
-
if (context.kind !==
|
|
28
|
+
if (context.kind !== 'class') {
|
|
28
29
|
throw new Error('Invalid injection target')
|
|
29
30
|
}
|
|
30
|
-
if (
|
|
31
|
-
throw new Error('
|
|
31
|
+
if (typeof clazz !== 'function' || !clazz.prototype) {
|
|
32
|
+
throw new Error('Target must be a class constructor')
|
|
32
33
|
}
|
|
33
|
-
|
|
34
|
-
|
|
34
|
+
const key = name ?? clazz
|
|
35
|
+
if (instances.has(key)) {
|
|
36
|
+
throw new Error('A different class is already registered under this name. This may be possibly a circular dependency. Try using @InjectLazy')
|
|
35
37
|
}
|
|
36
|
-
|
|
38
|
+
instances.set(key, {clazz, type: 'singleton'})
|
|
37
39
|
}
|
|
38
40
|
}
|
|
39
41
|
|
|
@@ -42,25 +44,26 @@ export function Singleton(name) {
|
|
|
42
44
|
* Factory instances are created via the @Inject decorator. Each call to the factory will create a new instance.
|
|
43
45
|
*
|
|
44
46
|
* @param {string} [name] The name of the factory. If not provided, the class will be used as the key.
|
|
45
|
-
* @return {(function(
|
|
47
|
+
* @return {(function(Function, {kind: string}): void)}
|
|
46
48
|
* @example @Factory() class MyFactory {}
|
|
47
49
|
* @example @Factory('customName') class MyFactory {}
|
|
48
50
|
* @throws {Error} If the injection target is not a class
|
|
49
|
-
* @throws {Error} If a factory with the same name is already defined
|
|
50
|
-
* @throws {Error} If
|
|
51
|
+
* @throws {Error} If a factory or singleton with the same name is already defined
|
|
52
|
+
* @throws {Error} If the target is not a class constructor
|
|
51
53
|
*/
|
|
52
54
|
export function Factory(name) {
|
|
53
55
|
return function (clazz, context) {
|
|
54
|
-
if (context.kind !==
|
|
56
|
+
if (context.kind !== 'class') {
|
|
55
57
|
throw new Error('Invalid injection target')
|
|
56
58
|
}
|
|
57
|
-
if (
|
|
58
|
-
throw new Error('
|
|
59
|
+
if (typeof clazz !== 'function' || !clazz.prototype) {
|
|
60
|
+
throw new Error('Target must be a class constructor')
|
|
59
61
|
}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
+
const key = name ?? clazz
|
|
63
|
+
if (instances.has(key)) {
|
|
64
|
+
throw new Error('A different class is already registered under this name, This may be possibly a circular dependency. Try using @InjectLazy')
|
|
62
65
|
}
|
|
63
|
-
|
|
66
|
+
instances.set(key, {clazz, type: 'factory'})
|
|
64
67
|
}
|
|
65
68
|
}
|
|
66
69
|
|
|
@@ -69,70 +72,120 @@ export function Factory(name) {
|
|
|
69
72
|
* If the instance is a singleton, it will only be created once with the first set of parameters it encounters.
|
|
70
73
|
*
|
|
71
74
|
* @param {string|Class} clazzOrName The singleton or factory class or name
|
|
72
|
-
* @param {*} params Parameters to pass to the constructor
|
|
73
|
-
* @return {(function(
|
|
75
|
+
* @param {*} params Parameters to pass to the constructor. Recommended to use only with factories.
|
|
76
|
+
* @return {(function(*, {kind: string, name: string}): void)}
|
|
74
77
|
* @example @Inject(MySingleton) mySingleton
|
|
75
78
|
* @example @Inject("myCustomName") myFactory
|
|
76
79
|
* @throws {Error} If the injection target is not a field
|
|
77
80
|
* @throws {Error} If the injected field is assigned a value
|
|
78
81
|
*/
|
|
79
82
|
export function Inject(clazzOrName, ...params) {
|
|
80
|
-
return function(initialValue, context) {
|
|
81
|
-
if (context.kind
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
const instanceContext = singletons.get(clazzOrName)
|
|
88
|
-
if (!instanceContext.instance) {
|
|
89
|
-
singletons.set(clazzOrName, {clazz: clazzOrName, instance: new instanceContext.clazz(...params), original: instanceContext.original})
|
|
90
|
-
}
|
|
91
|
-
return singletons.get(clazzOrName).instance
|
|
92
|
-
} else if (factories.has(clazzOrName)) {
|
|
93
|
-
const factoryClass = factories.get(clazzOrName).clazz
|
|
94
|
-
return new factoryClass(...params)
|
|
95
|
-
} else {
|
|
96
|
-
throw new Error('Cannot find injection source with the provided name')
|
|
97
|
-
}
|
|
83
|
+
return function (initialValue, context) {
|
|
84
|
+
if (context.kind !== 'field') {
|
|
85
|
+
throw new Error('Invalid injection target')
|
|
86
|
+
}
|
|
87
|
+
return function (initialValue) {
|
|
88
|
+
if (initialValue) {
|
|
89
|
+
throw new Error('Cannot assign value to injected field')
|
|
98
90
|
}
|
|
99
|
-
|
|
91
|
+
const instanceContext = getContext(clazzOrName)
|
|
92
|
+
return getInjectedInstance(instanceContext, params)
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Inject a singleton or factory instance lazily into a class field. You can also provide parameters to the constructor.
|
|
99
|
+
* If the instance is a singleton, it will only be created once with the first set of parameters it encounters.
|
|
100
|
+
* @param {string|Class} clazzOrName The singleton or factory class or name
|
|
101
|
+
* @param {*} params Parameters to pass to the constructor. Recommended to use only with factories.
|
|
102
|
+
* @return {(function(*, {kind: string, name: string, addInitializer: Function}): void)}
|
|
103
|
+
* @example @InjectLazy(MySingleton) mySingleton
|
|
104
|
+
* @example @InjectLazy("myCustomName") myFactory
|
|
105
|
+
* @throws {Error} If the injection target is not a field
|
|
106
|
+
* @throws {Error} If the injected field is assigned a value
|
|
107
|
+
*/
|
|
108
|
+
export function InjectLazy(clazzOrName, ...params) {
|
|
109
|
+
const cache = new WeakMap()
|
|
110
|
+
return (initialValue, context) => {
|
|
111
|
+
if (context.kind !== 'field') {
|
|
100
112
|
throw new Error('Invalid injection target')
|
|
101
113
|
}
|
|
114
|
+
context.addInitializer(function () {
|
|
115
|
+
Object.defineProperty(this, context.name, {
|
|
116
|
+
get() {
|
|
117
|
+
if (!cache.has(this)) {
|
|
118
|
+
const instanceContext = getContext(clazzOrName)
|
|
119
|
+
const value = getInjectedInstance(instanceContext, params)
|
|
120
|
+
cache.set(this, value)
|
|
121
|
+
}
|
|
122
|
+
return cache.get(this)
|
|
123
|
+
},
|
|
124
|
+
configurable: true,
|
|
125
|
+
enumerable: true
|
|
126
|
+
})
|
|
127
|
+
})
|
|
102
128
|
}
|
|
103
129
|
}
|
|
104
130
|
|
|
131
|
+
/**
|
|
132
|
+
* Get a proxy for the mock instance. This allows the mock to call methods on the original class if they are not defined in the mock.
|
|
133
|
+
* @param {Object} mock The mock instance
|
|
134
|
+
* @param {Object} original The original class instance
|
|
135
|
+
* @return {*|object} The proxy instance
|
|
136
|
+
*/
|
|
137
|
+
function getProxy(mock, original) {
|
|
138
|
+
return new Proxy(mock, {
|
|
139
|
+
get(target, prop, receiver) {
|
|
140
|
+
if (prop in target) {
|
|
141
|
+
return Reflect.get(target, prop, receiver)
|
|
142
|
+
}
|
|
143
|
+
return Reflect.get(original, prop, receiver)
|
|
144
|
+
}
|
|
145
|
+
})
|
|
146
|
+
}
|
|
147
|
+
|
|
105
148
|
/**
|
|
106
149
|
* Mark a class as a mock. This will replace the class with a mock instance when injected.
|
|
107
150
|
* @param {string|Class} mockedClazzOrName The singleton or factory class or name to be mocked
|
|
108
|
-
* @
|
|
151
|
+
* @param {boolean} [proxy=false] If true, the mock will be a proxy to the original class. Any methods not defined in the mock will be called on the original class.
|
|
152
|
+
* @return {(function(Function, {kind: string}): void)}
|
|
109
153
|
* @example @Mock(MySingleton) class MyMock {}
|
|
110
154
|
* @example @Mock("myCustomName") class MyMock {}
|
|
111
155
|
* @throws {Error} If the injection target is not a class
|
|
112
156
|
* @throws {Error} If the injection source is not found
|
|
113
157
|
*/
|
|
114
|
-
export function Mock(mockedClazzOrName) {
|
|
115
|
-
return function(clazz, context) {
|
|
116
|
-
if (context.kind !==
|
|
158
|
+
export function Mock(mockedClazzOrName, proxy = false) {
|
|
159
|
+
return function (clazz, context) {
|
|
160
|
+
if (context.kind !== 'class') {
|
|
117
161
|
throw new Error('Invalid injection target')
|
|
118
162
|
}
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
throw new Error('Mock already defined, reset before mocking again')
|
|
123
|
-
}
|
|
124
|
-
instanceContext.original = instanceContext.clazz
|
|
125
|
-
instanceContext.clazz = clazz
|
|
126
|
-
} else if (factories.has(mockedClazzOrName)) {
|
|
127
|
-
const instanceContext = factories.get(mockedClazzOrName)
|
|
128
|
-
if (instanceContext.original) {
|
|
129
|
-
throw new Error('Mock already defined, reset before mocking again')
|
|
130
|
-
}
|
|
131
|
-
instanceContext.original = instanceContext.clazz
|
|
132
|
-
instanceContext.clazz = clazz
|
|
133
|
-
} else {
|
|
134
|
-
throw new Error('Cannot find injection source with the provided name')
|
|
163
|
+
const instanceContext = getContext(mockedClazzOrName)
|
|
164
|
+
if (instanceContext.originalClazz) {
|
|
165
|
+
throw new Error('Mock already defined, reset before mocking again')
|
|
135
166
|
}
|
|
167
|
+
instanceContext.originalClazz = instanceContext.clazz
|
|
168
|
+
instanceContext.proxy = proxy
|
|
169
|
+
instanceContext.clazz = clazz
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Internal: Get the context for a given class or name.
|
|
175
|
+
*
|
|
176
|
+
* @param {string|Class} mockedClazzOrName - The class or name to look up.
|
|
177
|
+
* @returns {InstanceContext}
|
|
178
|
+
* @throws {Error} If the context is not found.
|
|
179
|
+
*/
|
|
180
|
+
function getContext(mockedClazzOrName) {
|
|
181
|
+
if (instances.has(mockedClazzOrName)) {
|
|
182
|
+
return instances.get(mockedClazzOrName)
|
|
183
|
+
} else {
|
|
184
|
+
const available = Array.from(instances.keys()).map(k => typeof k === 'string' ? k : k.name).join(', ')
|
|
185
|
+
throw new Error(
|
|
186
|
+
`Cannot find injection source for "${mockedClazzOrName?.name || mockedClazzOrName}". ` +
|
|
187
|
+
`Available: [${available}]`
|
|
188
|
+
)
|
|
136
189
|
}
|
|
137
190
|
}
|
|
138
191
|
|
|
@@ -140,11 +193,8 @@ export function Mock(mockedClazzOrName) {
|
|
|
140
193
|
* Reset all mocks to their original classes.
|
|
141
194
|
*/
|
|
142
195
|
export function resetMocks() {
|
|
143
|
-
for (const instanceContext of
|
|
144
|
-
|
|
145
|
-
}
|
|
146
|
-
for (const instanceContext of factories.values()) {
|
|
147
|
-
reset(instanceContext)
|
|
196
|
+
for (const instanceContext of instances.values()) {
|
|
197
|
+
restoreOriginal(instanceContext)
|
|
148
198
|
}
|
|
149
199
|
}
|
|
150
200
|
|
|
@@ -153,8 +203,7 @@ export function resetMocks() {
|
|
|
153
203
|
* @param {string|Class} clazzOrName The singleton or factory class or name to reset
|
|
154
204
|
*/
|
|
155
205
|
export function resetMock(clazzOrName) {
|
|
156
|
-
|
|
157
|
-
reset(instanceContext)
|
|
206
|
+
restoreOriginal(getContext(clazzOrName))
|
|
158
207
|
}
|
|
159
208
|
|
|
160
209
|
/**
|
|
@@ -162,13 +211,46 @@ export function resetMock(clazzOrName) {
|
|
|
162
211
|
* @param {InstanceContext} instanceContext The instance context to reset
|
|
163
212
|
* @private
|
|
164
213
|
*/
|
|
165
|
-
function
|
|
214
|
+
function restoreOriginal(instanceContext) {
|
|
166
215
|
if (!instanceContext) {
|
|
167
216
|
throw new Error('Cannot find injection source with the provided name')
|
|
168
217
|
}
|
|
169
|
-
if (instanceContext.
|
|
170
|
-
instanceContext.clazz = instanceContext.
|
|
171
|
-
delete instanceContext.original
|
|
218
|
+
if (instanceContext.originalClazz) {
|
|
219
|
+
instanceContext.clazz = instanceContext.originalClazz
|
|
172
220
|
delete instanceContext.instance
|
|
221
|
+
delete instanceContext.originalClazz
|
|
222
|
+
delete instanceContext.originalInstance
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Get the injected instance based on the context and parameters.
|
|
228
|
+
* @param {InstanceContext} instanceContext The instance context
|
|
229
|
+
* @param {Array} params The parameters to pass to the constructor
|
|
230
|
+
* @return {Object} The injected instance
|
|
231
|
+
*/
|
|
232
|
+
function getInjectedInstance(instanceContext, params) {
|
|
233
|
+
if (instanceContext.type === 'singleton' && !instanceContext.originalClazz && instanceContext.instance) {
|
|
234
|
+
return instanceContext.instance
|
|
235
|
+
}
|
|
236
|
+
let instance
|
|
237
|
+
try {
|
|
238
|
+
instance = new instanceContext.clazz(...params)
|
|
239
|
+
} catch (err) {
|
|
240
|
+
if (err instanceof RangeError) {
|
|
241
|
+
throw new Error(
|
|
242
|
+
`Circular dependency detected for ${instanceContext.clazz.name || instanceContext.clazz}. ` +
|
|
243
|
+
`Use @InjectLazy to break the cycle.`
|
|
244
|
+
)
|
|
245
|
+
}
|
|
246
|
+
throw err
|
|
247
|
+
}
|
|
248
|
+
if (instanceContext.proxy && instanceContext.originalClazz) {
|
|
249
|
+
const originalInstance = new instanceContext.originalClazz(...params)
|
|
250
|
+
instance = getProxy(instance, originalInstance)
|
|
251
|
+
}
|
|
252
|
+
if (instanceContext.type === 'singleton') {
|
|
253
|
+
instanceContext.instance = instance
|
|
173
254
|
}
|
|
255
|
+
return instance
|
|
174
256
|
}
|
package/package.json
CHANGED