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
|
@@ -0,0 +1,808 @@
|
|
|
1
|
+
# Framework and Environment Integration
|
|
2
|
+
|
|
3
|
+
This guide shows how to use decorator-dependency-injection with frontend frameworks, SSR, and various deployment targets.
|
|
4
|
+
|
|
5
|
+
## Table of Contents
|
|
6
|
+
|
|
7
|
+
- [Quick Setup](#quick-setup)
|
|
8
|
+
- [React](#react)
|
|
9
|
+
- [Vue 3](#vue-3)
|
|
10
|
+
- [Svelte](#svelte)
|
|
11
|
+
- [Project Structure](#project-structure)
|
|
12
|
+
- [Frontend Frameworks](#frontend-frameworks)
|
|
13
|
+
- [React](#react-1)
|
|
14
|
+
- [Vue 3](#vue-3-1)
|
|
15
|
+
- [Svelte](#svelte-1)
|
|
16
|
+
- [Angular](#angular)
|
|
17
|
+
- [Server-Side Rendering](#server-side-rendering)
|
|
18
|
+
- [Node.js Server Middleware](#nodejs-server-middleware)
|
|
19
|
+
- [Bundler Configuration](#bundler-configuration)
|
|
20
|
+
- [Vite](#vite)
|
|
21
|
+
- [Webpack](#webpack-create-react-app-etc)
|
|
22
|
+
- [esbuild](#esbuild)
|
|
23
|
+
- [Bun](#bun)
|
|
24
|
+
- [Runtime Environments](#runtime-environments)
|
|
25
|
+
- [Node.js](#nodejs)
|
|
26
|
+
- [AWS Lambda](#aws-lambda)
|
|
27
|
+
- [Cloudflare Workers / Vercel Edge](#cloudflare-workers--vercel-edge)
|
|
28
|
+
- [Electron](#electron)
|
|
29
|
+
- [Troubleshooting](#troubleshooting)
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## Quick Setup
|
|
34
|
+
|
|
35
|
+
### React
|
|
36
|
+
|
|
37
|
+
```jsx
|
|
38
|
+
// services/UserService.js
|
|
39
|
+
import { Singleton } from 'decorator-dependency-injection'
|
|
40
|
+
|
|
41
|
+
@Singleton()
|
|
42
|
+
export class UserService {
|
|
43
|
+
async getUser(id) {
|
|
44
|
+
const res = await fetch(`/api/users/${id}`)
|
|
45
|
+
return res.json()
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// components/UserProfile.jsx
|
|
50
|
+
import { resolve } from 'decorator-dependency-injection'
|
|
51
|
+
import { useState, useEffect, useMemo } from 'react'
|
|
52
|
+
import { UserService } from '../services/UserService'
|
|
53
|
+
|
|
54
|
+
export function UserProfile({ userId }) {
|
|
55
|
+
const [user, setUser] = useState(null)
|
|
56
|
+
const userService = useMemo(() => resolve(UserService), [])
|
|
57
|
+
|
|
58
|
+
useEffect(() => {
|
|
59
|
+
userService.getUser(userId).then(setUser)
|
|
60
|
+
}, [userId, userService])
|
|
61
|
+
|
|
62
|
+
return <div>{user?.name}</div>
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### Vue 3
|
|
67
|
+
|
|
68
|
+
```javascript
|
|
69
|
+
// services/UserService.js
|
|
70
|
+
import { Singleton } from 'decorator-dependency-injection'
|
|
71
|
+
|
|
72
|
+
@Singleton()
|
|
73
|
+
export class UserService {
|
|
74
|
+
async getUser(id) {
|
|
75
|
+
const res = await fetch(`/api/users/${id}`)
|
|
76
|
+
return res.json()
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
```vue
|
|
82
|
+
<!-- components/UserProfile.vue -->
|
|
83
|
+
<script setup>
|
|
84
|
+
import { ref, onMounted } from 'vue'
|
|
85
|
+
import { resolve } from 'decorator-dependency-injection'
|
|
86
|
+
import { UserService } from '../services/UserService'
|
|
87
|
+
|
|
88
|
+
const userService = resolve(UserService)
|
|
89
|
+
const user = ref(null)
|
|
90
|
+
|
|
91
|
+
onMounted(async () => {
|
|
92
|
+
user.value = await userService.getUser(1)
|
|
93
|
+
})
|
|
94
|
+
</script>
|
|
95
|
+
|
|
96
|
+
<template>
|
|
97
|
+
<div>{{ user?.name }}</div>
|
|
98
|
+
</template>
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### Svelte
|
|
102
|
+
|
|
103
|
+
```javascript
|
|
104
|
+
// services/UserService.js
|
|
105
|
+
import { Singleton } from 'decorator-dependency-injection'
|
|
106
|
+
|
|
107
|
+
@Singleton()
|
|
108
|
+
export class UserService {
|
|
109
|
+
async getUser(id) {
|
|
110
|
+
const res = await fetch(`/api/users/${id}`)
|
|
111
|
+
return res.json()
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
```svelte
|
|
117
|
+
<!-- components/UserProfile.svelte -->
|
|
118
|
+
<script>
|
|
119
|
+
import { onMount } from 'svelte'
|
|
120
|
+
import { resolve } from 'decorator-dependency-injection'
|
|
121
|
+
import { UserService } from '../services/UserService'
|
|
122
|
+
|
|
123
|
+
const userService = resolve(UserService)
|
|
124
|
+
let user = null
|
|
125
|
+
|
|
126
|
+
onMount(async () => {
|
|
127
|
+
user = await userService.getUser(1)
|
|
128
|
+
})
|
|
129
|
+
</script>
|
|
130
|
+
|
|
131
|
+
<div>{user?.name}</div>
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
---
|
|
135
|
+
|
|
136
|
+
## Project Structure
|
|
137
|
+
|
|
138
|
+
Recommended organization for DI-based projects:
|
|
139
|
+
|
|
140
|
+
```
|
|
141
|
+
src/
|
|
142
|
+
├── services/ # Classes with @Singleton/@Factory
|
|
143
|
+
│ ├── UserService.js
|
|
144
|
+
│ ├── AuthService.js
|
|
145
|
+
│ └── ApiClient.js
|
|
146
|
+
├── components/ # UI components (use resolve())
|
|
147
|
+
│ └── UserProfile.jsx
|
|
148
|
+
├── hooks/ # Optional: framework-specific wrappers
|
|
149
|
+
│ └── useService.js
|
|
150
|
+
└── index.js # App entry point
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
**Key principle**: Keep your service layer (classes with decorators) separate from your UI layer (components that use `resolve()`).
|
|
154
|
+
|
|
155
|
+
### Optional: Custom Hook/Composable
|
|
156
|
+
|
|
157
|
+
If you find yourself writing `useMemo(() => resolve(...), [])` repeatedly:
|
|
158
|
+
|
|
159
|
+
```javascript
|
|
160
|
+
// hooks/useService.js (React)
|
|
161
|
+
import { useMemo } from 'react'
|
|
162
|
+
import { resolve } from 'decorator-dependency-injection'
|
|
163
|
+
|
|
164
|
+
export function useService(ServiceClass) {
|
|
165
|
+
return useMemo(() => resolve(ServiceClass), [ServiceClass])
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Usage
|
|
169
|
+
const userService = useService(UserService)
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
---
|
|
173
|
+
|
|
174
|
+
## Frontend Frameworks
|
|
175
|
+
|
|
176
|
+
### React
|
|
177
|
+
|
|
178
|
+
The `resolve()` call should be memoized to avoid creating lookups on every render:
|
|
179
|
+
|
|
180
|
+
```jsx
|
|
181
|
+
import { resolve } from 'decorator-dependency-injection'
|
|
182
|
+
import { useMemo } from 'react'
|
|
183
|
+
|
|
184
|
+
function MyComponent() {
|
|
185
|
+
// Memoize the resolution
|
|
186
|
+
const userService = useMemo(() => resolve(UserService), [])
|
|
187
|
+
|
|
188
|
+
// Now use userService in effects, handlers, etc.
|
|
189
|
+
}
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
#### With TanStack Query
|
|
193
|
+
|
|
194
|
+
```jsx
|
|
195
|
+
import { useQuery } from '@tanstack/react-query'
|
|
196
|
+
import { resolve } from 'decorator-dependency-injection'
|
|
197
|
+
import { useMemo } from 'react'
|
|
198
|
+
|
|
199
|
+
function UserProfile({ userId }) {
|
|
200
|
+
const userService = useMemo(() => resolve(UserService), [])
|
|
201
|
+
|
|
202
|
+
const { data: user } = useQuery({
|
|
203
|
+
queryKey: ['user', userId],
|
|
204
|
+
queryFn: () => userService.getUser(userId)
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
return <div>{user?.name}</div>
|
|
208
|
+
}
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
### Vue 3
|
|
212
|
+
|
|
213
|
+
In Vue's Composition API, `resolve()` can be called directly in `<script setup>`:
|
|
214
|
+
|
|
215
|
+
```vue
|
|
216
|
+
<script setup>
|
|
217
|
+
import { resolve } from 'decorator-dependency-injection'
|
|
218
|
+
|
|
219
|
+
// Called once during setup - no memoization needed
|
|
220
|
+
const userService = resolve(UserService)
|
|
221
|
+
</script>
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
#### With Pinia
|
|
225
|
+
|
|
226
|
+
```javascript
|
|
227
|
+
import { defineStore } from 'pinia'
|
|
228
|
+
import { resolve } from 'decorator-dependency-injection'
|
|
229
|
+
|
|
230
|
+
export const useUserStore = defineStore('user', {
|
|
231
|
+
state: () => ({ user: null }),
|
|
232
|
+
actions: {
|
|
233
|
+
async fetchUser(id) {
|
|
234
|
+
const userService = resolve(UserService)
|
|
235
|
+
this.user = await userService.getUser(id)
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
})
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
### Svelte
|
|
242
|
+
|
|
243
|
+
In Svelte, `resolve()` in the `<script>` block runs once per component instance:
|
|
244
|
+
|
|
245
|
+
```svelte
|
|
246
|
+
<script>
|
|
247
|
+
import { resolve } from 'decorator-dependency-injection'
|
|
248
|
+
|
|
249
|
+
const userService = resolve(UserService)
|
|
250
|
+
</script>
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
### Angular
|
|
254
|
+
|
|
255
|
+
Angular has its own DI system. If you need to bridge:
|
|
256
|
+
|
|
257
|
+
```typescript
|
|
258
|
+
import { Injectable } from '@angular/core'
|
|
259
|
+
import { resolve } from 'decorator-dependency-injection'
|
|
260
|
+
|
|
261
|
+
@Injectable({ providedIn: 'root' })
|
|
262
|
+
export class UserServiceBridge {
|
|
263
|
+
private service = resolve(UserService)
|
|
264
|
+
|
|
265
|
+
getUser(id: number) {
|
|
266
|
+
return this.service.getUser(id)
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
Generally, stick with Angular's native DI for Angular projects.
|
|
272
|
+
|
|
273
|
+
---
|
|
274
|
+
|
|
275
|
+
## Server-Side Rendering
|
|
276
|
+
|
|
277
|
+
### The Problem
|
|
278
|
+
|
|
279
|
+
Singletons persist across requests on the server, potentially leaking user data:
|
|
280
|
+
|
|
281
|
+
```javascript
|
|
282
|
+
@Singleton()
|
|
283
|
+
class AuthService {
|
|
284
|
+
currentUser = null // DANGER: Shared across ALL requests!
|
|
285
|
+
}
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
### The Solution
|
|
289
|
+
|
|
290
|
+
Create a new container per request:
|
|
291
|
+
|
|
292
|
+
```javascript
|
|
293
|
+
import { Container } from 'decorator-dependency-injection'
|
|
294
|
+
|
|
295
|
+
async function handleRequest(req) {
|
|
296
|
+
// Each request gets its own container
|
|
297
|
+
const container = new Container()
|
|
298
|
+
container.registerSingleton(AuthService)
|
|
299
|
+
container.registerSingleton(UserService)
|
|
300
|
+
|
|
301
|
+
// This instance is isolated to this request
|
|
302
|
+
const auth = container.resolve(AuthService)
|
|
303
|
+
auth.currentUser = req.user
|
|
304
|
+
|
|
305
|
+
// Process request with isolated state
|
|
306
|
+
const userService = container.resolve(UserService)
|
|
307
|
+
return await userService.getData()
|
|
308
|
+
}
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
This pattern works identically in **Next.js**, **Nuxt**, **SvelteKit**, **Remix**, or any SSR framework.
|
|
312
|
+
|
|
313
|
+
### Client vs Server
|
|
314
|
+
|
|
315
|
+
| Context | Container | Why |
|
|
316
|
+
|---------|-----------|-----|
|
|
317
|
+
| Client (browser) | Global (default) | One user per browser tab |
|
|
318
|
+
| Server (SSR) | Per-request | Multiple users share the process |
|
|
319
|
+
| API Routes | Per-request | Same as SSR |
|
|
320
|
+
|
|
321
|
+
### Example: Next.js App Router
|
|
322
|
+
|
|
323
|
+
```javascript
|
|
324
|
+
// app/users/[id]/page.js
|
|
325
|
+
import { Container } from 'decorator-dependency-injection'
|
|
326
|
+
|
|
327
|
+
export default async function UserPage({ params }) {
|
|
328
|
+
const container = new Container()
|
|
329
|
+
container.registerSingleton(UserService)
|
|
330
|
+
|
|
331
|
+
const userService = container.resolve(UserService)
|
|
332
|
+
const user = await userService.getUser(params.id)
|
|
333
|
+
|
|
334
|
+
return <div>{user.name}</div>
|
|
335
|
+
}
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
```javascript
|
|
339
|
+
// Client component - uses global container (safe)
|
|
340
|
+
'use client'
|
|
341
|
+
import { resolve } from 'decorator-dependency-injection'
|
|
342
|
+
import { useMemo } from 'react'
|
|
343
|
+
|
|
344
|
+
export function UserProfile({ userId }) {
|
|
345
|
+
const userService = useMemo(() => resolve(UserService), [])
|
|
346
|
+
// ...
|
|
347
|
+
}
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
---
|
|
351
|
+
|
|
352
|
+
## Node.js Server Middleware
|
|
353
|
+
|
|
354
|
+
For Express, Koa, Fastify, Hono, and other Node.js servers, we provide middleware that automatically creates **request-scoped containers** using `AsyncLocalStorage`.
|
|
355
|
+
|
|
356
|
+
### ⚠️ Important: How Request Scoping Works
|
|
357
|
+
|
|
358
|
+
When you use this middleware, **a new container is created for each HTTP request**:
|
|
359
|
+
|
|
360
|
+
```
|
|
361
|
+
Request 1 ──────────────────────────────────────────────────────►
|
|
362
|
+
│ Container A created │ UserService instance A │ Container A garbage collected
|
|
363
|
+
|
|
364
|
+
Request 2 ──────────────────────────────────────────────────────►
|
|
365
|
+
│ Container B created │ UserService instance B │ Container B garbage collected
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
**Key points:**
|
|
369
|
+
- `resolve()` from `decorator-dependency-injection/middleware` returns instances from the **current request's container**
|
|
370
|
+
- Singletons are **isolated per-request** - each request gets its own instance
|
|
371
|
+
- This prevents data leaking between users (critical for SSR safety)
|
|
372
|
+
- Services are **auto-registered** from the global container - no need to list them explicitly
|
|
373
|
+
|
|
374
|
+
### Basic Setup (Recommended)
|
|
375
|
+
|
|
376
|
+
```javascript
|
|
377
|
+
import express from 'express'
|
|
378
|
+
import { containerMiddleware, resolve } from 'decorator-dependency-injection/middleware'
|
|
379
|
+
|
|
380
|
+
const app = express()
|
|
381
|
+
|
|
382
|
+
// Creates a new container for each request
|
|
383
|
+
app.use(containerMiddleware())
|
|
384
|
+
|
|
385
|
+
app.get('/users/:id', async (req, res) => {
|
|
386
|
+
// This UserService instance is ISOLATED to this request
|
|
387
|
+
// @Singleton() decorated services are auto-registered
|
|
388
|
+
const userService = resolve(UserService)
|
|
389
|
+
const user = await userService.getUser(req.params.id)
|
|
390
|
+
res.json(user)
|
|
391
|
+
})
|
|
392
|
+
|
|
393
|
+
app.listen(3000)
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
### Why Request Scoping Matters
|
|
397
|
+
|
|
398
|
+
Without request scoping, stateful services leak between users:
|
|
399
|
+
|
|
400
|
+
```javascript
|
|
401
|
+
// ❌ DANGEROUS - shared across ALL requests
|
|
402
|
+
@Singleton()
|
|
403
|
+
class AuthService {
|
|
404
|
+
currentUser = null // User A's data leaks to User B!
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// ✅ SAFE - with request-scoped containers
|
|
408
|
+
app.use(containerMiddleware())
|
|
409
|
+
|
|
410
|
+
app.get('/me', (req, res) => {
|
|
411
|
+
const auth = resolve(AuthService) // Fresh instance per request
|
|
412
|
+
auth.currentUser = req.user // Safe! Isolated to this request
|
|
413
|
+
res.json(auth.currentUser)
|
|
414
|
+
})
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
### Scope Options
|
|
418
|
+
|
|
419
|
+
You can control the scope per-middleware:
|
|
420
|
+
|
|
421
|
+
| Scope | Behavior | Use Case |
|
|
422
|
+
|-------|----------|----------|
|
|
423
|
+
| `'request'` (default) | New container per request, isolated singletons | Stateful services, SSR, user-specific data |
|
|
424
|
+
| `'global'` | Use the global container directly | Stateless services, connection pools, config |
|
|
425
|
+
|
|
426
|
+
```javascript
|
|
427
|
+
// Request scope (default) - isolated singletons
|
|
428
|
+
app.use(containerMiddleware()) // or { scope: 'request' }
|
|
429
|
+
|
|
430
|
+
// Global scope - shared singletons (use carefully!)
|
|
431
|
+
app.use(containerMiddleware({ scope: 'global' }))
|
|
432
|
+
```
|
|
433
|
+
|
|
434
|
+
**When to use global scope:**
|
|
435
|
+
- Database connection pools (should be shared)
|
|
436
|
+
- Configuration services (immutable, stateless)
|
|
437
|
+
- Caches (shared across requests)
|
|
438
|
+
|
|
439
|
+
**When to use request scope (default):**
|
|
440
|
+
- Authentication/session services
|
|
441
|
+
- Request-specific loggers
|
|
442
|
+
- Any service that holds user state
|
|
443
|
+
|
|
444
|
+
### Mixing Scopes in the Same Handler
|
|
445
|
+
|
|
446
|
+
Sometimes you need both request-scoped and global services in the same handler. Use the `scope` option on `resolve()`:
|
|
447
|
+
|
|
448
|
+
```javascript
|
|
449
|
+
import { containerMiddleware, resolve } from 'decorator-dependency-injection/middleware'
|
|
450
|
+
|
|
451
|
+
app.use(containerMiddleware())
|
|
452
|
+
|
|
453
|
+
app.get('/users/:id', async (req, res) => {
|
|
454
|
+
// Request-scoped (default) - isolated per request
|
|
455
|
+
const authService = resolve(AuthService)
|
|
456
|
+
|
|
457
|
+
// Explicitly global - shared across all requests
|
|
458
|
+
const dbPool = resolve(DatabasePool, { scope: 'global' })
|
|
459
|
+
const cache = resolve(CacheService, { scope: 'global' })
|
|
460
|
+
|
|
461
|
+
// Use both together
|
|
462
|
+
const user = await dbPool.query('SELECT * FROM users WHERE id = ?', [req.params.id])
|
|
463
|
+
authService.setUser(user) // Safe - isolated to this request
|
|
464
|
+
|
|
465
|
+
res.json(user)
|
|
466
|
+
})
|
|
467
|
+
```
|
|
468
|
+
|
|
469
|
+
**Alternative:** Import `resolve` from the main module as `resolveGlobal`:
|
|
470
|
+
|
|
471
|
+
```javascript
|
|
472
|
+
import { containerMiddleware, resolve } from 'decorator-dependency-injection/middleware'
|
|
473
|
+
import { resolve as resolveGlobal } from 'decorator-dependency-injection'
|
|
474
|
+
|
|
475
|
+
app.get('/users/:id', async (req, res) => {
|
|
476
|
+
const authService = resolve(AuthService) // Request-scoped
|
|
477
|
+
const dbPool = resolveGlobal(DatabasePool) // Global
|
|
478
|
+
// ...
|
|
479
|
+
})
|
|
480
|
+
```
|
|
481
|
+
|
|
482
|
+
**Warning behavior:** If you explicitly request `{ scope: 'request' }` but no middleware is set up, a warning is logged:
|
|
483
|
+
|
|
484
|
+
```javascript
|
|
485
|
+
// Outside any request context
|
|
486
|
+
const auth = resolve(AuthService, { scope: 'request' })
|
|
487
|
+
// ⚠️ Console: [DI] resolve() called with scope='request' but no request context exists...
|
|
488
|
+
```
|
|
489
|
+
|
|
490
|
+
### Koa
|
|
491
|
+
|
|
492
|
+
```javascript
|
|
493
|
+
import Koa from 'koa'
|
|
494
|
+
import { koaContainerMiddleware, resolve } from 'decorator-dependency-injection/middleware'
|
|
495
|
+
|
|
496
|
+
const app = new Koa()
|
|
497
|
+
app.use(koaContainerMiddleware())
|
|
498
|
+
|
|
499
|
+
app.use(async (ctx) => {
|
|
500
|
+
const userService = resolve(UserService) // Request-scoped
|
|
501
|
+
ctx.body = await userService.getUser(ctx.params.id)
|
|
502
|
+
})
|
|
503
|
+
```
|
|
504
|
+
|
|
505
|
+
### Hono / Fastify (Handler Wrapper)
|
|
506
|
+
|
|
507
|
+
```javascript
|
|
508
|
+
import { Hono } from 'hono'
|
|
509
|
+
import { withContainer, resolve } from 'decorator-dependency-injection/middleware'
|
|
510
|
+
|
|
511
|
+
const app = new Hono()
|
|
512
|
+
|
|
513
|
+
app.get('/users/:id', withContainer()(async (c) => {
|
|
514
|
+
const userService = resolve(UserService)
|
|
515
|
+
return c.json(await userService.getUser(c.req.param('id')))
|
|
516
|
+
}))
|
|
517
|
+
```
|
|
518
|
+
|
|
519
|
+
### How Auto-Registration Works
|
|
520
|
+
|
|
521
|
+
When you call `resolve(UserService)`:
|
|
522
|
+
|
|
523
|
+
1. Middleware checks if `UserService` is registered in the request container
|
|
524
|
+
2. If not, it looks up the registration in the **global container** (where `@Singleton()` registered it)
|
|
525
|
+
3. It copies the registration type (singleton/factory) to the request container
|
|
526
|
+
4. A new instance is created **in the request container**
|
|
527
|
+
|
|
528
|
+
The global container is automatically set to the default container from the main module when you import `decorator-dependency-injection/middleware` - no manual setup needed.
|
|
529
|
+
|
|
530
|
+
### Direct Container Access
|
|
531
|
+
|
|
532
|
+
You can access the request's DI container directly via `req.di`:
|
|
533
|
+
|
|
534
|
+
```javascript
|
|
535
|
+
app.get('/users/:id', async (req, res) => {
|
|
536
|
+
// Recommended: use resolve()
|
|
537
|
+
const userService = resolve(UserService)
|
|
538
|
+
|
|
539
|
+
// Alternative: direct container access via req.di
|
|
540
|
+
console.log(req.di.has(UserService)) // true after first resolve
|
|
541
|
+
|
|
542
|
+
// Register request-specific services
|
|
543
|
+
req.di.registerSingleton(RequestLogger)
|
|
544
|
+
})
|
|
545
|
+
```
|
|
546
|
+
|
|
547
|
+
### Testing with runWithContainer
|
|
548
|
+
|
|
549
|
+
For unit tests, use `runWithContainer` to control the container:
|
|
550
|
+
|
|
551
|
+
```javascript
|
|
552
|
+
import { runWithContainer, resolve } from 'decorator-dependency-injection/middleware'
|
|
553
|
+
import { Container } from 'decorator-dependency-injection'
|
|
554
|
+
|
|
555
|
+
it('uses the mocked service', () => {
|
|
556
|
+
const testContainer = new Container()
|
|
557
|
+
testContainer.registerSingleton(MockUserService)
|
|
558
|
+
|
|
559
|
+
const result = runWithContainer(testContainer, () => {
|
|
560
|
+
return resolve(UserService).getUser(1) // Gets MockUserService
|
|
561
|
+
})
|
|
562
|
+
|
|
563
|
+
expect(result).toEqual({ id: 1, name: 'Mock User' })
|
|
564
|
+
})
|
|
565
|
+
```
|
|
566
|
+
|
|
567
|
+
---
|
|
568
|
+
|
|
569
|
+
## Bundler Configuration
|
|
570
|
+
|
|
571
|
+
Decorators require transpilation. Here's the setup for common bundlers:
|
|
572
|
+
|
|
573
|
+
### Vite
|
|
574
|
+
|
|
575
|
+
```bash
|
|
576
|
+
npm install -D @babel/core @babel/plugin-proposal-decorators
|
|
577
|
+
```
|
|
578
|
+
|
|
579
|
+
```javascript
|
|
580
|
+
// vite.config.js
|
|
581
|
+
import { defineConfig } from 'vite'
|
|
582
|
+
import react from '@vitejs/plugin-react'
|
|
583
|
+
|
|
584
|
+
export default defineConfig({
|
|
585
|
+
plugins: [
|
|
586
|
+
react({
|
|
587
|
+
babel: {
|
|
588
|
+
plugins: [
|
|
589
|
+
['@babel/plugin-proposal-decorators', { version: '2023-11' }]
|
|
590
|
+
]
|
|
591
|
+
}
|
|
592
|
+
})
|
|
593
|
+
]
|
|
594
|
+
})
|
|
595
|
+
```
|
|
596
|
+
|
|
597
|
+
### Webpack (Create React App, etc.)
|
|
598
|
+
|
|
599
|
+
```bash
|
|
600
|
+
npm install -D @babel/plugin-proposal-decorators
|
|
601
|
+
```
|
|
602
|
+
|
|
603
|
+
```javascript
|
|
604
|
+
// babel.config.js
|
|
605
|
+
module.exports = {
|
|
606
|
+
plugins: [
|
|
607
|
+
['@babel/plugin-proposal-decorators', { version: '2023-11' }]
|
|
608
|
+
]
|
|
609
|
+
}
|
|
610
|
+
```
|
|
611
|
+
|
|
612
|
+
### esbuild
|
|
613
|
+
|
|
614
|
+
esbuild doesn't support decorators natively. Use esbuild-plugin-babel:
|
|
615
|
+
|
|
616
|
+
```bash
|
|
617
|
+
npm install -D esbuild-plugin-babel @babel/core @babel/plugin-proposal-decorators
|
|
618
|
+
```
|
|
619
|
+
|
|
620
|
+
```javascript
|
|
621
|
+
import babel from 'esbuild-plugin-babel'
|
|
622
|
+
|
|
623
|
+
esbuild.build({
|
|
624
|
+
plugins: [
|
|
625
|
+
babel({
|
|
626
|
+
filter: /\.js$/,
|
|
627
|
+
config: {
|
|
628
|
+
plugins: [['@babel/plugin-proposal-decorators', { version: '2023-11' }]]
|
|
629
|
+
}
|
|
630
|
+
})
|
|
631
|
+
]
|
|
632
|
+
})
|
|
633
|
+
```
|
|
634
|
+
|
|
635
|
+
### Bun
|
|
636
|
+
|
|
637
|
+
Bun supports TC39 decorators natively - no configuration needed:
|
|
638
|
+
|
|
639
|
+
```bash
|
|
640
|
+
bun run index.js
|
|
641
|
+
```
|
|
642
|
+
|
|
643
|
+
---
|
|
644
|
+
|
|
645
|
+
## Runtime Environments
|
|
646
|
+
|
|
647
|
+
### Node.js
|
|
648
|
+
|
|
649
|
+
Requires Babel:
|
|
650
|
+
|
|
651
|
+
```bash
|
|
652
|
+
npm install -D @babel/core @babel/node @babel/plugin-proposal-decorators
|
|
653
|
+
npx babel-node index.js
|
|
654
|
+
```
|
|
655
|
+
|
|
656
|
+
### Bun
|
|
657
|
+
|
|
658
|
+
Native support, no setup required.
|
|
659
|
+
|
|
660
|
+
### AWS Lambda
|
|
661
|
+
|
|
662
|
+
Works well - each Lambda instance has its own container. Singletons persist across warm invocations (efficient for connection pooling, etc.):
|
|
663
|
+
|
|
664
|
+
```javascript
|
|
665
|
+
import { resolve } from 'decorator-dependency-injection'
|
|
666
|
+
|
|
667
|
+
export const handler = async (event) => {
|
|
668
|
+
const userService = resolve(UserService)
|
|
669
|
+
return await userService.getUser(event.userId)
|
|
670
|
+
}
|
|
671
|
+
```
|
|
672
|
+
|
|
673
|
+
### Cloudflare Workers / Vercel Edge
|
|
674
|
+
|
|
675
|
+
These environments have limited decorator support. Use programmatic registration:
|
|
676
|
+
|
|
677
|
+
```javascript
|
|
678
|
+
import { Container } from 'decorator-dependency-injection'
|
|
679
|
+
|
|
680
|
+
// Define classes without decorators
|
|
681
|
+
class UserService {
|
|
682
|
+
getUser(id) { /* ... */ }
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
export default {
|
|
686
|
+
async fetch(request) {
|
|
687
|
+
const container = new Container()
|
|
688
|
+
container.registerSingleton(UserService)
|
|
689
|
+
|
|
690
|
+
const userService = container.resolve(UserService)
|
|
691
|
+
return Response.json(await userService.getUser(1))
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
```
|
|
695
|
+
|
|
696
|
+
### Electron
|
|
697
|
+
|
|
698
|
+
Main and renderer processes have separate JavaScript contexts - each has its own container automatically.
|
|
699
|
+
|
|
700
|
+
---
|
|
701
|
+
|
|
702
|
+
## Troubleshooting
|
|
703
|
+
|
|
704
|
+
### "X is not registered"
|
|
705
|
+
|
|
706
|
+
**Cause**: The class wasn't decorated with `@Singleton()` or `@Factory()`, or the decorator hasn't run yet.
|
|
707
|
+
|
|
708
|
+
**Fix**:
|
|
709
|
+
1. Ensure the class has `@Singleton()` or `@Factory()`
|
|
710
|
+
2. Import the service file before calling `resolve()` (decorators run at import time)
|
|
711
|
+
|
|
712
|
+
```javascript
|
|
713
|
+
// Wrong - UserService not imported yet
|
|
714
|
+
const userService = resolve(UserService) // Error!
|
|
715
|
+
|
|
716
|
+
// Correct
|
|
717
|
+
import { UserService } from './services/UserService' // Decorator runs here
|
|
718
|
+
const userService = resolve(UserService) // Works
|
|
719
|
+
```
|
|
720
|
+
|
|
721
|
+
### Stale singletons after code changes (HMR)
|
|
722
|
+
|
|
723
|
+
During development, singleton instances persist across hot reloads.
|
|
724
|
+
|
|
725
|
+
**Fix**: Clear the container on HMR:
|
|
726
|
+
|
|
727
|
+
```javascript
|
|
728
|
+
// Vite
|
|
729
|
+
if (import.meta.hot) {
|
|
730
|
+
import.meta.hot.accept(() => {
|
|
731
|
+
clearContainer({ preserveRegistrations: true })
|
|
732
|
+
})
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
// Webpack
|
|
736
|
+
if (module.hot) {
|
|
737
|
+
module.hot.accept('./services', () => {
|
|
738
|
+
clearContainer({ preserveRegistrations: true })
|
|
739
|
+
})
|
|
740
|
+
}
|
|
741
|
+
```
|
|
742
|
+
|
|
743
|
+
### "Cannot use decorators" / Syntax Error
|
|
744
|
+
|
|
745
|
+
**Cause**: Bundler isn't configured to transpile decorators.
|
|
746
|
+
|
|
747
|
+
**Fix**: See [Bundler Configuration](#bundler-configuration) above.
|
|
748
|
+
|
|
749
|
+
### SSR: Data leaking between requests
|
|
750
|
+
|
|
751
|
+
**Cause**: Using global container on server.
|
|
752
|
+
|
|
753
|
+
**Fix**: Create a new `Container()` per request. See [Server-Side Rendering](#server-side-rendering).
|
|
754
|
+
|
|
755
|
+
### Mock not working in tests
|
|
756
|
+
|
|
757
|
+
**Cause**: Mock defined after the code that uses the service runs.
|
|
758
|
+
|
|
759
|
+
**Fix**: Define mocks before importing code that resolves services:
|
|
760
|
+
|
|
761
|
+
```javascript
|
|
762
|
+
// test file
|
|
763
|
+
import { Mock, removeAllMocks } from 'decorator-dependency-injection'
|
|
764
|
+
|
|
765
|
+
// Define mock FIRST
|
|
766
|
+
@Mock(UserService)
|
|
767
|
+
class MockUserService {
|
|
768
|
+
getUser = vi.fn().mockResolvedValue({ id: 1 })
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
// THEN import the component that uses UserService
|
|
772
|
+
import { UserProfile } from './UserProfile'
|
|
773
|
+
|
|
774
|
+
afterEach(() => removeAllMocks())
|
|
775
|
+
```
|
|
776
|
+
|
|
777
|
+
### Circular dependency error
|
|
778
|
+
|
|
779
|
+
**Cause**: ServiceA imports ServiceB which imports ServiceA.
|
|
780
|
+
|
|
781
|
+
**Fix**: Use `@InjectLazy` to break the cycle:
|
|
782
|
+
|
|
783
|
+
```javascript
|
|
784
|
+
@Singleton()
|
|
785
|
+
class ServiceA {
|
|
786
|
+
@InjectLazy(ServiceB) serviceB // Deferred until first access
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
@Singleton()
|
|
790
|
+
class ServiceB {
|
|
791
|
+
@Inject(ServiceA) serviceA
|
|
792
|
+
}
|
|
793
|
+
```
|
|
794
|
+
|
|
795
|
+
---
|
|
796
|
+
|
|
797
|
+
## Environment Support Matrix
|
|
798
|
+
|
|
799
|
+
| Environment | Decorators | Notes |
|
|
800
|
+
|-------------|------------|-------|
|
|
801
|
+
| Node.js + Babel | Yes | Full support |
|
|
802
|
+
| Bun | Yes | Native support |
|
|
803
|
+
| Vite | Yes | With Babel plugin |
|
|
804
|
+
| Webpack | Yes | With Babel plugin |
|
|
805
|
+
| AWS Lambda | Yes | Singletons persist across warm starts |
|
|
806
|
+
| Cloudflare Workers | No | Use programmatic `Container` API |
|
|
807
|
+
| Vercel Edge | No | Use programmatic `Container` API |
|
|
808
|
+
| Electron | Yes | Separate container per process |
|