decorator-dependency-injection 1.0.6 → 1.0.7
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 +318 -386
- package/index.d.ts +173 -13
- package/index.js +246 -21
- package/package.json +18 -5
- package/src/Container.js +248 -26
- package/src/proxy.js +15 -15
package/README.md
CHANGED
|
@@ -1,15 +1,72 @@
|
|
|
1
1
|
# Decorator Dependency Injection
|
|
2
2
|
|
|
3
3
|
[](http://badge.fury.io/js/decorator-dependency-injection)
|
|
4
|
+
[](https://www.npmjs.com/package/decorator-dependency-injection)
|
|
4
5
|
[](https://github.com/mallocator/decorator-dependency-injection/actions/workflows/release.yml)
|
|
5
|
-
[](https://github.com/mallocator/decorator-dependency-injection)
|
|
7
|
+
[](https://github.com/mallocator/decorator-dependency-injection/blob/main/LICENSE)
|
|
8
|
+
|
|
9
|
+
**A lightweight dependency injection (DI) library for JavaScript and TypeScript using native TC39 Stage 3 decorators.**
|
|
10
|
+
|
|
11
|
+
No reflection. No metadata. No configuration files. Just decorators that work.
|
|
12
|
+
|
|
13
|
+
**Why this library?**
|
|
14
|
+
- Modern TC39 decorator syntax - no `reflect-metadata` or `emitDecoratorMetadata` needed
|
|
15
|
+
- Zero dependencies - tiny bundle size
|
|
16
|
+
- Built-in mocking support for unit testing with Jest, Vitest, or Mocha
|
|
17
|
+
- Full TypeScript support with type inference
|
|
18
|
+
- Works with Node.js, Babel, and modern bundlers
|
|
19
|
+
|
|
20
|
+
## Table of Contents
|
|
21
|
+
|
|
22
|
+
- [Quick Start](#quick-start)
|
|
23
|
+
- [Installation](#installation)
|
|
24
|
+
- [Core Concepts](#core-concepts)
|
|
25
|
+
- [Singleton](#singleton)
|
|
26
|
+
- [Factory](#factory)
|
|
27
|
+
- [Lazy Injection](#lazy-injection)
|
|
28
|
+
- [Passing Parameters](#passing-parameters)
|
|
29
|
+
- [Testing](#testing)
|
|
30
|
+
- [Mocking Dependencies](#mocking-dependencies)
|
|
31
|
+
- [Proxy Mocking](#proxy-mocking)
|
|
32
|
+
- [Test Lifecycle](#test-lifecycle)
|
|
33
|
+
- [Best Practices](#testing-best-practices)
|
|
34
|
+
- [Advanced Features](#advanced-features)
|
|
35
|
+
- [Private Fields](#private-fields)
|
|
36
|
+
- [Static Fields](#static-fields)
|
|
37
|
+
- [Named Registrations](#named-registrations)
|
|
38
|
+
- [Manual Resolution](#manual-resolution)
|
|
39
|
+
- [Container Introspection](#container-introspection)
|
|
40
|
+
- [Isolated Containers](#isolated-containers)
|
|
41
|
+
- [API Reference](#api-reference)
|
|
42
|
+
- [TypeScript Support](#typescript-support)
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## Quick Start
|
|
6
47
|
|
|
7
|
-
|
|
48
|
+
```javascript
|
|
49
|
+
import { Singleton, Inject } from 'decorator-dependency-injection'
|
|
50
|
+
|
|
51
|
+
@Singleton()
|
|
52
|
+
class Database {
|
|
53
|
+
query(sql) { return db.execute(sql) }
|
|
54
|
+
}
|
|
8
55
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
56
|
+
class UserService {
|
|
57
|
+
@Inject(Database) db
|
|
58
|
+
|
|
59
|
+
getUser(id) {
|
|
60
|
+
return this.db.query(`SELECT * FROM users WHERE id = ${id}`)
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
new UserService().getUser(1) // Database is automatically injected
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
**That's it.** The `Database` instance is created once and shared everywhere it's injected.
|
|
68
|
+
|
|
69
|
+
---
|
|
13
70
|
|
|
14
71
|
## Installation
|
|
15
72
|
|
|
@@ -17,555 +74,429 @@ to help you inject dependencies into your classes and mock them for testing.
|
|
|
17
74
|
npm install decorator-dependency-injection
|
|
18
75
|
```
|
|
19
76
|
|
|
20
|
-
|
|
21
|
-
|
|
77
|
+
<details>
|
|
78
|
+
<summary><strong>Babel Configuration (required until decorators reach Stage 4)</strong></summary>
|
|
79
|
+
|
|
80
|
+
Add to your `.babelrc` or `babel.config.json`:
|
|
22
81
|
|
|
23
82
|
```json
|
|
24
83
|
{
|
|
25
|
-
"plugins": [
|
|
26
|
-
"@babel/plugin-proposal-decorators"
|
|
27
|
-
]
|
|
84
|
+
"plugins": ["@babel/plugin-proposal-decorators"]
|
|
28
85
|
}
|
|
29
86
|
```
|
|
30
87
|
|
|
31
|
-
|
|
32
|
-
following command in your project root.
|
|
33
|
-
|
|
88
|
+
Run with Babel:
|
|
34
89
|
```bash
|
|
35
90
|
npx babel-node index.js
|
|
36
91
|
```
|
|
37
92
|
|
|
38
|
-
|
|
39
|
-
adding the following configuration to your `package.json` file.
|
|
40
|
-
|
|
93
|
+
For Jest, add to `package.json`:
|
|
41
94
|
```json
|
|
42
95
|
{
|
|
43
96
|
"jest": {
|
|
44
|
-
"transform": {
|
|
45
|
-
"^.+\\.jsx?$": "babel-jest"
|
|
46
|
-
}
|
|
97
|
+
"transform": { "^.+\\.jsx?$": "babel-jest" }
|
|
47
98
|
}
|
|
48
99
|
}
|
|
49
100
|
```
|
|
50
101
|
|
|
51
|
-
|
|
102
|
+
See this project's `package.json` for a complete working example.
|
|
52
103
|
|
|
53
|
-
|
|
104
|
+
</details>
|
|
54
105
|
|
|
55
|
-
|
|
106
|
+
---
|
|
56
107
|
|
|
57
|
-
|
|
108
|
+
## Core Concepts
|
|
58
109
|
|
|
59
110
|
### Singleton
|
|
60
111
|
|
|
61
|
-
|
|
62
|
-
want to share the same instance of a class across multiple classes.
|
|
112
|
+
A singleton creates **one shared instance** across your entire application:
|
|
63
113
|
|
|
64
114
|
```javascript
|
|
65
|
-
import {Singleton, Inject} from 'decorator-dependency-injection'
|
|
115
|
+
import { Singleton, Inject } from 'decorator-dependency-injection'
|
|
66
116
|
|
|
67
117
|
@Singleton()
|
|
68
|
-
class
|
|
118
|
+
class ConfigService {
|
|
119
|
+
apiUrl = 'https://api.example.com'
|
|
69
120
|
}
|
|
70
121
|
|
|
71
|
-
class
|
|
72
|
-
@Inject(
|
|
122
|
+
class ServiceA {
|
|
123
|
+
@Inject(ConfigService) config
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
class ServiceB {
|
|
127
|
+
@Inject(ConfigService) config // Same instance as ServiceA
|
|
73
128
|
}
|
|
74
129
|
```
|
|
75
130
|
|
|
76
131
|
### Factory
|
|
77
132
|
|
|
78
|
-
|
|
79
|
-
This is useful when you want to create a new instance of a class each time it is injected.
|
|
133
|
+
A factory creates a **new instance** each time it's injected:
|
|
80
134
|
|
|
81
135
|
```javascript
|
|
82
|
-
import {Factory, Inject} from 'decorator-dependency-injection'
|
|
136
|
+
import { Factory, Inject } from 'decorator-dependency-injection'
|
|
83
137
|
|
|
84
138
|
@Factory()
|
|
85
|
-
class
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
class Consumer {
|
|
89
|
-
@Inject(Dependency) dependency // creates a new instance each time a new Consumer is created
|
|
139
|
+
class RequestLogger {
|
|
140
|
+
id = Math.random()
|
|
90
141
|
}
|
|
91
|
-
```
|
|
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
142
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
@Singleton()
|
|
105
|
-
class Dependency {
|
|
143
|
+
class Handler {
|
|
144
|
+
@Inject(RequestLogger) logger // New instance for each Handler
|
|
106
145
|
}
|
|
107
146
|
|
|
108
|
-
|
|
109
|
-
@InjectLazy(Dependency) dependency // creates an instance only when the property is accessed
|
|
110
|
-
}
|
|
147
|
+
new Handler().logger.id !== new Handler().logger.id // true
|
|
111
148
|
```
|
|
112
149
|
|
|
113
|
-
###
|
|
150
|
+
### Lazy Injection
|
|
114
151
|
|
|
115
|
-
|
|
152
|
+
By default, dependencies are created when the parent class is instantiated. Use `@InjectLazy` to defer creation until first access:
|
|
116
153
|
|
|
117
154
|
```javascript
|
|
118
|
-
import {Singleton,
|
|
155
|
+
import { Singleton, InjectLazy } from 'decorator-dependency-injection'
|
|
119
156
|
|
|
120
157
|
@Singleton()
|
|
121
|
-
class
|
|
122
|
-
|
|
158
|
+
class ExpensiveService {
|
|
159
|
+
constructor() {
|
|
160
|
+
console.log('ExpensiveService created') // Only when accessed
|
|
161
|
+
}
|
|
123
162
|
}
|
|
124
163
|
|
|
125
|
-
class
|
|
126
|
-
@
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
164
|
+
class MyClass {
|
|
165
|
+
@InjectLazy(ExpensiveService) service
|
|
166
|
+
|
|
167
|
+
doWork() {
|
|
168
|
+
this.service.process() // ExpensiveService created here
|
|
130
169
|
}
|
|
131
170
|
}
|
|
132
|
-
|
|
133
|
-
const service = new UserService()
|
|
134
|
-
service.#db // SyntaxError: Private field '#db' must be declared
|
|
135
171
|
```
|
|
136
172
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
The `accessor` keyword (part of the TC39 decorators proposal) creates an auto-accessor - a private backing field with
|
|
140
|
-
automatic getter/setter. This is particularly useful for **lazy injection with private fields**.
|
|
173
|
+
This is also useful for breaking circular dependencies.
|
|
141
174
|
|
|
142
|
-
|
|
143
|
-
class Example {
|
|
144
|
-
accessor myField = 'value'
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
// Roughly equivalent to:
|
|
148
|
-
class Example {
|
|
149
|
-
#myField = 'value'
|
|
150
|
-
get myField() { return this.#myField }
|
|
151
|
-
set myField(v) { this.#myField = v }
|
|
152
|
-
}
|
|
153
|
-
```
|
|
175
|
+
### Passing Parameters
|
|
154
176
|
|
|
155
|
-
|
|
177
|
+
Pass constructor arguments after the class reference:
|
|
156
178
|
|
|
157
179
|
```javascript
|
|
158
|
-
import {
|
|
180
|
+
import { Factory, Inject } from 'decorator-dependency-injection'
|
|
159
181
|
|
|
160
|
-
@
|
|
161
|
-
class
|
|
162
|
-
constructor() {
|
|
163
|
-
|
|
182
|
+
@Factory()
|
|
183
|
+
class Logger {
|
|
184
|
+
constructor(prefix, level) {
|
|
185
|
+
this.prefix = prefix
|
|
186
|
+
this.level = level
|
|
164
187
|
}
|
|
165
188
|
}
|
|
166
189
|
|
|
167
|
-
class
|
|
168
|
-
|
|
169
|
-
@Inject(ExpensiveService) accessor service
|
|
170
|
-
|
|
171
|
-
// Private accessor - recommended for lazy private injection
|
|
172
|
-
@InjectLazy(ExpensiveService) accessor #privateService
|
|
173
|
-
|
|
174
|
-
doWork() {
|
|
175
|
-
// Instance created only when first accessed
|
|
176
|
-
return this.#privateService.process()
|
|
177
|
-
}
|
|
190
|
+
class MyService {
|
|
191
|
+
@Inject(Logger, 'MyService', 'debug') logger
|
|
178
192
|
}
|
|
179
193
|
```
|
|
180
194
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
| Decorator | Syntax | Lazy? | Notes |
|
|
184
|
-
|-----------|--------|-------|-------|
|
|
185
|
-
| `@Inject` | `@Inject(Dep) field` | No | Standard injection |
|
|
186
|
-
| `@Inject` | `@Inject(Dep) #field` | No | Private field injection |
|
|
187
|
-
| `@Inject` | `@Inject(Dep) accessor field` | No* | Accessor injection |
|
|
188
|
-
| `@Inject` | `@Inject(Dep) accessor #field` | No* | Private accessor injection |
|
|
189
|
-
| `@Inject` | `@Inject(Dep) static field` | No | Static field injection |
|
|
190
|
-
| `@Inject` | `@Inject(Dep) static #field` | No | Static private field |
|
|
191
|
-
| `@Inject` | `@Inject(Dep) static accessor field` | No* | Static accessor |
|
|
192
|
-
| `@Inject` | `@Inject(Dep) static accessor #field` | No* | Static private accessor |
|
|
193
|
-
| `@InjectLazy` | `@InjectLazy(Dep) field` | ✅ Yes | Lazy public field |
|
|
194
|
-
| `@InjectLazy` | `@InjectLazy(Dep) #field` | ⚠️ No | See caveat below |
|
|
195
|
-
| `@InjectLazy` | `@InjectLazy(Dep) accessor field` | ✅ Yes | Lazy accessor |
|
|
196
|
-
| `@InjectLazy` | `@InjectLazy(Dep) accessor #field` | ✅ Yes | **Recommended for lazy private** |
|
|
197
|
-
| `@InjectLazy` | `@InjectLazy(Dep) static field` | ✅ Yes | Lazy static field |
|
|
198
|
-
| `@InjectLazy` | `@InjectLazy(Dep) static #field` | ⚠️ No | Same caveat as instance private |
|
|
199
|
-
| `@InjectLazy` | `@InjectLazy(Dep) static accessor #field` | ✅ Yes | Lazy static private accessor |
|
|
195
|
+
For singletons, parameters are only used on the first instantiation.
|
|
200
196
|
|
|
201
|
-
|
|
197
|
+
---
|
|
202
198
|
|
|
203
|
-
|
|
199
|
+
## Testing
|
|
204
200
|
|
|
205
|
-
|
|
206
|
-
created at construction time (or class definition time for static fields), not on first access. This is because
|
|
207
|
-
`Object.defineProperty()` cannot create getters on private fields.
|
|
201
|
+
### Mocking Dependencies
|
|
208
202
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
**Recommendation:** For true lazy injection with private members, use the `accessor` keyword:
|
|
203
|
+
Use `@Mock` to replace a dependency with a test double:
|
|
212
204
|
|
|
213
205
|
```javascript
|
|
214
|
-
|
|
215
|
-
@InjectLazy(ExpensiveService) #service
|
|
216
|
-
|
|
217
|
-
// ✅ Truly lazy (created on first access)
|
|
218
|
-
@InjectLazy(ExpensiveService) accessor #service
|
|
219
|
-
|
|
220
|
-
// Static fields work the same way:
|
|
221
|
-
// ❌ Not truly lazy (created at class definition)
|
|
222
|
-
@InjectLazy(ExpensiveService) static #service
|
|
223
|
-
|
|
224
|
-
// ✅ Truly lazy
|
|
225
|
-
@InjectLazy(ExpensiveService) static accessor #service
|
|
226
|
-
```
|
|
227
|
-
|
|
228
|
-
### Static Field Injection
|
|
229
|
-
|
|
230
|
-
All injection decorators work with static fields. Static injections are shared across all instances of the class:
|
|
231
|
-
|
|
232
|
-
```javascript
|
|
233
|
-
import {Factory, Singleton, Inject} from 'decorator-dependency-injection';
|
|
206
|
+
import { Singleton, Mock, removeMock, resolve } from 'decorator-dependency-injection'
|
|
234
207
|
|
|
235
208
|
@Singleton()
|
|
236
|
-
class
|
|
237
|
-
|
|
209
|
+
class UserService {
|
|
210
|
+
getUser(id) { return fetchFromDatabase(id) }
|
|
238
211
|
}
|
|
239
212
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
id
|
|
213
|
+
// In your test file:
|
|
214
|
+
@Mock(UserService)
|
|
215
|
+
class MockUserService {
|
|
216
|
+
getUser(id) { return { id, name: 'Test User' } }
|
|
244
217
|
}
|
|
245
218
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
@Inject(RequestLogger) logger // New instance per ApiService
|
|
249
|
-
|
|
250
|
-
getUrl() {
|
|
251
|
-
return ApiService.config.apiUrl
|
|
252
|
-
}
|
|
253
|
-
}
|
|
219
|
+
// Now all injections of UserService receive MockUserService
|
|
220
|
+
const user = resolve(UserService).getUser(1) // { id: 1, name: 'Test User' }
|
|
254
221
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
console.log(a.logger.id) // 1
|
|
258
|
-
console.log(b.logger.id) // 2
|
|
259
|
-
console.log(ApiService.config === ApiService.config) // true (singleton)
|
|
222
|
+
// Restore the original
|
|
223
|
+
removeMock(UserService)
|
|
260
224
|
```
|
|
261
225
|
|
|
262
|
-
###
|
|
263
|
-
|
|
264
|
-
The injection decorators also support:
|
|
265
|
-
|
|
266
|
-
- **Computed property names**: `@Inject(Dep) [dynamicPropertyName]`
|
|
267
|
-
- **Symbol property names**: `@Inject(Dep) [Symbol('key')]`
|
|
268
|
-
- **Inheritance**: Subclasses inherit parent class injections
|
|
269
|
-
- **Multiple decorators**: Combine `@Inject` with other decorators
|
|
270
|
-
- **Nested injection**: Singletons/Factories can have their own injected dependencies
|
|
271
|
-
|
|
272
|
-
## Passing parameters to a dependency
|
|
226
|
+
### Proxy Mocking
|
|
273
227
|
|
|
274
|
-
|
|
275
|
-
dependency.
|
|
228
|
+
Mock only specific methods while keeping the rest of the original implementation:
|
|
276
229
|
|
|
277
230
|
```javascript
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
constructor(param1, param2) {
|
|
283
|
-
this.param1 = param1
|
|
284
|
-
this.param2 = param2
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
class Consumer {
|
|
289
|
-
@Inject(Dependency, 'myParam', 'myOtherParam') dependency
|
|
231
|
+
@Mock(UserService, true) // true enables proxy mode
|
|
232
|
+
class PartialMock {
|
|
233
|
+
getUser(id) { return { id, name: 'Mocked' } }
|
|
234
|
+
// All other methods delegate to the real UserService
|
|
290
235
|
}
|
|
291
236
|
```
|
|
292
237
|
|
|
293
|
-
|
|
294
|
-
will only be passed to the dependency the first time it is created.
|
|
295
|
-
|
|
296
|
-
## Mocking dependencies for testing
|
|
238
|
+
### Test Lifecycle
|
|
297
239
|
|
|
298
|
-
|
|
240
|
+
| Function | Purpose |
|
|
241
|
+
|----------|---------|
|
|
242
|
+
| `removeMock(Class)` | Remove a specific mock, restore original |
|
|
243
|
+
| `removeAllMocks()` | Remove all mocks, restore all originals |
|
|
244
|
+
| `resetSingletons()` | Clear cached instances (keeps mocks) |
|
|
245
|
+
| `clearContainer()` | Remove all registrations entirely |
|
|
299
246
|
|
|
300
247
|
```javascript
|
|
301
|
-
import {
|
|
248
|
+
import { removeAllMocks, resetSingletons } from 'decorator-dependency-injection'
|
|
302
249
|
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
250
|
+
afterEach(() => {
|
|
251
|
+
removeAllMocks() // Restore original implementations
|
|
252
|
+
// OR
|
|
253
|
+
resetSingletons() // Keep mocks, but get fresh instances
|
|
254
|
+
})
|
|
255
|
+
```
|
|
309
256
|
|
|
310
|
-
|
|
311
|
-
@Inject(Dependency) dependency
|
|
257
|
+
**Note:** These functions remove/restore mocks. They do NOT clear mock call history. If using Vitest/Jest spies, call `.mockClear()` separately.
|
|
312
258
|
|
|
313
|
-
|
|
314
|
-
console.log(this.dependency.method())
|
|
315
|
-
}
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
// Test Code
|
|
259
|
+
### Testing Best Practices
|
|
319
260
|
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
return 'mock'
|
|
324
|
-
}
|
|
325
|
-
}
|
|
261
|
+
```javascript
|
|
262
|
+
import { Mock, removeAllMocks, resetSingletons } from 'decorator-dependency-injection'
|
|
263
|
+
import { vi, describe, it, beforeEach, afterEach } from 'vitest'
|
|
326
264
|
|
|
327
|
-
|
|
265
|
+
// Hoist mock functions for per-test configuration
|
|
266
|
+
const mockGetUser = vi.hoisted(() => vi.fn())
|
|
328
267
|
|
|
329
|
-
|
|
268
|
+
@Mock(UserService)
|
|
269
|
+
class MockUserService {
|
|
270
|
+
getUser = mockGetUser
|
|
271
|
+
}
|
|
330
272
|
|
|
331
|
-
|
|
273
|
+
describe('MyFeature', () => {
|
|
274
|
+
beforeEach(() => {
|
|
275
|
+
mockGetUser.mockClear() // Clear call history
|
|
276
|
+
resetSingletons() // Fresh instances per test
|
|
277
|
+
})
|
|
278
|
+
|
|
279
|
+
afterEach(() => {
|
|
280
|
+
removeAllMocks() // Restore originals
|
|
281
|
+
})
|
|
282
|
+
|
|
283
|
+
it('should work', () => {
|
|
284
|
+
mockGetUser.mockReturnValue({ id: 1 })
|
|
285
|
+
// ... test code ...
|
|
286
|
+
expect(mockGetUser).toHaveBeenCalled()
|
|
287
|
+
})
|
|
288
|
+
})
|
|
332
289
|
```
|
|
333
290
|
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
The `resetMock` utility function allows you to remove any active mock for a dependency and restore the original
|
|
337
|
-
implementation. This is useful for cleaning up after tests or switching between real and mock dependencies.
|
|
291
|
+
Additional test utilities:
|
|
338
292
|
|
|
339
293
|
```javascript
|
|
340
|
-
import {
|
|
294
|
+
import { isMocked, getMockInstance } from 'decorator-dependency-injection'
|
|
341
295
|
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
```
|
|
296
|
+
// Check if a class is currently mocked
|
|
297
|
+
if (isMocked(UserService)) { /* ... */ }
|
|
345
298
|
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
299
|
+
// Access the mock instance to configure it
|
|
300
|
+
getMockInstance(UserService).someMethod.mockReturnValue('test')
|
|
301
|
+
```
|
|
349
302
|
|
|
350
|
-
|
|
351
|
-
import {clearContainer} from 'decorator-dependency-injection';
|
|
303
|
+
---
|
|
352
304
|
|
|
353
|
-
|
|
354
|
-
```
|
|
305
|
+
## Advanced Features
|
|
355
306
|
|
|
356
|
-
###
|
|
307
|
+
### Private Fields
|
|
357
308
|
|
|
358
|
-
|
|
309
|
+
Both `@Inject` and `@InjectLazy` support private fields:
|
|
359
310
|
|
|
360
311
|
```javascript
|
|
361
|
-
import {Singleton, Factory, resolve} from 'decorator-dependency-injection';
|
|
362
|
-
|
|
363
|
-
@Singleton()
|
|
364
312
|
class UserService {
|
|
365
|
-
|
|
366
|
-
return { id, name: 'John' }
|
|
367
|
-
}
|
|
368
|
-
}
|
|
313
|
+
@Inject(Database) #db // Truly private
|
|
369
314
|
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
constructor(prefix) {
|
|
373
|
-
this.prefix = prefix
|
|
374
|
-
}
|
|
375
|
-
log(msg) {
|
|
376
|
-
console.log(`[${this.prefix}] ${msg}`)
|
|
315
|
+
getUser(id) {
|
|
316
|
+
return this.#db.query(`SELECT * FROM users WHERE id = ${id}`)
|
|
377
317
|
}
|
|
378
318
|
}
|
|
319
|
+
```
|
|
379
320
|
|
|
380
|
-
|
|
381
|
-
function handleRequest(req) {
|
|
382
|
-
const userService = resolve(UserService)
|
|
383
|
-
return userService.getUser(req.userId)
|
|
384
|
-
}
|
|
321
|
+
For lazy injection with private fields, use the `accessor` keyword:
|
|
385
322
|
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
323
|
+
```javascript
|
|
324
|
+
class UserService {
|
|
325
|
+
@InjectLazy(Database) accessor #db // Lazy AND private
|
|
389
326
|
}
|
|
390
|
-
|
|
391
|
-
// Use with named registrations
|
|
392
|
-
const db = resolve('databaseConnection')
|
|
393
327
|
```
|
|
394
328
|
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
- Writing utility functions that need DI access
|
|
398
|
-
- Bridging between decorator-based and non-decorator code
|
|
399
|
-
- Testing or debugging the container directly
|
|
329
|
+
<details>
|
|
330
|
+
<summary><strong>Why accessor for lazy private fields?</strong></summary>
|
|
400
331
|
|
|
401
|
-
|
|
332
|
+
JavaScript doesn't allow `Object.defineProperty()` on private fields, so `@InjectLazy` on `#field` creates the instance at construction time (not truly lazy). The `accessor` keyword creates a private backing field with getter/setter that enables true lazy behavior.
|
|
402
333
|
|
|
403
|
-
|
|
404
|
-
errors early:
|
|
334
|
+
</details>
|
|
405
335
|
|
|
406
|
-
|
|
336
|
+
### Static Fields
|
|
407
337
|
|
|
408
|
-
|
|
338
|
+
Inject at the class level (shared across all instances):
|
|
409
339
|
|
|
410
340
|
```javascript
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
@
|
|
414
|
-
class MyService {}
|
|
415
|
-
|
|
416
|
-
console.log(isRegistered(MyService)); // true
|
|
417
|
-
console.log(isRegistered('unknownName')); // false
|
|
418
|
-
```
|
|
419
|
-
|
|
420
|
-
#### `validateRegistrations(...tokens)`
|
|
421
|
-
|
|
422
|
-
Validate multiple registrations at once. Throws an error with helpful details if any are missing:
|
|
341
|
+
class ApiService {
|
|
342
|
+
@Inject(Config) static config // Class-level singleton
|
|
343
|
+
@Inject(Logger) logger // Instance-level
|
|
423
344
|
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
// At application startup - fail fast if dependencies are missing
|
|
428
|
-
try {
|
|
429
|
-
validateRegistrations(UserService, AuthService, 'databaseConnection');
|
|
430
|
-
} catch (err) {
|
|
431
|
-
// Error: Missing registrations: [UserService, databaseConnection].
|
|
432
|
-
// Ensure these classes are decorated with @Singleton() or @Factory() before use.
|
|
345
|
+
getUrl() {
|
|
346
|
+
return ApiService.config.apiUrl
|
|
347
|
+
}
|
|
433
348
|
}
|
|
434
349
|
```
|
|
435
350
|
|
|
436
|
-
|
|
437
|
-
- Application bootstrap to catch missing dependencies before runtime failures
|
|
438
|
-
- Test setup to ensure mocks are properly configured
|
|
439
|
-
- Module initialization to validate external dependencies
|
|
440
|
-
|
|
441
|
-
### Debug Mode
|
|
351
|
+
### Named Registrations
|
|
442
352
|
|
|
443
|
-
|
|
353
|
+
Register dependencies under string names instead of class references:
|
|
444
354
|
|
|
445
355
|
```javascript
|
|
446
|
-
|
|
356
|
+
@Singleton('database')
|
|
357
|
+
class PostgresDatabase { }
|
|
447
358
|
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
// - Classes are registered: [DI] Registered singleton: UserService
|
|
452
|
-
// - Instances are created: [DI] Creating singleton: UserService
|
|
453
|
-
// - Cached singletons are returned: [DI] Returning cached singleton: UserService
|
|
454
|
-
// - Mocks are registered: [DI] Mocked UserService with MockUserService
|
|
359
|
+
class UserService {
|
|
360
|
+
@Inject('database') db
|
|
361
|
+
}
|
|
455
362
|
```
|
|
456
363
|
|
|
457
|
-
|
|
458
|
-
- Debugging injection order issues
|
|
459
|
-
- Understanding when instances are created (eager vs lazy)
|
|
460
|
-
- Troubleshooting circular dependencies
|
|
461
|
-
- Verifying test mocks are applied correctly
|
|
364
|
+
### Manual Resolution
|
|
462
365
|
|
|
463
|
-
|
|
464
|
-
mock will be passed to the real dependency.
|
|
366
|
+
Retrieve instances programmatically (useful for non-class code):
|
|
465
367
|
|
|
466
368
|
```javascript
|
|
467
|
-
import {
|
|
369
|
+
import { resolve } from 'decorator-dependency-injection'
|
|
468
370
|
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
return 'real'
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
otherMethod() {
|
|
476
|
-
return 'other'
|
|
477
|
-
}
|
|
371
|
+
function handleRequest(req) {
|
|
372
|
+
const userService = resolve(UserService)
|
|
373
|
+
return userService.getUser(req.userId)
|
|
478
374
|
}
|
|
479
375
|
|
|
480
|
-
|
|
481
|
-
|
|
376
|
+
// With parameters
|
|
377
|
+
const logger = resolve(Logger, 'my-module')
|
|
482
378
|
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
}
|
|
379
|
+
// With named registration
|
|
380
|
+
const db = resolve('database')
|
|
381
|
+
```
|
|
487
382
|
|
|
488
|
-
|
|
383
|
+
### Container Introspection
|
|
489
384
|
|
|
490
|
-
|
|
491
|
-
class MockDependency {
|
|
492
|
-
method() {
|
|
493
|
-
return 'mock'
|
|
494
|
-
}
|
|
495
|
-
}
|
|
385
|
+
Debug and inspect the container state:
|
|
496
386
|
|
|
497
|
-
|
|
387
|
+
```javascript
|
|
388
|
+
import {
|
|
389
|
+
getContainer,
|
|
390
|
+
listRegistrations,
|
|
391
|
+
isRegistered,
|
|
392
|
+
validateRegistrations,
|
|
393
|
+
setDebug
|
|
394
|
+
} from 'decorator-dependency-injection'
|
|
395
|
+
|
|
396
|
+
// Check registration status
|
|
397
|
+
isRegistered(UserService) // true/false
|
|
398
|
+
|
|
399
|
+
// Fail fast at startup
|
|
400
|
+
validateRegistrations(UserService, AuthService, 'database')
|
|
401
|
+
// Throws if any are missing
|
|
402
|
+
|
|
403
|
+
// List all registrations
|
|
404
|
+
listRegistrations().forEach(reg => {
|
|
405
|
+
console.log(`${reg.name}: ${reg.type}, mocked: ${reg.isMocked}`)
|
|
406
|
+
})
|
|
407
|
+
|
|
408
|
+
// Enable debug logging
|
|
409
|
+
setDebug(true)
|
|
410
|
+
// [DI] Registered singleton: UserService
|
|
411
|
+
// [DI] Creating singleton: UserService
|
|
412
|
+
// [DI] Mocked UserService with MockUserService
|
|
413
|
+
```
|
|
498
414
|
|
|
499
|
-
|
|
415
|
+
### Isolated Containers
|
|
500
416
|
|
|
501
|
-
|
|
502
|
-
```
|
|
417
|
+
Create separate containers for parallel test execution or module isolation:
|
|
503
418
|
|
|
504
|
-
|
|
419
|
+
```javascript
|
|
420
|
+
import { Container } from 'decorator-dependency-injection'
|
|
505
421
|
|
|
506
|
-
|
|
422
|
+
const container = new Container()
|
|
423
|
+
container.registerSingleton(MyService)
|
|
424
|
+
const instance = container.resolve(MyService)
|
|
425
|
+
```
|
|
507
426
|
|
|
508
|
-
|
|
427
|
+
---
|
|
509
428
|
|
|
510
|
-
|
|
429
|
+
## API Reference
|
|
511
430
|
|
|
512
|
-
|
|
513
|
-
import {Container} from 'decorator-dependency-injection';
|
|
431
|
+
### Decorators
|
|
514
432
|
|
|
515
|
-
|
|
516
|
-
|
|
433
|
+
| Decorator | Description |
|
|
434
|
+
|-----------|-------------|
|
|
435
|
+
| `@Singleton(name?)` | Register a class as a singleton |
|
|
436
|
+
| `@Factory(name?)` | Register a class as a factory |
|
|
437
|
+
| `@Inject(target, ...params)` | Inject a dependency into a field |
|
|
438
|
+
| `@InjectLazy(target, ...params)` | Inject lazily (on first access) |
|
|
439
|
+
| `@Mock(target, proxy?)` | Replace a dependency with a mock |
|
|
517
440
|
|
|
518
|
-
|
|
441
|
+
### Functions
|
|
519
442
|
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
443
|
+
| Function | Description |
|
|
444
|
+
|----------|-------------|
|
|
445
|
+
| `resolve(target, ...params)` | Get an instance from the container |
|
|
446
|
+
| `removeMock(target)` | Remove a mock, restore original |
|
|
447
|
+
| `removeAllMocks()` | Remove all mocks |
|
|
448
|
+
| `resetSingletons(options?)` | Clear cached singleton instances |
|
|
449
|
+
| `clearContainer(options?)` | Clear all registrations |
|
|
450
|
+
| `isRegistered(target)` | Check if target is registered |
|
|
451
|
+
| `isMocked(target)` | Check if target is mocked |
|
|
452
|
+
| `getMockInstance(target)` | Get the mock instance |
|
|
453
|
+
| `validateRegistrations(...targets)` | Throw if any target is not registered |
|
|
454
|
+
| `listRegistrations()` | List all registrations |
|
|
455
|
+
| `getContainer()` | Get the default container |
|
|
456
|
+
| `setDebug(enabled)` | Enable/disable debug logging |
|
|
457
|
+
| `unregister(target)` | Remove a registration |
|
|
523
458
|
|
|
524
|
-
|
|
525
|
-
const ctx1 = container1.getContext(MyService);
|
|
526
|
-
const ctx2 = container2.getContext(MyService);
|
|
459
|
+
---
|
|
527
460
|
|
|
528
|
-
|
|
529
|
-
const instance2 = container2.getInstance(ctx2, []);
|
|
461
|
+
## TypeScript Support
|
|
530
462
|
|
|
531
|
-
|
|
532
|
-
```
|
|
463
|
+
Full TypeScript definitions are included:
|
|
533
464
|
|
|
534
|
-
|
|
465
|
+
```typescript
|
|
466
|
+
import { Constructor, InjectionToken, RegistrationInfo } from 'decorator-dependency-injection'
|
|
535
467
|
|
|
536
|
-
|
|
468
|
+
// Constructor<T> - a class constructor
|
|
469
|
+
const MyClass: Constructor<MyService> = MyService
|
|
537
470
|
|
|
538
|
-
|
|
539
|
-
|
|
471
|
+
// InjectionToken<T> - class or string name
|
|
472
|
+
const token: InjectionToken<MyService> = MyService
|
|
473
|
+
const named: InjectionToken = 'myService'
|
|
540
474
|
|
|
541
|
-
|
|
542
|
-
|
|
475
|
+
// RegistrationInfo - from listRegistrations()
|
|
476
|
+
// { key, name, type, isMocked, hasInstance }
|
|
543
477
|
```
|
|
544
478
|
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
The library includes TypeScript definitions with helpful type aliases:
|
|
479
|
+
---
|
|
548
480
|
|
|
549
|
-
|
|
550
|
-
import {Constructor, InjectionToken} from 'decorator-dependency-injection';
|
|
481
|
+
## Why Not [Other Library]?
|
|
551
482
|
|
|
552
|
-
|
|
553
|
-
|
|
483
|
+
| Feature | This Library | InversifyJS | TSyringe | TypeDI |
|
|
484
|
+
|---------|--------------|-------------|----------|--------|
|
|
485
|
+
| Native decorators (Stage 3) | Yes | No (legacy) | No (legacy) | No (legacy) |
|
|
486
|
+
| Zero dependencies | Yes | No | No | No |
|
|
487
|
+
| No reflect-metadata | Yes | No | No | No |
|
|
488
|
+
| Built-in mocking | Yes | No | No | No |
|
|
489
|
+
| Bundle size | ~3KB | ~50KB | ~15KB | ~20KB |
|
|
554
490
|
|
|
555
|
-
|
|
556
|
-
const token1: InjectionToken<MyService> = MyService;
|
|
557
|
-
const token2: InjectionToken = 'myServiceName';
|
|
558
|
-
```
|
|
491
|
+
This library is ideal if you want simple, modern DI without the complexity of container configuration or reflection APIs.
|
|
559
492
|
|
|
560
|
-
|
|
493
|
+
---
|
|
561
494
|
|
|
562
|
-
##
|
|
495
|
+
## Related Topics
|
|
563
496
|
|
|
564
|
-
|
|
497
|
+
Searching for: JavaScript dependency injection, TypeScript DI container, decorator-based IoC, inversion of control JavaScript, @Inject decorator, @Singleton pattern, service locator pattern, unit test mocking, Jest dependency injection, Vitest mocking.
|
|
565
498
|
|
|
566
|
-
|
|
567
|
-
npm test
|
|
568
|
-
```
|
|
499
|
+
---
|
|
569
500
|
|
|
570
501
|
## Version History
|
|
571
502
|
|
|
@@ -575,4 +506,5 @@ npm test
|
|
|
575
506
|
- 1.0.3 - Added @InjectLazy decorator
|
|
576
507
|
- 1.0.4 - Added Container abstraction, clearContainer(), TypeScript definitions, improved proxy support
|
|
577
508
|
- 1.0.5 - Added private field and accessor support for @Inject and @InjectLazy, debug mode, validation helpers
|
|
578
|
-
- 1.0.6 - Added resolve() function for non-decorator code
|
|
509
|
+
- 1.0.6 - Added resolve() function for non-decorator code
|
|
510
|
+
- 1.0.7 - Added more control for mocking in tests and improved compatibility
|