@nextrush/di 3.0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Tanzim (NextRush)
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,481 @@
1
+ # @nextrush/di
2
+
3
+ Lightweight dependency injection container for NextRush v3.
4
+
5
+ ## Features
6
+
7
+ - **Constructor Injection** — Automatic dependency resolution via TypeScript metadata
8
+ - **Singleton & Transient Scopes** — Control instance lifecycle per service
9
+ - **Circular Dependency Detection** — O(1) Set-based detection with cycle visualization, catches tsyringe-internal chains
10
+ - **Production-Grade Errors** — Actionable messages with fix suggestions (`@Service()`, `@Repository()`, `@Config()` hints)
11
+ - **Optional Dependencies** — `@Optional()` decorator for graceful handling of missing services
12
+ - **Test-Friendly** — Isolated containers and instance clearing for test setup
13
+
14
+ ## Development
15
+
16
+ For the best development experience with full decorator and DI support, use **`@nextrush/dev`**.
17
+
18
+ ```bash
19
+ pnpm add -D @nextrush/dev
20
+ ```
21
+
22
+ Then in your `package.json`:
23
+
24
+ ```json
25
+ {
26
+ "scripts": {
27
+ "dev": "nextrush dev"
28
+ }
29
+ }
30
+ ```
31
+
32
+ ### Why?
33
+
34
+ TypeScript's `emitDecoratorMetadata` option emits runtime type information that allows the DI container to automatically resolve constructor dependencies. Most modern fast runners (`tsx`, `esbuild`, `node --experimental-strip-types`) strip types but **do not** emit this metadata, causing errors like:
35
+
36
+ ```
37
+ TypeInfo not known for "UserService"
38
+ ```
39
+
40
+ | Runtime | Decorator Metadata | Recommended |
41
+ | ----------------- | ------------------ | ---------------- |
42
+ | **nextrush dev** | Full Support | Yes |
43
+ | **tsc + node** | Full Support | Yes (Production) |
44
+ | **tsx / esbuild** | Not Supported | No |
45
+ | **ts-node --esm** | Issues | No |
46
+
47
+ ## Installation
48
+
49
+ ```bash
50
+ pnpm add @nextrush/di
51
+ ```
52
+
53
+ > If you use the `nextrush` meta-package, `reflect-metadata` is auto-imported. Otherwise, install it separately: `pnpm add reflect-metadata` and add `import 'reflect-metadata'` at your entry point.
54
+
55
+ ## TypeScript Configuration
56
+
57
+ **Required** `tsconfig.json` settings:
58
+
59
+ ```json
60
+ {
61
+ "compilerOptions": {
62
+ "experimentalDecorators": true,
63
+ "emitDecoratorMetadata": true
64
+ }
65
+ }
66
+ ```
67
+
68
+ ## Quick Start
69
+
70
+ ```typescript
71
+ import 'reflect-metadata'; // Required when NOT using the nextrush meta-package
72
+ import { Service, Repository, container } from '@nextrush/di';
73
+
74
+ @Repository()
75
+ class UserRepository {
76
+ findAll() {
77
+ return [{ id: 1, name: 'Alice' }];
78
+ }
79
+ }
80
+
81
+ @Service()
82
+ class UserService {
83
+ constructor(private repo: UserRepository) {}
84
+
85
+ getUsers() {
86
+ return this.repo.findAll();
87
+ }
88
+ }
89
+
90
+ const userService = container.resolve(UserService);
91
+ console.log(userService.getUsers()); // [{ id: 1, name: 'Alice' }]
92
+ ```
93
+
94
+ ## API Reference
95
+
96
+ ### Decorators
97
+
98
+ #### `@Service(options?)`
99
+
100
+ Mark a class as an injectable service. Singletons by default.
101
+
102
+ ```typescript
103
+ @Service()
104
+ class MyService {}
105
+
106
+ // Transient (new instance each time)
107
+ @Service({ scope: 'transient' })
108
+ class RequestLogger {}
109
+ ```
110
+
111
+ **`ServiceOptions`:**
112
+
113
+ | Property | Type | Default | Description |
114
+ | -------- | ---------------------------- | ------------- | ------------------ |
115
+ | `scope` | `'singleton' \| 'transient'` | `'singleton'` | Instance lifecycle |
116
+
117
+ #### `@Repository(options?)`
118
+
119
+ Semantic alias for `@Service()`. Sets metadata type to `'repository'` instead of `'service'`. Use for data access layers.
120
+
121
+ ```typescript
122
+ @Repository()
123
+ class UserRepository {
124
+ findById(id: string) {
125
+ return db.users.find(id);
126
+ }
127
+ }
128
+ ```
129
+
130
+ #### `@inject(token)`
131
+
132
+ Explicitly inject a dependency by token. Use for interfaces, string tokens, or symbol tokens.
133
+
134
+ ```typescript
135
+ const DATABASE_TOKEN = Symbol('Database');
136
+
137
+ @Service()
138
+ class UserService {
139
+ constructor(
140
+ @inject(DATABASE_TOKEN) private db: IDatabase,
141
+ @inject('API_KEY') private apiKey: string
142
+ ) {}
143
+ }
144
+ ```
145
+
146
+ #### `@AutoInjectable()`
147
+
148
+ Mark a class as injectable in the container. Sets service type metadata to `'service'`.
149
+
150
+ ```typescript
151
+ @AutoInjectable()
152
+ class FeatureService {
153
+ constructor(private logger: Logger) {}
154
+ }
155
+
156
+ // Resolve through the container
157
+ const service = container.resolve(FeatureService);
158
+ ```
159
+
160
+ > **Note:** Despite the name, `@AutoInjectable()` does not enable dependency injection via `new`. The current implementation uses tsyringe's `injectable()` decorator internally. Dependencies are resolved only through `container.resolve()`.
161
+
162
+ #### `delay(tokenFactory)`
163
+
164
+ Defer resolution to break circular dependencies. Returns a lazy token for use with `@inject()`.
165
+
166
+ ```typescript
167
+ import { delay, inject, Service } from '@nextrush/di';
168
+
169
+ @Service()
170
+ class ServiceA {
171
+ constructor(@inject(delay(() => ServiceB)) private b: ServiceB) {}
172
+ }
173
+
174
+ @Service()
175
+ class ServiceB {
176
+ constructor(@inject(delay(() => ServiceA)) private a: ServiceA) {}
177
+ }
178
+ ```
179
+
180
+ #### `@Optional()`
181
+
182
+ Mark a constructor parameter as optional. When the dependency is not registered, the container injects `undefined` instead of throwing.
183
+
184
+ ```typescript
185
+ import { Service, Optional, inject } from '@nextrush/di';
186
+
187
+ @Service()
188
+ class NotificationService {
189
+ constructor(
190
+ @Optional() private emailService?: EmailService,
191
+ @inject('SLACK_TOKEN') @Optional() private slackToken?: string
192
+ ) {}
193
+
194
+ notify(message: string) {
195
+ if (this.emailService) {
196
+ this.emailService.send(message);
197
+ }
198
+ if (this.slackToken) {
199
+ // send to Slack
200
+ }
201
+ }
202
+ }
203
+ ```
204
+
205
+ #### `isParameterOptional(target, parameterIndex)`
206
+
207
+ Check if a specific constructor parameter is marked as optional.
208
+
209
+ ```typescript
210
+ import { isParameterOptional, Optional, Service } from '@nextrush/di';
211
+
212
+ @Service()
213
+ class MyService {
214
+ constructor(@Optional() private dep?: SomeDep) {}
215
+ }
216
+
217
+ isParameterOptional(MyService, 0); // true
218
+ isParameterOptional(MyService, 1); // false
219
+ ```
220
+
221
+ #### `getOptionalParams(target)`
222
+
223
+ Get all optional parameter indices for a class. Returns a `ReadonlySet<number>`.
224
+
225
+ ```typescript
226
+ import { getOptionalParams } from '@nextrush/di';
227
+
228
+ const optionals = getOptionalParams(MyService);
229
+ // Set { 0 }
230
+ ```
231
+
232
+ ### Utility Functions
233
+
234
+ #### `hasServiceMetadata(target)`
235
+
236
+ Check if a class has DI metadata (decorated with `@Service()` or `@Repository()`).
237
+
238
+ ```typescript
239
+ import { hasServiceMetadata, Service } from '@nextrush/di';
240
+
241
+ @Service()
242
+ class MyService {}
243
+
244
+ hasServiceMetadata(MyService); // true
245
+ ```
246
+
247
+ #### `getServiceType(target)`
248
+
249
+ Get the service type from a decorated class. Returns `'service'`, `'repository'`, or `undefined`.
250
+
251
+ #### `getServiceScope(target)`
252
+
253
+ Get the scope from a decorated class. Returns `'singleton'`, `'transient'`, or `undefined`.
254
+
255
+ ### Container
256
+
257
+ #### `container.register(token, provider)`
258
+
259
+ Register a dependency with the container.
260
+
261
+ ```typescript
262
+ // Class provider
263
+ container.register(UserService, { useClass: UserService });
264
+
265
+ // Value provider
266
+ container.register('CONFIG', { useValue: { port: 3000 } });
267
+
268
+ // Factory provider — receives the container for nested resolution
269
+ container.register(Logger, {
270
+ useFactory: (c) => new Logger(c.resolve('CONFIG')),
271
+ });
272
+ ```
273
+
274
+ #### `container.resolve(token)`
275
+
276
+ Resolve a dependency from the container.
277
+
278
+ ```typescript
279
+ const service = container.resolve(UserService);
280
+ const config = container.resolve<Config>('CONFIG');
281
+ ```
282
+
283
+ #### `container.resolveAll(token)`
284
+
285
+ Resolve all dependencies registered under a token. Returns an empty array if none are registered.
286
+
287
+ ```typescript
288
+ container.register('Plugin', { useValue: pluginA });
289
+ container.register('Plugin', { useValue: pluginB });
290
+
291
+ const plugins = container.resolveAll<Plugin>('Plugin');
292
+ ```
293
+
294
+ #### `container.isRegistered(token)`
295
+
296
+ Check if a token is registered.
297
+
298
+ ```typescript
299
+ if (container.isRegistered(UserService)) {
300
+ // ...
301
+ }
302
+ ```
303
+
304
+ #### `container.clearInstances()`
305
+
306
+ Clear cached singleton instances. Registrations remain — the next `resolve()` creates fresh instances.
307
+
308
+ ```typescript
309
+ beforeEach(() => {
310
+ container.clearInstances();
311
+ });
312
+ ```
313
+
314
+ #### `container.reset()`
315
+
316
+ Reset the container completely, removing all registrations and instances.
317
+
318
+ #### `container.createChild()`
319
+
320
+ Create a child container. The child inherits parent registrations but can override them independently.
321
+
322
+ #### `createContainer()`
323
+
324
+ Create a new isolated container with no inherited registrations.
325
+
326
+ ```typescript
327
+ const testContainer = createContainer();
328
+ testContainer.register(UserService, { useClass: MockUserService });
329
+ ```
330
+
331
+ ## Error Handling
332
+
333
+ All errors extend `DIError` and include actionable messages:
334
+
335
+ ```typescript
336
+ import {
337
+ DIError,
338
+ DependencyResolutionError,
339
+ CircularDependencyError,
340
+ TypeInferenceError,
341
+ MissingDependencyError,
342
+ InvalidProviderError,
343
+ ContainerDisposedError,
344
+ } from '@nextrush/di';
345
+
346
+ try {
347
+ container.resolve(UnregisteredService);
348
+ } catch (error) {
349
+ if (error instanceof DependencyResolutionError) {
350
+ console.log(error.missingDependency); // token name
351
+ console.log(error.chain); // resolution path
352
+ }
353
+ if (error instanceof CircularDependencyError) {
354
+ console.log(error.cycle); // ['ServiceA', 'ServiceB', ...]
355
+ }
356
+ }
357
+ ```
358
+
359
+ | Error | Cause |
360
+ | --------------------------- | --------------------------------------------------------------------------------------------- |
361
+ | `DependencyResolutionError` | Token not registered — includes fix suggestions (`@Service()`, import order, manual register) |
362
+ | `CircularDependencyError` | Circular dependency detected (wrapper-level + tsyringe-internal chains) |
363
+ | `MissingDependencyError` | _(Deprecated)_ Use `DependencyResolutionError` instead |
364
+ | `InvalidProviderError` | Provider missing `useClass`, `useValue`, or `useFactory` |
365
+ | `TypeInferenceError` | Constructor parameter type not available at runtime |
366
+ | `ContainerDisposedError` | Container has been reset or disposed |
367
+
368
+ ## TypeScript Exports
369
+
370
+ ```typescript
371
+ // Container
372
+ import { container, createContainer } from '@nextrush/di';
373
+
374
+ // Decorators
375
+ import { Service, Repository, AutoInjectable, Optional, inject, delay } from '@nextrush/di';
376
+
377
+ // Utility functions
378
+ import {
379
+ hasServiceMetadata,
380
+ getServiceType,
381
+ getServiceScope,
382
+ isParameterOptional,
383
+ getOptionalParams,
384
+ } from '@nextrush/di';
385
+
386
+ // Metadata keys
387
+ import { METADATA_KEYS } from '@nextrush/di';
388
+
389
+ // Error classes
390
+ import {
391
+ DIError,
392
+ DependencyResolutionError,
393
+ CircularDependencyError,
394
+ MissingDependencyError,
395
+ InvalidProviderError,
396
+ TypeInferenceError,
397
+ ContainerDisposedError,
398
+ } from '@nextrush/di';
399
+
400
+ // Types
401
+ import type {
402
+ ContainerInterface,
403
+ Provider,
404
+ ClassProvider,
405
+ ValueProvider,
406
+ FactoryProvider,
407
+ Token,
408
+ Constructor,
409
+ Scope,
410
+ ServiceOptions,
411
+ } from '@nextrush/di';
412
+ ```
413
+
414
+ ## Integration with Guards
415
+
416
+ Class-based guards implementing `CanActivate` are resolved from the DI container:
417
+
418
+ ```typescript
419
+ import { Service } from '@nextrush/di';
420
+ import type { CanActivate, GuardContext } from '@nextrush/decorators';
421
+
422
+ @Service()
423
+ class AuthGuard implements CanActivate {
424
+ constructor(private authService: AuthService) {}
425
+
426
+ async canActivate(ctx: GuardContext): Promise<boolean> {
427
+ const token = ctx.get('authorization');
428
+ if (!token) return false;
429
+
430
+ const user = await this.authService.verify(token);
431
+ ctx.state.user = user;
432
+ return Boolean(user);
433
+ }
434
+ }
435
+ ```
436
+
437
+ The `@nextrush/controllers` plugin automatically detects class guards and resolves them from the container.
438
+
439
+ ## Troubleshooting
440
+
441
+ ### Error: "TypeInfo not known for X"
442
+
443
+ **Cause**: `emitDecoratorMetadata` is not being emitted at runtime.
444
+
445
+ **Fix**: Use `@nextrush/dev` for development. It automatically handles metadata emission.
446
+
447
+ ```bash
448
+ # ❌ Doesn't work (no decorator metadata)
449
+ npx tsx src/index.ts
450
+
451
+ # ✅ Works (full decorator support)
452
+ npx nextrush dev
453
+ ```
454
+
455
+ ### Error: "reflect-metadata not found"
456
+
457
+ **Cause**: `reflect-metadata` must be imported before decorators.
458
+
459
+ **Fix**: Import it first in your entry point:
460
+
461
+ ```typescript
462
+ import 'reflect-metadata'; // MUST be first!
463
+ import { Service } from '@nextrush/di';
464
+ ```
465
+
466
+ ### Constructor parameters not injected
467
+
468
+ **Cause**: Class is missing `@Service()` decorator.
469
+
470
+ **Fix**: Add the decorator:
471
+
472
+ ```typescript
473
+ @Service() // Required for DI!
474
+ class MyService {
475
+ constructor(private dep: SomeDependency) {}
476
+ }
477
+ ```
478
+
479
+ ## License
480
+
481
+ MIT