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 +246 -227
- package/dist/index.d.mts +179 -117
- package/dist/index.mjs +3 -3
- package/package.json +7 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# inwire
|
|
2
2
|
|
|
3
|
-
|
|
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
|
[](https://www.npmjs.com/package/inwire)
|
|
6
6
|
[](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
|
|
16
|
+
npm i inwire
|
|
17
17
|
```
|
|
18
18
|
|
|
19
19
|
## Quick Start
|
|
20
20
|
|
|
21
21
|
```typescript
|
|
22
|
-
import {
|
|
22
|
+
import { container } from 'inwire';
|
|
23
23
|
|
|
24
|
-
const
|
|
25
|
-
logger
|
|
26
|
-
db
|
|
27
|
-
|
|
28
|
-
|
|
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
|
-
|
|
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
|
-
|
|
56
|
+
The string key acts as a token (like NestJS), but type-safe at compile time.
|
|
57
|
+
|
|
58
|
+
## Instance Values (Eager)
|
|
35
59
|
|
|
36
|
-
|
|
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
|
|
48
|
-
db
|
|
49
|
-
|
|
83
|
+
const app = container()
|
|
84
|
+
.add('db', () => new Database())
|
|
85
|
+
.build();
|
|
50
86
|
|
|
51
|
-
//
|
|
52
|
-
|
|
87
|
+
// BAD — onInit() fires but is NOT awaited, errors are lost
|
|
88
|
+
app.db;
|
|
53
89
|
|
|
54
|
-
//
|
|
55
|
-
await
|
|
56
|
-
|
|
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
|
|
75
|
-
db
|
|
76
|
-
|
|
106
|
+
const app = container()
|
|
107
|
+
.add('db', () => new Database(process.env.DB_URL!))
|
|
108
|
+
.build();
|
|
77
109
|
|
|
78
|
-
|
|
79
|
-
|
|
110
|
+
app.db; // creates Database
|
|
111
|
+
app.db; // same instance (cached)
|
|
80
112
|
```
|
|
81
113
|
|
|
82
|
-
###
|
|
114
|
+
### Transient
|
|
83
115
|
|
|
84
|
-
|
|
116
|
+
Fresh instance on every access via `addTransient()`:
|
|
85
117
|
|
|
86
118
|
```typescript
|
|
87
|
-
|
|
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
|
-
|
|
121
|
+
const app = container()
|
|
122
|
+
.add('logger', () => new LoggerService())
|
|
123
|
+
.addTransient('requestId', () => crypto.randomUUID())
|
|
124
|
+
.build();
|
|
97
125
|
|
|
98
|
-
|
|
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
|
-
|
|
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
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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
|
|
142
|
+
Create child containers for request-level isolation:
|
|
130
143
|
|
|
131
144
|
```typescript
|
|
132
|
-
const
|
|
133
|
-
logger
|
|
134
|
-
db
|
|
135
|
-
|
|
145
|
+
const app = container()
|
|
146
|
+
.add('logger', () => new LoggerService())
|
|
147
|
+
.add('db', () => new Database())
|
|
148
|
+
.build();
|
|
136
149
|
|
|
137
|
-
|
|
138
|
-
const request = container.scope({
|
|
150
|
+
const request = app.scope({
|
|
139
151
|
requestId: () => crypto.randomUUID(),
|
|
140
|
-
|
|
152
|
+
handler: (c) => new Handler(c.logger), // c typed as typeof app
|
|
141
153
|
});
|
|
142
154
|
|
|
143
|
-
request.requestId;
|
|
144
|
-
request.logger;
|
|
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 =
|
|
162
|
+
const request = app.scope(
|
|
153
163
|
{ requestId: () => crypto.randomUUID() },
|
|
154
164
|
{ name: 'request-123' },
|
|
155
165
|
);
|
|
156
166
|
|
|
157
|
-
String(request);
|
|
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
|
|
190
|
-
db
|
|
191
|
-
|
|
181
|
+
const app = container()
|
|
182
|
+
.add('db', () => new Database())
|
|
183
|
+
.build();
|
|
192
184
|
|
|
193
|
-
|
|
194
|
-
await
|
|
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
|
|
191
|
+
Add dependencies to an existing container without mutating it:
|
|
202
192
|
|
|
203
193
|
```typescript
|
|
204
|
-
const base =
|
|
205
|
-
logger
|
|
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
|
|
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
|
-
###
|
|
208
|
+
### Modules
|
|
219
209
|
|
|
220
|
-
|
|
210
|
+
A module is a function `(builder) => builder` that chains `.add()` calls. `c` is fully typed in every factory.
|
|
221
211
|
|
|
222
|
-
|
|
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
|
-
|
|
230
|
-
|
|
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
|
-
|
|
233
|
-
|
|
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
|
-
|
|
237
|
-
|
|
238
|
-
### Reset
|
|
232
|
+
#### Post-build: `module()` on the container
|
|
239
233
|
|
|
240
|
-
|
|
234
|
+
Compose modules after `.build()` — same DX, applied to an existing container:
|
|
241
235
|
|
|
242
236
|
```typescript
|
|
243
|
-
const
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
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
|
-
//
|
|
253
|
-
|
|
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
|
-
|
|
253
|
+
`module()` uses the builder internally for typed `c`, then delegates to `extend()`. Works on `scope()` and `extend()` results too.
|
|
257
254
|
|
|
258
|
-
|
|
255
|
+
### Preload
|
|
259
256
|
|
|
260
257
|
```typescript
|
|
261
|
-
//
|
|
262
|
-
|
|
263
|
-
|
|
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
|
-
|
|
270
|
-
container.health();
|
|
271
|
-
// { totalProviders: 4, resolved: ['db', 'logger'], unresolved: ['cache'], warnings: [] }
|
|
262
|
+
### Reset
|
|
272
263
|
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
264
|
+
```typescript
|
|
265
|
+
app.db; // creates Database
|
|
266
|
+
app.reset('db');
|
|
267
|
+
app.db; // creates a NEW Database instance
|
|
276
268
|
```
|
|
277
269
|
|
|
278
|
-
|
|
270
|
+
### Introspection
|
|
279
271
|
|
|
280
272
|
```typescript
|
|
281
|
-
|
|
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
|
-
|
|
284
|
-
|
|
285
|
-
|
|
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
|
-
|
|
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
|
-
|
|
306
|
-
// ProviderNotFoundError:
|
|
307
|
-
// hint: "Did you mean 'userService'?"
|
|
295
|
+
app.userServce; // typo
|
|
296
|
+
// ProviderNotFoundError: Did you mean 'userService'?
|
|
308
297
|
|
|
309
298
|
// Circular dependency
|
|
310
|
-
// CircularDependencyError:
|
|
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
|
-
|
|
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
|
-
###
|
|
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
|
-
|
|
341
|
-
|
|
312
|
+
import { detectDuplicateKeys } from 'inwire';
|
|
313
|
+
|
|
314
|
+
detectDuplicateKeys(authModule, userModule);
|
|
315
|
+
// ['logger']
|
|
342
316
|
```
|
|
343
317
|
|
|
344
|
-
|
|
318
|
+
## Examples
|
|
345
319
|
|
|
346
|
-
|
|
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
|
-
|
|
349
|
-
import { detectDuplicateKeys } from 'inwire';
|
|
327
|
+
## Performance
|
|
350
328
|
|
|
351
|
-
|
|
352
|
-
|
|
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
|
-
|
|
|
368
|
-
|
|
|
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
|
-
|
|
404
|
+
resolver.ts # Resolver (Proxy handler, cache, cycle detection)
|
|
382
405
|
transient.ts # transient() marker
|
|
383
406
|
application/
|
|
384
|
-
|
|
385
|
-
|
|
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
|
-
| `
|
|
417
|
-
| `transient(factory)` | Marks a factory as transient (
|
|
418
|
-
| `detectDuplicateKeys(...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
|
-
|
|
|
425
|
-
|
|
|
426
|
-
|
|
|
427
|
-
|
|
|
428
|
-
|
|
|
429
|
-
|
|
|
430
|
-
|
|
|
431
|
-
|
|
|
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
|
-
| `
|
|
439
|
-
| `
|
|
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`
|
|
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 =
|
|
10
|
+
type Factory<T = unknown> = (container: unknown) => T;
|
|
11
11
|
/**
|
|
12
|
-
*
|
|
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
|
-
|
|
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
|
|
50
|
-
* logger
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
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,
|
|
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,
|
|
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
|
-
*
|
|
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
|
|
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
|
|
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
|
-
|
|
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'
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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,
|
|
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
|
-
|
|
2
|
-
`),this.details={key:e,chain:t,cycle:n}}},
|
|
3
|
-
`),this.details={singleton:e,transient:t}}},d=class{validateConfig(
|
|
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
|
|
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": {
|