inwire 1.0.2 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # inwire
2
2
 
3
- Zero-ceremony dependency injection for TypeScript. Full inference, no decorators, no tokens. Built-in introspection for AI tooling and debugging.
3
+ Type-safe dependency injection for TypeScript. Builder pattern, full inference, no decorators, no tokens. Built-in introspection for AI tooling and debugging.
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)
@@ -13,27 +13,63 @@ Zero-ceremony dependency injection for TypeScript. Full inference, no decorators
13
13
  ## Install
14
14
 
15
15
  ```bash
16
- npm/pnpm i inwire
16
+ npm i inwire
17
17
  ```
18
18
 
19
19
  ## Quick Start
20
20
 
21
21
  ```typescript
22
- import { createContainer } from 'inwire';
22
+ import { container } from 'inwire';
23
23
 
24
- const container = createContainer({
25
- logger: () => new LoggerService(),
26
- db: () => new Database(process.env.DB_URL!),
27
- userRepo: (c): UserRepository => new PgUserRepo(c.db),
28
- userService: (c) => new UserService(c.userRepo, c.logger),
29
- });
24
+ 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))
28
+ .build();
29
+
30
+ app.userService; // lazy, singleton, fully typed
31
+ // c.logger in the db factory is typed as LoggerService
32
+ ```
33
+
34
+ Each `.add()` accumulates the type — `c` in every factory knows about all previously registered dependencies.
35
+
36
+ ## Contract Mode (Interface-First)
37
+
38
+ Pass an interface to the builder to constrain keys and return types at compile time:
39
+
40
+ ```typescript
41
+ interface AppDeps {
42
+ ILogger: Logger;
43
+ IDatabase: Database;
44
+ IUserService: UserService;
45
+ }
30
46
 
31
- container.userService; // lazy, singleton, fully typed
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();
52
+
53
+ app.ILogger; // typed as Logger (not ConsoleLogger)
32
54
  ```
33
55
 
34
- That's it. Every dependency is a factory function `(container) => instance`. Access a property, get a singleton. TypeScript infers everything.
56
+ The string key acts as a token (like NestJS), but type-safe at compile time.
57
+
58
+ ## Instance Values (Eager)
35
59
 
36
- ## ⚠️ Important: Async Lifecycle
60
+ Non-function values are registered eagerly:
61
+
62
+ ```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
+ ```
68
+
69
+ Convention: `typeof value === 'function'` → factory (lazy). Otherwise → instance (eager, wrapped in `() => value`).
70
+ To register a function as a value: `.add('fn', () => myFunction)`.
71
+
72
+ ## Async Lifecycle
37
73
 
38
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.
39
75
 
@@ -44,22 +80,20 @@ class Database implements OnInit {
44
80
  async onInit() { await this.connect(); }
45
81
  }
46
82
 
47
- const container = createContainer({
48
- db: () => new Database(),
49
- });
83
+ const app = container()
84
+ .add('db', () => new Database())
85
+ .build();
50
86
 
51
- // BAD — onInit() fires but is NOT awaited, errors are lost
52
- container.db;
87
+ // BAD — onInit() fires but is NOT awaited, errors are lost
88
+ app.db;
53
89
 
54
- // GOOD — onInit() is awaited, errors surface immediately
55
- await container.preload('db');
56
- container.db; // safe to use, fully initialized
90
+ // GOOD — onInit() is awaited, errors surface immediately
91
+ await app.preload('db');
92
+ app.db; // safe to use, fully initialized
57
93
  ```
58
94
 
59
95
  ## Why use a DI container?
60
96
 
61
- Most JS/TS projects wire dependencies through direct imports. A DI container gives you three things imports don't:
62
-
63
97
  - **Testability** — swap any dependency for a mock at creation time, no monkey-patching or `jest.mock`
64
98
  - **Decoupling** — program against interfaces, not concrete imports; swap implementations without touching consumers
65
99
  - **Visibility** — inspect the full dependency graph at runtime, catch scope mismatches, and monitor container health
@@ -68,116 +102,74 @@ Most JS/TS projects wire dependencies through direct imports. A DI container giv
68
102
 
69
103
  ### Lazy Singletons (default)
70
104
 
71
- Factories run on first access and the result is cached forever. No eager init, no manual wiring.
72
-
73
105
  ```typescript
74
- const container = createContainer({
75
- db: () => new Database(process.env.DB_URL!),
76
- });
106
+ const app = container()
107
+ .add('db', () => new Database(process.env.DB_URL!))
108
+ .build();
77
109
 
78
- container.db; // creates Database
79
- container.db; // same instance (cached)
110
+ app.db; // creates Database
111
+ app.db; // same instance (cached)
80
112
  ```
81
113
 
82
- ### Dependency Inversion
114
+ ### Transient
83
115
 
84
- Annotate the return type to program against an interface:
116
+ Fresh instance on every access via `addTransient()`:
85
117
 
86
118
  ```typescript
87
- const container = createContainer({
88
- userRepo: (c): UserRepository => new PgUserRepo(c.db),
89
- // ^^^^^^^^^^^^^^^^^
90
- // contract, not implementation
91
- });
92
-
93
- container.userRepo; // typed as UserRepository
94
- ```
119
+ import { container } from 'inwire';
95
120
 
96
- ### Modules = Spread Objects
121
+ const app = container()
122
+ .add('logger', () => new LoggerService())
123
+ .addTransient('requestId', () => crypto.randomUUID())
124
+ .build();
97
125
 
98
- Group related factories into plain objects and spread them:
99
-
100
- ```typescript
101
- const dbModule = {
102
- db: () => new Database(process.env.DB_URL!),
103
- redis: () => new Redis(process.env.REDIS_URL!),
104
- };
105
-
106
- const serviceModule = {
107
- userService: (c) => new UserService(c.db),
108
- };
109
-
110
- const container = createContainer({
111
- ...dbModule,
112
- ...serviceModule,
113
- });
126
+ app.logger === app.logger; // true — singleton
127
+ app.requestId === app.requestId; // false — new every time
114
128
  ```
115
129
 
116
- ### Test Overrides
117
-
118
- Replace any dependency with a mock at container creation:
130
+ `transient()` wrapper is still available for `scope()`/`extend()`:
119
131
 
120
132
  ```typescript
