decorator-dependency-injection 1.0.2 → 1.0.4
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 +124 -26
- package/eslint.config.js +73 -0
- package/index.d.ts +146 -0
- package/index.js +103 -107
- package/package.json +46 -7
- package/src/Container.js +184 -0
- package/src/proxy.js +42 -0
- package/.github/workflows/release.yml +0 -129
- package/babel.config.json +0 -6
- package/test/injection.test.js +0 -152
- package/test/mock.test.js +0 -77
- package/test/proxy.test.js +0 -73
package/README.md
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
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
|
+
[](https://github.com/mallocator/decorator-dependency-injection)
|
|
5
6
|
|
|
6
7
|
## Description
|
|
7
8
|
|
|
8
|
-
With [TC39](https://github.com/tc39/proposal-decorators) reaching stage 3
|
|
9
|
+
With the [TC39 proposal-decorators](https://github.com/tc39/proposal-decorators) reaching stage 3, it's time to start
|
|
10
|
+
thinking about how we can use them in our projects. One of the most common patterns in JavaScript is dependency
|
|
11
|
+
injection. This pattern is used to make our code more testable and maintainable. This library provides simple decorators
|
|
12
|
+
to help you inject dependencies into your classes and mock them for testing.
|
|
9
13
|
|
|
10
14
|
## Installation
|
|
11
15
|
|
|
@@ -13,21 +17,26 @@ With [TC39](https://github.com/tc39/proposal-decorators) reaching stage 3 on the
|
|
|
13
17
|
npm install decorator-dependency-injection
|
|
14
18
|
```
|
|
15
19
|
|
|
16
|
-
Until we reach stage 4, you will need to enable the decorators proposal in your project. You can do this by adding the
|
|
20
|
+
Until we reach stage 4, you will need to enable the decorators proposal in your project. You can do this by adding the
|
|
21
|
+
following babel transpiler options to your `.babelrc` file.
|
|
17
22
|
|
|
18
23
|
```json
|
|
19
24
|
{
|
|
20
|
-
"plugins": [
|
|
25
|
+
"plugins": [
|
|
26
|
+
"@babel/plugin-proposal-decorators"
|
|
27
|
+
]
|
|
21
28
|
}
|
|
22
29
|
```
|
|
23
30
|
|
|
24
|
-
To run your project with decorators enabled you will need to use the babel transpiler. You can do this by running the
|
|
31
|
+
To run your project with decorators enabled, you will need to use the babel transpiler. You can do this by running the
|
|
32
|
+
following command in your project root.
|
|
25
33
|
|
|
26
34
|
```bash
|
|
27
35
|
npx babel-node index.js
|
|
28
36
|
```
|
|
29
37
|
|
|
30
|
-
Finally, for running tests with decorators enabled you will need to use the babel-jest package. You can do this by
|
|
38
|
+
Finally, for running tests with decorators enabled, you will need to use the babel-jest package. You can do this by
|
|
39
|
+
adding the following configuration to your `package.json` file.
|
|
31
40
|
|
|
32
41
|
```json
|
|
33
42
|
{
|
|
@@ -43,20 +52,21 @@ Other testing frameworks may require a different configuration.
|
|
|
43
52
|
|
|
44
53
|
For a full example of how to set up a project with decorators, see this project's ```package.json``` file.
|
|
45
54
|
|
|
46
|
-
|
|
47
55
|
## Usage
|
|
48
56
|
|
|
49
|
-
There are
|
|
57
|
+
There are two ways of specifying injectable dependencies: ```@Singleton``` and ```@Factory```:
|
|
50
58
|
|
|
51
59
|
### Singleton
|
|
52
60
|
|
|
53
|
-
The ```@Singleton``` decorator is used to inject a single instance of a dependency into a class. This is useful when you
|
|
61
|
+
The ```@Singleton``` decorator is used to inject a single instance of a dependency into a class. This is useful when you
|
|
62
|
+
want to share the same instance of a class across multiple classes.
|
|
54
63
|
|
|
55
64
|
```javascript
|
|
56
|
-
import {
|
|
65
|
+
import {Singleton, Inject} from 'decorator-dependency-injection';
|
|
57
66
|
|
|
58
|
-
@Singleton
|
|
59
|
-
class Dependency {
|
|
67
|
+
@Singleton()
|
|
68
|
+
class Dependency {
|
|
69
|
+
}
|
|
60
70
|
|
|
61
71
|
class Consumer {
|
|
62
72
|
@Inject(Dependency) dependency // creates an instance only once
|
|
@@ -65,25 +75,48 @@ class Consumer {
|
|
|
65
75
|
|
|
66
76
|
### Factory
|
|
67
77
|
|
|
68
|
-
The ```@Factory``` decorator is used to inject a new instance of a dependency into a class each time it is requested.
|
|
78
|
+
The ```@Factory``` decorator is used to inject a new instance of a dependency into a class each time it is requested.
|
|
79
|
+
This is useful when you want to create a new instance of a class each time it is injected.
|
|
69
80
|
|
|
70
81
|
```javascript
|
|
71
|
-
import {
|
|
82
|
+
import {Factory, Inject} from 'decorator-dependency-injection';
|
|
72
83
|
|
|
73
|
-
@Factory
|
|
74
|
-
class Dependency {
|
|
84
|
+
@Factory()
|
|
85
|
+
class Dependency {
|
|
86
|
+
}
|
|
75
87
|
|
|
76
88
|
class Consumer {
|
|
77
89
|
@Inject(Dependency) dependency // creates a new instance each time a new Consumer is created
|
|
78
90
|
}
|
|
79
91
|
```
|
|
80
92
|
|
|
93
|
+
### InjectLazy
|
|
94
|
+
|
|
95
|
+
```@Inject``` annotated properties are evaluated during instance initialization. That means that all properties should
|
|
96
|
+
be accessible in the constructor. That also means that we're creating an instance no matter if you access the property
|
|
97
|
+
or not. If you want to only create an instance when you access the property, you can use the ```@InjectLazy```
|
|
98
|
+
decorator. This will create the instance only when the property is accessed for the first time. Note that this also
|
|
99
|
+
works from the constructor, same as the regular ```@Inject```.
|
|
100
|
+
|
|
101
|
+
```javascript
|
|
102
|
+
import {Singleton, InjectLazy} from 'decorator-dependency-injection';
|
|
103
|
+
|
|
104
|
+
@Singleton()
|
|
105
|
+
class Dependency {
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
class Consumer {
|
|
109
|
+
@InjectLazy(Dependency) dependency // creates an instance only when the property is accessed
|
|
110
|
+
}
|
|
111
|
+
```
|
|
112
|
+
|
|
81
113
|
## Passing parameters to a dependency
|
|
82
114
|
|
|
83
|
-
You can pass parameters to a dependency by using the ```@Inject``` decorator with a function that returns the
|
|
115
|
+
You can pass parameters to a dependency by using the ```@Inject``` decorator with a function that returns the
|
|
116
|
+
dependency.
|
|
84
117
|
|
|
85
118
|
```javascript
|
|
86
|
-
import {
|
|
119
|
+
import {Factory, Inject} from 'decorator-dependency-injection';
|
|
87
120
|
|
|
88
121
|
@Factory
|
|
89
122
|
class Dependency {
|
|
@@ -98,16 +131,17 @@ class Consumer {
|
|
|
98
131
|
}
|
|
99
132
|
```
|
|
100
133
|
|
|
101
|
-
While this is most useful for Factory dependencies, it can also be used with Singleton dependencies. However, parameters
|
|
134
|
+
While this is most useful for Factory dependencies, it can also be used with Singleton dependencies. However, parameters
|
|
135
|
+
will only be passed to the dependency the first time it is created.
|
|
102
136
|
|
|
103
137
|
## Mocking dependencies for testing
|
|
104
138
|
|
|
105
139
|
You can mock dependencies by using the ```@Mock``` decorator with a function that returns the mock dependency.
|
|
106
140
|
|
|
107
141
|
```javascript
|
|
108
|
-
import {
|
|
142
|
+
import {Factory, Inject, Mock, resetMock} from 'decorator-dependency-injection'
|
|
109
143
|
|
|
110
|
-
@Factory
|
|
144
|
+
@Factory()
|
|
111
145
|
class Dependency {
|
|
112
146
|
method() {
|
|
113
147
|
return 'real'
|
|
@@ -138,12 +172,35 @@ resetMock(Dependency)
|
|
|
138
172
|
const consumer = new Consumer() // prints 'real'
|
|
139
173
|
```
|
|
140
174
|
|
|
141
|
-
|
|
175
|
+
### Resetting Mocks
|
|
176
|
+
|
|
177
|
+
The `resetMock` utility function allows you to remove any active mock for a dependency and restore the original
|
|
178
|
+
implementation. This is useful for cleaning up after tests or switching between real and mock dependencies.
|
|
142
179
|
|
|
143
180
|
```javascript
|
|
144
|
-
import {
|
|
181
|
+
import {resetMock, resetMocks} from 'decorator-dependency-injection';
|
|
145
182
|
|
|
146
|
-
|
|
183
|
+
resetMock(Dependency); // Restores the original Dependency implementation
|
|
184
|
+
resetMocks(); // Restores all mocked dependencies
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
### Clearing the Container
|
|
188
|
+
|
|
189
|
+
For complete test isolation, you can clear all registered instances from the container:
|
|
190
|
+
|
|
191
|
+
```javascript
|
|
192
|
+
import {clearContainer} from 'decorator-dependency-injection';
|
|
193
|
+
|
|
194
|
+
clearContainer(); // Removes all registered singletons, factories, and mocks
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
You can also use the ```@Mock``` decorator as a proxy instead of a full mock. Any method calls not implemented in the
|
|
198
|
+
mock will be passed to the real dependency.
|
|
199
|
+
|
|
200
|
+
```javascript
|
|
201
|
+
import {Factory, Inject, Mock, resetMock} from 'decorator-dependency-injection'
|
|
202
|
+
|
|
203
|
+
@Factory()
|
|
147
204
|
class Dependency {
|
|
148
205
|
method() {
|
|
149
206
|
return 'real'
|
|
@@ -180,6 +237,45 @@ const consumer = new Consumer() // prints 'real other'
|
|
|
180
237
|
|
|
181
238
|
For more examples, see the tests in the ```test``` directory.
|
|
182
239
|
|
|
240
|
+
## Advanced Usage
|
|
241
|
+
|
|
242
|
+
### Using Isolated Containers
|
|
243
|
+
|
|
244
|
+
For advanced scenarios like parallel test execution or module isolation, you can create separate containers:
|
|
245
|
+
|
|
246
|
+
```javascript
|
|
247
|
+
import {Container} from 'decorator-dependency-injection';
|
|
248
|
+
|
|
249
|
+
const container1 = new Container();
|
|
250
|
+
const container2 = new Container();
|
|
251
|
+
|
|
252
|
+
class MyService {}
|
|
253
|
+
|
|
254
|
+
// Register the same class in different containers
|
|
255
|
+
container1.registerSingleton(MyService);
|
|
256
|
+
container2.registerSingleton(MyService);
|
|
257
|
+
|
|
258
|
+
// Each container maintains its own singleton instance
|
|
259
|
+
const ctx1 = container1.getContext(MyService);
|
|
260
|
+
const ctx2 = container2.getContext(MyService);
|
|
261
|
+
|
|
262
|
+
const instance1 = container1.getInstance(ctx1, []);
|
|
263
|
+
const instance2 = container2.getInstance(ctx2, []);
|
|
264
|
+
|
|
265
|
+
console.log(instance1 === instance2); // false - different containers
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
### Accessing the Default Container
|
|
269
|
+
|
|
270
|
+
You can access the default global container for programmatic registration:
|
|
271
|
+
|
|
272
|
+
```javascript
|
|
273
|
+
import {getContainer} from 'decorator-dependency-injection';
|
|
274
|
+
|
|
275
|
+
const container = getContainer();
|
|
276
|
+
console.log(container.has(MyService)); // Check if a class is registered
|
|
277
|
+
```
|
|
278
|
+
|
|
183
279
|
## Running the tests
|
|
184
280
|
|
|
185
281
|
To run the tests, run the following command in the project root.
|
|
@@ -192,4 +288,6 @@ npm test
|
|
|
192
288
|
|
|
193
289
|
- 1.0.0 - Initial release
|
|
194
290
|
- 1.0.1 - Automated release with GitHub Actions
|
|
195
|
-
- 1.0.2 - Added proxy option to @Mock decorator
|
|
291
|
+
- 1.0.2 - Added proxy option to @Mock decorator
|
|
292
|
+
- 1.0.3 - Added @InjectLazy decorator
|
|
293
|
+
- 1.0.4 - Added Container abstraction, clearContainer(), TypeScript definitions, improved proxy support
|
package/eslint.config.js
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { defineConfig } from "eslint/config";
|
|
2
|
+
import js from "@eslint/js";
|
|
3
|
+
import babelParser from "@babel/eslint-parser";
|
|
4
|
+
|
|
5
|
+
export default defineConfig([
|
|
6
|
+
// Recommended base config
|
|
7
|
+
js.configs.recommended,
|
|
8
|
+
|
|
9
|
+
// Global ignores
|
|
10
|
+
{
|
|
11
|
+
ignores: ["node_modules/**", "coverage/**", "docs/**", ".history/**"]
|
|
12
|
+
},
|
|
13
|
+
|
|
14
|
+
// Source files configuration
|
|
15
|
+
{
|
|
16
|
+
name: "source-files",
|
|
17
|
+
files: ["index.js", "src/**/*.js"],
|
|
18
|
+
languageOptions: {
|
|
19
|
+
ecmaVersion: 2022,
|
|
20
|
+
sourceType: "module",
|
|
21
|
+
parser: babelParser,
|
|
22
|
+
parserOptions: {
|
|
23
|
+
requireConfigFile: true,
|
|
24
|
+
babelOptions: {
|
|
25
|
+
configFile: "./babel.config.json"
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
globals: {
|
|
29
|
+
console: "readonly",
|
|
30
|
+
process: "readonly"
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
rules: {
|
|
34
|
+
"no-unused-vars": ["error", { "argsIgnorePattern": "^_" }],
|
|
35
|
+
"prefer-const": "error",
|
|
36
|
+
"no-var": "error",
|
|
37
|
+
"eqeqeq": ["error", "always"],
|
|
38
|
+
"no-throw-literal": "error"
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
|
|
42
|
+
// Test files with relaxed rules and Jest globals
|
|
43
|
+
{
|
|
44
|
+
name: "test-files",
|
|
45
|
+
files: ["test/**/*.js"],
|
|
46
|
+
languageOptions: {
|
|
47
|
+
ecmaVersion: 2022,
|
|
48
|
+
sourceType: "module",
|
|
49
|
+
parser: babelParser,
|
|
50
|
+
parserOptions: {
|
|
51
|
+
requireConfigFile: true,
|
|
52
|
+
babelOptions: {
|
|
53
|
+
configFile: "./babel.config.json"
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
globals: {
|
|
57
|
+
console: "readonly",
|
|
58
|
+
process: "readonly",
|
|
59
|
+
describe: "readonly",
|
|
60
|
+
it: "readonly",
|
|
61
|
+
expect: "readonly",
|
|
62
|
+
beforeEach: "readonly",
|
|
63
|
+
afterEach: "readonly",
|
|
64
|
+
jest: "readonly",
|
|
65
|
+
fail: "readonly"
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
rules: {
|
|
69
|
+
// In tests, decorated classes are used by the decorator system, not directly
|
|
70
|
+
"no-unused-vars": ["warn", { "argsIgnorePattern": "^_", "varsIgnorePattern": "^_" }]
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
]);
|
package/index.d.ts
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type definitions for decorator-dependency-injection
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Context for registered instances in the container
|
|
7
|
+
*/
|
|
8
|
+
export interface InstanceContext {
|
|
9
|
+
/** The type of registration */
|
|
10
|
+
type: 'singleton' | 'factory'
|
|
11
|
+
/** The current class constructor (may be a mock) */
|
|
12
|
+
clazz: new (...args: any[]) => any
|
|
13
|
+
/** The original class constructor if mocked */
|
|
14
|
+
originalClazz?: new (...args: any[]) => any
|
|
15
|
+
/** The cached singleton instance */
|
|
16
|
+
instance?: any
|
|
17
|
+
/** Whether to use proxy mocking */
|
|
18
|
+
proxy?: boolean
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* A dependency injection container that manages singleton and factory instances.
|
|
23
|
+
*/
|
|
24
|
+
export declare class Container {
|
|
25
|
+
/**
|
|
26
|
+
* Register a class as a singleton.
|
|
27
|
+
*/
|
|
28
|
+
registerSingleton(clazz: new (...args: any[]) => any, name?: string): void
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Register a class as a factory.
|
|
32
|
+
*/
|
|
33
|
+
registerFactory(clazz: new (...args: any[]) => any, name?: string): void
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Get the context for a given class or name.
|
|
37
|
+
*/
|
|
38
|
+
getContext(clazzOrName: string | (new (...args: any[]) => any)): InstanceContext
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Check if a class or name is registered.
|
|
42
|
+
*/
|
|
43
|
+
has(clazzOrName: string | (new (...args: any[]) => any)): boolean
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Get or create an instance based on the context.
|
|
47
|
+
*/
|
|
48
|
+
getInstance(instanceContext: InstanceContext, params: any[]): any
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Register a mock for an existing class.
|
|
52
|
+
*/
|
|
53
|
+
registerMock(
|
|
54
|
+
targetClazzOrName: string | (new (...args: any[]) => any),
|
|
55
|
+
mockClazz: new (...args: any[]) => any,
|
|
56
|
+
useProxy?: boolean
|
|
57
|
+
): void
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Reset a specific mock to its original class.
|
|
61
|
+
*/
|
|
62
|
+
resetMock(clazzOrName: string | (new (...args: any[]) => any)): void
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Reset all mocks to their original classes.
|
|
66
|
+
*/
|
|
67
|
+
resetAllMocks(): void
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Clear all registered instances and mocks.
|
|
71
|
+
*/
|
|
72
|
+
clear(): void
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Register a class as a singleton.
|
|
77
|
+
* @param name Optional name to register the singleton under
|
|
78
|
+
*/
|
|
79
|
+
export declare function Singleton(name?: string): ClassDecorator
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Register a class as a factory.
|
|
83
|
+
* @param name Optional name to register the factory under
|
|
84
|
+
*/
|
|
85
|
+
export declare function Factory(name?: string): ClassDecorator
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Inject a singleton or factory instance into a class field.
|
|
89
|
+
* @param clazzOrName The class or name to inject
|
|
90
|
+
* @param params Optional parameters to pass to the constructor
|
|
91
|
+
*/
|
|
92
|
+
export declare function Inject<T>(
|
|
93
|
+
clazzOrName: string | (new (...args: any[]) => T),
|
|
94
|
+
...params: any[]
|
|
95
|
+
): PropertyDecorator
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Inject a singleton or factory instance lazily into a class field.
|
|
99
|
+
* The instance is created on first access.
|
|
100
|
+
* @param clazzOrName The class or name to inject
|
|
101
|
+
* @param params Optional parameters to pass to the constructor
|
|
102
|
+
*/
|
|
103
|
+
export declare function InjectLazy<T>(
|
|
104
|
+
clazzOrName: string | (new (...args: any[]) => T),
|
|
105
|
+
...params: any[]
|
|
106
|
+
): PropertyDecorator
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Mark a class as a mock for another class.
|
|
110
|
+
* @param mockedClazzOrName The class or name to mock
|
|
111
|
+
* @param proxy If true, unmocked methods delegate to the original
|
|
112
|
+
*/
|
|
113
|
+
export declare function Mock(
|
|
114
|
+
mockedClazzOrName: string | (new (...args: any[]) => any),
|
|
115
|
+
proxy?: boolean
|
|
116
|
+
): ClassDecorator
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Reset all mocks to their original classes.
|
|
120
|
+
*/
|
|
121
|
+
export declare function resetMocks(): void
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Reset a specific mock to its original class.
|
|
125
|
+
* @param clazzOrName The class or name to reset
|
|
126
|
+
*/
|
|
127
|
+
export declare function resetMock(clazzOrName: string | (new (...args: any[]) => any)): void
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Clear all registered instances and mocks from the container.
|
|
131
|
+
*/
|
|
132
|
+
export declare function clearContainer(): void
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Get the default container instance.
|
|
136
|
+
*/
|
|
137
|
+
export declare function getContainer(): Container
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Create a proxy that delegates to the mock first, then falls back to the original.
|
|
141
|
+
* This is an internal utility but exported for advanced use cases.
|
|
142
|
+
*
|
|
143
|
+
* @param mock The mock instance
|
|
144
|
+
* @param original The original instance to fall back to
|
|
145
|
+
*/
|
|
146
|
+
export declare function createProxy<T extends object>(mock: T, original: T): T
|