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.
Files changed (3) hide show
  1. package/README.md +408 -363
  2. package/dist/index.d.mts +13 -0
  3. 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. Builder pattern, full inference, no decorators, no tokens. Built-in introspection for AI tooling and debugging. Zero dependencies.
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
  [![NPM Version](https://img.shields.io/npm/v/inwire)](https://www.npmjs.com/package/inwire)
6
6
  [![CI](https://img.shields.io/github/actions/workflow/status/axelhamil/inwire/ci.yml)](https://github.com/axelhamil/inwire/actions)
@@ -10,250 +10,354 @@ Type-safe dependency injection for TypeScript. Builder pattern, full inference,
10
10
  [![License](https://img.shields.io/npm/l/inwire)](https://github.com/axelhamil/inwire/blob/main/LICENSE)
11
11
  [![Zero Dependencies](https://img.shields.io/badge/dependencies-0-brightgreen)](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 LoggerService())
26
- .add('db', (c) => new Database(c.logger))
27
- .add('userService', (c) => new UserService(c.db, c.logger))
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.userService; // lazy, singleton, fully typed
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
- Each `.add()` accumulates the type — `c` in every factory knows about all previously registered dependencies.
25
+ ---
35
26
 
36
- ## Contract Mode (Interface-First)
27
+ ## Why inwire?
37
28
 
38
- Pass an interface to the builder to constrain keys and return types at compile time:
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
- ```typescript
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
- const app = container<AppDeps>()
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
- app.ILogger; // typed as Logger (not ConsoleLogger)
44
+ ## Install
45
+
46
+ ```bash
47
+ pnpm add inwire # or npm i inwire / bun add inwire
54
48
  ```
55
49
 
56
- The string key acts as a token (like NestJS), but type-safe at compile time.
50
+ Requires TypeScript 5.0 and an ESM-aware bundler / runtime.
57
51
 
58
- ## Instance Values (Eager)
52
+ ---
59
53
 
60
- Non-function values are registered eagerly:
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
- const app = container()
64
- .add('config', { port: 3000, host: 'localhost' }) // object, not factory — eager
65
- .add('db', (c) => new Database(c.config)) // factory — lazy
66
- .build();
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
- Convention: `typeof value === 'function'` → factory (lazy). Otherwise → instance (eager, wrapped in `() => value`).
70
- To register a function as a value: `.add('fn', () => myFunction)`.
64
+ declare module 'inwire' {
65
+ interface AppDeps {
66
+ IUserRepository: IUserRepository;
67
+ }
68
+ }
71
69
 
72
- ## Async Lifecycle
70
+ export const persistenceModule = defineModule()((b) =>
71
+ b.add('IUserRepository', (): IUserRepository => new DrizzleUserRepository()),
72
+ );
73
+ ```
73
74
 
74
- Property access on the container is **synchronous**. If your service implements `onInit()` with an async function, it will be called but **not awaited** — errors are silently swallowed and your service may be used before it's ready.
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
- **`preload()` is the only way to safely initialize async services.**
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
- class Database implements OnInit {
80
- async onInit() { await this.connect(); }
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 app = container()
84
- .add('db', () => new Database())
104
+ export const di = container()
105
+ .addModule(persistenceModule)
106
+ .addModule(authModule)
85
107
  .build();
86
108
 
87
- // BAD onInit() fires but is NOT awaited, errors are lost
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
- ## Why use a DI container?
112
+ **Why this scales:**
96
113
 
97
- - **Testability** swap any dependency for a mock at creation time, no monkey-patching or `jest.mock`
98
- - **Decoupling** program against interfaces, not concrete imports; swap implementations without touching consumers
99
- - **Visibility** inspect the full dependency graph at runtime, catch scope mismatches, and monitor container health
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
- ## Why inwire?
119
+ > Other patterns are supported when this one doesn't fit — see [Modules reference](#modules-reference).
102
120
 
103
- - **Full type inference** — `c.db` gives you native autocomplete with zero annotations. No tokens, no decorators, no `container.get<T>('key')`.
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
- ## Features
123
+ ## Core Concepts
113
124
 
114
- ### Lazy Singletons (default)
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(process.env.DB_URL!))
131
+ .add('db', () => new Database())
119
132
  .build();
120
133
 
121
- app.db; // creates Database
122
- app.db; // same instance (cached)
134
+ app.db; // first access → factory runs, instance cached
135
+ app.db; // subsequent access → cached instance returned
123
136
  ```
124
137
 
125
- ### Transient
138
+ ### Auto-tracked dependency graph
126
139
 
127
- Fresh instance on every access via `addTransient()`:
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
- import { container } from 'inwire';
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('logger', () => new LoggerService())
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.logger === app.logger; // true — singleton
138
- app.requestId === app.requestId; // false — new every time
160
+ app.db === app.db; // true
161
+ app.requestId === app.requestId; // false
139
162
  ```
140
163
 
141
- `transient()` wrapper is still available for `scope()`/`extend()`:
164
+ For `scope()` and `extend()`, use the `transient()` wrapper:
142
165
 
143
166
  ```typescript
144
167
  import { transient } from 'inwire';
145
168
 
146
- const extended = app.extend({
169
+ const scoped = app.extend({
147
170
  timestamp: transient(() => Date.now()),
148
171
  });
149
172
  ```
150
173
 
151
- ### Scopes
174
+ ### Eager instances
152
175
 
153
- Create child containers for request-level isolation:
176
+ A non-function value passed to `.add()` is registered eagerly (wrapped in `() => value`):
154
177
 
155
178
  ```typescript
156
- const app = container()
157
- .add('logger', () => new LoggerService())
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
- const request = app.scope({
162
- requestId: () => crypto.randomUUID(),
163
- handler: (c) => new Handler(c.logger), // c typed as typeof app
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
- request.requestId; // scoped singleton
167
- request.logger; // inherited from parent
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
- #### Named Scopes
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
- { requestId: () => crypto.randomUUID() },
175
- { name: 'request-123' },
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
- String(request); // "Scope(request-123) { requestId (pending) }"
179
- request.inspect().name; // "request-123"
236
+ request.requestId; // unique per scope
237
+ request.logger; // shared from parent
180
238
  ```
181
239
 
182
- ### Lifecycle (onInit / onDestroy / dispose)
240
+ ### Test overrides
183
241
 
184
- ```typescript
185
- import type { OnInit, OnDestroy } from 'inwire';
242
+ No special test API. Build a separate container with mocks:
186
243
 
187
- class Database implements OnInit, OnDestroy {
188
- async onInit() { await this.connect(); }
189
- async onDestroy() { await this.disconnect(); }
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
- const app = container()
193
- .add('db', () => new Database())
194
- .build();
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
- app.db; // resolves + calls onInit()
197
- await app.dispose(); // calls onDestroy() on all resolved instances (LIFO order)
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
- ### Extend
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
- Add dependencies to an existing container without mutating it:
274
+ ### Graceful shutdown `dispose()`
203
275
 
204
- ```typescript
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
- const extended = base.extend({
210
- db: (c) => new Database(c.logger), // c typed as typeof base
278
+ ```typescript
279
+ process.on('SIGTERM', async () => {
280
+ await app.dispose();
281
+ process.exit(0);
211
282
  });
283
+ ```
212
284
 
213
- extended.logger; // shared singleton from base
214
- extended.db; // new dependency
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
- > **scope vs extend:** `scope()` creates a parent-child chain. `extend()` creates a flat container with merged factories and shared cache. Use `scope()` for per-request isolation, `extend()` for additive composition.
293
+ `reset()` is scope-local it doesn't affect parent caches.
218
294
 
219
- ### Modules
295
+ ### Introspection for AI / observability
220
296
 
221
- Split a large container into reusable modules. Each module declares its prerequisites locally — no shared `AppDeps` interface, no manual generics.
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
- #### `defineModule()` — recommended pattern
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
- `defineModule<Prerequisites>()(builder => builder.add(...))` infers the module's output from the chained `.add()` calls. Prerequisites are explicit and local to the module file.
317
+ See [Modular Setup](#modular-setup-recommended) above for the full recipe. TL;DR:
226
318
 
227
319
  ```typescript
228
- import { container, defineModule } from 'inwire';
320
+ declare module 'inwire' {
321
+ interface AppDeps { IUserRepository: IUserRepository }
322
+ }
229
323
 
230
- interface Logger { log: (msg: string) => void }
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', (c) => new Database(c.logger))
340
+ .add('db', (c) => new Database(c.logger))
235
341
  .add('cache', (c) => new Redis(c.logger)),
236
342
  );
343
+ ```
237
344
 
238
- const userModule = defineModule<{ db: Database; logger: Logger }>()((b) =>
239
- b.add('userService', (c) => new UserService(c.db, c.logger)),
240
- );
345
+ Trade-offs vs Pinia-style:
241
346
 
242
- const app = container()
243
- .add('logger', (): Logger => new ConsoleLogger())
244
- .addModule(dbModule)
245
- .addModule(userModule)
246
- .build();
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
- Why this works:
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
- #### `.merge()` — fuse standalone builders
358
+ ### `.merge()` — fuse standalone builders
255
359
 
256
- When a module has no prerequisites (or just bundles independent bindings), define it as a standalone builder and merge it:
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
- `.merge()` copies factories into the host builder. Cross-builder dependencies are resolved at build time. Duplicate keys override (last write wins). Reserved keys throw.
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
- #### Post-build: `module()` on the container
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('userService', (c) => new UserService(c.db, c.logger)),
388
+ b.add('users', (c) => new UserService(c.db, c.logger)),
299
389
  );
300
390
  ```
301
391
 
302
- `module()` uses the builder internally for typed `c`, then delegates to `extend()`. Works on `scope()` and `extend()` results too.
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
- > Note: `Di` is your own type alias, not the global `AppDeps` interface inwire exports for the Pinia-style pattern below. They serve different purposes — `Di` is consumed by your code; `AppDeps` augments inwire's typing.
394
+ ### Anti-pattern (avoid)
327
395
 
328
- Each module declares only the contracts it consumes via `<TDeps>` and those contracts are the **interfaces you already have** in `domain/` or `contracts/` (Clean Architecture, DDD):
396
+ Older code may show this manual genericverbose, couples the module to a global `AppDeps`, forces redeclaring prerequisites:
329
397
 
330
398
  ```typescript
331
- // modules/auth.module.ts
332
- import { defineModule } from 'inwire';
333
- import type { IUserRepository } from '../contracts/IUserRepository';
334
- import type { IAuthProvider } from '../contracts/IAuthProvider';
335
- import { BetterAuthProvider } from '../infrastructure/BetterAuthProvider';
336
- import { SignInUseCase } from '../application/SignInUseCase';
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
- Why this is the right pattern:
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
- See [examples/05-zod-style-typing.ts](examples/05-zod-style-typing.ts) for a full walk-through with three modules and a derived `Di`.
409
+ ## Contract Mode (single-file containers)
352
410
 
353
- #### Cross-module forward references (Pinia-style)
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
- import 'inwire';
359
-
360
- declare module 'inwire' {
361
- interface AppDeps {
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
- ```typescript
371
- // modules/auth.module.ts
372
- declare module 'inwire' {
373
- interface AppDeps {
374
- IAuthProvider: IAuthProvider;
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
- export const authModule = defineModule()((b) =>
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
- Trade-off vs `defineModule<TDeps>()`:
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
- | Pattern | You declare | Cross-module forward ref |
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
- Both patterns coexist. Mix freely — the explicit `<TDeps>` always overrides the global mode for that module. Why two? Because not every project needs the global augmentation, and not every module needs a tight prerequisite list. Pick the one that feels lighter for the file you're writing.
433
+ ## Errors & Diagnostics
396
434
 
397
- See [examples/06-pinia-augmentation.ts](examples/06-pinia-augmentation.ts) for a full walk-through with two modules cross-referencing each other.
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
- ### Preload
439
+ Designed to be parsed by both humans and LLMs.
400
440
 
401
- ```typescript
402
- await app.preload('db', 'cache'); // resolve specific deps
403
- await app.preload(); // resolve ALL
404
- ```
441
+ ### Fuzzy missing-key suggestions
405
442
 
406
- `preload()` **awaits `onInit()`** on every resolved service and runs independent branches in parallel using topological sorting:
407
-
408
- ```
409
- Level 0: [config] ← no deps, inits first
410
- Level 1: [db] [cache] ← depend on config, init in parallel
411
- Level 2: [api] ← depends on db + cache, inits last
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
- Errors thrown in `onInit()` propagate to the caller — use `try/catch` around `preload()` for startup validation.
451
+ Powered by Levenshtein distance ( 50% similarity threshold).
415
452
 
416
- ### Reset
453
+ ### Circular dependency — full chain
417
454
 
418
455
  ```typescript
419
- app.db; // creates Database
420
- app.reset('db');
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
- ### Introspection
460
+ No stack overflow, no cryptic crash — just the resolution chain.
425
461
 
426
- ```typescript
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
- Feed the graph to an LLM:
464
+ `scope`, `extend`, `module`, `preload`, `reset`, `inspect`, `describe`, `health`, `dispose`, `toString` cannot be used as dependency keys.
434
465
 
435
466
  ```typescript
436
- const graph = JSON.stringify(app.inspect(), null, 2);
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
- ### Smart Errors
472
+ ### Scope mismatch detection (warning)
440
473
 
441
- 7 error types, each with `hint`, `details`, and actionable suggestions:
474
+ A singleton depending on a transient freezes the transient value. Surface via `health()`:
442
475
 
443
476
  ```typescript
444
- // Reserved key
445
- container().add('inspect', () => 'foo');
446
- // ReservedKeyError: 'inspect' is a reserved container method.
447
-
448
- // Missing dependency with fuzzy suggestion
449
- app.userServce; // typo
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
- ### Scope Mismatch Detection
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: 'scope_mismatch', message: "Singleton 'userService' depends on transient 'requestId'." }]
491
+ // [{ type: 'async_init_error', message: "onInit() for 'db' rejected: connection refused", ... }]
461
492
  ```
462
493
 
463
- ### Duplicate Key Detection
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
- | [01-web-service.ts](examples/01-web-service.ts) | `npm run example:web` | Contract mode, lifecycle, dependency inversion, scope, introspection |
477
- | [02-modular-testing.ts](examples/02-modular-testing.ts) | `npm run example:test` | Free mode, instance values, test overrides, extend + transient |
478
- | [03-plugin-system.ts](examples/03-plugin-system.ts) | `npm run example:plugin` | Extend chain, scoped jobs, health, JSON graph for LLM |
479
- | [04-modules.ts](examples/04-modules.ts) | `npm run example:modules` | addModule, module() post-build, typed reusable modules |
480
- | [05-zod-style-typing.ts](examples/05-zod-style-typing.ts) | `npm run example:typing` | `type AppDeps = typeof di` pattern, Clean Arch contracts, no manual interface |
481
- | [06-pinia-augmentation.ts](examples/06-pinia-augmentation.ts) | `npm run example:pinia` | Cross-module forward references via `declare module 'inwire'`, order-independent typing |
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 / SOLID internals. The dependency rule is enforced: `domain/` has zero imports from other layers.
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 import
595
+ index.ts # public barrel — only file consumers see
490
596
  domain/ # pure contracts — no framework deps
491
- types.ts # interfaces (IResolver, ICycleDetector, IDependencyTracker, IValidator)
492
- errors.ts # 7 error classes + 2 warning types, each with hint + details
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/ # low-level mechanisms — depends on domain/ only
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 builder
499
- transient.ts # transient() marker (Symbol-based)
500
- application/ # use cases + orchestration — depends on domain/ + infrastructure/
501
- container-builder.ts # fluent builder + container() factory
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 + cleanup
505
- introspection.ts # inspect, describe, health, toString
611
+ disposer.ts # reverse-order onDestroy + resilient errors
612
+ introspection.ts # inspect / describe / health / toString
506
613
  ```
507
614
 
508
- Each file has a single responsibility. The Resolver receives its collaborators (`CycleDetector`, `DependencyTracker`) via constructor injection — no internal `new`, no hidden coupling. `Preloader`, `Disposer`, and `Introspection` depend on the `IResolver` interface, not the concrete class.
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 with [llms.txt](https://llmstxt.org/) files for AI-assisted development:
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
- ## API Reference
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "inwire",
3
- "version": "2.3.1",
3
+ "version": "2.3.2",
4
4
  "description": "Zero-ceremony dependency injection for TypeScript. Full inference, no decorators, no tokens. Built-in introspection for AI tooling.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.mjs",