121
- const container = createContainer({
122
- ...productionDeps,
123
- db: () => new InMemoryDatabase(), // override
133
+ import { transient } from 'inwire';
134
+
135
+ const extended = app.extend({
136
+ timestamp: transient(() => Date.now()),
124
137
  });
125
138
  ```
126
139
 
127
140
  ### Scopes
128
141
 
129
- Create child containers for request-level isolation. The child inherits all parent singletons and adds its own:
142
+ Create child containers for request-level isolation:
130
143
 
131
144
  ```typescript
132
- const container = createContainer({
133
- logger: () => new LoggerService(),
134
- db: () => new Database(),
135
- });
145
+ const app = container()
146
+ .add('logger', () => new LoggerService())
147
+ .add('db', () => new Database())
148
+ .build();
136
149
 
137
- // Per-request child container
138
- const request = container.scope({
150
+ const request = app.scope({
139
151
  requestId: () => crypto.randomUUID(),
140
- currentUser: () => getCurrentUser(),
152
+ handler: (c) => new Handler(c.logger), // c typed as typeof app
141
153
  });
142
154
 
143
- request.requestId; // scoped singleton (unique to this child)
144
- request.logger; // inherited from parent
155
+ request.requestId; // scoped singleton
156
+ request.logger; // inherited from parent
145
157
  ```
146
158
 
147
159
  #### Named Scopes
148
160
 
149
- Pass an options object to name a scope for debugging and introspection:
150
-
151
161
  ```typescript
152
- const request = container.scope(
162
+ const request = app.scope(
153
163
  { requestId: () => crypto.randomUUID() },
154
164
  { name: 'request-123' },
155
165
  );
156
166
 
157
- String(request); // "Scope(request-123) { requestId (pending) }"
167
+ String(request); // "Scope(request-123) { requestId (pending) }"
158
168
  request.inspect().name; // "request-123"
159
169
  ```
160
170
 
161
- ### Transient
162
-
163
- By default every dependency is a **singleton** (created once, cached forever). When you need a **fresh instance on every access**, wrap the factory with `transient()`:
164
-
165
- ```typescript
166
- import { createContainer, transient } from 'inwire';
167
-
168
- const container = createContainer({
169
- logger: () => new LoggerService(), // singleton (default)
170
- requestId: transient(() => crypto.randomUUID()), // new value every time
171
- });
172
-
173
- container.logger === container.logger; // true — same instance
174
- container.requestId === container.requestId; // false — different every time
175
- ```
176
-
177
171
  ### Lifecycle (onInit / onDestroy / dispose)
178
172
 
179
- Implement `onInit()` for post-creation setup and `onDestroy()` for cleanup:
180
-
181
173
  ```typescript
182
174
  import type { OnInit, OnDestroy } from 'inwire';
183
175
 
@@ -186,104 +178,108 @@ class Database implements OnInit, OnDestroy {
186
178
  async onDestroy() { await this.disconnect(); }
187
179
  }
188
180
 
189
- const container = createContainer({
190
- db: () => new Database(),
191
- });
181
+ const app = container()
182
+ .add('db', () => new Database())
183
+ .build();
192
184
 
193
- container.db; // resolves + calls onInit()
194
- await container.dispose(); // calls onDestroy() on all resolved instances (LIFO order)
185
+ app.db; // resolves + calls onInit()
186
+ await app.dispose(); // calls onDestroy() on all resolved instances (LIFO order)
195
187
  ```
196
188
 
197
- **Async `onInit()` is fire-and-forget during property access.** Because container property access is synchronous, any async `onInit()` runs without being awaited — errors won't surface and the service may not be ready. Use `preload()` to await async initialization. See [⚠️ Important: Async Lifecycle](#️-important-async-lifecycle) above.
198
-
199
189
  ### Extend
200
190
 
201
- Add dependencies to an existing container without mutating it. Existing singletons are shared:
191
+ Add dependencies to an existing container without mutating it:
202
192
 
203
193
  ```typescript
204
- const base = createContainer({
205
- logger: () => new LoggerService(),
206
- });
194
+ const base = container()
195
+ .add('logger', () => new LoggerService())
196
+ .build();
207
197
 
208
198
  const extended = base.extend({
209
- db: (c) => new Database(c.logger),
199
+ db: (c) => new Database(c.logger), // c typed as typeof base
210
200
  });
211
201
 
212
202
  extended.logger; // shared singleton from base
213
203
  extended.db; // new dependency
214
204
  ```
215
205
 
216
- > **scope vs extend:** `scope()` creates a parent-child chain — the child delegates unresolved keys to the parent. `extend()` creates a flat container with a merged factory map and shared cache. Use `scope()` for per-request isolation, `extend()` for additive composition.
206
+ > **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.
217
207
 
218
- ### Preload
208
+ ### Modules
219
209
 
220
- Eagerly resolve specific dependencies at startup (warm-up):
210
+ A module is a function `(builder) => builder` that chains `.add()` calls. `c` is fully typed in every factory.
221
211
 
222
- ```typescript
223
- const container = createContainer({
224
- db: () => new Database(),
225
- cache: () => new Redis(),
226
- logger: () => new LoggerService(),
227
- });
212
+ #### Pre-build: `addModule()` on the builder
228
213
 
229
- await container.preload('db', 'cache');
230
- // db and cache are now resolved, logger is still lazy
214
+ ```typescript
215
+ import { container, ContainerBuilder } from 'inwire';
216
+
217
+ function dbModule<T extends { config: { dbUrl: string }; logger: Logger }>(
218
+ b: ContainerBuilder<Record<string, unknown>, T>,
219
+ ) {
220
+ return b
221
+ .add('db', (c) => new Database(c.config.dbUrl))
222
+ .add('cache', (c) => new Redis(c.config.dbUrl));
223
+ }
231
224
 
232
- await container.preload();
233
- // resolve ALL dependencies at once
225
+ const app = container()
226
+ .add('config', { dbUrl: 'postgres://...', port: 3000 })
227
+ .add('logger', () => new Logger())
228
+ .addModule(dbModule)
229
+ .build();
234
230
  ```
235
231
 
236
- **This is how you safely initialize async services.** See [⚠️ Important: Async Lifecycle](#️-important-async-lifecycle) above.
237
-
238
- ### Reset
232
+ #### Post-build: `module()` on the container
239
233
 
240
- Invalidate cached singletons to force re-creation on next access:
234
+ Compose modules after `.build()` same DX, applied to an existing container:
241
235
 
242
236
  ```typescript
243
- const container = createContainer({
244
- db: () => new Database(),
245
- cache: () => new Redis(),
246
- });
247
-
248
- container.db; // creates Database
249
- container.reset('db');
250
- container.db; // creates a NEW Database instance
237
+ const core = container()
238
+ .add('config', { dbUrl: 'postgres://...' })
239
+ .add('logger', () => new Logger())
240
+ .build();
241
+
242
+ const withDb = core.module((b) => b
243
+ .add('db', (c) => new Database(c.config.dbUrl))
244
+ .add('cache', (c) => new Redis(c.config.dbUrl))
245
+ );
251
246
 
252
- // Other singletons are untouched
253
- // Reset in a scope does not affect the parent
247
+ // Chainable
248
+ const full = withDb.module((b) => b
249
+ .add('userService', (c) => new UserService(c.db, c.logger))
250
+ );
254
251
  ```
255
252
 
256
- ### Introspection
253
+ `module()` uses the builder internally for typed `c`, then delegates to `extend()`. Works on `scope()` and `extend()` results too.
257
254
 
258
- Built-in tools for debugging and AI analysis:
255
+ ### Preload
259
256
 
260
257
  ```typescript
261
- // Full dependency graph
262
- container.inspect();
263
- // { providers: { db: { key: 'db', resolved: true, deps: [], scope: 'singleton' }, ... } }
264
-
265
- // Single provider details
266
- container.describe('userService');
267
- // { key: 'userService', resolved: true, deps: ['userRepo', 'logger'], scope: 'singleton' }
258
+ await app.preload('db', 'cache'); // resolve specific deps
259
+ await app.preload(); // resolve ALL
260
+ ```
268
261
 
269
- // Health check
270
- container.health();
271
- // { totalProviders: 4, resolved: ['db', 'logger'], unresolved: ['cache'], warnings: [] }
262
+ ### Reset
272
263
 
273
- // Human-readable string
274
- String(container);
275
- // "Container { db -> [] (resolved), logger (pending) }"
264
+ ```typescript
265
+ app.db; // creates Database
266
+ app.reset('db');
267
+ app.db; // creates a NEW Database instance
276
268
  ```
277
269
 
278
- Feed the graph to an LLM or diagnostic tool:
270
+ ### Introspection
279
271
 
280
272
  ```typescript
281
- const graph = JSON.stringify(container.inspect(), null, 2);
273
+ app.inspect(); // full dependency graph (JSON)
274
+ app.describe('db'); // single provider info
275
+ app.health(); // health status + warnings
276
+ String(app); // human-readable
277
+ ```
282
278
 
283
- const response = await anthropic.messages.create({
284
- model: 'claude-sonnet-4-5-20250929',
285
- messages: [{ role: 'user', content: `Analyze this dependency graph for issues:\n${graph}` }],
286
- });
279
+ Feed the graph to an LLM:
280
+
281
+ ```typescript
282
+ const graph = JSON.stringify(app.inspect(), null, 2);
287
283
  ```
288
284
 
289
285
  ### Smart Errors
@@ -291,66 +287,91 @@ const response = await anthropic.messages.create({
291
287
  7 error types, each with `hint`, `details`, and actionable suggestions:
292
288
 
293
289
  ```typescript
294
- // Non-function value
295
- createContainer({ apiKey: 'sk-123' });
296
- // ContainerConfigError: 'apiKey' must be a factory function, got string.
297
- // hint: "Wrap it: apiKey: () => 'sk-123'"
298
-
299
290
  // Reserved key
300
- createContainer({ inspect: () => 'foo' });
291
+ container().add('inspect', () => 'foo');
301
292
  // ReservedKeyError: 'inspect' is a reserved container method.
302
- // hint: "Rename this dependency, e.g. 'inspectService' or 'myInspect'."
303
293
 
304
294
  // Missing dependency with fuzzy suggestion
305
- container.userServce; // typo
306
- // ProviderNotFoundError: Cannot resolve 'userServce': dependency 'userServce' not found.
307
- // hint: "Did you mean 'userService'?"
295
+ app.userServce; // typo
296
+ // ProviderNotFoundError: Did you mean 'userService'?
308
297
 
309
298
  // Circular dependency
310
- // CircularDependencyError: Circular dependency detected while resolving 'authService'.
311
- // Cycle: authService -> userService -> authService
312
-
313
- // Undefined return
314
- // UndefinedReturnError: Factory 'db' returned undefined.
315
- // hint: "Did you forget a return statement?"
316
-
317
- // Factory runtime error
318
- // FactoryError: Factory 'db' threw an error: "Connection refused"
299
+ // CircularDependencyError: Cycle: authService -> userService -> authService
319
300
  ```
320
301
 
321
302
  ### Scope Mismatch Detection
322
303
 
323
- Warns when a singleton depends on a transient (the transient value gets frozen inside the singleton):
324
-
325
304
  ```typescript
326
- const container = createContainer({
327
- requestId: transient(() => crypto.randomUUID()),
328
- userService: (c) => new UserService(c.requestId), // singleton depends on transient!
329
- });
330
-
331
- container.health().warnings;
305
+ app.health().warnings;
332
306
  // [{ type: 'scope_mismatch', message: "Singleton 'userService' depends on transient 'requestId'." }]
333
307
  ```
334
308
 
335
- ### Fuzzy Key Suggestion
336
-
337
- When a key is not found, Levenshtein-based matching suggests the closest registered key (>= 50% similarity):
309
+ ### Duplicate Key Detection
338
310
 
339
311
  ```typescript
340
- container.userServce;
341
- // ProviderNotFoundError: Did you mean 'userService'?
312
+ import { detectDuplicateKeys } from 'inwire';
313
+
314
+ detectDuplicateKeys(authModule, userModule);
315
+ // ['logger']
342
316
  ```
343
317
 
344
- ### Duplicate Key Detection
318
+ ## Examples
345
319
 
346
- Detect accidental key collisions when spreading modules:
320
+ | Example | Run | Showcases |
321
+ |---|---|---|
322
+ | [01-web-service.ts](examples/01-web-service.ts) | `npm run example:web` | Contract mode, lifecycle, dependency inversion, scope, introspection |
323
+ | [02-modular-testing.ts](examples/02-modular-testing.ts) | `npm run example:test` | Free mode, instance values, test overrides, extend + transient |
324
+ | [03-plugin-system.ts](examples/03-plugin-system.ts) | `npm run example:plugin` | Extend chain, scoped jobs, health, JSON graph for LLM |
325
+ | [04-modules.ts](examples/04-modules.ts) | `npm run example:modules` | addModule, module() post-build, typed reusable modules |
347
326
 
348
- ```typescript
349
- import { detectDuplicateKeys } from 'inwire';
327
+ ## Performance
350
328
 
351
- const duplicates = detectDuplicateKeys(authModule, userModule, dbModule);
352
- // ['logger'] — appears in more than one module
353
- ```
329
+ Performance is not the differentiator — **every lightweight DI container lands in the same ~20 ns range for cached singletons**. The differentiator is what you get on top of that.
330
+
331
+ ### Head-to-head (same scenario: 5-dep chain)
332
+
333
+ | Library | warm singleton | http handler (3 deps) | build |
334
+ |---|---|---|---|
335
+ | **inwire** | **~22 ns** | **~71 ns** | ~1,840 ns |
336
+ | ioctopus | ~20 ns | ~77 ns | ~530 ns |
337
+ | awilix | ~25 ns | ~74 ns | ~5,820 ns |
338
+ | inversify | ~580 ns | ~1,020 ns | ~10,280 ns |
339
+
340
+ On the hot path, inwire, ioctopus, and awilix are within measurement noise. Inversify is the outlier at 25x slower — it pays for its planning tree on every resolve.
341
+
342
+ Build is a cold path (runs once at startup). ioctopus is fastest because it stores raw entries with no validation. inwire sits in the middle — the Proxy setup, validation, and tracking cost ~1.5 μs extra, but that's 1.5 μs you pay once.
343
+
344
+ **What ~22 ns means in practice:** DI overhead for a full HTTP handler (3 deps) is ~71 ns — faster than `JSON.parse` on a 40-byte string (~130 ns). It's not a factor.
345
+
346
+ Run yourself: `npm run bench` / `npm run bench:compare` (Node.js v24, V8 13.6).
347
+
348
+ ### What you get for the same performance
349
+
350
+ | | inwire | ioctopus | awilix | inversify |
351
+ |---|---|---|---|---|
352
+ | `c.db` — native autocomplete | Yes | No | No | No |
353
+ | Full type inference (zero annotations) | Yes | No | No | No |
354
+ | Auto dependency tracking | Yes | No | No | No |
355
+ | Introspection (JSON graph) | Yes | No | No | No |
356
+ | Fuzzy errors ("did you mean?") | Yes | No | No | No |
357
+ | Scope mismatch warnings | Yes | No | No | No |
358
+ | Lifecycle (onInit/onDestroy) | Yes | No | dispose only | No |
359
+ | Decorators required | No | No | No | Yes |
360
+ | Runtime deps | 0 | 0 | 1 | 7 |
361
+ | Bundle (unpacked) | 8.8 KB | 43 KB | 320 KB | 1,466 KB |
362
+
363
+ ### Runtime compatibility
364
+
365
+ Pure ES2022. No decorators, no `reflect-metadata`, no compiler plugins.
366
+
367
+ | Runtime | Status |
368
+ |---|---|
369
+ | Node.js | Works |
370
+ | Deno | Works |
371
+ | Bun | Works |
372
+ | Cloudflare Workers | Works |
373
+ | Vercel Edge | Works |
374
+ | Browser | Works |
354
375
 
355
376
  ## Comparison
356
377
 
@@ -358,14 +379,16 @@ const duplicates = detectDuplicateKeys(authModule, userModule, dbModule);
358
379
  |---|---|---|---|---|---|
359
380
  | Decorators required | No | No | Yes | Yes | Yes |
360
381
  | Tokens/symbols | No | No | Yes | Yes | Yes |
361
- | Full TS inference | Yes | No (manual Cradle) | Partial | Partial | Partial |
382
+ | Full TS inference | Yes (builder) | No (manual Cradle) | Partial | Partial | Partial |
383
+ | Typed `c` in factories | Yes | No | N/A | N/A | N/A |
362
384
  | Lazy singletons | Default | Default | Manual | Manual | Manual |
363
385
  | Scoped containers | `.scope()` | `.createScope()` | `.createChildContainer()` | `.createChild()` | Module scope |
364
386
  | Lifecycle hooks | `onInit`/`onDestroy` | `dispose()` | `beforeResolution`/`afterResolution` | No | `onModuleInit`/`onModuleDestroy` |
365
387
  | Introspection | Built-in JSON graph | `.registrations` | `isRegistered()` | No | DevTools |
366
388
  | Smart errors | 7 types + hints | Resolution chain | Generic | Generic | Generic |
367
- | Bundle size (gzip) | ~4.7 KB | ~3.6 KB | ~5.6 KB (+reflect-metadata) | ~50 KB | Framework |
368
- | Runtime deps | 0 | 1 | 1 (+reflect-metadata) | 2 | Many |
389
+ | Warm singleton | ~22 ns | N/A | N/A | ~580 ns | N/A |
390
+ | Bundle (unpacked) | 8.8 KB | 320 KB | 149 KB (+reflect-metadata) | 1,466 KB | Framework |
391
+ | Runtime deps | 0 | 1 (fast-glob) | 1 (reflect-metadata) | 7 (+reflect-metadata) | Many |
369
392
 
370
393
  ## Architecture
371
394
 
@@ -378,11 +401,11 @@ src/
378
401
  lifecycle.ts # OnInit / OnDestroy interfaces
379
402
  validation.ts # Validator, detectDuplicateKeys, Levenshtein
380
403
  infrastructure/
381
- proxy-handler.ts # Resolver (Proxy handler, cache, cycle detection)
404
+ resolver.ts # Resolver (Proxy handler, cache, cycle detection)
382
405
  transient.ts # transient() marker
383
406
  application/
384
- create-container.ts # createContainer, buildContainerProxy
385
- scope.ts # createScope (child containers)
407
+ container-builder.ts # ContainerBuilder class + container() function
408
+ container-proxy.ts # buildContainerProxy + scope/extend inline
386
409
  introspection.ts # inspect, describe, health, toString
387
410
  ```
388
411
 
@@ -393,58 +416,54 @@ This package ships with [llms.txt](https://llmstxt.org/) files for AI-assisted d
393
416
  - **`llms.txt`** — Concise index following the llms.txt standard
394
417
  - **`llms-full.txt`** — Complete API reference optimized for LLM context windows
395
418
 
396
- Use them to feed inwire documentation to any LLM or AI coding tool:
397
-
398
- ```bash
399
- cat node_modules/inwire/llms-full.txt
400
- ```
401
-
402
419
  Compatible with [Context7](https://context7.com/) and any tool that supports the llms.txt standard.
403
420
 
404
- At runtime, `.inspect()` returns the full dependency graph as serializable JSON — pipe it directly into an LLM for architecture analysis:
405
-
406
- ```typescript
407
- const graph = JSON.stringify(container.inspect(), null, 2);
408
- ```
409
-
410
421
  ## API Reference
411
422
 
412
423
  ### Functions
413
424
 
414
425
  | Export | Description |
415
426
  |---|---|
416
- | `createContainer(defs)` | Creates a DI container from factory functions |
417
- | `transient(factory)` | Marks a factory as transient (new instance per access) |
418
- | `detectDuplicateKeys(...modules)` | Detects duplicate keys across spread modules |
427
+ | `container<T?>()` | Creates a new `ContainerBuilder`. Pass interface `T` for contract mode. |
428
+ | `transient(factory)` | Marks a factory as transient (for scope/extend) |
429
+ | `detectDuplicateKeys(...modules)` | Pre-spread validation detects duplicate keys |
430
+
431
+ ### ContainerBuilder Methods
432
+
433
+ | Method | Description |
434
+ |---|---|
435
+ | `.add(key, factory)` | Register a dependency (factory or instance) |
436
+ | `.addTransient(key, factory)` | Register a transient dependency |
437
+ | `.addModule(module)` | Apply a module `(builder) => builder` |
438
+ | `.build()` | Build and return the container |
419
439
 
420
440
  ### Container Methods
421
441
 
422
442
  | Method | Description |
423
443
  |---|---|
424
- | `container.scope(extra, options?)` | Creates a child container with additional deps. Pass `{ name }` for debugging |
425
- | `container.extend(extra)` | Returns a new container with additional deps (shared cache) |
426
- | `container.preload(...keys)` | Eagerly resolves specific dependencies, or all if no keys given |
427
- | `container.reset(...keys)` | Invalidates cached singletons, forcing re-creation on next access |
428
- | `container.inspect()` | Returns the full dependency graph |
429
- | `container.describe(key)` | Returns info about a single provider |
430
- | `container.health()` | Returns health status and warnings |
431
- | `container.dispose()` | Calls `onDestroy()` on all resolved instances |
444
+ | `.scope(extra, options?)` | Creates a child container with additional deps |
445
+ | `.extend(extra)` | Returns a new container with additional deps (shared cache) |
446
+ | `.module(fn)` | Applies a module post-build using the builder for typed `c` |
447
+ | `.preload(...keys)` | Eagerly resolves dependencies |
448
+ | `.reset(...keys)` | Invalidates cached singletons |
449
+ | `.inspect()` | Returns the full dependency graph |
450
+ | `.describe(key)` | Returns info about a single provider |
451
+ | `.health()` | Returns health status and warnings |
452
+ | `.dispose()` | Calls `onDestroy()` on all resolved instances |
432
453
 
433
454
  ### Types
434
455
 
435
456
  | Export | Description |
436
457
  |---|---|
437
458
  | `Container<T>` | Full container type (resolved deps + methods) |
438
- | `DepsDefinition` | `Record<string, Factory>` |
439
- | `Factory<T>` | `(container) => T` |
440
- | `ResolvedDeps<T>` | Extracts return types from a `DepsDefinition` |
459
+ | `ContainerBuilder<TContract, TBuilt>` | Fluent builder class (also used in `module()` callbacks) |
460
+ | `IContainer<T>` | Container methods interface |
441
461
  | `OnInit` | Interface with `onInit(): void \| Promise<void>` |
442
462
  | `OnDestroy` | Interface with `onDestroy(): void \| Promise<void>` |
443
463
  | `ContainerGraph` | Return type of `inspect()` |
444
464
  | `ContainerHealth` | Return type of `health()` |
445
- | `ContainerWarning` | Warning object (`scope_mismatch` \| `duplicate_key`) |
465
+ | `ContainerWarning` | Warning object (`scope_mismatch`) |
446
466
  | `ProviderInfo` | Return type of `describe()` |
447
- | `IContainer<T>` | Container methods interface |
448
467
  | `ScopeOptions` | Options for `scope()` (`{ name?: string }`) |
449
468
 
450
469
  ### Errors
package/dist/index.d.mts CHANGED
@@ -7,32 +7,11 @@
7
7
  * const factory: Factory<MyService> = (c) => new MyService(c.db);
8
8
  * ```
9
9
  */
10
- type Factory<T = any> = (container: any) => T;
10
+ type Factory<T = unknown> = (container: unknown) => T;
11
11
  /**
12
- * An object of factory functions the definition of a container.
13
- * Each key maps to a factory that produces the dependency.
14
- *
15
- * @example
16
- * ```typescript
17
- * const deps: DepsDefinition = {
18
- * logger: () => new LoggerService(),
19
- * db: () => new Database(process.env.DB_URL!),
20
- * userRepo: (c) => new PgUserRepo(c.db),
21
- * };
22
- * ```
23
- */
24
- type DepsDefinition = Record<string, Factory>;
25
- /**
26
- * Extracts the resolved types from a deps definition.
27
- * Maps each key to the return type of its factory function.
28
- *
29
- * @example
30
- * ```typescript
31
- * type Resolved = ResolvedDeps<{ logger: () => LoggerService }>;
32
- * // { logger: LoggerService }
33
- * ```
12
+ * Reserved method names on the container that cannot be used as dependency keys.
34
13
  */
35
- type ResolvedDeps<T extends DepsDefinition> = { readonly [K in keyof T]: ReturnType<T[K]> };
14
+ declare const RESERVED_KEYS: readonly ["scope", "extend", "module", "preload", "reset", "inspect", "describe", "health", "dispose", "toString"];
36
15
  /**
37
16
  * Options for creating a scoped container.
38
17
  */
@@ -46,18 +25,18 @@ interface ScopeOptions {
46
25
  *
47
26
  * @example
48
27
  * ```typescript
49
- * const container: Container<{ logger: LoggerService }> = createContainer({
50
- * logger: () => new LoggerService(),
51
- * });
52
- * container.logger; // LoggerService
53
- * container.inspect(); // ContainerGraph
28
+ * const c = container()
29
+ * .add('logger', () => new LoggerService())
30
+ * .build();
31
+ * c.logger; // LoggerService
32
+ * c.inspect(); // ContainerGraph
54
33
  * ```
55
34
  */
56
- type Container<T extends Record<string, any> = Record<string, any>> = T & IContainer<T>;
35
+ type Container<T extends Record<string, unknown> = Record<string, unknown>> = T & IContainer<T>;
57
36
  /**
58
37
  * Container methods interface. Defines the API available on every container.
59
38
  */
60
- interface IContainer<T extends Record<string, any> = Record<string, any>> {
39
+ interface IContainer<T extends Record<string, unknown> = Record<string, unknown>> {
61
40
  /**
62
41
  * Creates a child container with additional dependencies.
63
42
  * Child inherits all parent singletons and can add/override deps.
@@ -66,23 +45,37 @@ interface IContainer<T extends Record<string, any> = Record<string, any>> {
66
45
  * ```typescript
67
46
  * const request = app.scope({
68
47
  * requestId: () => crypto.randomUUID(),
69
- * currentUser: () => extractUser(req),
48
+ * handler: (c) => new Handler(c.logger), // c typed as parent
70
49
  * });
71
- * request.requestId; // scoped singleton
72
- * request.logger; // inherited from parent
73
50
  * ```
74
51
  */
75
- scope<E extends DepsDefinition>(extra: E, options?: ScopeOptions): Container<T & ResolvedDeps<E>>;
52
+ scope<E extends Record<string, (c: T) => unknown>>(extra: E, options?: ScopeOptions): Container<Omit<T, keyof { [K in keyof E]: ReturnType<E[K]> }> & { [K in keyof E]: ReturnType<E[K]> }>;
76
53
  /**
77
54
  * Returns a new container with additional dependencies.
78
55
  * Existing singletons are shared. The original container is not modified.
79
56
  *
80
57
  * @example
81
58
  * ```typescript
82
- * const appWithAuth = app.extend(authDeps);
59
+ * const full = app.extend({
60
+ * cache: (c) => new Redis(c.logger), // c typed as parent
61
+ * });
62
+ * ```
63
+ */
64
+ extend<E extends Record<string, (c: T) => unknown>>(extra: E): Container<Omit<T, keyof { [K in keyof E]: ReturnType<E[K]> }> & { [K in keyof E]: ReturnType<E[K]> }>;
65
+ /**
66
+ * Applies a module post-build using the builder pattern.
67
+ * Semantically equivalent to `extend()` but uses `ContainerBuilder` for
68
+ * incremental type accumulation of `c` in factories.
69
+ *
70
+ * @example
71
+ * ```typescript
72
+ * const withDb = app.module((b) => b
73
+ * .add('db', (c) => new Database(c.config))
74
+ * .add('cache', (c) => new Redis(c.config))
75
+ * );
83
76
  * ```
84
77
  */
85
- extend<E extends DepsDefinition>(extra: E): Container<T & ResolvedDeps<E>>;
78
+ module<TNew extends Record<string, unknown>>(fn: (builder: ContainerBuilder<Record<string, unknown>, T>) => ContainerBuilder<Record<string, unknown>, TNew>): Container<TNew>;
86
79
  /**
87
80
  * Pre-resolves dependencies (warm-up).
88
81
  * Call with specific keys to resolve only those, or without arguments to resolve all.
@@ -114,7 +107,7 @@ interface IContainer<T extends Record<string, any> = Record<string, any>> {
114
107
  * // { key: 'userService', resolved: true, deps: ['userRepo', 'logger'], scope: 'singleton' }
115
108
  * ```
116
109
  */
117
- describe(key: keyof T): ProviderInfo;
110
+ describe(key: keyof T | string): ProviderInfo;
118
111
  /**
119
112
  * Returns container health status and warnings.
120
113
  *
@@ -176,11 +169,125 @@ interface ContainerHealth {
176
169
  * A warning detected by the container's runtime analysis.
177
170
  */
178
171
  interface ContainerWarning {
179
- type: 'scope_mismatch' | 'duplicate_key';
172
+ type: 'scope_mismatch';
180
173
  message: string;
181
174
  details: Record<string, unknown>;
182
175
  }
183
176
  //#endregion
177
+ //#region src/application/container-builder.d.ts
178
+ /**
179
+ * Fluent builder that constructs a typed DI container incrementally.
180
+ *
181
+ * Two modes, one class:
182
+ * - `container<AppDeps>()` — contract mode: keys restricted to `keyof AppDeps`, return types constrained
183
+ * - `container()` — free mode: keys are any `string`, types inferred freely
184
+ *
185
+ * Each `.add()` call accumulates the type so that subsequent factories
186
+ * receive a fully-typed `c` parameter with all previously registered deps.
187
+ */
188
+ declare class ContainerBuilder<TContract extends Record<string, unknown> = Record<string, unknown>, TBuilt extends Record<string, unknown> = {}> {
189
+ private readonly factories;
190
+ private readonly transientKeys;
191
+ /**
192
+ * Registers a dependency — factory (lazy) or instance (eager).
193
+ *
194
+ * Convention: `typeof value === 'function'` → factory. Otherwise → instance (wrapped in `() => value`).
195
+ * To register a function as a value: `add('fn', () => myFunction)`.
196
+ */
197
+ add<K extends string & keyof TContract, V extends TContract[K]>(key: K & (K extends (typeof RESERVED_KEYS)[number] ? never : K), factoryOrInstance: ((c: TBuilt) => V) | (V & (V extends Function ? never : V))): ContainerBuilder<TContract, TBuilt & Record<K, V>>;
198
+ /**
199
+ * Registers a transient dependency (new instance on every access).
200
+ */
201
+ addTransient<K extends string & keyof TContract, V extends TContract[K]>(key: K & (K extends (typeof RESERVED_KEYS)[number] ? never : K), factory: (c: TBuilt) => V): ContainerBuilder<TContract, TBuilt & Record<K, V>>;
202
+ /**
203
+ * Applies a module — a function that chains `.add()` calls on this builder.
204
+ * `c` in the module's factories is fully typed with all previously registered deps.
205
+ */
206
+ addModule<TNew extends Record<string, unknown>>(module: (builder: ContainerBuilder<TContract, TBuilt>) => ContainerBuilder<TContract, TNew>): ContainerBuilder<TContract, TNew>;
207
+ /**
208
+ * Returns the accumulated factories as a plain record.
209
+ * @internal Used by `module()` on the container.
210
+ */
211
+ _toRecord(): Record<string, Factory>;
212
+ /**
213
+ * Builds and returns the final container.
214
+ */
215
+ build(): Container<TBuilt>;
216
+ private validateKey;
217
+ }
218
+ /**
219
+ * Creates a new container builder.
220
+ *
221
+ * @example Contract mode (interface-first):
222
+ * ```typescript
223
+ * interface AppDeps { logger: Logger; db: Database }
224
+ *
225
+ * const app = container<AppDeps>()
226
+ * .add('logger', () => new ConsoleLogger())
227
+ * .add('db', (c) => new PgDatabase(c.logger))
228
+ * .build()
229
+ * ```
230
+ *
231
+ * @example Free mode:
232
+ * ```typescript
233
+ * const app = container()
234
+ * .add('logger', () => new ConsoleLogger())
235
+ * .add('db', (c) => new PgDatabase(c.logger))
236
+ * .build()
237
+ * ```
238
+ */
239
+ declare function container<T extends Record<string, unknown> = Record<string, unknown>>(): ContainerBuilder<T>;
240
+ //#endregion
241
+ //#region src/infrastructure/transient.d.ts
242
+ /**
243
+ * Wraps a factory function to produce a new instance on every access,
244
+ * instead of the default singleton behavior.
245
+ *
246
+ * @example
247
+ * ```typescript
248
+ * import { createContainer, transient } from 'inwire';
249
+ *
250
+ * const container = createContainer({
251
+ * logger: () => new LoggerService(), // singleton (default)
252
+ * requestId: transient(() => crypto.randomUUID()), // new instance every access
253
+ * });
254
+ *
255
+ * container.requestId; // 'abc-123'
256
+ * container.requestId; // 'def-456' (different!)
257
+ * ```
258
+ */
259
+ declare function transient<T>(factory: Factory<T>): Factory<T>;
260
+ //#endregion
261
+ //#region src/domain/lifecycle.d.ts
262
+ /**
263
+ * Implement this interface (or just add an `onInit` method) to run
264
+ * initialization logic when the dependency is first resolved.
265
+ *
266
+ * @example
267
+ * ```typescript
268
+ * class Database implements OnInit {
269
+ * async onInit() { await this.connect(); }
270
+ * }
271
+ * ```
272
+ */
273
+ interface OnInit {
274
+ onInit(): void | Promise<void>;
275
+ }
276
+ /**
277
+ * Implement this interface (or just add an `onDestroy` method) to run
278
+ * cleanup logic when `container.dispose()` is called.
279
+ *
280
+ * @example
281
+ * ```typescript
282
+ * class Database implements OnDestroy {
283
+ * async onDestroy() { await this.disconnect(); }
284
+ * }
285
+ * ```
286
+ */
287
+ interface OnDestroy {
288
+ onDestroy(): void | Promise<void>;
289
+ }
290
+ //#endregion
184
291
  //#region src/domain/errors.d.ts
185
292
  /**
186
293
  * Base class for all container errors.
@@ -215,7 +322,10 @@ declare abstract class ContainerError extends Error {
215
322
  */
216
323
  declare class ContainerConfigError extends ContainerError {
217
324
  readonly hint: string;
218
- readonly details: Record<string, unknown>;
325
+ readonly details: {
326
+ key: string;
327
+ actualType: string;
328
+ };
219
329
  constructor(key: string, actualType: string);
220
330
  }
221
331
  /**
@@ -230,7 +340,10 @@ declare class ContainerConfigError extends ContainerError {
230
340
  */
231
341
  declare class ReservedKeyError extends ContainerError {
232
342
  readonly hint: string;
233
- readonly details: Record<string, unknown>;
343
+ readonly details: {
344
+ key: string;
345
+ reserved: string[];
346
+ };
234
347
  constructor(key: string, reserved: readonly string[]);
235
348
  }
236
349
  /**
@@ -246,7 +359,12 @@ declare class ReservedKeyError extends ContainerError {
246
359
  */
247
360
  declare class ProviderNotFoundError extends ContainerError {
248
361
  readonly hint: string;
249
- readonly details: Record<string, unknown>;
362
+ readonly details: {
363
+ key: string;
364
+ chain: string[];
365
+ registered: string[];
366
+ suggestion: string | undefined;
367
+ };
250
368
  constructor(key: string, chain: string[], registered: string[], suggestion?: string);
251
369
  }
252
370
  /**
@@ -260,7 +378,11 @@ declare class ProviderNotFoundError extends ContainerError {
260
378
  */
261
379
  declare class CircularDependencyError extends ContainerError {
262
380
  readonly hint: string;
263
- readonly details: Record<string, unknown>;
381
+ readonly details: {
382
+ key: string;
383
+ chain: string[];
384
+ cycle: string;
385
+ };
264
386
  constructor(key: string, chain: string[]);
265
387
  }
266
388
  /**
@@ -275,7 +397,10 @@ declare class CircularDependencyError extends ContainerError {
275
397
  */
276
398
  declare class UndefinedReturnError extends ContainerError {
277
399
  readonly hint: string;
278
- readonly details: Record<string, unknown>;
400
+ readonly details: {
401
+ key: string;
402
+ chain: string[];
403
+ };
279
404
  constructor(key: string, chain: string[]);
280
405
  }
281
406
  /**
@@ -290,7 +415,11 @@ declare class UndefinedReturnError extends ContainerError {
290
415
  */
291
416
  declare class FactoryError extends ContainerError {
292
417
  readonly hint: string;
293
- readonly details: Record<string, unknown>;
418
+ readonly details: {
419
+ key: string;
420
+ chain: string[];
421
+ originalError: string;
422
+ };
294
423
  readonly originalError: unknown;
295
424
  constructor(key: string, chain: string[], originalError: unknown);
296
425
  }
@@ -307,80 +436,13 @@ declare class ScopeMismatchWarning implements ContainerWarning {
307
436
  readonly type: "scope_mismatch";
308
437
  readonly message: string;
309
438
  readonly hint: string;
310
- readonly details: Record<string, unknown>;
439
+ readonly details: {
440
+ singleton: string;
441
+ transient: string;
442
+ };
311
443
  constructor(singletonKey: string, transientKey: string);
312
444
  }
313
445
  //#endregion
314
- //#region src/application/create-container.d.ts
315
- /**
316
- * Creates a dependency injection container from an object of factory functions.
317
- * Each factory receives the container and returns an instance.
318
- * Dependencies are resolved lazily and cached as singletons by default.
319
- *
320
- * @example
321
- * ```typescript
322
- * const container = createContainer({
323
- * logger: () => new LoggerService(),
324
- * db: () => new Database(process.env.DB_URL!),
325
- * userRepo: (c): UserRepository => new PgUserRepo(c.db),
326
- * userService: (c) => new UserService(c.userRepo, c.logger),
327
- * });
328
- *
329
- * container.userService; // type: UserService — lazy, singleton, fully resolved
330
- * ```
331
- */
332
- declare function createContainer<T extends DepsDefinition>(defs: T): Container<ResolvedDeps<T>>;
333
- //#endregion
334
- //#region src/infrastructure/transient.d.ts
335
- /**
336
- * Wraps a factory function to produce a new instance on every access,
337
- * instead of the default singleton behavior.
338
- *
339
- * @example
340
- * ```typescript
341
- * import { createContainer, transient } from 'inwire';
342
- *
343
- * const container = createContainer({
344
- * logger: () => new LoggerService(), // singleton (default)
345
- * requestId: transient(() => crypto.randomUUID()), // new instance every access
346
- * });
347
- *
348
- * container.requestId; // 'abc-123'
349
- * container.requestId; // 'def-456' (different!)
350
- * ```
351
- */
352
- declare function transient<T>(factory: Factory<T>): Factory<T>;
353
- //#endregion
354
- //#region src/domain/lifecycle.d.ts
355
- /**
356
- * Implement this interface (or just add an `onInit` method) to run
357
- * initialization logic when the dependency is first resolved.
358
- *
359
- * @example
360
- * ```typescript
361
- * class Database implements OnInit {
362
- * async onInit() { await this.connect(); }
363
- * }
364
- * ```
365
- */
366
- interface OnInit {
367
- onInit(): void | Promise<void>;
368
- }
369
- /**
370
- * Implement this interface (or just add an `onDestroy` method) to run
371
- * cleanup logic when `container.dispose()` is called.
372
- *
373
- * @example
374
- * ```typescript
375
- * class Database implements OnDestroy {
376
- * async onDestroy() { await this.disconnect(); }
377
- * }
378
- * ```
379
- */
380
- interface OnDestroy {
381
- onDestroy(): void | Promise<void>;
382
- }
383
- //#endregion
384
446
  //#region src/domain/validation.d.ts
385
447
  /**
386
448
  * Detects duplicate keys across multiple modules (spread objects).
@@ -388,5 +450,5 @@ interface OnDestroy {
388
450
  */
389
451
  declare function detectDuplicateKeys(...modules: Record<string, unknown>[]): string[];
390
452
  //#endregion
391
- export { CircularDependencyError, type Container, ContainerConfigError, ContainerError, type ContainerGraph, type ContainerHealth, type ContainerWarning, type DepsDefinition, type Factory, FactoryError, type IContainer, type OnDestroy, type OnInit, type ProviderInfo, ProviderNotFoundError, ReservedKeyError, type ResolvedDeps, ScopeMismatchWarning, type ScopeOptions, UndefinedReturnError, createContainer, detectDuplicateKeys, transient };
453
+ export { CircularDependencyError, type Container, ContainerBuilder, ContainerConfigError, ContainerError, type ContainerGraph, type ContainerHealth, type ContainerWarning, FactoryError, type IContainer, type OnDestroy, type OnInit, type ProviderInfo, ProviderNotFoundError, ReservedKeyError, ScopeMismatchWarning, type ScopeOptions, UndefinedReturnError, container, detectDuplicateKeys, transient };
392
454
  //# sourceMappingURL=index.d.mts.map
package/dist/index.mjs CHANGED
@@ -1,4 +1,4 @@
1
- function e(e){return typeof e==`object`&&!!e&&`onInit`in e&&typeof e.onInit==`function`}function t(e){return typeof e==`object`&&!!e&&`onDestroy`in e&&typeof e.onDestroy==`function`}const n=[`scope`,`extend`,`preload`,`reset`,`inspect`,`describe`,`health`,`dispose`,`toString`];var r=class extends Error{constructor(e){super(e),this.name=this.constructor.name}},i=class extends r{hint;details;constructor(e,t){super(`'${e}' must be a factory function, got ${t}.`),this.hint=`Wrap it: ${e}: () => ${JSON.stringify(e===e?`<your ${t} value>`:e)}`,this.details={key:e,actualType:t}}},a=class extends r{hint;details;constructor(e,t){super(`'${e}' is a reserved container method.`),this.hint=`Rename this dependency, e.g. '${e}Service' or 'my${e[0].toUpperCase()}${e.slice(1)}'.`,this.details={key:e,reserved:[...t]}}},o=class extends r{hint;details;constructor(e,t,n,r){let i=t.length>0?`\n\nResolution chain: ${[...t,`${e} (not found)`].join(` -> `)}`:``,a=`\nRegistered keys: [${n.join(`, `)}]`,o=r?`\n\nDid you mean '${r}'?`:``;super(`Cannot resolve '${t[0]??e}': dependency '${e}' not found.${i}${a}${o}`),this.hint=r?`Did you mean '${r}'? Or add '${e}' to your container:\n createContainer({\n ...existing,\n ${e}: (c) => new Your${e[0].toUpperCase()}${e.slice(1)}(/* deps */),\n });`:`Add '${e}' to your container:\n createContainer({\n ...existing,\n ${e}: (c) => new Your${e[0].toUpperCase()}${e.slice(1)}(/* deps */),\n });`,this.details={key:e,chain:t,registered:n,suggestion:r}}},s=class extends r{hint;details;constructor(e,t){let n=[...t,e].join(` -> `);super(`Circular dependency detected while resolving '${t[0]}'.\n\nCycle: ${n}`),this.hint=[`To fix:`,` 1. Extract shared logic into a new dependency both can use`,` 2. Restructure so one doesn't depend on the other`,` 3. Use a mediator/event pattern to decouple them`].join(`
2
- `),this.details={key:e,chain:t,cycle:n}}},c=class extends r{hint;details;constructor(e,t){let n=t.length>1?`\n\nResolution chain: ${t.join(` -> `)}`:``;super(`Factory '${e}' returned undefined.${n}`),this.hint=`Your factory function returned undefined. Did you forget a return statement?`,this.details={key:e,chain:t}}},l=class extends r{hint;details;originalError;constructor(e,t,n){let r=n instanceof Error?n.message:String(n),i=t.length>1?`\n\nResolution chain: ${[...t.slice(0,-1),`${e} (factory threw)`].join(` -> `)}`:``;super(`Factory '${e}' threw an error: "${r}"${i}`),this.hint=`Check the factory function for '${e}'. The error occurred during instantiation.`,this.details={key:e,chain:t,originalError:r},this.originalError=n}},u=class{type=`scope_mismatch`;message;hint;details;constructor(e,t){this.message=`Singleton '${e}' depends on transient '${t}'.`,this.hint=[`The transient value was resolved once and is now frozen inside the singleton.`,`This is almost always a bug.`,``,`To fix:`,` 1. Make '${e}' transient too: transient((c) => new ${e[0].toUpperCase()}${e.slice(1)}(c.${t}))`,` 2. Make '${t}' singleton if it doesn't need to change`,` 3. Inject a factory instead: ${t}Factory: () => () => <your value>`].join(`
3
- `),this.details={singleton:e,transient:t}}},d=class{validateConfig(e){for(let[t,r]of Object.entries(e)){if(n.includes(t))throw new a(t,n);if(typeof r!=`function`)throw new i(t,typeof r)}}suggestKey(e,t){let n,r=1/0;for(let i of t){let t=p(e,i);t<r&&(r=t,n=i)}if(!n)return;let i=Math.max(e.length,n.length);return 1-r/i>=.5?n:void 0}};function f(...e){let t=new Map,n=[];for(let r of e)for(let e of Object.keys(r)){let r=(t.get(e)??0)+1;t.set(e,r),r===2&&n.push(e)}return n}function p(e,t){let n=e.length,r=t.length;if(n===0)return r;if(r===0)return n;let i=Array(r+1),a=Array(r+1);for(let e=0;e<=r;e++)i[e]=e;for(let o=1;o<=n;o++){a[0]=o;for(let n=1;n<=r;n++){let r=e[o-1]===t[n-1]?0:1;a[n]=Math.min(i[n]+1,a[n-1]+1,i[n-1]+r)}[i,a]=[a,i]}return i[r]}const m=Symbol.for(`inwire:transient`);function h(e){let t=(t=>e(t));return t[m]=!0,t}function g(e){return typeof e==`function`&&m in e&&e[m]===!0}var _=class{factories;cache;resolving=new Set;depGraph=new Map;warnings=[];validator=new d;parent;name;constructor(e,t,n,r){this.factories=e,this.cache=t??new Map,this.parent=n,this.name=r}getName(){return this.name}resolve(t,n=[]){let r=this.factories.get(t);if(r&&!g(r)&&this.cache.has(t))return this.cache.get(t);if(!r){if(this.parent)return this.parent.resolve(t,n);let e=this.getAllRegisteredKeys();throw new o(t,n,e,this.validator.suggestKey(t,e))}if(this.resolving.has(t))throw new s(t,[...n]);this.resolving.add(t);let i=[...n,t];try{let n=[],a=r(this.createTrackingProxy(t,n,i));if(a===void 0)throw new c(t,i);if(this.depGraph.set(t,n),!g(r))for(let e of n){let n=this.getFactory(e);n&&g(n)&&this.warnings.push(new u(t,e))}if(g(r)||this.cache.set(t,a),e(a)){let e=a.onInit();e instanceof Promise&&e.catch(()=>{})}return a}catch(e){throw e instanceof s||e instanceof o||e instanceof c||e instanceof l?e:new l(t,i,e)}finally{this.resolving.delete(t)}}isResolved(e){return this.cache.has(e)}getDepGraph(){return new Map(this.depGraph)}getResolvedKeys(){return[...this.cache.keys()]}getFactories(){return this.factories}getCache(){return this.cache}getWarnings(){return[...this.warnings]}getAllRegisteredKeys(){let e=new Set(this.factories.keys());if(this.parent)for(let t of this.parent.getAllRegisteredKeys())e.add(t);return[...e]}createTrackingProxy(e,t,n){return new Proxy({},{get:(e,r)=>{if(typeof r==`symbol`)return;let i=r;return t.push(i),this.resolve(i,n)}})}getFactory(e){return this.factories.get(e)??this.parent?.getFactory(e)}},v=class{constructor(e){this.resolver=e}inspect(){let e={};for(let[t,n]of this.resolver.getFactories())e[t]={key:t,resolved:this.resolver.isResolved(t),deps:this.resolver.getDepGraph().get(t)??[],scope:g(n)?`transient`:`singleton`};let t=this.resolver.getName();return t?{name:t,providers:e}:{providers:e}}describe(e){let t=this.resolver.getFactories().get(e);return t?{key:e,resolved:this.resolver.isResolved(e),deps:this.resolver.getDepGraph().get(e)??[],scope:g(t)?`transient`:`singleton`}:{key:e,resolved:!1,deps:[],scope:`singleton`}}health(){let e=[...this.resolver.getFactories().keys()],t=this.resolver.getResolvedKeys(),n=new Set(t),r=this.resolver.getWarnings().map(e=>({type:e.type,message:e.message,details:e.details}));return{totalProviders:e.length,resolved:t,unresolved:e.filter(e=>!n.has(e)),warnings:r}}toString(){let e=[];for(let[t]of this.resolver.getFactories()){let n=this.resolver.isResolved(t),r=this.resolver.getDepGraph().get(t),i=r&&r.length>0?` -> [${r.join(`, `)}]`:``,a=n?`(resolved)`:`(pending)`;e.push(`${t}${i} ${a}`)}let t=this.resolver.getName();return`${t?`Scope(${t})`:`Container`} { ${e.join(`, `)} }`}};function y(e,t,n){let r=new Map;for(let[e,n]of Object.entries(t))r.set(e,n);return S(new _(r,new Map,e,n?.name))}const b=new d;function x(e){b.validateConfig(e);let t=new Map;for(let[n,r]of Object.entries(e))t.set(n,r);return S(new _(t))}function S(e){let n=new v(e),r={scope:(t,n)=>y(e,t,n),extend:t=>{b.validateConfig(t);let n=new Map(e.getFactories());for(let[e,r]of Object.entries(t))n.set(e,r);return S(new _(n,new Map(e.getCache())))},preload:async(...t)=>{let n=t.length>0?t:[...e.getFactories().keys()];for(let t of n)e.resolve(t)},reset:(...t)=>{let n=e.getCache();for(let e of t)n.delete(e)},inspect:()=>n.inspect(),describe:e=>n.describe(e),health:()=>n.health(),toString:()=>n.toString(),dispose:async()=>{let n=e.getCache(),r=[...n.entries()].reverse();for(let[,e]of r)t(e)&&await e.onDestroy();n.clear()}};return new Proxy({},{get(t,i){if(typeof i==`symbol`)return i===Symbol.toPrimitive||i===Symbol.toStringTag?()=>n.toString():void 0;let a=i;return a in r?r[a]:e.resolve(a)},has(t,n){if(typeof n==`symbol`)return!1;let i=n;return i in r||e.getFactories().has(i)||e.getAllRegisteredKeys().includes(i)},ownKeys(){return[...e.getAllRegisteredKeys(),...Object.keys(r)]},getOwnPropertyDescriptor(t,n){if(typeof n==`symbol`)return;let i=n;if(i in r||e.getFactories().has(i)||e.getAllRegisteredKeys().includes(i))return{configurable:!0,enumerable:!(i in r),writable:!1}}})}export{s as CircularDependencyError,i as ContainerConfigError,r as ContainerError,l as FactoryError,o as ProviderNotFoundError,a as ReservedKeyError,u as ScopeMismatchWarning,c as UndefinedReturnError,x as createContainer,f as detectDuplicateKeys,h as transient};
1
+ const e=[`scope`,`extend`,`module`,`preload`,`reset`,`inspect`,`describe`,`health`,`dispose`,`toString`];var t=class extends Error{constructor(e){super(e),this.name=this.constructor.name}},n=class extends t{hint;details;constructor(e,t){super(`'${e}' must be a factory function, got ${t}.`),this.hint=`Wrap it: ${e}: () => <your ${t} value>`,this.details={key:e,actualType:t}}},r=class extends t{hint;details;constructor(e,t){super(`'${e}' is a reserved container method.`),this.hint=`Rename this dependency, e.g. '${e}Service' or 'my${e[0].toUpperCase()}${e.slice(1)}'.`,this.details={key:e,reserved:[...t]}}},i=class extends t{hint;details;constructor(e,t,n,r){let i=t.length>0?`\n\nResolution chain: ${[...t,`${e} (not found)`].join(` -> `)}`:``,a=`\nRegistered keys: [${n.join(`, `)}]`,o=r?`\n\nDid you mean '${r}'?`:``;super(`Cannot resolve '${t[0]??e}': dependency '${e}' not found.${i}${a}${o}`),this.hint=r?`Did you mean '${r}'? Or add '${e}' to your container:\n createContainer({\n ...existing,\n ${e}: (c) => new Your${e[0].toUpperCase()}${e.slice(1)}(/* deps */),\n });`:`Add '${e}' to your container:\n createContainer({\n ...existing,\n ${e}: (c) => new Your${e[0].toUpperCase()}${e.slice(1)}(/* deps */),\n });`,this.details={key:e,chain:t,registered:n,suggestion:r}}},a=class extends t{hint;details;constructor(e,t){let n=[...t,e].join(` -> `);super(`Circular dependency detected while resolving '${t[0]}'.\n\nCycle: ${n}`),this.hint=[`To fix:`,` 1. Extract shared logic into a new dependency both can use`,` 2. Restructure so one doesn't depend on the other`,` 3. Use a mediator/event pattern to decouple them`].join(`
2
+ `),this.details={key:e,chain:t,cycle:n}}},o=class extends t{hint;details;constructor(e,t){let n=t.length>1?`\n\nResolution chain: ${t.join(` -> `)}`:``;super(`Factory '${e}' returned undefined.${n}`),this.hint=`Your factory function returned undefined. Did you forget a return statement?`,this.details={key:e,chain:t}}},s=class extends t{hint;details;originalError;constructor(e,t,n){let r=n instanceof Error?n.message:String(n),i=t.length>1?`\n\nResolution chain: ${[...t.slice(0,-1),`${e} (factory threw)`].join(` -> `)}`:``;super(`Factory '${e}' threw an error: "${r}"${i}`),this.hint=`Check the factory function for '${e}'. The error occurred during instantiation.`,this.details={key:e,chain:t,originalError:r},this.originalError=n}},c=class{type=`scope_mismatch`;message;hint;details;constructor(e,t){this.message=`Singleton '${e}' depends on transient '${t}'.`,this.hint=[`The transient value was resolved once and is now frozen inside the singleton.`,`This is almost always a bug.`,``,`To fix:`,` 1. Make '${e}' transient too: transient((c) => new ${e[0].toUpperCase()}${e.slice(1)}(c.${t}))`,` 2. Make '${t}' singleton if it doesn't need to change`,` 3. Inject a factory instead: ${t}Factory: () => () => <your value>`].join(`
3
+ `),this.details={singleton:e,transient:t}}};const l=Symbol.for(`inwire:transient`);function u(e){let t=(t=>e(t));return t[l]=!0,t}function d(e){return typeof e==`function`&&l in e&&e[l]===!0}function f(e){return typeof e==`object`&&!!e&&`onInit`in e&&typeof e.onInit==`function`}function p(e){return typeof e==`object`&&!!e&&`onDestroy`in e&&typeof e.onDestroy==`function`}var m=class{validateConfig(t){for(let[i,a]of Object.entries(t)){if(e.includes(i))throw new r(i,e);if(typeof a!=`function`)throw new n(i,typeof a)}}suggestKey(e,t){let n,r=1/0;for(let i of t){let t=g(e,i);t<r&&(r=t,n=i)}if(!n)return;let i=Math.max(e.length,n.length);return 1-r/i>=.5?n:void 0}};function h(...e){let t=new Map,n=[];for(let r of e)for(let e of Object.keys(r)){let r=(t.get(e)??0)+1;t.set(e,r),r===2&&n.push(e)}return n}function g(e,t){let n=e.length,r=t.length;if(n===0)return r;if(r===0)return n;let i=Array(r+1),a=Array(r+1);for(let e=0;e<=r;e++)i[e]=e;for(let o=1;o<=n;o++){a[0]=o;for(let n=1;n<=r;n++){let r=e[o-1]===t[n-1]?0:1;a[n]=Math.min(i[n]+1,a[n-1]+1,i[n-1]+r)}[i,a]=[a,i]}return i[r]}var _=class{factories;cache;resolving=new Set;depGraph=new Map;warnings=[];validator=new m;parent;name;constructor(e,t,n,r){this.factories=e,this.cache=t??new Map,this.parent=n,this.name=r}getName(){return this.name}resolve(e,t=[]){let n=this.factories.get(e);if(n&&!d(n)&&this.cache.has(e))return this.cache.get(e);if(!n){if(this.parent)return this.parent.resolve(e,t);let n=this.getAllRegisteredKeys();throw new i(e,t,n,this.validator.suggestKey(e,n))}if(this.resolving.has(e))throw new a(e,[...t]);this.resolving.add(e);let r=[...t,e];try{let t=[],i=n(this.createTrackingProxy(e,t,r));if(i===void 0)throw new o(e,r);if(this.depGraph.set(e,t),!d(n))for(let n of t){let t=this.getFactory(n);t&&d(t)&&this.warnings.push(new c(e,n))}if(d(n)||this.cache.set(e,i),f(i)){let e=i.onInit();e instanceof Promise&&e.catch(()=>{})}return i}catch(t){throw t instanceof a||t instanceof i||t instanceof o||t instanceof s?t:new s(e,r,t)}finally{this.resolving.delete(e)}}isResolved(e){return this.cache.has(e)}getDepGraph(){return new Map(this.depGraph)}getResolvedKeys(){return[...this.cache.keys()]}getFactories(){return this.factories}getCache(){return this.cache}getWarnings(){return[...this.warnings]}getAllRegisteredKeys(){let e=new Set(this.factories.keys());if(this.parent)for(let t of this.parent.getAllRegisteredKeys())e.add(t);return[...e]}createTrackingProxy(e,t,n){return new Proxy({},{get:(e,r)=>{if(typeof r==`symbol`)return;let i=r;return t.push(i),this.resolve(i,n)}})}getFactory(e){return this.factories.get(e)??this.parent?.getFactory(e)}},v=class{constructor(e){this.resolver=e}inspect(){let e={};for(let[t,n]of this.resolver.getFactories())e[t]={key:t,resolved:this.resolver.isResolved(t),deps:this.resolver.getDepGraph().get(t)??[],scope:d(n)?`transient`:`singleton`};let t=this.resolver.getName();return t?{name:t,providers:e}:{providers:e}}describe(e){let t=this.resolver.getFactories().get(e);return t?{key:e,resolved:this.resolver.isResolved(e),deps:this.resolver.getDepGraph().get(e)??[],scope:d(t)?`transient`:`singleton`}:{key:e,resolved:!1,deps:[],scope:`singleton`}}health(){let e=[...this.resolver.getFactories().keys()],t=this.resolver.getResolvedKeys(),n=new Set(t),r=this.resolver.getWarnings().map(e=>({type:e.type,message:e.message,details:e.details}));return{totalProviders:e.length,resolved:t,unresolved:e.filter(e=>!n.has(e)),warnings:r}}toString(){let e=[];for(let[t]of this.resolver.getFactories()){let n=this.resolver.isResolved(t),r=this.resolver.getDepGraph().get(t),i=r&&r.length>0?` -> [${r.join(`, `)}]`:``,a=n?`(resolved)`:`(pending)`;e.push(`${t}${i} ${a}`)}let t=this.resolver.getName();return`${t?`Scope(${t})`:`Container`} { ${e.join(`, `)} }`}};const y=new m;function b(e,t){let n=new v(e),r={scope:(n,r)=>{let i=new Map;for(let[e,t]of Object.entries(n))i.set(e,t);return b(new _(i,new Map,e,r?.name),t)},extend:n=>{y.validateConfig(n);let r=new Map(e.getFactories());for(let[e,t]of Object.entries(n))r.set(e,t);return b(new _(r,new Map(e.getCache())),t)},module:e=>{if(!t)throw Error(`module() is not available`);let n=e(t());return r.extend(n._toRecord())},preload:async(...t)=>{let n=t.length>0?t:[...e.getFactories().keys()];for(let t of n)e.resolve(t)},reset:(...t)=>{let n=e.getCache();for(let e of t)n.delete(e)},inspect:()=>n.inspect(),describe:e=>n.describe(e),health:()=>n.health(),toString:()=>n.toString(),dispose:async()=>{let t=e.getCache(),n=[...t.entries()].reverse();for(let[,e]of n)p(e)&&await e.onDestroy();t.clear()}};return new Proxy({},{get(t,i){if(typeof i==`symbol`)return i===Symbol.toPrimitive||i===Symbol.toStringTag?()=>n.toString():void 0;let a=i;return a in r?r[a]:e.resolve(a)},has(t,n){if(typeof n==`symbol`)return!1;let i=n;return i in r||e.getFactories().has(i)||e.getAllRegisteredKeys().includes(i)},ownKeys(){return[...e.getAllRegisteredKeys(),...Object.keys(r)]},getOwnPropertyDescriptor(t,n){if(typeof n==`symbol`)return;let i=n;if(i in r||e.getFactories().has(i)||e.getAllRegisteredKeys().includes(i))return{configurable:!0,enumerable:!(i in r),writable:!1}}})}var x=class t{factories=new Map;transientKeys=new Set;add(e,t){return this.validateKey(e),typeof t==`function`?this.factories.set(e,t):this.factories.set(e,()=>t),this}addTransient(e,t){return this.validateKey(e),this.factories.set(e,u(t)),this.transientKeys.add(e),this}addModule(e){return e(this)}_toRecord(){return Object.fromEntries(this.factories)}build(){return b(new _(new Map(this.factories)),()=>new t)}validateKey(t){if(e.includes(t))throw new r(t,e)}};function S(){return new x}export{a as CircularDependencyError,x as ContainerBuilder,n as ContainerConfigError,t as ContainerError,s as FactoryError,i as ProviderNotFoundError,r as ReservedKeyError,c as ScopeMismatchWarning,o as UndefinedReturnError,S as container,h as detectDuplicateKeys,u as transient};
4
4
  //# sourceMappingURL=index.mjs.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "inwire",
3
- "version": "1.0.2",
3
+ "version": "1.1.0",
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",
@@ -21,6 +21,12 @@
21
21
  "test:watch": "vitest",
22
22
  "test:coverage": "vitest run --coverage",
23
23
  "lint": "tsc --noEmit",
24
+ "example:web": "tsx examples/01-web-service.ts",
25
+ "example:test": "tsx examples/02-modular-testing.ts",
26
+ "example:plugin": "tsx examples/03-plugin-system.ts",
27
+ "example:modules": "tsx examples/04-modules.ts",
28
+ "bench": "tsx benchmarks/bench.ts",
29
+ "bench:compare": "tsx benchmarks/compare.ts",
24
30
  "prepublishOnly": "npm run build"
25
31
  },
26
32
  "publishConfig": {