decorator-dependency-injection 1.0.3 → 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 +68 -16
- package/eslint.config.js +73 -0
- package/index.d.ts +146 -0
- package/index.js +56 -126
- 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 -309
- package/test/injectionLazy.test.js +0 -249
- package/test/mock.test.js +0 -295
- package/test/proxy.test.js +0 -130
package/README.md
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
[](http://badge.fury.io/js/decorator-dependency-injection)
|
|
4
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
|
|
|
@@ -61,9 +62,9 @@ The ```@Singleton``` decorator is used to inject a single instance of a dependen
|
|
|
61
62
|
want to share the same instance of a class across multiple classes.
|
|
62
63
|
|
|
63
64
|
```javascript
|
|
64
|
-
import {Singleton} from 'decorator-dependency-injection';
|
|
65
|
+
import {Singleton, Inject} from 'decorator-dependency-injection';
|
|
65
66
|
|
|
66
|
-
@Singleton
|
|
67
|
+
@Singleton()
|
|
67
68
|
class Dependency {
|
|
68
69
|
}
|
|
69
70
|
|
|
@@ -78,9 +79,9 @@ The ```@Factory``` decorator is used to inject a new instance of a dependency in
|
|
|
78
79
|
This is useful when you want to create a new instance of a class each time it is injected.
|
|
79
80
|
|
|
80
81
|
```javascript
|
|
81
|
-
import {Factory} from 'decorator-dependency-injection';
|
|
82
|
+
import {Factory, Inject} from 'decorator-dependency-injection';
|
|
82
83
|
|
|
83
|
-
@Factory
|
|
84
|
+
@Factory()
|
|
84
85
|
class Dependency {
|
|
85
86
|
}
|
|
86
87
|
|
|
@@ -89,23 +90,23 @@ class Consumer {
|
|
|
89
90
|
}
|
|
90
91
|
```
|
|
91
92
|
|
|
92
|
-
###
|
|
93
|
+
### InjectLazy
|
|
93
94
|
|
|
94
95
|
```@Inject``` annotated properties are evaluated during instance initialization. That means that all properties should
|
|
95
96
|
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 ```@
|
|
97
|
+
or not. If you want to only create an instance when you access the property, you can use the ```@InjectLazy```
|
|
97
98
|
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
|
+
works from the constructor, same as the regular ```@Inject```.
|
|
99
100
|
|
|
100
101
|
```javascript
|
|
101
|
-
import {
|
|
102
|
+
import {Singleton, InjectLazy} from 'decorator-dependency-injection';
|
|
102
103
|
|
|
103
|
-
@Singleton
|
|
104
|
+
@Singleton()
|
|
104
105
|
class Dependency {
|
|
105
106
|
}
|
|
106
107
|
|
|
107
108
|
class Consumer {
|
|
108
|
-
@
|
|
109
|
+
@InjectLazy(Dependency) dependency // creates an instance only when the property is accessed
|
|
109
110
|
}
|
|
110
111
|
```
|
|
111
112
|
|
|
@@ -138,9 +139,9 @@ will only be passed to the dependency the first time it is created.
|
|
|
138
139
|
You can mock dependencies by using the ```@Mock``` decorator with a function that returns the mock dependency.
|
|
139
140
|
|
|
140
141
|
```javascript
|
|
141
|
-
import {Factory, Inject, Mock} from 'decorator-dependency-injection'
|
|
142
|
+
import {Factory, Inject, Mock, resetMock} from 'decorator-dependency-injection'
|
|
142
143
|
|
|
143
|
-
@Factory
|
|
144
|
+
@Factory()
|
|
144
145
|
class Dependency {
|
|
145
146
|
method() {
|
|
146
147
|
return 'real'
|
|
@@ -177,18 +178,29 @@ The `resetMock` utility function allows you to remove any active mock for a depe
|
|
|
177
178
|
implementation. This is useful for cleaning up after tests or switching between real and mock dependencies.
|
|
178
179
|
|
|
179
180
|
```javascript
|
|
180
|
-
import {resetMock} from 'decorator-dependency-injection';
|
|
181
|
+
import {resetMock, resetMocks} from 'decorator-dependency-injection';
|
|
181
182
|
|
|
182
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
|
|
183
195
|
```
|
|
184
196
|
|
|
185
197
|
You can also use the ```@Mock``` decorator as a proxy instead of a full mock. Any method calls not implemented in the
|
|
186
198
|
mock will be passed to the real dependency.
|
|
187
199
|
|
|
188
200
|
```javascript
|
|
189
|
-
import {Factory, Inject, Mock} from 'decorator-dependency-injection'
|
|
201
|
+
import {Factory, Inject, Mock, resetMock} from 'decorator-dependency-injection'
|
|
190
202
|
|
|
191
|
-
@Factory
|
|
203
|
+
@Factory()
|
|
192
204
|
class Dependency {
|
|
193
205
|
method() {
|
|
194
206
|
return 'real'
|
|
@@ -225,6 +237,45 @@ const consumer = new Consumer() // prints 'real other'
|
|
|
225
237
|
|
|
226
238
|
For more examples, see the tests in the ```test``` directory.
|
|
227
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
|
+
|
|
228
279
|
## Running the tests
|
|
229
280
|
|
|
230
281
|
To run the tests, run the following command in the project root.
|
|
@@ -238,4 +289,5 @@ npm test
|
|
|
238
289
|
- 1.0.0 - Initial release
|
|
239
290
|
- 1.0.1 - Automated release with GitHub Actions
|
|
240
291
|
- 1.0.2 - Added proxy option to @Mock decorator
|
|
241
|
-
- 1.0.3 - Added @
|
|
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
|
package/index.js
CHANGED
|
@@ -1,22 +1,22 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* @
|
|
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.
|
|
2
|
+
* Decorator Dependency Injection
|
|
3
|
+
*
|
|
4
|
+
* A simple library for dependency injection using TC39 Stage 3 decorators.
|
|
5
|
+
*
|
|
6
|
+
* @module decorator-dependency-injection
|
|
9
7
|
*/
|
|
10
8
|
|
|
11
|
-
|
|
12
|
-
|
|
9
|
+
import {Container} from './src/Container.js'
|
|
10
|
+
|
|
11
|
+
/** @type {Container} The default global container */
|
|
12
|
+
const defaultContainer = new Container()
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
15
|
* Register a class as a singleton. If a name is provided, it will be used as the key in the singleton map.
|
|
16
16
|
* Singleton instances only ever have one instance created via the @Inject decorator.
|
|
17
17
|
*
|
|
18
18
|
* @param {string} [name] The name of the singleton. If not provided, the class will be used as the key.
|
|
19
|
-
* @
|
|
19
|
+
* @returns {(function(Function, {kind: string}): void)}
|
|
20
20
|
* @example @Singleton() class MySingleton {}
|
|
21
21
|
* @example @Singleton('customName') class MySingleton {}
|
|
22
22
|
* @throws {Error} If the injection target is not a class
|
|
@@ -31,11 +31,7 @@ export function Singleton(name) {
|
|
|
31
31
|
if (typeof clazz !== 'function' || !clazz.prototype) {
|
|
32
32
|
throw new Error('Target must be a class constructor')
|
|
33
33
|
}
|
|
34
|
-
|
|
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')
|
|
37
|
-
}
|
|
38
|
-
instances.set(key, {clazz, type: 'singleton'})
|
|
34
|
+
defaultContainer.registerSingleton(clazz, name)
|
|
39
35
|
}
|
|
40
36
|
}
|
|
41
37
|
|
|
@@ -44,7 +40,7 @@ export function Singleton(name) {
|
|
|
44
40
|
* Factory instances are created via the @Inject decorator. Each call to the factory will create a new instance.
|
|
45
41
|
*
|
|
46
42
|
* @param {string} [name] The name of the factory. If not provided, the class will be used as the key.
|
|
47
|
-
* @
|
|
43
|
+
* @returns {(function(Function, {kind: string}): void)}
|
|
48
44
|
* @example @Factory() class MyFactory {}
|
|
49
45
|
* @example @Factory('customName') class MyFactory {}
|
|
50
46
|
* @throws {Error} If the injection target is not a class
|
|
@@ -59,11 +55,7 @@ export function Factory(name) {
|
|
|
59
55
|
if (typeof clazz !== 'function' || !clazz.prototype) {
|
|
60
56
|
throw new Error('Target must be a class constructor')
|
|
61
57
|
}
|
|
62
|
-
|
|
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')
|
|
65
|
-
}
|
|
66
|
-
instances.set(key, {clazz, type: 'factory'})
|
|
58
|
+
defaultContainer.registerFactory(clazz, name)
|
|
67
59
|
}
|
|
68
60
|
}
|
|
69
61
|
|
|
@@ -71,16 +63,16 @@ export function Factory(name) {
|
|
|
71
63
|
* Inject a singleton or factory instance into a class field. You can also provide parameters to the constructor.
|
|
72
64
|
* If the instance is a singleton, it will only be created once with the first set of parameters it encounters.
|
|
73
65
|
*
|
|
74
|
-
* @param {string|
|
|
75
|
-
* @param {
|
|
76
|
-
* @
|
|
66
|
+
* @param {string|Function} clazzOrName The singleton or factory class or name
|
|
67
|
+
* @param {...*} params Parameters to pass to the constructor. Recommended to use only with factories.
|
|
68
|
+
* @returns {(function(*, {kind: string, name: string}): function(): Object)}
|
|
77
69
|
* @example @Inject(MySingleton) mySingleton
|
|
78
70
|
* @example @Inject("myCustomName") myFactory
|
|
79
71
|
* @throws {Error} If the injection target is not a field
|
|
80
72
|
* @throws {Error} If the injected field is assigned a value
|
|
81
73
|
*/
|
|
82
74
|
export function Inject(clazzOrName, ...params) {
|
|
83
|
-
return function (
|
|
75
|
+
return function (_, context) {
|
|
84
76
|
if (context.kind !== 'field') {
|
|
85
77
|
throw new Error('Invalid injection target')
|
|
86
78
|
}
|
|
@@ -88,8 +80,8 @@ export function Inject(clazzOrName, ...params) {
|
|
|
88
80
|
if (initialValue) {
|
|
89
81
|
throw new Error('Cannot assign value to injected field')
|
|
90
82
|
}
|
|
91
|
-
const instanceContext = getContext(clazzOrName)
|
|
92
|
-
return
|
|
83
|
+
const instanceContext = defaultContainer.getContext(clazzOrName)
|
|
84
|
+
return defaultContainer.getInstance(instanceContext, params)
|
|
93
85
|
}
|
|
94
86
|
}
|
|
95
87
|
}
|
|
@@ -97,9 +89,10 @@ export function Inject(clazzOrName, ...params) {
|
|
|
97
89
|
/**
|
|
98
90
|
* Inject a singleton or factory instance lazily into a class field. You can also provide parameters to the constructor.
|
|
99
91
|
* If the instance is a singleton, it will only be created once with the first set of parameters it encounters.
|
|
100
|
-
*
|
|
101
|
-
* @param {
|
|
102
|
-
* @
|
|
92
|
+
*
|
|
93
|
+
* @param {string|Function} clazzOrName The singleton or factory class or name
|
|
94
|
+
* @param {...*} params Parameters to pass to the constructor. Recommended to use only with factories.
|
|
95
|
+
* @returns {(function(*, {kind: string, name: string, addInitializer: Function}): void)}
|
|
103
96
|
* @example @InjectLazy(MySingleton) mySingleton
|
|
104
97
|
* @example @InjectLazy("myCustomName") myFactory
|
|
105
98
|
* @throws {Error} If the injection target is not a field
|
|
@@ -107,7 +100,7 @@ export function Inject(clazzOrName, ...params) {
|
|
|
107
100
|
*/
|
|
108
101
|
export function InjectLazy(clazzOrName, ...params) {
|
|
109
102
|
const cache = new WeakMap()
|
|
110
|
-
return (
|
|
103
|
+
return (_, context) => {
|
|
111
104
|
if (context.kind !== 'field') {
|
|
112
105
|
throw new Error('Invalid injection target')
|
|
113
106
|
}
|
|
@@ -115,12 +108,15 @@ export function InjectLazy(clazzOrName, ...params) {
|
|
|
115
108
|
Object.defineProperty(this, context.name, {
|
|
116
109
|
get() {
|
|
117
110
|
if (!cache.has(this)) {
|
|
118
|
-
const instanceContext = getContext(clazzOrName)
|
|
119
|
-
const value =
|
|
111
|
+
const instanceContext = defaultContainer.getContext(clazzOrName)
|
|
112
|
+
const value = defaultContainer.getInstance(instanceContext, params)
|
|
120
113
|
cache.set(this, value)
|
|
121
114
|
}
|
|
122
115
|
return cache.get(this)
|
|
123
116
|
},
|
|
117
|
+
set() {
|
|
118
|
+
throw new Error(`Cannot assign value to lazy-injected field "${context.name}"`)
|
|
119
|
+
},
|
|
124
120
|
configurable: true,
|
|
125
121
|
enumerable: true
|
|
126
122
|
})
|
|
@@ -128,30 +124,15 @@ export function InjectLazy(clazzOrName, ...params) {
|
|
|
128
124
|
}
|
|
129
125
|
}
|
|
130
126
|
|
|
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
|
-
|
|
148
127
|
/**
|
|
149
128
|
* Mark a class as a mock. This will replace the class with a mock instance when injected.
|
|
150
|
-
*
|
|
151
|
-
* @param {
|
|
152
|
-
* @
|
|
129
|
+
*
|
|
130
|
+
* @param {string|Function} mockedClazzOrName The singleton or factory class or name to be mocked
|
|
131
|
+
* @param {boolean} [proxy=false] If true, the mock will proxy to the original class.
|
|
132
|
+
* Any methods not defined in the mock will be called on the original class.
|
|
133
|
+
* @returns {(function(Function, {kind: string}): void)}
|
|
153
134
|
* @example @Mock(MySingleton) class MyMock {}
|
|
154
|
-
* @example @Mock("myCustomName") class MyMock {}
|
|
135
|
+
* @example @Mock("myCustomName", true) class MyMock {}
|
|
155
136
|
* @throws {Error} If the injection target is not a class
|
|
156
137
|
* @throws {Error} If the injection source is not found
|
|
157
138
|
*/
|
|
@@ -160,32 +141,7 @@ export function Mock(mockedClazzOrName, proxy = false) {
|
|
|
160
141
|
if (context.kind !== 'class') {
|
|
161
142
|
throw new Error('Invalid injection target')
|
|
162
143
|
}
|
|
163
|
-
|
|
164
|
-
if (instanceContext.originalClazz) {
|
|
165
|
-
throw new Error('Mock already defined, reset before mocking again')
|
|
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
|
-
)
|
|
144
|
+
defaultContainer.registerMock(mockedClazzOrName, clazz, proxy)
|
|
189
145
|
}
|
|
190
146
|
}
|
|
191
147
|
|
|
@@ -193,64 +149,38 @@ function getContext(mockedClazzOrName) {
|
|
|
193
149
|
* Reset all mocks to their original classes.
|
|
194
150
|
*/
|
|
195
151
|
export function resetMocks() {
|
|
196
|
-
|
|
197
|
-
restoreOriginal(instanceContext)
|
|
198
|
-
}
|
|
152
|
+
defaultContainer.resetAllMocks()
|
|
199
153
|
}
|
|
200
154
|
|
|
201
155
|
/**
|
|
202
156
|
* Reset a specific mock to its original class.
|
|
203
|
-
*
|
|
157
|
+
*
|
|
158
|
+
* @param {string|Function} clazzOrName The singleton or factory class or name to reset
|
|
204
159
|
*/
|
|
205
160
|
export function resetMock(clazzOrName) {
|
|
206
|
-
|
|
161
|
+
defaultContainer.resetMock(clazzOrName)
|
|
207
162
|
}
|
|
208
163
|
|
|
209
164
|
/**
|
|
210
|
-
*
|
|
211
|
-
*
|
|
212
|
-
* @private
|
|
165
|
+
* Clear all registered instances and mocks from the container.
|
|
166
|
+
* Useful for complete test isolation between test suites.
|
|
213
167
|
*/
|
|
214
|
-
function
|
|
215
|
-
|
|
216
|
-
throw new Error('Cannot find injection source with the provided name')
|
|
217
|
-
}
|
|
218
|
-
if (instanceContext.originalClazz) {
|
|
219
|
-
instanceContext.clazz = instanceContext.originalClazz
|
|
220
|
-
delete instanceContext.instance
|
|
221
|
-
delete instanceContext.originalClazz
|
|
222
|
-
delete instanceContext.originalInstance
|
|
223
|
-
}
|
|
168
|
+
export function clearContainer() {
|
|
169
|
+
defaultContainer.clear()
|
|
224
170
|
}
|
|
225
171
|
|
|
226
172
|
/**
|
|
227
|
-
* Get the
|
|
228
|
-
*
|
|
229
|
-
*
|
|
230
|
-
* @
|
|
173
|
+
* Get the default container instance.
|
|
174
|
+
* Useful for advanced use cases or testing the container itself.
|
|
175
|
+
*
|
|
176
|
+
* @returns {Container} The default container
|
|
231
177
|
*/
|
|
232
|
-
function
|
|
233
|
-
|
|
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
|
|
254
|
-
}
|
|
255
|
-
return instance
|
|
178
|
+
export function getContainer() {
|
|
179
|
+
return defaultContainer
|
|
256
180
|
}
|
|
181
|
+
|
|
182
|
+
// Export Container class for advanced use cases (e.g., isolated containers)
|
|
183
|
+
export {Container}
|
|
184
|
+
|
|
185
|
+
// Export createProxy for advanced proxy use cases
|
|
186
|
+
export {createProxy} from './src/proxy.js'
|