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