@stitchem/core 0.0.3
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/CHANGELOG.md +6 -0
- package/LICENSE +21 -0
- package/README.md +815 -0
- package/dist/container/container.d.ts +79 -0
- package/dist/container/container.js +156 -0
- package/dist/container/module.map.d.ts +22 -0
- package/dist/container/module.map.js +40 -0
- package/dist/context/context.d.ts +181 -0
- package/dist/context/context.js +395 -0
- package/dist/context/scope.d.ts +30 -0
- package/dist/context/scope.js +42 -0
- package/dist/core/core.lifecycle.d.ts +41 -0
- package/dist/core/core.lifecycle.js +37 -0
- package/dist/core/core.lifetime.d.ts +21 -0
- package/dist/core/core.lifetime.js +22 -0
- package/dist/core/core.types.d.ts +2 -0
- package/dist/core/core.types.js +2 -0
- package/dist/core/core.utils.d.ts +8 -0
- package/dist/core/core.utils.js +13 -0
- package/dist/decorator/inject.decorator.d.ts +50 -0
- package/dist/decorator/inject.decorator.js +78 -0
- package/dist/decorator/injectable.decorator.d.ts +45 -0
- package/dist/decorator/injectable.decorator.js +46 -0
- package/dist/errors/core.error.d.ts +24 -0
- package/dist/errors/core.error.js +59 -0
- package/dist/errors/error.codes.d.ts +17 -0
- package/dist/errors/error.codes.js +21 -0
- package/dist/index.d.ts +25 -0
- package/dist/index.js +23 -0
- package/dist/injector/injector.d.ts +78 -0
- package/dist/injector/injector.js +295 -0
- package/dist/instance-wrapper/instance-wrapper.d.ts +61 -0
- package/dist/instance-wrapper/instance-wrapper.js +142 -0
- package/dist/instance-wrapper/instance-wrapper.types.d.ts +18 -0
- package/dist/instance-wrapper/instance-wrapper.types.js +2 -0
- package/dist/logger/console.logger.d.ts +52 -0
- package/dist/logger/console.logger.js +90 -0
- package/dist/logger/logger.token.d.ts +23 -0
- package/dist/logger/logger.token.js +23 -0
- package/dist/logger/logger.types.d.ts +38 -0
- package/dist/logger/logger.types.js +12 -0
- package/dist/module/module.d.ts +104 -0
- package/dist/module/module.decorator.d.ts +28 -0
- package/dist/module/module.decorator.js +42 -0
- package/dist/module/module.graph.d.ts +52 -0
- package/dist/module/module.graph.js +263 -0
- package/dist/module/module.js +181 -0
- package/dist/module/module.ref.d.ts +81 -0
- package/dist/module/module.ref.js +123 -0
- package/dist/module/module.types.d.ts +80 -0
- package/dist/module/module.types.js +10 -0
- package/dist/provider/provider.guards.d.ts +46 -0
- package/dist/provider/provider.guards.js +62 -0
- package/dist/provider/provider.interface.d.ts +39 -0
- package/dist/provider/provider.interface.js +2 -0
- package/dist/test/test.d.ts +22 -0
- package/dist/test/test.js +23 -0
- package/dist/test/test.module-builder.d.ts +136 -0
- package/dist/test/test.module-builder.js +377 -0
- package/dist/test/test.module.d.ts +71 -0
- package/dist/test/test.module.js +151 -0
- package/dist/token/lazy.token.d.ts +44 -0
- package/dist/token/lazy.token.js +42 -0
- package/dist/token/token.types.d.ts +8 -0
- package/dist/token/token.types.js +2 -0
- package/dist/token/token.utils.d.ts +9 -0
- package/dist/token/token.utils.js +19 -0
- package/package.json +62 -0
package/README.md
ADDED
|
@@ -0,0 +1,815 @@
|
|
|
1
|
+
# @stitchem/core
|
|
2
|
+
|
|
3
|
+
Extensible modular dependency injection for TypeScript.
|
|
4
|
+
|
|
5
|
+
Built on TC39 decorators and modern JavaScript — no `reflect-metadata`, no legacy experimentalDecorators. Uses `AsyncLocalStorage` for scope propagation and `Symbol.asyncDispose` for deterministic cleanup.
|
|
6
|
+
|
|
7
|
+
## Requirements
|
|
8
|
+
|
|
9
|
+
- TypeScript >= 5.2 (TC39 decorators + `AsyncDisposable`)
|
|
10
|
+
- Node.js >= 22 (or any runtime with `AsyncLocalStorage` + `Symbol.asyncDispose`)
|
|
11
|
+
|
|
12
|
+
## Install
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
npm install @stitchem/core
|
|
16
|
+
# or
|
|
17
|
+
pnpm add @stitchem/core
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Quick Start
|
|
21
|
+
|
|
22
|
+
```ts
|
|
23
|
+
import { injectable, module, Context } from '@stitchem/core';
|
|
24
|
+
|
|
25
|
+
@injectable()
|
|
26
|
+
class GreetingService {
|
|
27
|
+
greet(name: string) {
|
|
28
|
+
return `Hello, ${name}!`;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
@module({ providers: [GreetingService] })
|
|
33
|
+
class AppModule {}
|
|
34
|
+
|
|
35
|
+
await using ctx = await Context.create(AppModule);
|
|
36
|
+
|
|
37
|
+
const svc = await ctx.resolve(GreetingService);
|
|
38
|
+
console.log(svc.greet('World')); // Hello, World!
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
The `await using` syntax ensures the context (and all managed instances) is disposed when the scope exits. You can also call `await ctx[Symbol.asyncDispose]()` manually.
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
## Table of Contents
|
|
46
|
+
|
|
47
|
+
- [Providers](#providers)
|
|
48
|
+
- [Constructor Provider](#constructor-provider)
|
|
49
|
+
- [Value Provider](#value-provider)
|
|
50
|
+
- [Factory Provider](#factory-provider)
|
|
51
|
+
- [Class Provider](#class-provider)
|
|
52
|
+
- [Existing Provider](#existing-provider)
|
|
53
|
+
- [Dependency Injection](#dependency-injection)
|
|
54
|
+
- [Constructor Injection](#constructor-injection)
|
|
55
|
+
- [Accessor Injection](#accessor-injection)
|
|
56
|
+
- [Lifetimes](#lifetimes)
|
|
57
|
+
- [Singleton](#singleton)
|
|
58
|
+
- [Transient](#transient)
|
|
59
|
+
- [Scoped](#scoped)
|
|
60
|
+
- [Scopes](#scopes)
|
|
61
|
+
- [withScope](#withscope)
|
|
62
|
+
- [createScope](#createscope)
|
|
63
|
+
- [Modules](#modules)
|
|
64
|
+
- [Imports and Exports](#imports-and-exports)
|
|
65
|
+
- [Global Modules](#global-modules)
|
|
66
|
+
- [Dynamic Modules](#dynamic-modules)
|
|
67
|
+
- [Re-exports](#re-exports)
|
|
68
|
+
- [Lifecycle Hooks](#lifecycle-hooks)
|
|
69
|
+
- [OnInit](#oninit)
|
|
70
|
+
- [OnReady](#onready)
|
|
71
|
+
- [OnDispose](#ondispose)
|
|
72
|
+
- [Circular Dependencies](#circular-dependencies)
|
|
73
|
+
- [Logger](#logger)
|
|
74
|
+
- [Components](#components)
|
|
75
|
+
- [ModuleRef](#moduleref)
|
|
76
|
+
- [Error Handling](#error-handling)
|
|
77
|
+
- [Testing](#testing)
|
|
78
|
+
- [API Reference](#api-reference)
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
## Providers
|
|
83
|
+
|
|
84
|
+
Providers are the building blocks of the DI system. A provider tells the container how to create or locate a dependency.
|
|
85
|
+
|
|
86
|
+
### Constructor Provider
|
|
87
|
+
|
|
88
|
+
The simplest form — just pass a class decorated with `@injectable()`:
|
|
89
|
+
|
|
90
|
+
```ts
|
|
91
|
+
@injectable()
|
|
92
|
+
class UserService {
|
|
93
|
+
getUsers() { return ['alice', 'bob']; }
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
@module({ providers: [UserService] })
|
|
97
|
+
class AppModule {}
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### Value Provider
|
|
101
|
+
|
|
102
|
+
Register a static value:
|
|
103
|
+
|
|
104
|
+
```ts
|
|
105
|
+
const DB_URL = Symbol('DB_URL');
|
|
106
|
+
|
|
107
|
+
@module({
|
|
108
|
+
providers: [
|
|
109
|
+
{ provide: DB_URL, useValue: 'postgres://localhost/mydb' },
|
|
110
|
+
],
|
|
111
|
+
})
|
|
112
|
+
class AppModule {}
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Value providers are always singletons. The value is used as-is — no instantiation or lifecycle hooks.
|
|
116
|
+
|
|
117
|
+
### Factory Provider
|
|
118
|
+
|
|
119
|
+
Use a factory function to create instances. Dependencies can be injected into the factory via `inject`:
|
|
120
|
+
|
|
121
|
+
```ts
|
|
122
|
+
@module({
|
|
123
|
+
providers: [
|
|
124
|
+
ConfigService,
|
|
125
|
+
{
|
|
126
|
+
provide: 'DB_CONNECTION',
|
|
127
|
+
useFactory: (config: ConfigService) => createConnection(config.dbUrl),
|
|
128
|
+
inject: [ConfigService],
|
|
129
|
+
lifetime: Lifetime.SCOPED,
|
|
130
|
+
},
|
|
131
|
+
],
|
|
132
|
+
})
|
|
133
|
+
class DbModule {}
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
### Class Provider
|
|
137
|
+
|
|
138
|
+
Map a token to a different class implementation:
|
|
139
|
+
|
|
140
|
+
```ts
|
|
141
|
+
@module({
|
|
142
|
+
providers: [
|
|
143
|
+
{ provide: PaymentService, useClass: StripePaymentService },
|
|
144
|
+
],
|
|
145
|
+
})
|
|
146
|
+
class PaymentModule {}
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
### Existing Provider
|
|
150
|
+
|
|
151
|
+
Alias one token to another:
|
|
152
|
+
|
|
153
|
+
```ts
|
|
154
|
+
@module({
|
|
155
|
+
providers: [
|
|
156
|
+
RealLogger,
|
|
157
|
+
{ provide: LOGGER, useExisting: RealLogger },
|
|
158
|
+
],
|
|
159
|
+
})
|
|
160
|
+
class AppModule {}
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
---
|
|
164
|
+
|
|
165
|
+
## Dependency Injection
|
|
166
|
+
|
|
167
|
+
### Constructor Injection
|
|
168
|
+
|
|
169
|
+
Declare dependencies with a static `inject` property. The container resolves them in order and passes them to the constructor:
|
|
170
|
+
|
|
171
|
+
```ts
|
|
172
|
+
@injectable()
|
|
173
|
+
class UserService {
|
|
174
|
+
static inject = [UserRepository, LOGGER] as const;
|
|
175
|
+
constructor(
|
|
176
|
+
private repo: UserRepository,
|
|
177
|
+
private logger: Logger,
|
|
178
|
+
) {}
|
|
179
|
+
}
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
The `as const` assertion ensures type safety. Any `Token` type can be used — classes, symbols, or strings.
|
|
183
|
+
|
|
184
|
+
### Accessor Injection
|
|
185
|
+
|
|
186
|
+
Use the `@inject()` decorator on accessor properties for field-based injection:
|
|
187
|
+
|
|
188
|
+
```ts
|
|
189
|
+
@injectable()
|
|
190
|
+
class OrderService {
|
|
191
|
+
@inject(LOGGER) accessor logger!: Logger;
|
|
192
|
+
@inject(UserService) accessor users!: UserService;
|
|
193
|
+
|
|
194
|
+
process() {
|
|
195
|
+
this.logger.info('Processing order...');
|
|
196
|
+
return this.users.getUsers();
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
Accessor injection is resolved after construction. Both injection styles can be mixed in the same class.
|
|
202
|
+
|
|
203
|
+
---
|
|
204
|
+
|
|
205
|
+
## Lifetimes
|
|
206
|
+
|
|
207
|
+
Every provider has a lifetime that controls how long its instance lives.
|
|
208
|
+
|
|
209
|
+
### Singleton
|
|
210
|
+
|
|
211
|
+
**Default.** One instance shared across the entire container. Created once during initialization.
|
|
212
|
+
|
|
213
|
+
```ts
|
|
214
|
+
@injectable() // or @injectable({ lifetime: Lifetime.SINGLETON })
|
|
215
|
+
class DatabasePool {}
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
### Transient
|
|
219
|
+
|
|
220
|
+
A new instance is created every time the dependency is resolved. Never cached.
|
|
221
|
+
|
|
222
|
+
```ts
|
|
223
|
+
@injectable({ lifetime: Lifetime.TRANSIENT })
|
|
224
|
+
class RequestHandler {}
|
|
225
|
+
|
|
226
|
+
const a = await ctx.resolve(RequestHandler);
|
|
227
|
+
const b = await ctx.resolve(RequestHandler);
|
|
228
|
+
// a !== b — always a fresh instance
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
### Scoped
|
|
232
|
+
|
|
233
|
+
One instance per scope. Within the same scope, the same instance is returned. Different scopes get different instances.
|
|
234
|
+
|
|
235
|
+
```ts
|
|
236
|
+
@injectable({ lifetime: Lifetime.SCOPED })
|
|
237
|
+
class RequestContext {
|
|
238
|
+
requestId = crypto.randomUUID();
|
|
239
|
+
}
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
Scoped providers require an active scope (via `withScope` or `createScope`). Attempting to resolve a scoped provider without a scope throws `SCOPED_RESOLUTION`. A singleton cannot depend on a scoped provider — this is caught at initialization time.
|
|
243
|
+
|
|
244
|
+
---
|
|
245
|
+
|
|
246
|
+
## Scopes
|
|
247
|
+
|
|
248
|
+
Scopes enable request-level isolation. Every HTTP request, WebSocket connection, or job can have its own scope with isolated state.
|
|
249
|
+
|
|
250
|
+
### withScope
|
|
251
|
+
|
|
252
|
+
The simplest way to create a scope. The scope is automatically disposed when the callback completes:
|
|
253
|
+
|
|
254
|
+
```ts
|
|
255
|
+
await ctx.withScope(async () => {
|
|
256
|
+
// All resolutions inside this callback share the same scope.
|
|
257
|
+
const reqCtx = await ctx.resolve(RequestContext);
|
|
258
|
+
const logger = await ctx.resolve(RequestLogger);
|
|
259
|
+
// logger.reqCtx === reqCtx (same scope → same instance)
|
|
260
|
+
});
|
|
261
|
+
// Scope disposed here — all scoped instances cleaned up.
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
`withScope` also disposes scoped instances when the callback throws:
|
|
265
|
+
|
|
266
|
+
```ts
|
|
267
|
+
await ctx.withScope(async () => {
|
|
268
|
+
await ctx.resolve(DbTransaction);
|
|
269
|
+
throw new Error('failed');
|
|
270
|
+
});
|
|
271
|
+
// DbTransaction.onDispose() still called
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
### createScope
|
|
275
|
+
|
|
276
|
+
For manual scope management. Useful when the scope lifetime doesn't align with a single callback:
|
|
277
|
+
|
|
278
|
+
```ts
|
|
279
|
+
const scope = ctx.createScope();
|
|
280
|
+
|
|
281
|
+
// Run code within the scope
|
|
282
|
+
const svc = await scope.run(() => ctx.resolve(RequestContext));
|
|
283
|
+
|
|
284
|
+
// Same scope → same instance
|
|
285
|
+
const same = await scope.run(() => ctx.resolve(RequestContext));
|
|
286
|
+
assert(svc === same);
|
|
287
|
+
|
|
288
|
+
// Dispose when done
|
|
289
|
+
await scope[Symbol.asyncDispose]();
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
Or with `await using`:
|
|
293
|
+
|
|
294
|
+
```ts
|
|
295
|
+
{
|
|
296
|
+
await using scope = ctx.createScope();
|
|
297
|
+
const svc = await scope.run(() => ctx.resolve(RequestContext));
|
|
298
|
+
}
|
|
299
|
+
// Scope auto-disposed at block exit
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
Scopes nest naturally. Inner scopes get their own instances; outer scopes are unaffected:
|
|
303
|
+
|
|
304
|
+
```ts
|
|
305
|
+
await ctx.withScope(async () => {
|
|
306
|
+
const outer = await ctx.resolve(ScopedSvc);
|
|
307
|
+
|
|
308
|
+
await ctx.withScope(async () => {
|
|
309
|
+
const inner = await ctx.resolve(ScopedSvc);
|
|
310
|
+
// inner !== outer — different scope
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
const stillOuter = await ctx.resolve(ScopedSvc);
|
|
314
|
+
// stillOuter === outer — outer scope unchanged
|
|
315
|
+
});
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
---
|
|
319
|
+
|
|
320
|
+
## Modules
|
|
321
|
+
|
|
322
|
+
Modules group related providers and define the dependency graph. Every stitchem application has at least one root module.
|
|
323
|
+
|
|
324
|
+
```ts
|
|
325
|
+
@module({
|
|
326
|
+
imports: [DatabaseModule, AuthModule],
|
|
327
|
+
providers: [UserService, UserRepository],
|
|
328
|
+
exports: [UserService],
|
|
329
|
+
})
|
|
330
|
+
class UserModule {}
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
### Imports and Exports
|
|
334
|
+
|
|
335
|
+
Providers are **private to their module** by default. To make a provider available to other modules, export it:
|
|
336
|
+
|
|
337
|
+
```ts
|
|
338
|
+
// DatabaseModule exports DbConnection
|
|
339
|
+
@module({
|
|
340
|
+
providers: [DbConnection],
|
|
341
|
+
exports: [DbConnection],
|
|
342
|
+
})
|
|
343
|
+
class DatabaseModule {}
|
|
344
|
+
|
|
345
|
+
// UserModule imports DatabaseModule to access DbConnection
|
|
346
|
+
@module({
|
|
347
|
+
imports: [DatabaseModule],
|
|
348
|
+
providers: [UserService],
|
|
349
|
+
})
|
|
350
|
+
class UserModule {}
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
Only exported tokens are visible to importing modules. This encapsulation prevents accidental coupling.
|
|
354
|
+
|
|
355
|
+
### Global Modules
|
|
356
|
+
|
|
357
|
+
A global module's exports are available to **all** modules without explicit imports:
|
|
358
|
+
|
|
359
|
+
```ts
|
|
360
|
+
@module({
|
|
361
|
+
global: true,
|
|
362
|
+
providers: [
|
|
363
|
+
{ provide: 'APP_NAME', useValue: 'MyApp' },
|
|
364
|
+
],
|
|
365
|
+
exports: ['APP_NAME'],
|
|
366
|
+
})
|
|
367
|
+
class ConfigModule {}
|
|
368
|
+
|
|
369
|
+
// No need to import ConfigModule — 'APP_NAME' is available everywhere
|
|
370
|
+
@injectable()
|
|
371
|
+
class AnyService {
|
|
372
|
+
static inject = ['APP_NAME'] as const;
|
|
373
|
+
constructor(public appName: string) {}
|
|
374
|
+
}
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
Use sparingly. Global modules are convenient for cross-cutting concerns like configuration, logging, and caching.
|
|
378
|
+
|
|
379
|
+
### Dynamic Modules
|
|
380
|
+
|
|
381
|
+
For configurable modules, use a static factory method that returns a `DynamicModule`:
|
|
382
|
+
|
|
383
|
+
```ts
|
|
384
|
+
import type { DynamicModule } from '@stitchem/core';
|
|
385
|
+
|
|
386
|
+
@module()
|
|
387
|
+
class CacheModule {
|
|
388
|
+
static forRoot(options: { ttl: number }): DynamicModule {
|
|
389
|
+
return {
|
|
390
|
+
module: CacheModule,
|
|
391
|
+
global: true,
|
|
392
|
+
providers: [
|
|
393
|
+
{ provide: 'CACHE_TTL', useValue: options.ttl },
|
|
394
|
+
CacheService,
|
|
395
|
+
],
|
|
396
|
+
exports: [CacheService],
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
@module({
|
|
402
|
+
imports: [CacheModule.forRoot({ ttl: 300 })],
|
|
403
|
+
})
|
|
404
|
+
class AppModule {}
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
Dynamic module metadata is merged with the base `@module()` metadata. Array fields (like `providers`) are concatenated.
|
|
408
|
+
|
|
409
|
+
### Re-exports
|
|
410
|
+
|
|
411
|
+
A module can re-export an imported module, making its exports available to its own consumers:
|
|
412
|
+
|
|
413
|
+
```ts
|
|
414
|
+
@module({
|
|
415
|
+
imports: [CoreModule],
|
|
416
|
+
exports: [CoreModule], // Re-exports everything CoreModule exports
|
|
417
|
+
})
|
|
418
|
+
class SharedModule {}
|
|
419
|
+
|
|
420
|
+
@module({ imports: [SharedModule] })
|
|
421
|
+
class AppModule {}
|
|
422
|
+
// AppModule can now resolve CoreModule's exports through SharedModule
|
|
423
|
+
```
|
|
424
|
+
|
|
425
|
+
---
|
|
426
|
+
|
|
427
|
+
## Lifecycle Hooks
|
|
428
|
+
|
|
429
|
+
Providers can implement lifecycle hooks for initialization, readiness, and cleanup.
|
|
430
|
+
|
|
431
|
+
### OnInit
|
|
432
|
+
|
|
433
|
+
Called during instantiation, after the constructor and dependency injection. Supports async.
|
|
434
|
+
|
|
435
|
+
```ts
|
|
436
|
+
import type { OnInit } from '@stitchem/core';
|
|
437
|
+
|
|
438
|
+
@injectable()
|
|
439
|
+
class DbConnection implements OnInit {
|
|
440
|
+
static inject = ['DB_URL'] as const;
|
|
441
|
+
private pool: any;
|
|
442
|
+
|
|
443
|
+
constructor(private url: string) {}
|
|
444
|
+
|
|
445
|
+
async onInit() {
|
|
446
|
+
this.pool = await createPool(this.url);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
```
|
|
450
|
+
|
|
451
|
+
If `onInit` throws, `Context.create()` fails — preventing the application from starting with broken dependencies.
|
|
452
|
+
|
|
453
|
+
### OnReady
|
|
454
|
+
|
|
455
|
+
Called after **all** singleton providers have been created and initialized. Fires once per application lifecycle. Use it for work that depends on the full dependency graph being ready.
|
|
456
|
+
|
|
457
|
+
```ts
|
|
458
|
+
import type { OnReady } from '@stitchem/core';
|
|
459
|
+
|
|
460
|
+
@injectable()
|
|
461
|
+
class HttpServer implements OnReady {
|
|
462
|
+
onReady() {
|
|
463
|
+
console.log('All services initialized, starting server...');
|
|
464
|
+
this.listen();
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
```
|
|
468
|
+
|
|
469
|
+
### OnDispose
|
|
470
|
+
|
|
471
|
+
Called when the context (or scope) is disposed. Use it for cleanup — closing connections, releasing resources, flushing buffers.
|
|
472
|
+
|
|
473
|
+
```ts
|
|
474
|
+
import type { OnDispose } from '@stitchem/core';
|
|
475
|
+
|
|
476
|
+
@injectable()
|
|
477
|
+
class DbConnection implements OnDispose {
|
|
478
|
+
async onDispose() {
|
|
479
|
+
await this.pool.end();
|
|
480
|
+
console.log('Database pool closed');
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
```
|
|
484
|
+
|
|
485
|
+
**Ordering:** `onInit` fires during creation (dependency order). `onReady` fires after all singletons are initialized. `onDispose` fires in reverse module order — dependents are disposed before their dependencies.
|
|
486
|
+
|
|
487
|
+
---
|
|
488
|
+
|
|
489
|
+
## Circular Dependencies
|
|
490
|
+
|
|
491
|
+
Circular dependencies (`A → B → A`) are detected at resolution time and throw `CIRCULAR_DEPENDENCY`. To break the cycle, use `lazy()` on one side:
|
|
492
|
+
|
|
493
|
+
```ts
|
|
494
|
+
import { lazy } from '@stitchem/core';
|
|
495
|
+
|
|
496
|
+
@injectable()
|
|
497
|
+
class AuthService {
|
|
498
|
+
static inject = [lazy(() => UserService)] as const;
|
|
499
|
+
constructor(private users: UserService) {}
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
@injectable()
|
|
503
|
+
class UserService {
|
|
504
|
+
static inject = [AuthService] as const;
|
|
505
|
+
constructor(private auth: AuthService) {}
|
|
506
|
+
}
|
|
507
|
+
```
|
|
508
|
+
|
|
509
|
+
Only one side of the cycle needs `lazy()`. The lazy side receives a proxy that resolves on first property access. `lazy()` also works in factory `inject` arrays.
|
|
510
|
+
|
|
511
|
+
---
|
|
512
|
+
|
|
513
|
+
## Logger
|
|
514
|
+
|
|
515
|
+
Every module gets a `LOGGER` provider automatically. Inject it with the `LOGGER` symbol:
|
|
516
|
+
|
|
517
|
+
```ts
|
|
518
|
+
import { LOGGER } from '@stitchem/core';
|
|
519
|
+
import type { Logger } from '@stitchem/core';
|
|
520
|
+
|
|
521
|
+
@injectable()
|
|
522
|
+
class UserService {
|
|
523
|
+
static inject = [LOGGER] as const;
|
|
524
|
+
constructor(private logger: Logger) {}
|
|
525
|
+
|
|
526
|
+
create(name: string) {
|
|
527
|
+
this.logger.info(`Creating user: ${name}`);
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
```
|
|
531
|
+
|
|
532
|
+
The `Logger` interface is minimal and compatible with `console`, `pino()`, and `winston.createLogger()`:
|
|
533
|
+
|
|
534
|
+
```ts
|
|
535
|
+
interface Logger {
|
|
536
|
+
info(message: string, ...args: unknown[]): void;
|
|
537
|
+
warn(message: string, ...args: unknown[]): void;
|
|
538
|
+
error(message: string, ...args: unknown[]): void;
|
|
539
|
+
debug(message: string, ...args: unknown[]): void;
|
|
540
|
+
}
|
|
541
|
+
```
|
|
542
|
+
|
|
543
|
+
### Custom Logger
|
|
544
|
+
|
|
545
|
+
Pass your own logger (or disable logging) when creating the context:
|
|
546
|
+
|
|
547
|
+
```ts
|
|
548
|
+
// Use pino
|
|
549
|
+
const ctx = await Context.create(AppModule, { logger: pino() });
|
|
550
|
+
|
|
551
|
+
// Disable logging
|
|
552
|
+
const ctx = await Context.create(AppModule, { logger: false });
|
|
553
|
+
```
|
|
554
|
+
|
|
555
|
+
### ConsoleLogger
|
|
556
|
+
|
|
557
|
+
The built-in `ConsoleLogger` formats output with timestamps and colored symbols:
|
|
558
|
+
|
|
559
|
+
```
|
|
560
|
+
12:30:15 [http] ℹ Starting server on port 3000
|
|
561
|
+
12:30:15 [http] ⚠ TLS certificate expires in 7 days
|
|
562
|
+
12:30:17 [db] ✗ Connection failed: ECONNREFUSED
|
|
563
|
+
12:30:18 [db] ◆ Retry succeeded
|
|
564
|
+
```
|
|
565
|
+
|
|
566
|
+
Control verbosity with `LogLevel`:
|
|
567
|
+
|
|
568
|
+
```ts
|
|
569
|
+
import { ConsoleLogger, LogLevel } from '@stitchem/core';
|
|
570
|
+
|
|
571
|
+
ConsoleLogger.setLevel(LogLevel.WARN); // Only warn + error
|
|
572
|
+
```
|
|
573
|
+
|
|
574
|
+
---
|
|
575
|
+
|
|
576
|
+
## Components
|
|
577
|
+
|
|
578
|
+
Components are an extension mechanism for framework authors. They allow modules to declare classes (via custom metadata keys) that the core will instantiate with full DI and lifecycle support.
|
|
579
|
+
|
|
580
|
+
```ts
|
|
581
|
+
// Extend ModuleMetadata in your package
|
|
582
|
+
declare module '@stitchem/core' {
|
|
583
|
+
interface ModuleMetadata {
|
|
584
|
+
routers?: constructor[];
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
// Use the custom key in modules
|
|
589
|
+
@module({
|
|
590
|
+
providers: [DbService],
|
|
591
|
+
routers: [UserRouter, HealthRouter],
|
|
592
|
+
})
|
|
593
|
+
class ApiModule {}
|
|
594
|
+
|
|
595
|
+
// Tell the core to manage the 'routers' key
|
|
596
|
+
const ctx = await Context.create(AppModule, {
|
|
597
|
+
componentKeys: ['routers'],
|
|
598
|
+
});
|
|
599
|
+
|
|
600
|
+
// Retrieve all resolved router instances
|
|
601
|
+
const routers = ctx.getComponents('routers');
|
|
602
|
+
for (const { instance, classRef, moduleRef } of routers) {
|
|
603
|
+
console.log(`${classRef.name} from ${moduleRef.id}`);
|
|
604
|
+
}
|
|
605
|
+
```
|
|
606
|
+
|
|
607
|
+
Components receive full dependency injection, `onInit`, `onReady`, and `onDispose` lifecycle hooks. They are returned in module dependency order.
|
|
608
|
+
|
|
609
|
+
---
|
|
610
|
+
|
|
611
|
+
## ModuleRef
|
|
612
|
+
|
|
613
|
+
`ModuleRef` is automatically registered in every module. Inject it for dynamic resolution and module introspection:
|
|
614
|
+
|
|
615
|
+
```ts
|
|
616
|
+
import { ModuleRef } from '@stitchem/core';
|
|
617
|
+
|
|
618
|
+
@injectable()
|
|
619
|
+
class PluginLoader {
|
|
620
|
+
static inject = [ModuleRef] as const;
|
|
621
|
+
constructor(private moduleRef: ModuleRef) {}
|
|
622
|
+
|
|
623
|
+
async loadPlugin<T>(token: Token<T>): Promise<T> {
|
|
624
|
+
return this.moduleRef.resolve(token);
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
```
|
|
628
|
+
|
|
629
|
+
`ModuleRef` also supports:
|
|
630
|
+
- `create(ctor)` — creates a new instance bypassing the cache (useful for transient-like behavior on demand)
|
|
631
|
+
- `constructClass(ctor)` — instantiates any class by resolving its dependencies, even if unregistered
|
|
632
|
+
- `select(moduleClass)` — navigates to a different module
|
|
633
|
+
- `getComponents(key)` — gets component instances for the module
|
|
634
|
+
|
|
635
|
+
From the `Context`, use `select()` to navigate to a specific module:
|
|
636
|
+
|
|
637
|
+
```ts
|
|
638
|
+
const dbRef = ctx.select(DatabaseModule);
|
|
639
|
+
const connection = await dbRef.resolve(DbConnection);
|
|
640
|
+
```
|
|
641
|
+
|
|
642
|
+
---
|
|
643
|
+
|
|
644
|
+
## Error Handling
|
|
645
|
+
|
|
646
|
+
All DI errors are instances of `CoreError` with a machine-readable `code` and structured `context`:
|
|
647
|
+
|
|
648
|
+
```ts
|
|
649
|
+
import { CoreError, ErrorCode } from '@stitchem/core';
|
|
650
|
+
|
|
651
|
+
try {
|
|
652
|
+
await ctx.resolve(UnknownService);
|
|
653
|
+
} catch (err) {
|
|
654
|
+
if (err instanceof CoreError) {
|
|
655
|
+
switch (err.code) {
|
|
656
|
+
case ErrorCode.PROVIDER_NOT_FOUND:
|
|
657
|
+
console.error('Unknown service:', err.context.token);
|
|
658
|
+
break;
|
|
659
|
+
case ErrorCode.CIRCULAR_DEPENDENCY:
|
|
660
|
+
console.error('Cycle detected:', err.context.path);
|
|
661
|
+
break;
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
```
|
|
666
|
+
|
|
667
|
+
**Error codes:**
|
|
668
|
+
|
|
669
|
+
| Code | When |
|
|
670
|
+
|------|------|
|
|
671
|
+
| `CIRCULAR_DEPENDENCY` | Provider dependency cycle detected |
|
|
672
|
+
| `SCOPED_RESOLUTION` | Scoped provider resolved without a scope |
|
|
673
|
+
| `UNKNOWN_DEPENDENCY` | A dependency token could not be resolved |
|
|
674
|
+
| `INVALID_MODULE` | Class passed to `Context.create()` is not decorated with `@module()` |
|
|
675
|
+
| `CIRCULAR_MODULE_DEPENDENCY` | Module import graph has a cycle |
|
|
676
|
+
| `MODULE_NOT_FOUND` | Module class not found in container |
|
|
677
|
+
| `MODULE_NOT_EXPORTED` | Module listed in `exports` but not `imports` |
|
|
678
|
+
| `NOT_INJECTABLE` | Constructor provider not decorated with `@injectable()` |
|
|
679
|
+
| `INVALID_PROVIDER` | Provider configuration is malformed |
|
|
680
|
+
| `PROVIDER_NOT_FOUND` | Token not registered in any module |
|
|
681
|
+
| `PROVIDER_NOT_VISIBLE` | Provider exists but is not exported to the requesting module |
|
|
682
|
+
|
|
683
|
+
---
|
|
684
|
+
|
|
685
|
+
## Testing
|
|
686
|
+
|
|
687
|
+
The `Test` utility provides a builder for creating isolated test modules with provider overrides.
|
|
688
|
+
|
|
689
|
+
### Basic Setup
|
|
690
|
+
|
|
691
|
+
```ts
|
|
692
|
+
import { Test } from '@stitchem/core';
|
|
693
|
+
import type { TestModule } from '@stitchem/core';
|
|
694
|
+
import { describe, it, expect, afterEach } from 'vitest';
|
|
695
|
+
|
|
696
|
+
describe('UserService', () => {
|
|
697
|
+
let testModule: TestModule;
|
|
698
|
+
|
|
699
|
+
afterEach(async () => {
|
|
700
|
+
await testModule?.close();
|
|
701
|
+
});
|
|
702
|
+
|
|
703
|
+
it('should return users', async () => {
|
|
704
|
+
testModule = await Test.createModule({
|
|
705
|
+
imports: [UserModule],
|
|
706
|
+
}).compile();
|
|
707
|
+
|
|
708
|
+
const svc = await testModule.resolve(UserService);
|
|
709
|
+
expect(svc.getUsers()).toEqual(['alice', 'bob']);
|
|
710
|
+
});
|
|
711
|
+
});
|
|
712
|
+
```
|
|
713
|
+
|
|
714
|
+
Or with `await using` for automatic cleanup:
|
|
715
|
+
|
|
716
|
+
```ts
|
|
717
|
+
it('should return users', async () => {
|
|
718
|
+
await using testModule = await Test.createModule({
|
|
719
|
+
imports: [UserModule],
|
|
720
|
+
}).compile();
|
|
721
|
+
|
|
722
|
+
const svc = await testModule.resolve(UserService);
|
|
723
|
+
expect(svc.getUsers()).toEqual(['alice', 'bob']);
|
|
724
|
+
});
|
|
725
|
+
```
|
|
726
|
+
|
|
727
|
+
### Overriding Providers
|
|
728
|
+
|
|
729
|
+
Replace real dependencies with mocks:
|
|
730
|
+
|
|
731
|
+
```ts
|
|
732
|
+
await using testModule = await Test.createModule({
|
|
733
|
+
imports: [UserModule],
|
|
734
|
+
})
|
|
735
|
+
.overrideProvider(DatabaseService)
|
|
736
|
+
.useValue({ query: vi.fn().mockResolvedValue([]) })
|
|
737
|
+
.compile();
|
|
738
|
+
```
|
|
739
|
+
|
|
740
|
+
Override methods:
|
|
741
|
+
- `.useValue(value)` — replace with a static value
|
|
742
|
+
- `.useClass(MockClass)` — replace with a different class
|
|
743
|
+
- `.useFactory({ factory, inject? })` — replace with a factory
|
|
744
|
+
- `.useExisting(otherToken)` — alias to another token
|
|
745
|
+
|
|
746
|
+
### Scoped Testing
|
|
747
|
+
|
|
748
|
+
```ts
|
|
749
|
+
it('should isolate scoped state', async () => {
|
|
750
|
+
await using testModule = await Test.createModule({
|
|
751
|
+
providers: [ScopedService],
|
|
752
|
+
}).compile();
|
|
753
|
+
|
|
754
|
+
await using scope = testModule.createScope();
|
|
755
|
+
const a = await testModule.resolve(ScopedService, scope);
|
|
756
|
+
const b = await testModule.resolve(ScopedService, scope);
|
|
757
|
+
expect(a).toBe(b); // Same scope → same instance
|
|
758
|
+
});
|
|
759
|
+
```
|
|
760
|
+
|
|
761
|
+
### Module Navigation
|
|
762
|
+
|
|
763
|
+
Navigate to a specific module within the test:
|
|
764
|
+
|
|
765
|
+
```ts
|
|
766
|
+
const ref = testModule.select(DatabaseModule);
|
|
767
|
+
const db = await ref.resolve(DatabaseService);
|
|
768
|
+
```
|
|
769
|
+
|
|
770
|
+
---
|
|
771
|
+
|
|
772
|
+
## API Reference
|
|
773
|
+
|
|
774
|
+
### Decorators
|
|
775
|
+
|
|
776
|
+
| Decorator | Description |
|
|
777
|
+
|-----------|-------------|
|
|
778
|
+
| `@injectable(options?)` | Marks a class as injectable. Options: `{ lifetime?: Lifetime }` |
|
|
779
|
+
| `@inject(token)` | Accessor decorator for property injection |
|
|
780
|
+
| `@module(metadata?)` | Marks a class as a module |
|
|
781
|
+
|
|
782
|
+
### Context
|
|
783
|
+
|
|
784
|
+
| Method | Description |
|
|
785
|
+
|--------|-------------|
|
|
786
|
+
| `Context.create(rootModule, options?)` | Creates and initializes a context |
|
|
787
|
+
| `ctx.resolve(token, options?)` | Resolves a provider (async) |
|
|
788
|
+
| `ctx.resolveSync(token, options?)` | Resolves a provider (sync) |
|
|
789
|
+
| `ctx.constructClass(ctor, options?)` | Instantiates any class with DI |
|
|
790
|
+
| `ctx.withScope(fn)` | Runs callback in a new auto-disposed scope |
|
|
791
|
+
| `ctx.createScope()` | Creates a manual disposable scope |
|
|
792
|
+
| `ctx.select(moduleClass)` | Navigates to a specific module |
|
|
793
|
+
| `ctx.getComponents(key)` | Gets component instances for a metadata key |
|
|
794
|
+
| `ctx[Symbol.asyncDispose]()` | Disposes the context and all instances |
|
|
795
|
+
|
|
796
|
+
### Lifetime
|
|
797
|
+
|
|
798
|
+
| Value | Behavior |
|
|
799
|
+
|-------|----------|
|
|
800
|
+
| `Lifetime.SINGLETON` | One instance for the entire container (default) |
|
|
801
|
+
| `Lifetime.TRANSIENT` | New instance on every resolution |
|
|
802
|
+
| `Lifetime.SCOPED` | One instance per scope |
|
|
803
|
+
|
|
804
|
+
### Token Types
|
|
805
|
+
|
|
806
|
+
Tokens identify dependencies. Any of these can be used as a token:
|
|
807
|
+
|
|
808
|
+
- **Class constructor** — `UserService`
|
|
809
|
+
- **Symbol** — `Symbol('DB_URL')`
|
|
810
|
+
- **String** — `'CONFIG'`
|
|
811
|
+
- **Lazy token** — `lazy(() => UserService)`
|
|
812
|
+
|
|
813
|
+
## License
|
|
814
|
+
|
|
815
|
+
MIT
|