decorator-dependency-injection 1.0.5 → 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 +323 -345
- package/index.d.ts +202 -13
- package/index.js +273 -21
- package/package.json +18 -5
- package/src/Container.js +262 -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
|
+
}
|
|
55
|
+
|
|
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.
|
|
8
68
|
|
|
9
|
-
|
|
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.
|
|
69
|
+
---
|
|
13
70
|
|
|
14
71
|
## Installation
|
|
15
72
|
|
|
@@ -17,510 +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
|
|
139
|
+
class RequestLogger {
|
|
140
|
+
id = Math.random()
|
|
86
141
|
}
|
|
87
142
|
|
|
88
|
-
class
|
|
89
|
-
@Inject(
|
|
143
|
+
class Handler {
|
|
144
|
+
@Inject(RequestLogger) logger // New instance for each Handler
|
|
90
145
|
}
|
|
146
|
+
|
|
147
|
+
new Handler().logger.id !== new Handler().logger.id // true
|
|
91
148
|
```
|
|
92
149
|
|
|
93
|
-
###
|
|
150
|
+
### Lazy Injection
|
|
94
151
|
|
|
95
|
-
|
|
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```.
|
|
152
|
+
By default, dependencies are created when the parent class is instantiated. Use `@InjectLazy` to defer creation until first access:
|
|
100
153
|
|
|
101
154
|
```javascript
|
|
102
|
-
import {Singleton, InjectLazy} from 'decorator-dependency-injection'
|
|
155
|
+
import { Singleton, InjectLazy } from 'decorator-dependency-injection'
|
|
103
156
|
|
|
104
157
|
@Singleton()
|
|
105
|
-
class
|
|
158
|
+
class ExpensiveService {
|
|
159
|
+
constructor() {
|
|
160
|
+
console.log('ExpensiveService created') // Only when accessed
|
|
161
|
+
}
|
|
106
162
|
}
|
|
107
163
|
|
|
108
|
-
class
|
|
109
|
-
@InjectLazy(
|
|
164
|
+
class MyClass {
|
|
165
|
+
@InjectLazy(ExpensiveService) service
|
|
166
|
+
|
|
167
|
+
doWork() {
|
|
168
|
+
this.service.process() // ExpensiveService created here
|
|
169
|
+
}
|
|
110
170
|
}
|
|
111
171
|
```
|
|
112
172
|
|
|
113
|
-
|
|
173
|
+
This is also useful for breaking circular dependencies.
|
|
114
174
|
|
|
115
|
-
|
|
175
|
+
### Passing Parameters
|
|
116
176
|
|
|
117
|
-
|
|
118
|
-
import {Singleton, Inject} from 'decorator-dependency-injection';
|
|
177
|
+
Pass constructor arguments after the class reference:
|
|
119
178
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
query(sql) { /* ... */ }
|
|
123
|
-
}
|
|
179
|
+
```javascript
|
|
180
|
+
import { Factory, Inject } from 'decorator-dependency-injection'
|
|
124
181
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
182
|
+
@Factory()
|
|
183
|
+
class Logger {
|
|
184
|
+
constructor(prefix, level) {
|
|
185
|
+
this.prefix = prefix
|
|
186
|
+
this.level = level
|
|
130
187
|
}
|
|
131
188
|
}
|
|
132
189
|
|
|
133
|
-
|
|
134
|
-
|
|
190
|
+
class MyService {
|
|
191
|
+
@Inject(Logger, 'MyService', 'debug') logger
|
|
192
|
+
}
|
|
135
193
|
```
|
|
136
194
|
|
|
137
|
-
|
|
195
|
+
For singletons, parameters are only used on the first instantiation.
|
|
138
196
|
|
|
139
|
-
|
|
140
|
-
automatic getter/setter. This is particularly useful for **lazy injection with private fields**.
|
|
197
|
+
---
|
|
141
198
|
|
|
142
|
-
|
|
143
|
-
class Example {
|
|
144
|
-
accessor myField = 'value'
|
|
145
|
-
}
|
|
199
|
+
## Testing
|
|
146
200
|
|
|
147
|
-
|
|
148
|
-
class Example {
|
|
149
|
-
#myField = 'value'
|
|
150
|
-
get myField() { return this.#myField }
|
|
151
|
-
set myField(v) { this.#myField = v }
|
|
152
|
-
}
|
|
153
|
-
```
|
|
201
|
+
### Mocking Dependencies
|
|
154
202
|
|
|
155
|
-
|
|
203
|
+
Use `@Mock` to replace a dependency with a test double:
|
|
156
204
|
|
|
157
205
|
```javascript
|
|
158
|
-
import {Singleton,
|
|
206
|
+
import { Singleton, Mock, removeMock, resolve } from 'decorator-dependency-injection'
|
|
159
207
|
|
|
160
208
|
@Singleton()
|
|
161
|
-
class
|
|
162
|
-
|
|
163
|
-
console.log('ExpensiveService created')
|
|
164
|
-
}
|
|
209
|
+
class UserService {
|
|
210
|
+
getUser(id) { return fetchFromDatabase(id) }
|
|
165
211
|
}
|
|
166
212
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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
|
-
}
|
|
213
|
+
// In your test file:
|
|
214
|
+
@Mock(UserService)
|
|
215
|
+
class MockUserService {
|
|
216
|
+
getUser(id) { return { id, name: 'Test User' } }
|
|
178
217
|
}
|
|
179
|
-
```
|
|
180
218
|
|
|
181
|
-
|
|
219
|
+
// Now all injections of UserService receive MockUserService
|
|
220
|
+
const user = resolve(UserService).getUser(1) // { id: 1, name: 'Test User' }
|
|
182
221
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
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 |
|
|
222
|
+
// Restore the original
|
|
223
|
+
removeMock(UserService)
|
|
224
|
+
```
|
|
200
225
|
|
|
201
|
-
|
|
226
|
+
### Proxy Mocking
|
|
202
227
|
|
|
203
|
-
|
|
228
|
+
Mock only specific methods while keeping the rest of the original implementation:
|
|
204
229
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
230
|
+
```javascript
|
|
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
|
|
235
|
+
}
|
|
236
|
+
```
|
|
208
237
|
|
|
209
|
-
|
|
238
|
+
### Test Lifecycle
|
|
210
239
|
|
|
211
|
-
|
|
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 |
|
|
212
246
|
|
|
213
247
|
```javascript
|
|
214
|
-
|
|
215
|
-
@InjectLazy(ExpensiveService) #service
|
|
248
|
+
import { removeAllMocks, resetSingletons } from 'decorator-dependency-injection'
|
|
216
249
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
//
|
|
221
|
-
|
|
222
|
-
@InjectLazy(ExpensiveService) static #service
|
|
223
|
-
|
|
224
|
-
// ✅ Truly lazy
|
|
225
|
-
@InjectLazy(ExpensiveService) static accessor #service
|
|
250
|
+
afterEach(() => {
|
|
251
|
+
removeAllMocks() // Restore original implementations
|
|
252
|
+
// OR
|
|
253
|
+
resetSingletons() // Keep mocks, but get fresh instances
|
|
254
|
+
})
|
|
226
255
|
```
|
|
227
256
|
|
|
228
|
-
|
|
257
|
+
**Note:** These functions remove/restore mocks. They do NOT clear mock call history. If using Vitest/Jest spies, call `.mockClear()` separately.
|
|
229
258
|
|
|
230
|
-
|
|
259
|
+
### Testing Best Practices
|
|
231
260
|
|
|
232
261
|
```javascript
|
|
233
|
-
import {
|
|
262
|
+
import { Mock, removeAllMocks, resetSingletons } from 'decorator-dependency-injection'
|
|
263
|
+
import { vi, describe, it, beforeEach, afterEach } from 'vitest'
|
|
234
264
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
apiUrl = 'https://api.example.com'
|
|
238
|
-
}
|
|
265
|
+
// Hoist mock functions for per-test configuration
|
|
266
|
+
const mockGetUser = vi.hoisted(() => vi.fn())
|
|
239
267
|
|
|
240
|
-
@
|
|
241
|
-
class
|
|
242
|
-
|
|
243
|
-
id = ++RequestLogger.nextId
|
|
268
|
+
@Mock(UserService)
|
|
269
|
+
class MockUserService {
|
|
270
|
+
getUser = mockGetUser
|
|
244
271
|
}
|
|
245
272
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
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
|
+
})
|
|
260
289
|
```
|
|
261
290
|
|
|
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
|
|
273
|
-
|
|
274
|
-
You can pass parameters to a dependency by using the ```@Inject``` decorator with a function that returns the
|
|
275
|
-
dependency.
|
|
291
|
+
Additional test utilities:
|
|
276
292
|
|
|
277
293
|
```javascript
|
|
278
|
-
import {
|
|
294
|
+
import { isMocked, getMockInstance } from 'decorator-dependency-injection'
|
|
279
295
|
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
constructor(param1, param2) {
|
|
283
|
-
this.param1 = param1
|
|
284
|
-
this.param2 = param2
|
|
285
|
-
}
|
|
286
|
-
}
|
|
296
|
+
// Check if a class is currently mocked
|
|
297
|
+
if (isMocked(UserService)) { /* ... */ }
|
|
287
298
|
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
}
|
|
299
|
+
// Access the mock instance to configure it
|
|
300
|
+
getMockInstance(UserService).someMethod.mockReturnValue('test')
|
|
291
301
|
```
|
|
292
302
|
|
|
293
|
-
|
|
294
|
-
will only be passed to the dependency the first time it is created.
|
|
303
|
+
---
|
|
295
304
|
|
|
296
|
-
##
|
|
305
|
+
## Advanced Features
|
|
297
306
|
|
|
298
|
-
|
|
307
|
+
### Private Fields
|
|
299
308
|
|
|
300
|
-
|
|
301
|
-
import {Factory, Inject, Mock, resetMock} from 'decorator-dependency-injection'
|
|
302
|
-
|
|
303
|
-
@Factory()
|
|
304
|
-
class Dependency {
|
|
305
|
-
method() {
|
|
306
|
-
return 'real'
|
|
307
|
-
}
|
|
308
|
-
}
|
|
309
|
+
Both `@Inject` and `@InjectLazy` support private fields:
|
|
309
310
|
|
|
310
|
-
|
|
311
|
-
|
|
311
|
+
```javascript
|
|
312
|
+
class UserService {
|
|
313
|
+
@Inject(Database) #db // Truly private
|
|
312
314
|
|
|
313
|
-
|
|
314
|
-
|
|
315
|
+
getUser(id) {
|
|
316
|
+
return this.#db.query(`SELECT * FROM users WHERE id = ${id}`)
|
|
315
317
|
}
|
|
316
318
|
}
|
|
319
|
+
```
|
|
317
320
|
|
|
318
|
-
|
|
321
|
+
For lazy injection with private fields, use the `accessor` keyword:
|
|
319
322
|
|
|
320
|
-
|
|
321
|
-
class
|
|
322
|
-
|
|
323
|
-
return 'mock'
|
|
324
|
-
}
|
|
323
|
+
```javascript
|
|
324
|
+
class UserService {
|
|
325
|
+
@InjectLazy(Database) accessor #db // Lazy AND private
|
|
325
326
|
}
|
|
327
|
+
```
|
|
326
328
|
|
|
327
|
-
|
|
329
|
+
<details>
|
|
330
|
+
<summary><strong>Why accessor for lazy private fields?</strong></summary>
|
|
328
331
|
|
|
329
|
-
|
|
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.
|
|
330
333
|
|
|
331
|
-
|
|
332
|
-
```
|
|
334
|
+
</details>
|
|
333
335
|
|
|
334
|
-
###
|
|
336
|
+
### Static Fields
|
|
335
337
|
|
|
336
|
-
|
|
337
|
-
implementation. This is useful for cleaning up after tests or switching between real and mock dependencies.
|
|
338
|
+
Inject at the class level (shared across all instances):
|
|
338
339
|
|
|
339
340
|
```javascript
|
|
340
|
-
|
|
341
|
+
class ApiService {
|
|
342
|
+
@Inject(Config) static config // Class-level singleton
|
|
343
|
+
@Inject(Logger) logger // Instance-level
|
|
341
344
|
|
|
342
|
-
|
|
343
|
-
|
|
345
|
+
getUrl() {
|
|
346
|
+
return ApiService.config.apiUrl
|
|
347
|
+
}
|
|
348
|
+
}
|
|
344
349
|
```
|
|
345
350
|
|
|
346
|
-
###
|
|
351
|
+
### Named Registrations
|
|
347
352
|
|
|
348
|
-
|
|
353
|
+
Register dependencies under string names instead of class references:
|
|
349
354
|
|
|
350
355
|
```javascript
|
|
351
|
-
|
|
356
|
+
@Singleton('database')
|
|
357
|
+
class PostgresDatabase { }
|
|
352
358
|
|
|
353
|
-
|
|
359
|
+
class UserService {
|
|
360
|
+
@Inject('database') db
|
|
361
|
+
}
|
|
354
362
|
```
|
|
355
363
|
|
|
356
|
-
###
|
|
364
|
+
### Manual Resolution
|
|
357
365
|
|
|
358
|
-
|
|
359
|
-
errors early:
|
|
360
|
-
|
|
361
|
-
#### `isRegistered(clazzOrName)`
|
|
362
|
-
|
|
363
|
-
Check if a class or name is registered:
|
|
366
|
+
Retrieve instances programmatically (useful for non-class code):
|
|
364
367
|
|
|
365
368
|
```javascript
|
|
366
|
-
import {
|
|
367
|
-
|
|
368
|
-
@Singleton()
|
|
369
|
-
class MyService {}
|
|
369
|
+
import { resolve } from 'decorator-dependency-injection'
|
|
370
370
|
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
#### `validateRegistrations(...tokens)`
|
|
371
|
+
function handleRequest(req) {
|
|
372
|
+
const userService = resolve(UserService)
|
|
373
|
+
return userService.getUser(req.userId)
|
|
374
|
+
}
|
|
376
375
|
|
|
377
|
-
|
|
376
|
+
// With parameters
|
|
377
|
+
const logger = resolve(Logger, 'my-module')
|
|
378
378
|
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
// At application startup - fail fast if dependencies are missing
|
|
383
|
-
try {
|
|
384
|
-
validateRegistrations(UserService, AuthService, 'databaseConnection');
|
|
385
|
-
} catch (err) {
|
|
386
|
-
// Error: Missing registrations: [UserService, databaseConnection].
|
|
387
|
-
// Ensure these classes are decorated with @Singleton() or @Factory() before use.
|
|
388
|
-
}
|
|
379
|
+
// With named registration
|
|
380
|
+
const db = resolve('database')
|
|
389
381
|
```
|
|
390
382
|
|
|
391
|
-
|
|
392
|
-
- Application bootstrap to catch missing dependencies before runtime failures
|
|
393
|
-
- Test setup to ensure mocks are properly configured
|
|
394
|
-
- Module initialization to validate external dependencies
|
|
395
|
-
|
|
396
|
-
### Debug Mode
|
|
383
|
+
### Container Introspection
|
|
397
384
|
|
|
398
|
-
|
|
385
|
+
Debug and inspect the container state:
|
|
399
386
|
|
|
400
387
|
```javascript
|
|
401
|
-
import {
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
//
|
|
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
|
|
410
413
|
```
|
|
411
414
|
|
|
412
|
-
|
|
413
|
-
- Debugging injection order issues
|
|
414
|
-
- Understanding when instances are created (eager vs lazy)
|
|
415
|
-
- Troubleshooting circular dependencies
|
|
416
|
-
- Verifying test mocks are applied correctly
|
|
415
|
+
### Isolated Containers
|
|
417
416
|
|
|
418
|
-
|
|
419
|
-
mock will be passed to the real dependency.
|
|
417
|
+
Create separate containers for parallel test execution or module isolation:
|
|
420
418
|
|
|
421
419
|
```javascript
|
|
422
|
-
import {
|
|
423
|
-
|
|
424
|
-
@Factory()
|
|
425
|
-
class Dependency {
|
|
426
|
-
method() {
|
|
427
|
-
return 'real'
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
otherMethod() {
|
|
431
|
-
return 'other'
|
|
432
|
-
}
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
class Consumer {
|
|
436
|
-
@Inject(Dependency) dependency
|
|
437
|
-
|
|
438
|
-
constructor() {
|
|
439
|
-
console.log(this.dependency.method(), this.dependency.otherMethod())
|
|
440
|
-
}
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
// Test Code
|
|
444
|
-
|
|
445
|
-
@Mock(Dependency, true)
|
|
446
|
-
class MockDependency {
|
|
447
|
-
method() {
|
|
448
|
-
return 'mock'
|
|
449
|
-
}
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
const consumer = new Consumer() // prints 'mock other'
|
|
420
|
+
import { Container } from 'decorator-dependency-injection'
|
|
453
421
|
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
const
|
|
422
|
+
const container = new Container()
|
|
423
|
+
container.registerSingleton(MyService)
|
|
424
|
+
const instance = container.resolve(MyService)
|
|
457
425
|
```
|
|
458
426
|
|
|
459
|
-
|
|
427
|
+
---
|
|
460
428
|
|
|
461
|
-
##
|
|
429
|
+
## API Reference
|
|
462
430
|
|
|
463
|
-
###
|
|
431
|
+
### Decorators
|
|
464
432
|
|
|
465
|
-
|
|
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 |
|
|
466
440
|
|
|
467
|
-
|
|
468
|
-
import {Container} from 'decorator-dependency-injection';
|
|
441
|
+
### Functions
|
|
469
442
|
|
|
470
|
-
|
|
471
|
-
|
|
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 |
|
|
472
458
|
|
|
473
|
-
|
|
459
|
+
---
|
|
474
460
|
|
|
475
|
-
|
|
476
|
-
container1.registerSingleton(MyService);
|
|
477
|
-
container2.registerSingleton(MyService);
|
|
478
|
-
|
|
479
|
-
// Each container maintains its own singleton instance
|
|
480
|
-
const ctx1 = container1.getContext(MyService);
|
|
481
|
-
const ctx2 = container2.getContext(MyService);
|
|
482
|
-
|
|
483
|
-
const instance1 = container1.getInstance(ctx1, []);
|
|
484
|
-
const instance2 = container2.getInstance(ctx2, []);
|
|
461
|
+
## TypeScript Support
|
|
485
462
|
|
|
486
|
-
|
|
487
|
-
```
|
|
463
|
+
Full TypeScript definitions are included:
|
|
488
464
|
|
|
489
|
-
|
|
465
|
+
```typescript
|
|
466
|
+
import { Constructor, InjectionToken, RegistrationInfo } from 'decorator-dependency-injection'
|
|
490
467
|
|
|
491
|
-
|
|
468
|
+
// Constructor<T> - a class constructor
|
|
469
|
+
const MyClass: Constructor<MyService> = MyService
|
|
492
470
|
|
|
493
|
-
|
|
494
|
-
|
|
471
|
+
// InjectionToken<T> - class or string name
|
|
472
|
+
const token: InjectionToken<MyService> = MyService
|
|
473
|
+
const named: InjectionToken = 'myService'
|
|
495
474
|
|
|
496
|
-
|
|
497
|
-
|
|
475
|
+
// RegistrationInfo - from listRegistrations()
|
|
476
|
+
// { key, name, type, isMocked, hasInstance }
|
|
498
477
|
```
|
|
499
478
|
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
The library includes TypeScript definitions with helpful type aliases:
|
|
479
|
+
---
|
|
503
480
|
|
|
504
|
-
|
|
505
|
-
import {Constructor, InjectionToken} from 'decorator-dependency-injection';
|
|
481
|
+
## Why Not [Other Library]?
|
|
506
482
|
|
|
507
|
-
|
|
508
|
-
|
|
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 |
|
|
509
490
|
|
|
510
|
-
|
|
511
|
-
const token1: InjectionToken<MyService> = MyService;
|
|
512
|
-
const token2: InjectionToken = 'myServiceName';
|
|
513
|
-
```
|
|
491
|
+
This library is ideal if you want simple, modern DI without the complexity of container configuration or reflection APIs.
|
|
514
492
|
|
|
515
|
-
|
|
493
|
+
---
|
|
516
494
|
|
|
517
|
-
##
|
|
495
|
+
## Related Topics
|
|
518
496
|
|
|
519
|
-
|
|
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.
|
|
520
498
|
|
|
521
|
-
|
|
522
|
-
npm test
|
|
523
|
-
```
|
|
499
|
+
---
|
|
524
500
|
|
|
525
501
|
## Version History
|
|
526
502
|
|
|
@@ -529,4 +505,6 @@ npm test
|
|
|
529
505
|
- 1.0.2 - Added proxy option to @Mock decorator
|
|
530
506
|
- 1.0.3 - Added @InjectLazy decorator
|
|
531
507
|
- 1.0.4 - Added Container abstraction, clearContainer(), TypeScript definitions, improved proxy support
|
|
532
|
-
- 1.0.5 - Added private field and accessor support for @Inject and @InjectLazy, debug mode, validation helpers
|
|
508
|
+
- 1.0.5 - Added private field and accessor support for @Inject and @InjectLazy, debug mode, validation helpers
|
|
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
|