decorator-dependency-injection 1.0.1 → 1.0.2
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 +12 -7
- package/README.md +61 -11
- package/index.js +69 -53
- package/package.json +1 -1
- package/test/mock.test.js +6 -0
- package/test/proxy.test.js +73 -0
|
@@ -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
|
@@ -88,8 +88,8 @@ import { Factory, Inject } from 'decorator-dependency-injection';
|
|
|
88
88
|
@Factory
|
|
89
89
|
class Dependency {
|
|
90
90
|
constructor(param1, param2) {
|
|
91
|
-
this.param1 = param1
|
|
92
|
-
this.param2 = param2
|
|
91
|
+
this.param1 = param1
|
|
92
|
+
this.param2 = param2
|
|
93
93
|
}
|
|
94
94
|
}
|
|
95
95
|
|
|
@@ -105,33 +105,77 @@ While this is most useful for Factory dependencies, it can also be used with Sin
|
|
|
105
105
|
You can mock dependencies by using the ```@Mock``` decorator with a function that returns the mock dependency.
|
|
106
106
|
|
|
107
107
|
```javascript
|
|
108
|
-
import { Factory, Inject, Mock } from 'decorator-dependency-injection'
|
|
108
|
+
import { Factory, Inject, Mock } from 'decorator-dependency-injection'
|
|
109
109
|
|
|
110
110
|
@Factory
|
|
111
111
|
class Dependency {
|
|
112
112
|
method() {
|
|
113
|
-
return 'real'
|
|
113
|
+
return 'real'
|
|
114
114
|
}
|
|
115
115
|
}
|
|
116
116
|
|
|
117
117
|
class Consumer {
|
|
118
118
|
@Inject(Dependency) dependency
|
|
119
119
|
|
|
120
|
-
|
|
121
|
-
|
|
120
|
+
constructor() {
|
|
121
|
+
console.log(this.dependency.method())
|
|
122
122
|
}
|
|
123
123
|
}
|
|
124
124
|
|
|
125
|
-
|
|
125
|
+
// Test Code
|
|
126
|
+
|
|
127
|
+
@Mock(Dependency)
|
|
128
|
+
class MockDependency {
|
|
129
|
+
method() {
|
|
130
|
+
return 'mock'
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const consumer = new Consumer() // prints 'mock'
|
|
135
|
+
|
|
136
|
+
resetMock(Dependency)
|
|
137
|
+
|
|
138
|
+
const consumer = new Consumer() // prints 'real'
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
You can also use the ```@Mock``` decorator as a proxy instead of a full mock. Any method calls not implemented in the mock will be passed to the real dependency.
|
|
142
|
+
|
|
143
|
+
```javascript
|
|
144
|
+
import { Factory, Inject, Mock } from 'decorator-dependency-injection'
|
|
145
|
+
|
|
146
|
+
@Factory
|
|
147
|
+
class Dependency {
|
|
148
|
+
method() {
|
|
149
|
+
return 'real'
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
otherMethod() {
|
|
153
|
+
return 'other'
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
class Consumer {
|
|
158
|
+
@Inject(Dependency) dependency
|
|
159
|
+
|
|
160
|
+
constructor() {
|
|
161
|
+
console.log(this.dependency.method(), this.dependency.otherMethod())
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Test Code
|
|
166
|
+
|
|
167
|
+
@Mock(Dependency, true)
|
|
126
168
|
class MockDependency {
|
|
127
169
|
method() {
|
|
128
|
-
return 'mock'
|
|
170
|
+
return 'mock'
|
|
129
171
|
}
|
|
130
172
|
}
|
|
131
173
|
|
|
132
|
-
const consumer = new Consumer()
|
|
174
|
+
const consumer = new Consumer() // prints 'mock other'
|
|
133
175
|
|
|
134
|
-
|
|
176
|
+
resetMock(Dependency)
|
|
177
|
+
|
|
178
|
+
const consumer = new Consumer() // prints 'real other'
|
|
135
179
|
```
|
|
136
180
|
|
|
137
181
|
For more examples, see the tests in the ```test``` directory.
|
|
@@ -142,4 +186,10 @@ To run the tests, run the following command in the project root.
|
|
|
142
186
|
|
|
143
187
|
```bash
|
|
144
188
|
npm test
|
|
145
|
-
```
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
## Version History
|
|
192
|
+
|
|
193
|
+
- 1.0.0 - Initial release
|
|
194
|
+
- 1.0.1 - Automated release with GitHub Actions
|
|
195
|
+
- 1.0.2 - Added proxy option to @Mock decorator
|
package/index.js
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @typedef {Object} InstanceContext
|
|
3
|
+
* @property {string} type The type of the instance, either 'singleton' or 'factory'
|
|
3
4
|
* @property {Class} clazz The class of the instance
|
|
5
|
+
* @property {Class} [originalClazz] The original class if it is a mock
|
|
4
6
|
* @property {Object} [instance] The instance if it is a singleton
|
|
5
|
-
* @property {
|
|
7
|
+
* @property {Object} [originalInstance] The original instance if it is a mock
|
|
8
|
+
* @property {boolean} [proxy=false] If true, the mock if the injection instance will be a proxy to the original class
|
|
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.
|
|
@@ -27,13 +28,10 @@ export function Singleton(name) {
|
|
|
27
28
|
if (context.kind !== "class") {
|
|
28
29
|
throw new Error('Invalid injection target')
|
|
29
30
|
}
|
|
30
|
-
if (
|
|
31
|
-
throw new Error('
|
|
32
|
-
}
|
|
33
|
-
if (factories.has(name ?? clazz)) {
|
|
34
|
-
throw new Error('Factory with the same name already defined')
|
|
31
|
+
if (instances.has(name ?? clazz)) {
|
|
32
|
+
throw new Error('Instance with that name or class already instantiated')
|
|
35
33
|
}
|
|
36
|
-
|
|
34
|
+
instances.set(name ?? clazz, { clazz, type: 'singleton' })
|
|
37
35
|
}
|
|
38
36
|
}
|
|
39
37
|
|
|
@@ -54,13 +52,10 @@ export function Factory(name) {
|
|
|
54
52
|
if (context.kind !== "class") {
|
|
55
53
|
throw new Error('Invalid injection target')
|
|
56
54
|
}
|
|
57
|
-
if (
|
|
58
|
-
throw new Error('
|
|
55
|
+
if (instances.has(name ?? clazz)) {
|
|
56
|
+
throw new Error('Instance with that name or class already instantiated')
|
|
59
57
|
}
|
|
60
|
-
|
|
61
|
-
throw new Error('Singleton with the same name already defined')
|
|
62
|
-
}
|
|
63
|
-
factories.set(name ?? clazz, { clazz })
|
|
58
|
+
instances.set(name ?? clazz, { clazz, type: 'factory' })
|
|
64
59
|
}
|
|
65
60
|
}
|
|
66
61
|
|
|
@@ -69,7 +64,7 @@ export function Factory(name) {
|
|
|
69
64
|
* If the instance is a singleton, it will only be created once with the first set of parameters it encounters.
|
|
70
65
|
*
|
|
71
66
|
* @param {string|Class} clazzOrName The singleton or factory class or name
|
|
72
|
-
* @param {*} params Parameters to pass to the constructor
|
|
67
|
+
* @param {*} params Parameters to pass to the constructor. Recommended to use only with factories.
|
|
73
68
|
* @return {(function(*): void)|*}
|
|
74
69
|
* @example @Inject(MySingleton) mySingleton
|
|
75
70
|
* @example @Inject("myCustomName") myFactory
|
|
@@ -83,18 +78,32 @@ export function Inject(clazzOrName, ...params) {
|
|
|
83
78
|
if (initialValue) {
|
|
84
79
|
throw new Error('Cannot assign value to injected field')
|
|
85
80
|
}
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
81
|
+
const instanceContext = getContext(clazzOrName)
|
|
82
|
+
|
|
83
|
+
if (instanceContext.instance) {
|
|
84
|
+
return instanceContext.instance
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const instance = new instanceContext.clazz(...params)
|
|
88
|
+
|
|
89
|
+
if (instanceContext.type === 'singleton') {
|
|
90
|
+
if (instanceContext.originalClazz && instanceContext.proxy) {
|
|
91
|
+
instanceContext.instance = getProxy(instance, new instanceContext.originalClazz(...params))
|
|
92
|
+
} else {
|
|
93
|
+
instanceContext.instance = instance
|
|
90
94
|
}
|
|
91
|
-
return
|
|
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')
|
|
95
|
+
return instanceContext.instance
|
|
97
96
|
}
|
|
97
|
+
|
|
98
|
+
if (instanceContext.type === 'factory') {
|
|
99
|
+
if (instanceContext.originalClazz && instanceContext.proxy) {
|
|
100
|
+
return getProxy(instance, new instanceContext.originalClazz(...params))
|
|
101
|
+
} else {
|
|
102
|
+
return instance
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
throw new Error('Unexpected injection type')
|
|
98
107
|
}
|
|
99
108
|
} else {
|
|
100
109
|
throw new Error('Invalid injection target')
|
|
@@ -102,37 +111,47 @@ export function Inject(clazzOrName, ...params) {
|
|
|
102
111
|
}
|
|
103
112
|
}
|
|
104
113
|
|
|
114
|
+
function getProxy(mock, original) {
|
|
115
|
+
return new Proxy(mock, {
|
|
116
|
+
get(target, prop, receiver) {
|
|
117
|
+
if (prop in target) {
|
|
118
|
+
return Reflect.get(target, prop, receiver)
|
|
119
|
+
}
|
|
120
|
+
return Reflect.get(original, prop, receiver)
|
|
121
|
+
}
|
|
122
|
+
})
|
|
123
|
+
}
|
|
124
|
+
|
|
105
125
|
/**
|
|
106
126
|
* Mark a class as a mock. This will replace the class with a mock instance when injected.
|
|
107
127
|
* @param {string|Class} mockedClazzOrName The singleton or factory class or name to be mocked
|
|
128
|
+
* @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.
|
|
108
129
|
* @return {(function(*, *): void)|*}
|
|
109
130
|
* @example @Mock(MySingleton) class MyMock {}
|
|
110
131
|
* @example @Mock("myCustomName") class MyMock {}
|
|
111
132
|
* @throws {Error} If the injection target is not a class
|
|
112
133
|
* @throws {Error} If the injection source is not found
|
|
113
134
|
*/
|
|
114
|
-
export function Mock(mockedClazzOrName) {
|
|
135
|
+
export function Mock(mockedClazzOrName, proxy = false) {
|
|
115
136
|
return function(clazz, context) {
|
|
116
137
|
if (context.kind !== "class") {
|
|
117
138
|
throw new Error('Invalid injection target')
|
|
118
139
|
}
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
if (instanceContext.original) {
|
|
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) {
|
|
140
|
+
const instanceContext = getContext(mockedClazzOrName)
|
|
141
|
+
if (instanceContext.originalClazz) {
|
|
129
142
|
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')
|
|
135
143
|
}
|
|
144
|
+
instanceContext.originalClazz = instanceContext.clazz
|
|
145
|
+
instanceContext.proxy = proxy
|
|
146
|
+
instanceContext.clazz = clazz
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function getContext(mockedClazzOrName) {
|
|
151
|
+
if (instances.has(mockedClazzOrName)) {
|
|
152
|
+
return instances.get(mockedClazzOrName)
|
|
153
|
+
} else {
|
|
154
|
+
throw new Error('Cannot find injection source with the provided name')
|
|
136
155
|
}
|
|
137
156
|
}
|
|
138
157
|
|
|
@@ -140,10 +159,7 @@ export function Mock(mockedClazzOrName) {
|
|
|
140
159
|
* Reset all mocks to their original classes.
|
|
141
160
|
*/
|
|
142
161
|
export function resetMocks() {
|
|
143
|
-
for (const instanceContext of
|
|
144
|
-
reset(instanceContext)
|
|
145
|
-
}
|
|
146
|
-
for (const instanceContext of factories.values()) {
|
|
162
|
+
for (const instanceContext of instances.values()) {
|
|
147
163
|
reset(instanceContext)
|
|
148
164
|
}
|
|
149
165
|
}
|
|
@@ -153,8 +169,7 @@ export function resetMocks() {
|
|
|
153
169
|
* @param {string|Class} clazzOrName The singleton or factory class or name to reset
|
|
154
170
|
*/
|
|
155
171
|
export function resetMock(clazzOrName) {
|
|
156
|
-
|
|
157
|
-
reset(instanceContext)
|
|
172
|
+
reset(getContext(clazzOrName))
|
|
158
173
|
}
|
|
159
174
|
|
|
160
175
|
/**
|
|
@@ -166,9 +181,10 @@ function reset(instanceContext) {
|
|
|
166
181
|
if (!instanceContext) {
|
|
167
182
|
throw new Error('Cannot find injection source with the provided name')
|
|
168
183
|
}
|
|
169
|
-
if (instanceContext.
|
|
170
|
-
instanceContext.clazz = instanceContext.
|
|
171
|
-
|
|
172
|
-
delete instanceContext.
|
|
184
|
+
if (instanceContext.originalClazz) {
|
|
185
|
+
instanceContext.clazz = instanceContext.originalClazz
|
|
186
|
+
instanceContext.instance = instanceContext.originalInstance
|
|
187
|
+
delete instanceContext.originalClazz
|
|
188
|
+
delete instanceContext.originalInstance
|
|
173
189
|
}
|
|
174
190
|
}
|
package/package.json
CHANGED
package/test/mock.test.js
CHANGED
|
@@ -6,6 +6,10 @@ describe('Mocking', () => {
|
|
|
6
6
|
op() {
|
|
7
7
|
return 'original'
|
|
8
8
|
}
|
|
9
|
+
|
|
10
|
+
op2() {
|
|
11
|
+
return 'original2'
|
|
12
|
+
}
|
|
9
13
|
}
|
|
10
14
|
|
|
11
15
|
class TestInjection {
|
|
@@ -26,10 +30,12 @@ describe('Mocking', () => {
|
|
|
26
30
|
|
|
27
31
|
const result = new TestInjection()
|
|
28
32
|
expect(result.toBeMockedSingleton.op()).toBe('mocked')
|
|
33
|
+
expect(result.toBeMockedSingleton.op2).toBe.undefined
|
|
29
34
|
|
|
30
35
|
resetMocks()
|
|
31
36
|
const result2 = new TestInjection()
|
|
32
37
|
expect(result2.toBeMockedSingleton.op()).toBe('original')
|
|
38
|
+
expect(result2.toBeMockedSingleton.op2()).toBe('original2')
|
|
33
39
|
})
|
|
34
40
|
|
|
35
41
|
@Factory()
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import {resetMocks, Inject, Mock, Singleton, Factory} from '../index.js'
|
|
2
|
+
|
|
3
|
+
describe('Proxy Mocking', () => {
|
|
4
|
+
@Singleton()
|
|
5
|
+
class ToBeProxiedSingleton {
|
|
6
|
+
op() {
|
|
7
|
+
return 'original'
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
op2() {
|
|
11
|
+
return 'original2'
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
class TestInjection {
|
|
16
|
+
@Inject(ToBeProxiedSingleton) toBeProxiedSingleton
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
afterEach(() => {
|
|
20
|
+
resetMocks()
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
it('should inject a proxy singleton', () => {
|
|
24
|
+
@Mock(ToBeProxiedSingleton, true)
|
|
25
|
+
class ProxiedSingleton {
|
|
26
|
+
op() {
|
|
27
|
+
return 'mocked'
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const result = new TestInjection()
|
|
32
|
+
expect(result.toBeProxiedSingleton.op()).toBe('mocked')
|
|
33
|
+
expect(result.toBeProxiedSingleton.op2()).toBe('original2')
|
|
34
|
+
|
|
35
|
+
resetMocks()
|
|
36
|
+
const result2 = new TestInjection()
|
|
37
|
+
expect(result2.toBeProxiedSingleton.op()).toBe('original')
|
|
38
|
+
expect(result2.toBeProxiedSingleton.op2()).toBe('original2')
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
@Factory()
|
|
42
|
+
class ToBeProxiedFactory {
|
|
43
|
+
op() {
|
|
44
|
+
return 'original'
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
op2() {
|
|
48
|
+
return 'original2'
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
class TestInjectionFactory {
|
|
53
|
+
@Inject(ToBeProxiedFactory) toBeProxiedFactory
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
it('should inject a proxy factory', () => {
|
|
57
|
+
@Mock(ToBeProxiedFactory, true)
|
|
58
|
+
class ProxiedFactory {
|
|
59
|
+
op() {
|
|
60
|
+
return 'mocked'
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const result = new TestInjectionFactory()
|
|
65
|
+
expect(result.toBeProxiedFactory.op()).toBe('mocked')
|
|
66
|
+
expect(result.toBeProxiedFactory.op2()).toBe('original2')
|
|
67
|
+
|
|
68
|
+
resetMocks()
|
|
69
|
+
const result2 = new TestInjectionFactory()
|
|
70
|
+
expect(result2.toBeProxiedFactory.op()).toBe('original')
|
|
71
|
+
expect(result2.toBeProxiedFactory.op2()).toBe('original2')
|
|
72
|
+
})
|
|
73
|
+
})
|