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.
@@ -1,4 +1,4 @@
1
- name: Create Release
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 ${{ github.repository }}@${{ env.VERSION }} > /dev/null 2>&1; then
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: npm publish --access public
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
- [![npm version](https://badge.fury.io/js/decorator-dependency-injection.svg)](http://badge.fury.io/js/decorator-dependency-injection)
3
- [![Build Status](https://github.com/mallocator/decorator-dependency-injection/actions/workflows/node.js.yml/badge.svg)](https://github.com/mallocator/decorator-dependency-injection/actions/workflows/node.js.yml)
4
2
 
3
+ [![npm version](https://badge.fury.io/js/decorator-dependency-injection.svg)](http://badge.fury.io/js/decorator-dependency-injection)
4
+ [![Build Status](https://github.com/mallocator/decorator-dependency-injection/actions/workflows/release.yml/badge.svg)](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 on the decorators proposal, it's time to start thinking about how we can use them in our projects. One of the most common patterns in JavaScript is dependency injection. This pattern is used to make our code more testable and maintainable. This library provides simple decorators to help you inject dependencies into your classes and mock them for testing.
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 following babel transpiler options to your `.babelrc` file.
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": ["@babel/plugin-proposal-decorators"]
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 following command in your project root.
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 adding the following configuration to your `package.json` file.
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 2 ways of specifying injectable dependencies: ```@Singleton``` and ```@Factory```:
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 want to share the same instance of a class across multiple classes.
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 { Singleton } from 'decorator-dependency-injection';
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. This is useful when you want to create a new instance of a class each time it is injected.
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 { Factory } from 'decorator-dependency-injection';
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 dependency.
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 { Factory, Inject } from 'decorator-dependency-injection';
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 will only be passed to the dependency the first time it is created.
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 { Factory, Inject, Mock } from 'decorator-dependency-injection';
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
- test() {
121
- return this.dependency.method();
153
+ constructor() {
154
+ console.log(this.dependency.method())
122
155
  }
123
156
  }
124
157
 
125
- @Mock
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
- consumer.test(); // returns 'real'
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 {Class} clazz The class of the instance
4
- * @property {Object} [instance] The instance if it is a singleton
5
- * @property {Class} [original] The original class if it is a mock
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 singletons = new Map()
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(*, *): void)|*}
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 a factory with the same name is already defined
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 !== "class") {
28
+ if (context.kind !== 'class') {
28
29
  throw new Error('Invalid injection target')
29
30
  }
30
- if (singletons.has(name ?? clazz)) {
31
- throw new Error('Singleton already defined')
31
+ if (typeof clazz !== 'function' || !clazz.prototype) {
32
+ throw new Error('Target must be a class constructor')
32
33
  }
33
- if (factories.has(name ?? clazz)) {
34
- throw new Error('Factory with the same name already defined')
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
- singletons.set(name ?? clazz, { clazz })
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(*, *): void)|*}
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 a singleton with the same name is already defined
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 !== "class") {
56
+ if (context.kind !== 'class') {
55
57
  throw new Error('Invalid injection target')
56
58
  }
57
- if (factories.has(name ?? clazz)) {
58
- throw new Error('Factory already defined')
59
+ if (typeof clazz !== 'function' || !clazz.prototype) {
60
+ throw new Error('Target must be a class constructor')
59
61
  }
60
- if (singletons.has(name ?? clazz)) {
61
- throw new Error('Singleton with the same name already defined')
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
- factories.set(name ?? clazz, { clazz })
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(*): void)|*}
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 === "field") {
82
- return function(initialValue) {
83
- if (initialValue) {
84
- throw new Error('Cannot assign value to injected field')
85
- }
86
- if (singletons.has(clazzOrName)) {
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
- } else {
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
- * @return {(function(*, *): void)|*}
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 !== "class") {
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
- if (singletons.has(mockedClazzOrName)) {
120
- const instanceContext = singletons.get(mockedClazzOrName)
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) {
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 singletons.values()) {
144
- reset(instanceContext)
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
- const instanceContext = singletons.get(clazzOrName) ?? factories.get(clazzOrName)
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 reset(instanceContext) {
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.original) {
170
- instanceContext.clazz = instanceContext.original
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "decorator-dependency-injection",
3
- "version": "1.0.1",
3
+ "version": "1.0.3",
4
4
  "description": "A simple library for dependency injection using decorators",
5
5
  "author": "Ravi Gairola <mallox@pyxzl.net>",
6
6
  "license": "Apache-2.0",