inwire 2.3.1 → 2.3.2
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 +408 -363
- package/dist/index.d.mts +13 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# inwire
|
|
2
2
|
|
|
3
|
-
Type-safe dependency injection for TypeScript.
|
|
3
|
+
**Type-safe dependency injection for TypeScript.** No decorators. No tokens. No `reflect-metadata`. Just a fluent builder, a Proxy, and full type inference. ~4 KB gzip, zero runtime dependencies.
|
|
4
4
|
|
|
5
5
|
[](https://www.npmjs.com/package/inwire)
|
|
6
6
|
[](https://github.com/axelhamil/inwire/actions)
|
|
@@ -10,250 +10,354 @@ Type-safe dependency injection for TypeScript. Builder pattern, full inference,
|
|
|
10
10
|
[](https://github.com/axelhamil/inwire/blob/main/LICENSE)
|
|
11
11
|
[](https://www.npmjs.com/package/inwire)
|
|
12
12
|
|
|
13
|
-
## Install
|
|
14
|
-
|
|
15
|
-
```bash
|
|
16
|
-
pnpm add inwire # or npm i inwire
|
|
17
|
-
```
|
|
18
|
-
|
|
19
|
-
## Quick Start
|
|
20
|
-
|
|
21
13
|
```typescript
|
|
22
14
|
import { container } from 'inwire';
|
|
23
15
|
|
|
24
16
|
const app = container()
|
|
25
|
-
.add('logger', () => new
|
|
26
|
-
.add('db', (c) => new Database(c.logger))
|
|
27
|
-
.add('
|
|
17
|
+
.add('logger', () => new Logger())
|
|
18
|
+
.add('db', (c) => new Database(c.logger)) // c.logger is typed
|
|
19
|
+
.add('users', (c) => new UserService(c.db)) // c.db is typed
|
|
28
20
|
.build();
|
|
29
21
|
|
|
30
|
-
app.
|
|
31
|
-
// c.logger in the db factory is typed as LoggerService
|
|
22
|
+
app.users.findById('42'); // lazy, singleton, fully typed
|
|
32
23
|
```
|
|
33
24
|
|
|
34
|
-
|
|
25
|
+
---
|
|
35
26
|
|
|
36
|
-
##
|
|
27
|
+
## Why inwire?
|
|
37
28
|
|
|
38
|
-
|
|
29
|
+
| | inwire | typical DI container |
|
|
30
|
+
|---|---|---|
|
|
31
|
+
| **Type inference** | Full — `c.db` autocompletes from `.add()` history | Manual generics or token strings |
|
|
32
|
+
| **Decorators** | None | Required (`@Injectable`, `@Inject`) |
|
|
33
|
+
| **Runtime metadata** | None | `reflect-metadata` polyfill needed |
|
|
34
|
+
| **Circular deps** | Caught with full chain + fix hint | Stack overflow or cryptic crash |
|
|
35
|
+
| **Async lifecycle** | First-class `preload()` with topological parallelism | Manual `Promise.all` plumbing |
|
|
36
|
+
| **Introspection** | `inspect()` returns JSON graph for LLMs / dashboards | None |
|
|
37
|
+
| **Bundle size** | ~4 KB gzip | 10–50 KB |
|
|
38
|
+
| **Runtime** | Pure ES2022 — Node, Bun, Deno, Workers, browsers | Often Node-only |
|
|
39
39
|
|
|
40
|
-
|
|
41
|
-
interface AppDeps {
|
|
42
|
-
ILogger: Logger;
|
|
43
|
-
IDatabase: Database;
|
|
44
|
-
IUserService: UserService;
|
|
45
|
-
}
|
|
40
|
+
The **dependency graph is a side product**: a tracking Proxy records which keys each factory accesses, so `inspect()` returns the real graph without you ever annotating it.
|
|
46
41
|
|
|
47
|
-
|
|
48
|
-
.add('ILogger', () => new ConsoleLogger()) // key: autocomplete keyof AppDeps
|
|
49
|
-
.add('IDatabase', (c) => new PgDatabase(c.ILogger)) // return must be Database
|
|
50
|
-
.add('IUserService', (c) => new UserService(c.IDatabase, c.ILogger))
|
|
51
|
-
.build();
|
|
42
|
+
---
|
|
52
43
|
|
|
53
|
-
|
|
44
|
+
## Install
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
pnpm add inwire # or npm i inwire / bun add inwire
|
|
54
48
|
```
|
|
55
49
|
|
|
56
|
-
|
|
50
|
+
Requires TypeScript ≥ 5.0 and an ESM-aware bundler / runtime.
|
|
57
51
|
|
|
58
|
-
|
|
52
|
+
---
|
|
59
53
|
|
|
60
|
-
|
|
54
|
+
## Modular Setup (recommended)
|
|
55
|
+
|
|
56
|
+
For real-world apps, organize bindings per module file with **Pinia-style global type augmentation**. Each file declares what it *provides* by augmenting `AppDeps`; `defineModule()` types the factory's `c` against the merged interface — cross-module references resolve regardless of import order.
|
|
61
57
|
|
|
62
58
|
```typescript
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
```
|
|
59
|
+
// modules/persistence.module.ts
|
|
60
|
+
import { defineModule } from 'inwire';
|
|
61
|
+
import type { IUserRepository } from '../contracts/IUserRepository';
|
|
62
|
+
import { DrizzleUserRepository } from '../infrastructure/DrizzleUserRepository';
|
|
68
63
|
|
|
69
|
-
|
|
70
|
-
|
|
64
|
+
declare module 'inwire' {
|
|
65
|
+
interface AppDeps {
|
|
66
|
+
IUserRepository: IUserRepository;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
71
69
|
|
|
72
|
-
|
|
70
|
+
export const persistenceModule = defineModule()((b) =>
|
|
71
|
+
b.add('IUserRepository', (): IUserRepository => new DrizzleUserRepository()),
|
|
72
|
+
);
|
|
73
|
+
```
|
|
73
74
|
|
|
74
|
-
|
|
75
|
+
```typescript
|
|
76
|
+
// modules/auth.module.ts
|
|
77
|
+
import { defineModule } from 'inwire';
|
|
78
|
+
import type { IAuthProvider } from '../contracts/IAuthProvider';
|
|
79
|
+
import { BetterAuthProvider } from '../infrastructure/BetterAuthProvider';
|
|
80
|
+
import { SignInUseCase } from '../application/SignInUseCase';
|
|
81
|
+
|
|
82
|
+
declare module 'inwire' {
|
|
83
|
+
interface AppDeps {
|
|
84
|
+
IAuthProvider: IAuthProvider;
|
|
85
|
+
SignInUseCase: SignInUseCase;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
75
88
|
|
|
76
|
-
|
|
89
|
+
export const authModule = defineModule()((b) =>
|
|
90
|
+
b
|
|
91
|
+
.add('IAuthProvider', (): IAuthProvider => new BetterAuthProvider())
|
|
92
|
+
.add('SignInUseCase', (c) => new SignInUseCase(c.IUserRepository, c.IAuthProvider)),
|
|
93
|
+
// ^^^^^^^^^^^^^^^^^^^^
|
|
94
|
+
// provided by persistenceModule — typed via merged AppDeps
|
|
95
|
+
);
|
|
96
|
+
```
|
|
77
97
|
|
|
78
98
|
```typescript
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
}
|
|
99
|
+
// container.ts — single source of truth
|
|
100
|
+
import { container } from 'inwire';
|
|
101
|
+
import { persistenceModule } from './modules/persistence.module';
|
|
102
|
+
import { authModule } from './modules/auth.module';
|
|
82
103
|
|
|
83
|
-
const
|
|
84
|
-
.
|
|
104
|
+
export const di = container()
|
|
105
|
+
.addModule(persistenceModule)
|
|
106
|
+
.addModule(authModule)
|
|
85
107
|
.build();
|
|
86
108
|
|
|
87
|
-
|
|
88
|
-
app.db;
|
|
89
|
-
|
|
90
|
-
// GOOD — onInit() is awaited, errors surface immediately
|
|
91
|
-
await app.preload('db');
|
|
92
|
-
app.db; // safe to use, fully initialized
|
|
109
|
+
export type Di = typeof di; // derived — never hand-written
|
|
93
110
|
```
|
|
94
111
|
|
|
95
|
-
|
|
112
|
+
**Why this scales:**
|
|
96
113
|
|
|
97
|
-
- **
|
|
98
|
-
- **
|
|
99
|
-
- **
|
|
114
|
+
- **Locality.** Each module is self-contained: it states what it *provides*, in its own file. No global shape interface to maintain.
|
|
115
|
+
- **Order-independent.** `authModule` references `c.IUserRepository` even if `persistenceModule` is added later, in any file.
|
|
116
|
+
- **Familiar pattern.** Mirrors Pinia's `PiniaCustomProperties` and Vue's `ComponentCustomProperties`. Augmentations are erased after type-check — zero runtime cost.
|
|
117
|
+
- **Derived types.** `type Di = typeof di` — add a binding, `Di` grows; remove one, it shrinks. The compiler does the bookkeeping.
|
|
100
118
|
|
|
101
|
-
|
|
119
|
+
> Other patterns are supported when this one doesn't fit — see [Modules reference](#modules-reference).
|
|
102
120
|
|
|
103
|
-
|
|
104
|
-
- **Automatic dependency tracking** — a tracking Proxy records which keys each factory accesses at resolution time. The dependency graph builds itself.
|
|
105
|
-
- **Circular dependency detection** — cycles are caught at resolution time with the full chain (`A → B → C → A`) and actionable fix suggestions. No stack overflow, no cryptic errors. Most DI containers (awilix, ioctopus) just crash.
|
|
106
|
-
- **Smart errors** — 7 error types, each with `hint`, `details`, and fuzzy matching ("did you mean `userService`?"). Designed for both humans and LLMs to parse.
|
|
107
|
-
- **Built-in introspection** — `inspect()` returns a serializable JSON graph. Feed it to an LLM, render it in a dashboard, or use `health()` to catch scope mismatches at runtime.
|
|
108
|
-
- **Runtime agnostic** — pure ES2022. No decorators, no `reflect-metadata`, no compiler plugins. Works in Node.js, Deno, Bun, Cloudflare Workers, Vercel Edge, and browsers.
|
|
109
|
-
- **Clean internals** — Clean Architecture, SOLID, single-responsibility files. Open any file, understand it, change it without fear.
|
|
110
|
-
- **Tiny** — ~4 KB gzip, zero dependencies.
|
|
121
|
+
---
|
|
111
122
|
|
|
112
|
-
##
|
|
123
|
+
## Core Concepts
|
|
113
124
|
|
|
114
|
-
###
|
|
125
|
+
### The container is a Proxy
|
|
126
|
+
|
|
127
|
+
`container()` returns a fluent builder. Each `.add(key, factory)` accumulates the type so the next factory's `c` argument is typed with everything declared so far. `.build()` wraps the factories in an ES Proxy: property access triggers lazy resolution and caches the result.
|
|
115
128
|
|
|
116
129
|
```typescript
|
|
117
130
|
const app = container()
|
|
118
|
-
.add('db', () => new Database(
|
|
131
|
+
.add('db', () => new Database())
|
|
119
132
|
.build();
|
|
120
133
|
|
|
121
|
-
app.db; //
|
|
122
|
-
app.db; //
|
|
134
|
+
app.db; // first access → factory runs, instance cached
|
|
135
|
+
app.db; // subsequent access → cached instance returned
|
|
123
136
|
```
|
|
124
137
|
|
|
125
|
-
###
|
|
138
|
+
### Auto-tracked dependency graph
|
|
126
139
|
|
|
127
|
-
|
|
140
|
+
The `c` argument passed to each factory is itself a tracking Proxy. Every property access is recorded — that's how `inspect()` returns the real graph without you annotating it.
|
|
128
141
|
|
|
129
142
|
```typescript
|
|
130
|
-
|
|
143
|
+
const app = container()
|
|
144
|
+
.add('db', () => new Database())
|
|
145
|
+
.add('repo', (c) => new UserRepo(c.db)) // c.db touched → graph: repo → [db]
|
|
146
|
+
.build();
|
|
131
147
|
|
|
148
|
+
app.inspect();
|
|
149
|
+
// { providers: { db: { deps: [], ... }, repo: { deps: ['db'], ... } } }
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### Singleton by default, transient on demand
|
|
153
|
+
|
|
154
|
+
```typescript
|
|
132
155
|
const app = container()
|
|
133
|
-
.add('
|
|
134
|
-
.addTransient('requestId', () => crypto.randomUUID())
|
|
156
|
+
.add('db', () => new Database()) // singleton (cached)
|
|
157
|
+
.addTransient('requestId', () => crypto.randomUUID()) // transient (fresh each access)
|
|
135
158
|
.build();
|
|
136
159
|
|
|
137
|
-
app.
|
|
138
|
-
app.requestId === app.requestId;
|
|
160
|
+
app.db === app.db; // true
|
|
161
|
+
app.requestId === app.requestId; // false
|
|
139
162
|
```
|
|
140
163
|
|
|
141
|
-
`
|
|
164
|
+
For `scope()` and `extend()`, use the `transient()` wrapper:
|
|
142
165
|
|
|
143
166
|
```typescript
|
|
144
167
|
import { transient } from 'inwire';
|
|
145
168
|
|
|
146
|
-
const
|
|
169
|
+
const scoped = app.extend({
|
|
147
170
|
timestamp: transient(() => Date.now()),
|
|
148
171
|
});
|
|
149
172
|
```
|
|
150
173
|
|
|
151
|
-
###
|
|
174
|
+
### Eager instances
|
|
152
175
|
|
|
153
|
-
|
|
176
|
+
A non-function value passed to `.add()` is registered eagerly (wrapped in `() => value`):
|
|
154
177
|
|
|
155
178
|
```typescript
|
|
156
|
-
|
|
157
|
-
.add('
|
|
158
|
-
.add('db', () => new Database())
|
|
179
|
+
container()
|
|
180
|
+
.add('config', { port: 3000 }) // eager — `{ port: 3000 }` is the value
|
|
181
|
+
.add('db', (c) => new Database(c.config)) // lazy — function = factory
|
|
159
182
|
.build();
|
|
183
|
+
```
|
|
160
184
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
185
|
+
To register a function *as a value*, wrap it: `.add('handler', () => myFunction)`.
|
|
186
|
+
|
|
187
|
+
### Lifecycle (duck-typed)
|
|
188
|
+
|
|
189
|
+
Implement `onInit()` / `onDestroy()` on any class. inwire detects them at runtime — no base class required.
|
|
190
|
+
|
|
191
|
+
```typescript
|
|
192
|
+
import type { OnInit, OnDestroy } from 'inwire';
|
|
193
|
+
|
|
194
|
+
class Database implements OnInit, OnDestroy {
|
|
195
|
+
async onInit() { await this.connect(); }
|
|
196
|
+
async onDestroy() { await this.disconnect(); }
|
|
197
|
+
}
|
|
198
|
+
```
|
|
165
199
|
|
|
166
|
-
|
|
167
|
-
|
|
200
|
+
> **CRITICAL gotcha — sync property access cannot await.** When you access `app.db`, `onInit()` is called but **not awaited**. Async errors are silently captured as `health().warnings`. To safely await async startup, use [`preload()`](#async-startup-preload).
|
|
201
|
+
|
|
202
|
+
---
|
|
203
|
+
|
|
204
|
+
## Cookbook
|
|
205
|
+
|
|
206
|
+
### Async startup — `preload()`
|
|
207
|
+
|
|
208
|
+
`preload()` is the **only** way to safely await async `onInit()`. It runs independent branches in parallel using a topological sort (Kahn's BFS), levels sequentially:
|
|
209
|
+
|
|
210
|
+
```
|
|
211
|
+
Level 0: [config] ← no deps
|
|
212
|
+
Level 1: [db] [cache] ← parallel, both depend on config
|
|
213
|
+
Level 2: [api] ← depends on db + cache
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
```typescript
|
|
217
|
+
await app.preload('db', 'cache'); // specific keys
|
|
218
|
+
await app.preload(); // everything
|
|
168
219
|
```
|
|
169
220
|
|
|
170
|
-
|
|
221
|
+
Errors from `onInit()` propagate as a single `AggregateError` if multiple fail. Wrap in `try/catch` for startup validation.
|
|
222
|
+
|
|
223
|
+
### Per-request scopes
|
|
224
|
+
|
|
225
|
+
`scope()` creates a child container with extra bindings. The child inherits parent singletons via a parent-resolver chain; scoped bindings are isolated per scope.
|
|
171
226
|
|
|
172
227
|
```typescript
|
|
173
228
|
const request = app.scope(
|
|
174
|
-
{
|
|
175
|
-
|
|
229
|
+
{
|
|
230
|
+
requestId: () => crypto.randomUUID(),
|
|
231
|
+
handler: (c) => new Handler(c.logger, c.requestId), // c is typeof app
|
|
232
|
+
},
|
|
233
|
+
{ name: 'request-123' }, // optional, surfaces in inspect()/toString()
|
|
176
234
|
);
|
|
177
235
|
|
|
178
|
-
|
|
179
|
-
request.
|
|
236
|
+
request.requestId; // unique per scope
|
|
237
|
+
request.logger; // shared from parent
|
|
180
238
|
```
|
|
181
239
|
|
|
182
|
-
###
|
|
240
|
+
### Test overrides
|
|
183
241
|
|
|
184
|
-
|
|
185
|
-
import type { OnInit, OnDestroy } from 'inwire';
|
|
242
|
+
No special test API. Build a separate container with mocks:
|
|
186
243
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
244
|
+
```typescript
|
|
245
|
+
function createTestContainer() {
|
|
246
|
+
return container()
|
|
247
|
+
.add('logger', () => ({ log: () => {} })) // silent
|
|
248
|
+
.add('db', () => new InMemoryDatabase()) // mock
|
|
249
|
+
.add('users', (c) => new UserService(c.db, c.logger))
|
|
250
|
+
.build();
|
|
190
251
|
}
|
|
252
|
+
```
|
|
191
253
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
254
|
+
### Plugin system — `extend()`
|
|
255
|
+
|
|
256
|
+
`extend()` returns a new container with additional bindings. Unlike `scope()`, the existing singleton cache is **shared** — already-resolved instances are reused.
|
|
195
257
|
|
|
196
|
-
|
|
197
|
-
|
|
258
|
+
```typescript
|
|
259
|
+
const withCsv = core.extend({
|
|
260
|
+
csvParser: (c) => new CsvParser(c.logger),
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
const app = withCsv.extend({
|
|
264
|
+
jobRunner: transient((c) => new JobRunner(c.csvParser)),
|
|
265
|
+
});
|
|
198
266
|
```
|
|
199
267
|
|
|
200
|
-
|
|
268
|
+
| | `scope()` | `extend()` |
|
|
269
|
+
|---|---|---|
|
|
270
|
+
| Topology | Parent-child chain | Flat merged container |
|
|
271
|
+
| Cache | Independent per-scope cache | Shares parent's resolved cache |
|
|
272
|
+
| Use for | Per-request isolation | Additive composition / plugins |
|
|
201
273
|
|
|
202
|
-
|
|
274
|
+
### Graceful shutdown — `dispose()`
|
|
203
275
|
|
|
204
|
-
|
|
205
|
-
const base = container()
|
|
206
|
-
.add('logger', () => new LoggerService())
|
|
207
|
-
.build();
|
|
276
|
+
Calls `onDestroy()` on all resolved instances in **LIFO order**. Resilient: continues on errors, collects them into `AggregateError`.
|
|
208
277
|
|
|
209
|
-
|
|
210
|
-
|
|
278
|
+
```typescript
|
|
279
|
+
process.on('SIGTERM', async () => {
|
|
280
|
+
await app.dispose();
|
|
281
|
+
process.exit(0);
|
|
211
282
|
});
|
|
283
|
+
```
|
|
212
284
|
|
|
213
|
-
|
|
214
|
-
|
|
285
|
+
### Resetting cached singletons
|
|
286
|
+
|
|
287
|
+
```typescript
|
|
288
|
+
app.db; // creates instance
|
|
289
|
+
app.reset('db'); // invalidates cache
|
|
290
|
+
app.db; // creates a NEW instance (factory re-runs, onInit re-fires)
|
|
215
291
|
```
|
|
216
292
|
|
|
217
|
-
|
|
293
|
+
`reset()` is scope-local — it doesn't affect parent caches.
|
|
218
294
|
|
|
219
|
-
###
|
|
295
|
+
### Introspection for AI / observability
|
|
220
296
|
|
|
221
|
-
|
|
297
|
+
```typescript
|
|
298
|
+
app.inspect(); // ContainerGraph — full dependency graph (JSON)
|
|
299
|
+
app.describe('users'); // ProviderInfo for one binding
|
|
300
|
+
app.health(); // { totalProviders, resolved, unresolved, warnings }
|
|
301
|
+
String(app); // human-readable one-liner
|
|
302
|
+
```
|
|
222
303
|
|
|
223
|
-
|
|
304
|
+
```typescript
|
|
305
|
+
const graph = JSON.stringify(app.inspect(), null, 2);
|
|
306
|
+
// Pipe to an LLM, render in a dashboard, diff in CI.
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
---
|
|
310
|
+
|
|
311
|
+
## Modules reference
|
|
312
|
+
|
|
313
|
+
inwire offers four ways to compose modules. Pinia-style is the recommended default; the rest fit specific situations.
|
|
314
|
+
|
|
315
|
+
### Pinia-style augmentation — recommended
|
|
224
316
|
|
|
225
|
-
|
|
317
|
+
See [Modular Setup](#modular-setup-recommended) above for the full recipe. TL;DR:
|
|
226
318
|
|
|
227
319
|
```typescript
|
|
228
|
-
|
|
320
|
+
declare module 'inwire' {
|
|
321
|
+
interface AppDeps { IUserRepository: IUserRepository }
|
|
322
|
+
}
|
|
229
323
|
|
|
230
|
-
|
|
324
|
+
export const persistenceModule = defineModule()((b) =>
|
|
325
|
+
b.add('IUserRepository', (): IUserRepository => new DrizzleUserRepository()),
|
|
326
|
+
);
|
|
327
|
+
```
|
|
231
328
|
|
|
329
|
+
- Each module declares what it **provides**.
|
|
330
|
+
- `c` is typed as the merged `AppDeps`.
|
|
331
|
+
- Cross-module forward references work, order-independent.
|
|
332
|
+
|
|
333
|
+
### `defineModule<TDeps>()` — locally-declared prerequisites
|
|
334
|
+
|
|
335
|
+
When a module's prereqs are a tight, fixed surface and you'd rather not augment a global, declare what the module **consumes** inline. `c` is typed locally as `TDeps`:
|
|
336
|
+
|
|
337
|
+
```typescript
|
|
232
338
|
const dbModule = defineModule<{ logger: Logger }>()((b) =>
|
|
233
339
|
b
|
|
234
|
-
.add('db',
|
|
340
|
+
.add('db', (c) => new Database(c.logger))
|
|
235
341
|
.add('cache', (c) => new Redis(c.logger)),
|
|
236
342
|
);
|
|
343
|
+
```
|
|
237
344
|
|
|
238
|
-
|
|
239
|
-
b.add('userService', (c) => new UserService(c.db, c.logger)),
|
|
240
|
-
);
|
|
345
|
+
Trade-offs vs Pinia-style:
|
|
241
346
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
347
|
+
| Pattern | Declares | Cross-module forward ref | Global state |
|
|
348
|
+
|---|---|---|---|
|
|
349
|
+
| **Pinia-style** (`defineModule()` + `declare module`) | what the module **provides** | yes — order-independent | augments inwire's `AppDeps` |
|
|
350
|
+
| **Local** (`defineModule<TDeps>()`) | what the module **consumes** | no — prereqs added first | none |
|
|
351
|
+
|
|
352
|
+
Both modes coexist: passing `<TDeps>` always overrides the global mode for that module.
|
|
353
|
+
|
|
354
|
+
> **Why the double-call signature `defineModule<TDeps>()(fn)`?** TypeScript's generic inference is all-or-nothing — specifying `<TDeps>` in a flat single-call signature would force you to write `<TBuilt>` by hand too, defeating the inference of the `.add()` chain. The curry splits the two: first call fixes `TDeps` (or defaults to `AppDeps`), second call infers `TBuilt` from the factory return. Same workaround used by zod, TanStack Query, RTK. Tracking [microsoft/TypeScript#26242](https://github.com/microsoft/TypeScript/issues/26242).
|
|
248
355
|
|
|
249
|
-
|
|
250
|
-
- Each module declares only what it **needs** — no import of a global `AppDeps` interface.
|
|
251
|
-
- The output type is inferred from the `.add()` chain — no duplicated signatures.
|
|
252
|
-
- Order of `addModule()` is enforced at compile time: applying `userModule` before `dbModule` would fail the prerequisite check.
|
|
356
|
+
> `addModule()` does **not** enforce prereq satisfaction at the type level — missing keys raise `ProviderNotFoundError` at resolution time. This relaxation is what makes Pinia-style forward references possible.
|
|
253
357
|
|
|
254
|
-
|
|
358
|
+
### `.merge()` — fuse standalone builders
|
|
255
359
|
|
|
256
|
-
When a module has no prerequisites
|
|
360
|
+
When a module has no prerequisites, define it as a plain builder and merge it:
|
|
257
361
|
|
|
258
362
|
```typescript
|
|
259
363
|
const dbModule = container()
|
|
@@ -267,24 +371,11 @@ const app = container()
|
|
|
267
371
|
.build();
|
|
268
372
|
```
|
|
269
373
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
#### Anti-pattern (avoid)
|
|
273
|
-
|
|
274
|
-
Older code may show this manual generic pattern — it works but is verbose, couples the module to a global `AppDeps`, and forces you to redeclare every prerequisite by hand:
|
|
275
|
-
|
|
276
|
-
```typescript
|
|
277
|
-
// Don't do this anymore — use defineModule() instead.
|
|
278
|
-
function dbModule<T extends { logger: Logger }>(
|
|
279
|
-
b: ContainerBuilder<AppDeps, T>,
|
|
280
|
-
) {
|
|
281
|
-
return b.add('db', (c) => new Database(c.logger));
|
|
282
|
-
}
|
|
283
|
-
```
|
|
374
|
+
Cross-builder dependencies are resolved at build time. Duplicate keys override (last write wins). Reserved keys throw.
|
|
284
375
|
|
|
285
|
-
|
|
376
|
+
### Post-build — `container.module()`
|
|
286
377
|
|
|
287
|
-
Compose post-build using the same builder DX:
|
|
378
|
+
Compose post-build using the same builder DX. Each `.add()` in the callback types `c` incrementally:
|
|
288
379
|
|
|
289
380
|
```typescript
|
|
290
381
|
const core = container().add('logger', () => new Logger()).build();
|
|
@@ -293,294 +384,248 @@ const withDb = core.module((b) =>
|
|
|
293
384
|
b.add('db', (c) => new Database(c.logger)),
|
|
294
385
|
);
|
|
295
386
|
|
|
296
|
-
// Chainable — c accumulates previous bindings
|
|
297
387
|
const full = withDb.module((b) =>
|
|
298
|
-
b.add('
|
|
388
|
+
b.add('users', (c) => new UserService(c.db, c.logger)),
|
|
299
389
|
);
|
|
300
390
|
```
|
|
301
391
|
|
|
302
|
-
`module()`
|
|
303
|
-
|
|
304
|
-
#### Deriving the container shape (Zod-style)
|
|
305
|
-
|
|
306
|
-
You don't need to maintain a manual interface for the container's full shape. Derive it from the container itself, exactly like `z.infer<typeof schema>`:
|
|
307
|
-
|
|
308
|
-
```typescript
|
|
309
|
-
// container.ts
|
|
310
|
-
import { container } from 'inwire';
|
|
311
|
-
import { authModule } from './modules/auth.module';
|
|
312
|
-
import { billingModule } from './modules/billing.module';
|
|
313
|
-
import { persistenceModule } from './modules/persistence.module';
|
|
314
|
-
|
|
315
|
-
export const di = container()
|
|
316
|
-
.addModule(persistenceModule)
|
|
317
|
-
.addModule(authModule)
|
|
318
|
-
.addModule(billingModule)
|
|
319
|
-
.build();
|
|
320
|
-
|
|
321
|
-
// Single source of truth — derived, never written by hand.
|
|
322
|
-
// Use this as the type for handlers, controllers, etc.
|
|
323
|
-
export type Di = typeof di;
|
|
324
|
-
```
|
|
392
|
+
`module()` works on `scope()` and `extend()` results too. Internally it delegates to `extend()` after building the typed factory record.
|
|
325
393
|
|
|
326
|
-
|
|
394
|
+
### Anti-pattern (avoid)
|
|
327
395
|
|
|
328
|
-
|
|
396
|
+
Older code may show this manual generic — verbose, couples the module to a global `AppDeps`, forces redeclaring prerequisites:
|
|
329
397
|
|
|
330
398
|
```typescript
|
|
331
|
-
//
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
export const authModule = defineModule<{ IUserRepository: IUserRepository }>()((b) =>
|
|
339
|
-
b
|
|
340
|
-
.add('IAuthProvider', (): IAuthProvider => new BetterAuthProvider())
|
|
341
|
-
.add('SignInUseCase', (c) => new SignInUseCase(c.IUserRepository, c.IAuthProvider)),
|
|
342
|
-
);
|
|
399
|
+
// ✗ Don't do this — use defineModule() instead.
|
|
400
|
+
function dbModule<T extends { logger: Logger }>(
|
|
401
|
+
b: ContainerBuilder<AppDeps, T>,
|
|
402
|
+
) {
|
|
403
|
+
return b.add('db', (c) => new Database(c.logger));
|
|
404
|
+
}
|
|
343
405
|
```
|
|
344
406
|
|
|
345
|
-
|
|
346
|
-
- **No shape interface to maintain.** Add a binding anywhere → `Di` grows automatically. Remove one → it shrinks.
|
|
347
|
-
- **No `declare module` augmentation.** No global state, no import side-effects.
|
|
348
|
-
- **Local prerequisites.** A module's `<TDeps>` is its API contract — three lines max, exactly what it needs.
|
|
349
|
-
- **Cross-module references work at build time.** When `authModule` registers `SignInUseCase` that needs `IUserRepository` (provided by `persistenceModule`), the prerequisite check at `addModule()` time enforces the order.
|
|
407
|
+
---
|
|
350
408
|
|
|
351
|
-
|
|
409
|
+
## Contract Mode (single-file containers)
|
|
352
410
|
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
When a module needs to consume a binding **provided by another module loaded later**, the local `<TDeps>` pattern can't help — the prerequisite would have to list everything the module sees, defeating the locality. For that case, inwire exposes an augmentable global interface, exactly like Pinia's `PiniaCustomProperties` or Vue's `ComponentCustomProperties`:
|
|
411
|
+
For monolithic, single-file containers (no modules), pass an interface to `container<T>()` to constrain keys and return types at compile time:
|
|
356
412
|
|
|
357
413
|
```typescript
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
IUserRepository: IUserRepository;
|
|
363
|
-
SignInUseCase: SignInUseCase;
|
|
364
|
-
}
|
|
414
|
+
interface AppDeps {
|
|
415
|
+
ILogger: Logger;
|
|
416
|
+
IDatabase: Database;
|
|
417
|
+
IUserService: UserService;
|
|
365
418
|
}
|
|
366
|
-
```
|
|
367
|
-
|
|
368
|
-
Each module file augments `AppDeps` with the bindings **it provides**. When you call `defineModule()` *without* a `<TDeps>` generic, `c` is typed as the global `AppDeps` — so `c.X` resolves transparently across modules, regardless of declaration order:
|
|
369
419
|
|
|
370
|
-
|
|
371
|
-
//
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
SignInUseCase: SignInUseCase;
|
|
376
|
-
}
|
|
377
|
-
}
|
|
420
|
+
const app = container<AppDeps>()
|
|
421
|
+
.add('ILogger', () => new ConsoleLogger()) // key: keyof AppDeps
|
|
422
|
+
.add('IDatabase', (c) => new PgDatabase(c.ILogger)) // return must match Database
|
|
423
|
+
.add('IUserService', (c) => new UserService(c.IDatabase, c.ILogger))
|
|
424
|
+
.build();
|
|
378
425
|
|
|
379
|
-
|
|
380
|
-
b
|
|
381
|
-
.add('IAuthProvider', (): IAuthProvider => new BetterAuthProvider())
|
|
382
|
-
.add('SignInUseCase', (c) => new SignInUseCase(c.IUserRepository, c.IAuthProvider)),
|
|
383
|
-
// ^^^^^^^^^^^^^^^^^^^^
|
|
384
|
-
// provided by another module — type-checked via AppDeps
|
|
385
|
-
);
|
|
426
|
+
app.ILogger; // typed as Logger (interface), not ConsoleLogger
|
|
386
427
|
```
|
|
387
428
|
|
|
388
|
-
|
|
429
|
+
The string key acts as a token (à la NestJS) but is type-safe at compile time. For multi-module apps, **use Pinia-style instead** — it scales across files; Contract Mode does not.
|
|
389
430
|
|
|
390
|
-
|
|
391
|
-
|---|---|---|
|
|
392
|
-
| `defineModule<TDeps>()` | what the module **consumes** (inputs) | no — must be already added |
|
|
393
|
-
| `defineModule()` + `declare module` | what the module **adds** (outputs) | yes — order-independent |
|
|
431
|
+
---
|
|
394
432
|
|
|
395
|
-
|
|
433
|
+
## Errors & Diagnostics
|
|
396
434
|
|
|
397
|
-
|
|
435
|
+
Every error extends `ContainerError` and carries:
|
|
436
|
+
- `hint: string` — actionable fix suggestion
|
|
437
|
+
- `details: Record<string, unknown>` — structured context for programmatic consumption
|
|
398
438
|
|
|
399
|
-
|
|
439
|
+
Designed to be parsed by both humans and LLMs.
|
|
400
440
|
|
|
401
|
-
|
|
402
|
-
await app.preload('db', 'cache'); // resolve specific deps
|
|
403
|
-
await app.preload(); // resolve ALL
|
|
404
|
-
```
|
|
441
|
+
### Fuzzy missing-key suggestions
|
|
405
442
|
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
443
|
+
```typescript
|
|
444
|
+
app.userServce; // typo
|
|
445
|
+
// ProviderNotFoundError: Cannot resolve 'userServce'.
|
|
446
|
+
// Registered: [userService, logger, db]
|
|
447
|
+
// Did you mean 'userService'?
|
|
448
|
+
// hint: Add 'userServce' to your container, or fix the typo.
|
|
412
449
|
```
|
|
413
450
|
|
|
414
|
-
|
|
451
|
+
Powered by Levenshtein distance (≥ 50% similarity threshold).
|
|
415
452
|
|
|
416
|
-
###
|
|
453
|
+
### Circular dependency — full chain
|
|
417
454
|
|
|
418
455
|
```typescript
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
app.db; // creates a NEW Database instance
|
|
456
|
+
// CircularDependencyError: Circular dependency detected while resolving 'authService'.
|
|
457
|
+
// Cycle: authService → userService → authService
|
|
422
458
|
```
|
|
423
459
|
|
|
424
|
-
|
|
460
|
+
No stack overflow, no cryptic crash — just the resolution chain.
|
|
425
461
|
|
|
426
|
-
|
|
427
|
-
app.inspect(); // full dependency graph (JSON)
|
|
428
|
-
app.describe('db'); // single provider info
|
|
429
|
-
app.health(); // health status + warnings
|
|
430
|
-
String(app); // human-readable
|
|
431
|
-
```
|
|
462
|
+
### Reserved keys
|
|
432
463
|
|
|
433
|
-
|
|
464
|
+
`scope`, `extend`, `module`, `preload`, `reset`, `inspect`, `describe`, `health`, `dispose`, `toString` cannot be used as dependency keys.
|
|
434
465
|
|
|
435
466
|
```typescript
|
|
436
|
-
|
|
467
|
+
container().add('inspect', () => 'foo');
|
|
468
|
+
// ReservedKeyError: 'inspect' is a reserved container method.
|
|
469
|
+
// hint: Rename, e.g. 'inspectService' or 'myInspect'.
|
|
437
470
|
```
|
|
438
471
|
|
|
439
|
-
###
|
|
472
|
+
### Scope mismatch detection (warning)
|
|
440
473
|
|
|
441
|
-
|
|
474
|
+
A singleton depending on a transient freezes the transient value. Surface via `health()`:
|
|
442
475
|
|
|
443
476
|
```typescript
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
//
|
|
447
|
-
|
|
448
|
-
//
|
|
449
|
-
|
|
450
|
-
// ProviderNotFoundError: Did you mean 'userService'?
|
|
451
|
-
|
|
452
|
-
// Circular dependency
|
|
453
|
-
// CircularDependencyError: Cycle: authService -> userService -> authService
|
|
477
|
+
app.health().warnings;
|
|
478
|
+
// [{
|
|
479
|
+
// type: 'scope_mismatch',
|
|
480
|
+
// message: "Singleton 'userService' depends on transient 'requestId'.",
|
|
481
|
+
// details: { singleton: 'userService', transient: 'requestId' },
|
|
482
|
+
// }]
|
|
454
483
|
```
|
|
455
484
|
|
|
456
|
-
###
|
|
485
|
+
### Async-init errors (warning)
|
|
486
|
+
|
|
487
|
+
When `onInit()` rejects during *lazy* access (no `preload()`), the rejection is captured as a warning rather than crashing your app.
|
|
457
488
|
|
|
458
489
|
```typescript
|
|
459
490
|
app.health().warnings;
|
|
460
|
-
// [{ type: '
|
|
491
|
+
// [{ type: 'async_init_error', message: "onInit() for 'db' rejected: connection refused", ... }]
|
|
461
492
|
```
|
|
462
493
|
|
|
463
|
-
|
|
494
|
+
Use `preload()` to surface these as proper errors.
|
|
495
|
+
|
|
496
|
+
### Duplicate key detection (pre-spread)
|
|
464
497
|
|
|
465
498
|
```typescript
|
|
466
499
|
import { detectDuplicateKeys } from 'inwire';
|
|
467
500
|
|
|
468
501
|
detectDuplicateKeys(authModule, userModule);
|
|
469
|
-
// ['logger']
|
|
502
|
+
// ['logger'] — appears in both
|
|
470
503
|
```
|
|
471
504
|
|
|
505
|
+
### All error types
|
|
506
|
+
|
|
507
|
+
| Error | Thrown when |
|
|
508
|
+
|---|---|
|
|
509
|
+
| `ContainerError` | Base class for all errors |
|
|
510
|
+
| `ContainerConfigError` | Non-function value passed to `scope()` / `extend()` deps |
|
|
511
|
+
| `ReservedKeyError` | Reserved method name used as a key |
|
|
512
|
+
| `ProviderNotFoundError` | Key not registered (with fuzzy suggestion) |
|
|
513
|
+
| `CircularDependencyError` | Cycle detected during resolution |
|
|
514
|
+
| `UndefinedReturnError` | Factory returned `undefined` |
|
|
515
|
+
| `FactoryError` | Factory threw (wraps original error) |
|
|
516
|
+
| `ScopeMismatchWarning` | Singleton depends on transient (surfaced via `health()`) |
|
|
517
|
+
| `AsyncInitErrorWarning` | Async `onInit()` rejected during lazy access (surfaced via `health()`) |
|
|
518
|
+
|
|
519
|
+
---
|
|
520
|
+
|
|
472
521
|
## Examples
|
|
473
522
|
|
|
474
523
|
| Example | Run | Showcases |
|
|
475
524
|
|---|---|---|
|
|
476
|
-
| [
|
|
477
|
-
| [
|
|
478
|
-
| [
|
|
479
|
-
| [
|
|
480
|
-
| [
|
|
481
|
-
| [
|
|
525
|
+
| [06-pinia-augmentation.ts](examples/06-pinia-augmentation.ts) ★ | `npm run example:pinia` | **Recommended modular pattern.** `declare module 'inwire'` per file, order-independent cross-module typing |
|
|
526
|
+
| [05-zod-style-typing.ts](examples/05-zod-style-typing.ts) | `npm run example:typing` | `type Di = typeof di` derivation, Clean Arch contracts |
|
|
527
|
+
| [04-modules.ts](examples/04-modules.ts) | `npm run example:modules` | `defineModule<TDeps>()`, `.merge()`, `module()` post-build |
|
|
528
|
+
| [03-plugin-system.ts](examples/03-plugin-system.ts) | `npm run example:plugin` | Extend chain, scoped jobs, JSON graph for LLM |
|
|
529
|
+
| [02-modular-testing.ts](examples/02-modular-testing.ts) | `npm run example:test` | Free mode, instance values, test overrides |
|
|
530
|
+
| [01-web-service.ts](examples/01-web-service.ts) | `npm run example:web` | Contract mode, lifecycle, dependency inversion |
|
|
531
|
+
|
|
532
|
+
---
|
|
533
|
+
|
|
534
|
+
## API Reference
|
|
535
|
+
|
|
536
|
+
### Functions
|
|
537
|
+
|
|
538
|
+
| Export | Description |
|
|
539
|
+
|---|---|
|
|
540
|
+
| `container<T?>()` | Creates a `ContainerBuilder`. Pass `T` for [Contract Mode](#contract-mode-single-file-containers). |
|
|
541
|
+
| `defineModule<TDeps?>()(fn)` | Defines a typed reusable module. See [Modules reference](#modules-reference). |
|
|
542
|
+
| `transient(factory)` | Marks a factory as transient (for `scope()` / `extend()`). |
|
|
543
|
+
| `detectDuplicateKeys(...modules)` | Returns keys that appear in more than one module object. |
|
|
544
|
+
|
|
545
|
+
### Builder methods
|
|
546
|
+
|
|
547
|
+
| Method | Description |
|
|
548
|
+
|---|---|
|
|
549
|
+
| `.add(key, factoryOrInstance)` | Register a binding. Function = lazy factory; non-function = eager instance. |
|
|
550
|
+
| `.addTransient(key, factory)` | Register a transient binding (fresh each access). |
|
|
551
|
+
| `.addModule(module)` | Apply a `Module` (typically from `defineModule()`). |
|
|
552
|
+
| `.merge(otherBuilder)` | Fuse a standalone builder's factories into this one. |
|
|
553
|
+
| `.build()` | Build and return the container. |
|
|
554
|
+
|
|
555
|
+
### Container methods
|
|
556
|
+
|
|
557
|
+
| Method | Description |
|
|
558
|
+
|---|---|
|
|
559
|
+
| `.scope(extra, options?)` | Child container with additional deps. Inherits parent singletons via parent chain. |
|
|
560
|
+
| `.extend(extra)` | New container with additional deps. **Shares** singleton cache. |
|
|
561
|
+
| `.module(fn)` | Post-build `ContainerBuilder` for typed `c` accumulation. Delegates to `extend()`. |
|
|
562
|
+
| `.preload(...keys)` | Eagerly resolve and **await** `onInit()`. No args = preload all. |
|
|
563
|
+
| `.reset(...keys)` | Invalidate cached singletons. Scope-local. |
|
|
564
|
+
| `.inspect()` | Full dependency graph (`ContainerGraph`). |
|
|
565
|
+
| `.describe(key)` | Single binding info (`ProviderInfo`). |
|
|
566
|
+
| `.health()` | Health snapshot + warnings (`ContainerHealth`). |
|
|
567
|
+
| `.dispose()` | LIFO `onDestroy()` on all resolved instances. |
|
|
568
|
+
|
|
569
|
+
### Types
|
|
570
|
+
|
|
571
|
+
| Type | Description |
|
|
572
|
+
|---|---|
|
|
573
|
+
| `AppDeps` | Augmentable global interface for Pinia-style typing. |
|
|
574
|
+
| `Container<T>` | `T & IContainer<T>` — resolved deps + container methods. |
|
|
575
|
+
| `ContainerBuilder<TContract, TBuilt>` | Fluent builder (also passed to `module()` callbacks). |
|
|
576
|
+
| `IContainer<T>` | Container methods interface. |
|
|
577
|
+
| `Module<TDeps, TBuilt>` | Module shape returned by `defineModule()`. |
|
|
578
|
+
| `InferModuleDeps<M>` / `InferModuleBuilt<M>` | Extract a module's prereqs / full output. |
|
|
579
|
+
| `Factory<T>` | Raw factory signature `(c: unknown) => T`. |
|
|
580
|
+
| `OnInit` / `OnDestroy` | Lifecycle interfaces (duck-typed). |
|
|
581
|
+
| `ContainerGraph` | Return of `inspect()` — `{ name?, providers }`. |
|
|
582
|
+
| `ContainerHealth` | Return of `health()` — `{ totalProviders, resolved, unresolved, warnings }`. |
|
|
583
|
+
| `ContainerWarning` | `{ type: 'scope_mismatch' \| 'async_init_error', message, details }`. |
|
|
584
|
+
| `ProviderInfo` | Return of `describe()` — `{ key, resolved, deps, scope }`. |
|
|
585
|
+
| `ScopeOptions` | `{ name?: string }`. |
|
|
586
|
+
|
|
587
|
+
---
|
|
482
588
|
|
|
483
589
|
## Architecture
|
|
484
590
|
|
|
485
|
-
Clean Architecture
|
|
591
|
+
Clean Architecture with an enforced one-way dependency rule.
|
|
486
592
|
|
|
487
593
|
```
|
|
488
594
|
src/
|
|
489
|
-
index.ts # public barrel — only file consumers
|
|
595
|
+
index.ts # public barrel — only file consumers see
|
|
490
596
|
domain/ # pure contracts — no framework deps
|
|
491
|
-
types.ts #
|
|
492
|
-
errors.ts # 7 error classes + 2
|
|
597
|
+
types.ts # IResolver, ICycleDetector, IDependencyTracker, IValidator, AppDeps, ...
|
|
598
|
+
errors.ts # 7 error classes + 2 warnings, each with hint + details
|
|
493
599
|
lifecycle.ts # OnInit / OnDestroy (duck-typed)
|
|
494
600
|
validation.ts # Validator, detectDuplicateKeys, Levenshtein
|
|
495
|
-
infrastructure/ #
|
|
601
|
+
infrastructure/ # mechanisms — depends on domain/ only
|
|
496
602
|
resolver.ts # lazy resolution, singleton cache, parent chain
|
|
497
603
|
cycle-detector.ts # circular dependency detection
|
|
498
|
-
dependency-tracker.ts # tracking Proxy + dependency graph
|
|
499
|
-
transient.ts # transient() marker (Symbol-based)
|
|
500
|
-
application/ #
|
|
501
|
-
container-builder.ts #
|
|
502
|
-
container-proxy.ts # Proxy construction, scope/extend/reset
|
|
604
|
+
dependency-tracker.ts # tracking Proxy + auto-built dependency graph
|
|
605
|
+
transient.ts # transient() marker (Symbol.for-based)
|
|
606
|
+
application/ # orchestration — depends on domain/ + infrastructure/
|
|
607
|
+
container-builder.ts # ContainerBuilder + container() factory ▸ Composition Root
|
|
608
|
+
container-proxy.ts # Proxy construction, scope/extend/reset ▸ Composition Root
|
|
609
|
+
define-module.ts # defineModule() — both modes
|
|
503
610
|
preloader.ts # topological sort (Kahn) + parallel onInit
|
|
504
|
-
disposer.ts # reverse-order onDestroy +
|
|
505
|
-
introspection.ts # inspect
|
|
611
|
+
disposer.ts # reverse-order onDestroy + resilient errors
|
|
612
|
+
introspection.ts # inspect / describe / health / toString
|
|
506
613
|
```
|
|
507
614
|
|
|
508
|
-
|
|
615
|
+
The `Resolver` receives its collaborators via constructor injection — no internal `new`, no hidden coupling. Application code depends on `IResolver`, never on the concrete class. Only the two **Composition Roots** (`container-builder.ts`, `container-proxy.ts`) instantiate concrete infrastructure.
|
|
616
|
+
|
|
617
|
+
---
|
|
509
618
|
|
|
510
619
|
## LLM / AI Integration
|
|
511
620
|
|
|
512
|
-
This package ships
|
|
621
|
+
This package ships [llms.txt](https://llmstxt.org/) files for AI-assisted development:
|
|
513
622
|
|
|
514
623
|
- **`llms.txt`** — Concise index following the llms.txt standard
|
|
515
624
|
- **`llms-full.txt`** — Complete API reference optimized for LLM context windows
|
|
516
625
|
|
|
517
|
-
Compatible with [Context7](https://context7.com/) and any tool that supports the llms.txt standard.
|
|
626
|
+
Compatible with [Context7](https://context7.com/) and any tool that supports the llms.txt standard. The `inspect()` output is also designed to be piped directly into an LLM for architecture analysis.
|
|
518
627
|
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
### Functions
|
|
522
|
-
|
|
523
|
-
| Export | Description |
|
|
524
|
-
|---|---|
|
|
525
|
-
| `container<T?>()` | Creates a new `ContainerBuilder`. Pass interface `T` for contract mode. |
|
|
526
|
-
| `defineModule<Deps>()(fn)` | Creates a typed, reusable module with locally-declared prerequisites |
|
|
527
|
-
| `transient(factory)` | Marks a factory as transient (for scope/extend) |
|
|
528
|
-
| `detectDuplicateKeys(...modules)` | Pre-spread validation — detects duplicate keys |
|
|
529
|
-
|
|
530
|
-
### ContainerBuilder Methods
|
|
531
|
-
|
|
532
|
-
| Method | Description |
|
|
533
|
-
|---|---|
|
|
534
|
-
| `.add(key, factory)` | Register a dependency (factory or instance) |
|
|
535
|
-
| `.addTransient(key, factory)` | Register a transient dependency |
|
|
536
|
-
| `.addModule(module)` | Apply a module `(builder) => builder` (use with `defineModule()`) |
|
|
537
|
-
| `.merge(otherBuilder)` | Merge a standalone builder's factories into this one |
|
|
538
|
-
| `.build()` | Build and return the container |
|
|
539
|
-
|
|
540
|
-
### Container Methods
|
|
541
|
-
|
|
542
|
-
| Method | Description |
|
|
543
|
-
|---|---|
|
|
544
|
-
| `.scope(extra, options?)` | Creates a child container with additional deps |
|
|
545
|
-
| `.extend(extra)` | Returns a new container with additional deps (shared cache) |
|
|
546
|
-
| `.module(fn)` | Applies a module post-build using the builder for typed `c` |
|
|
547
|
-
| `.preload(...keys)` | Eagerly resolves dependencies |
|
|
548
|
-
| `.reset(...keys)` | Invalidates cached singletons |
|
|
549
|
-
| `.inspect()` | Returns the full dependency graph |
|
|
550
|
-
| `.describe(key)` | Returns info about a single provider |
|
|
551
|
-
| `.health()` | Returns health status and warnings |
|
|
552
|
-
| `.dispose()` | Calls `onDestroy()` on all resolved instances |
|
|
553
|
-
|
|
554
|
-
### Types
|
|
555
|
-
|
|
556
|
-
| Export | Description |
|
|
557
|
-
|---|---|
|
|
558
|
-
| `Container<T>` | Full container type (resolved deps + methods) |
|
|
559
|
-
| `ContainerBuilder<TContract, TBuilt>` | Fluent builder class (also used in `module()` callbacks) |
|
|
560
|
-
| `IContainer<T>` | Container methods interface |
|
|
561
|
-
| `Module<TDeps, TBuilt>` | Type of a reusable module (returned by `defineModule()`) |
|
|
562
|
-
| `InferModuleDeps<M>` / `InferModuleBuilt<M>` | Extract a module's prerequisites or full output type |
|
|
563
|
-
| `Factory<T>` | Function type for raw factories (`(c: unknown) => T`) |
|
|
564
|
-
| `OnInit` | Interface with `onInit(): void \| Promise<void>` |
|
|
565
|
-
| `OnDestroy` | Interface with `onDestroy(): void \| Promise<void>` |
|
|
566
|
-
| `ContainerGraph` | Return type of `inspect()` |
|
|
567
|
-
| `ContainerHealth` | Return type of `health()` |
|
|
568
|
-
| `ContainerWarning` | Warning object (`scope_mismatch`) |
|
|
569
|
-
| `ProviderInfo` | Return type of `describe()` |
|
|
570
|
-
| `ScopeOptions` | Options for `scope()` (`{ name?: string }`) |
|
|
571
|
-
|
|
572
|
-
### Errors
|
|
573
|
-
|
|
574
|
-
| Export | Thrown when |
|
|
575
|
-
|---|---|
|
|
576
|
-
| `ContainerError` | Base class for all errors |
|
|
577
|
-
| `ContainerConfigError` | Non-function value in deps definition |
|
|
578
|
-
| `ReservedKeyError` | Reserved key used as dependency name |
|
|
579
|
-
| `ProviderNotFoundError` | Dependency not found during resolution |
|
|
580
|
-
| `CircularDependencyError` | Circular dependency detected |
|
|
581
|
-
| `UndefinedReturnError` | Factory returned `undefined` |
|
|
582
|
-
| `FactoryError` | Factory threw during resolution |
|
|
583
|
-
| `ScopeMismatchWarning` | Singleton depends on transient |
|
|
628
|
+
---
|
|
584
629
|
|
|
585
630
|
## License
|
|
586
631
|
|
package/dist/index.d.mts
CHANGED
|
@@ -488,6 +488,19 @@ type Module<TDeps extends Record<string, any> = AppDeps, TBuilt extends Record<s
|
|
|
488
488
|
*
|
|
489
489
|
* The output type is always **inferred** from the chained `.add()` calls.
|
|
490
490
|
*
|
|
491
|
+
* ### Why the double-call signature `defineModule<TDeps>()(fn)`?
|
|
492
|
+
*
|
|
493
|
+
* TypeScript enforces an all-or-nothing rule on generic type arguments: if you
|
|
494
|
+
* specify one explicitly, you must specify them all. A single-call signature
|
|
495
|
+
* `defineModule<TDeps, TBuilt>(fn)` would force you to write `TBuilt` by hand,
|
|
496
|
+
* defeating the inference. The curry splits the two parameters across two calls:
|
|
497
|
+
*
|
|
498
|
+
* - 1st call `defineModule<TDeps>()` — fixes `TDeps` manually (or falls back to `AppDeps`).
|
|
499
|
+
* - 2nd call `(fn)` — `TBuilt` is inferred from the `.add()` chain in `fn`.
|
|
500
|
+
*
|
|
501
|
+
* Tracking issue: https://github.com/microsoft/TypeScript/issues/26242 (partial
|
|
502
|
+
* type argument inference). Same workaround used by zod, TanStack Query, RTK.
|
|
503
|
+
*
|
|
491
504
|
* @example Global mode (Pinia-style):
|
|
492
505
|
* ```typescript
|
|
493
506
|
* declare module 'inwire' {
|
package/package.json
CHANGED