inwire 1.0.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/LICENSE +21 -0
- package/README.md +465 -0
- package/dist/index.cjs +590 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +387 -0
- package/dist/index.d.ts +387 -0
- package/dist/index.js +553 -0
- package/dist/index.js.map +1 -0
- package/llms-full.txt +523 -0
- package/llms.txt +53 -0
- package/package.json +64 -0
package/llms-full.txt
ADDED
|
@@ -0,0 +1,523 @@
|
|
|
1
|
+
# inwire — Complete API Reference
|
|
2
|
+
|
|
3
|
+
> Zero-ceremony dependency injection for TypeScript. Full inference, no decorators, no tokens.
|
|
4
|
+
> Proxy-based lazy singleton resolution. ~4.7KB gzipped, 0 runtime deps.
|
|
5
|
+
|
|
6
|
+
Install: `npm i inwire`
|
|
7
|
+
License: MIT
|
|
8
|
+
Exports: ESM + CJS with full .d.ts
|
|
9
|
+
|
|
10
|
+
## How It Works
|
|
11
|
+
|
|
12
|
+
The container is a Proxy. Each dependency is defined as a factory function `(container) => instance`. When you access a property on the container, the Proxy intercepts it and delegates to the Resolver. The Resolver:
|
|
13
|
+
|
|
14
|
+
1. Checks the singleton cache — if already resolved, returns the cached instance
|
|
15
|
+
2. Detects circular dependencies via a `resolving` Set
|
|
16
|
+
3. Creates a tracking Proxy to record which other deps the factory accesses (builds the dep graph)
|
|
17
|
+
4. Calls the factory function with the tracking proxy
|
|
18
|
+
5. Caches the result (unless marked transient)
|
|
19
|
+
6. Calls `onInit()` if the instance implements it (fire-and-forget, not awaited)
|
|
20
|
+
|
|
21
|
+
TypeScript infers the full container type from the factory definitions — `Container<ResolvedDeps<T>>` maps each key to the return type of its factory.
|
|
22
|
+
|
|
23
|
+
## createContainer(defs)
|
|
24
|
+
|
|
25
|
+
```typescript
|
|
26
|
+
function createContainer<T extends DepsDefinition>(defs: T): Container<ResolvedDeps<T>>
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Creates a DI container from an object of factory functions. Each factory receives the container as its argument and returns an instance. Dependencies are resolved lazily on first property access and cached as singletons by default.
|
|
30
|
+
|
|
31
|
+
```typescript
|
|
32
|
+
import { createContainer } from 'inwire';
|
|
33
|
+
|
|
34
|
+
const container = createContainer({
|
|
35
|
+
logger: () => new LoggerService(),
|
|
36
|
+
db: () => new Database(process.env.DB_URL!),
|
|
37
|
+
userRepo: (c): UserRepository => new PgUserRepo(c.db),
|
|
38
|
+
userService: (c) => new UserService(c.userRepo, c.logger),
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
container.userService; // lazy, singleton, fully typed
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Annotate the return type to program against an interface (dependency inversion):
|
|
45
|
+
|
|
46
|
+
```typescript
|
|
47
|
+
const container = createContainer({
|
|
48
|
+
userRepo: (c): UserRepository => new PgUserRepo(c.db),
|
|
49
|
+
// ^^^^^^^^^^^^^^^^ contract, not implementation
|
|
50
|
+
});
|
|
51
|
+
container.userRepo; // typed as UserRepository
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Validation runs at creation time: non-function values throw `ContainerConfigError`, reserved keys throw `ReservedKeyError`.
|
|
55
|
+
|
|
56
|
+
## transient(factory)
|
|
57
|
+
|
|
58
|
+
```typescript
|
|
59
|
+
function transient<T>(factory: Factory<T>): Factory<T>
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Marks a factory as transient — a new instance is created on every property access, bypassing the singleton cache. Internally attaches a `Symbol.for('inwire:transient')` marker.
|
|
63
|
+
|
|
64
|
+
```typescript
|
|
65
|
+
import { createContainer, transient } from 'inwire';
|
|
66
|
+
|
|
67
|
+
const container = createContainer({
|
|
68
|
+
logger: () => new LoggerService(), // singleton (default)
|
|
69
|
+
requestId: transient(() => crypto.randomUUID()), // new value every time
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
container.logger === container.logger; // true — same instance
|
|
73
|
+
container.requestId === container.requestId; // false — different every time
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## container.scope(extra, options?)
|
|
77
|
+
|
|
78
|
+
```typescript
|
|
79
|
+
scope<E extends DepsDefinition>(extra: E, options?: ScopeOptions): Container<T & ResolvedDeps<E>>
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Creates a child container with additional dependencies. The child inherits all parent singletons via a parent Resolver chain. Scoped singletons are independent — they are cached in the child's own cache.
|
|
83
|
+
|
|
84
|
+
```typescript
|
|
85
|
+
const app = createContainer({
|
|
86
|
+
logger: () => new LoggerService(),
|
|
87
|
+
db: () => new Database(),
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
const request = app.scope({
|
|
91
|
+
requestId: () => crypto.randomUUID(),
|
|
92
|
+
currentUser: () => getCurrentUser(),
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
request.requestId; // scoped singleton (unique to this child)
|
|
96
|
+
request.logger; // inherited from parent (same instance)
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Use `scope()` for request-level isolation where child deps should not pollute the parent.
|
|
100
|
+
|
|
101
|
+
### Named Scopes
|
|
102
|
+
|
|
103
|
+
Pass `{ name }` for debugging and introspection:
|
|
104
|
+
|
|
105
|
+
```typescript
|
|
106
|
+
const request = app.scope(
|
|
107
|
+
{ requestId: () => crypto.randomUUID() },
|
|
108
|
+
{ name: 'request-123' },
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
String(request); // "Scope(request-123) { requestId (pending) }"
|
|
112
|
+
request.inspect().name; // "request-123"
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Named scopes show their name in `toString()` output (`Scope(name) { ... }` instead of `Container { ... }`) and include the name in `inspect()` results.
|
|
116
|
+
|
|
117
|
+
## container.extend(extra)
|
|
118
|
+
|
|
119
|
+
```typescript
|
|
120
|
+
extend<E extends DepsDefinition>(extra: E): Container<T & ResolvedDeps<E>>
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Returns a new container with additional dependencies. Unlike `scope()`, the existing singleton cache is **shared** (copied) — already-resolved singletons from the original container are available without re-resolution.
|
|
124
|
+
|
|
125
|
+
```typescript
|
|
126
|
+
const base = createContainer({
|
|
127
|
+
logger: () => new LoggerService(),
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
const extended = base.extend({
|
|
131
|
+
db: (c) => new Database(c.logger),
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
extended.logger; // shared singleton from base
|
|
135
|
+
extended.db; // new dependency
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
**scope vs extend**: `scope()` creates a parent-child chain (child delegates to parent for unknown keys). `extend()` creates a flat merged container with a shared cache snapshot. Use `scope()` for request isolation, `extend()` for composition.
|
|
139
|
+
|
|
140
|
+
**GOTCHA**: Because `extend()` shares the cache, singletons resolved in the original container will be the same objects in the extended container. New singletons resolved in the extended container do NOT propagate back to the original.
|
|
141
|
+
|
|
142
|
+
## container.preload(...keys)
|
|
143
|
+
|
|
144
|
+
```typescript
|
|
145
|
+
preload(...keys: (keyof T)[]): Promise<void>
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Eagerly resolves specific dependencies (warm-up). Call with specific keys to resolve only those, or **without arguments to resolve all dependencies**.
|
|
149
|
+
|
|
150
|
+
```typescript
|
|
151
|
+
const container = createContainer({
|
|
152
|
+
db: () => new Database(),
|
|
153
|
+
cache: () => new Redis(),
|
|
154
|
+
logger: () => new LoggerService(),
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
await container.preload('db', 'cache');
|
|
158
|
+
// db and cache are now resolved, logger is still lazy
|
|
159
|
+
|
|
160
|
+
await container.preload();
|
|
161
|
+
// resolve ALL dependencies at once
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
**GOTCHA — CRITICAL**: `preload()` is the **only** way to properly await async `onInit()` hooks. During normal property access, `onInit()` is called but NOT awaited (fire-and-forget, errors swallowed). If your service has async initialization (e.g. database connection), you MUST use `preload()` to surface errors.
|
|
165
|
+
|
|
166
|
+
## container.reset(...keys)
|
|
167
|
+
|
|
168
|
+
```typescript
|
|
169
|
+
reset(...keys: (keyof T)[]): void
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
Invalidates cached singletons, forcing re-creation on next access. The next property access will re-execute the factory and call `onInit()` again if implemented.
|
|
173
|
+
|
|
174
|
+
```typescript
|
|
175
|
+
const container = createContainer({
|
|
176
|
+
db: () => new Database(),
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
container.db; // creates Database
|
|
180
|
+
container.reset('db');
|
|
181
|
+
container.db; // creates a NEW Database instance
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
`reset()` does not affect parent scopes — it only clears the cache of the container it's called on. Resetting an unresolved key is a silent no-op.
|
|
185
|
+
|
|
186
|
+
## container.inspect()
|
|
187
|
+
|
|
188
|
+
```typescript
|
|
189
|
+
inspect(): ContainerGraph
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
Returns the full dependency graph as a serializable JSON object. Named scopes include a `name` field.
|
|
193
|
+
|
|
194
|
+
```typescript
|
|
195
|
+
interface ContainerGraph {
|
|
196
|
+
name?: string;
|
|
197
|
+
providers: Record<string, ProviderInfo>;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
interface ProviderInfo {
|
|
201
|
+
key: string;
|
|
202
|
+
resolved: boolean;
|
|
203
|
+
deps: string[];
|
|
204
|
+
scope: 'singleton' | 'transient';
|
|
205
|
+
}
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
```typescript
|
|
209
|
+
container.inspect();
|
|
210
|
+
// {
|
|
211
|
+
// providers: {
|
|
212
|
+
// db: { key: 'db', resolved: true, deps: [], scope: 'singleton' },
|
|
213
|
+
// userRepo: { key: 'userRepo', resolved: true, deps: ['db'], scope: 'singleton' },
|
|
214
|
+
// logger: { key: 'logger', resolved: false, deps: [], scope: 'singleton' }
|
|
215
|
+
// }
|
|
216
|
+
// }
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
**AI usage**: Pipe `JSON.stringify(container.inspect(), null, 2)` into an LLM to analyze the architecture, detect issues, or generate documentation from the live dependency graph.
|
|
220
|
+
|
|
221
|
+
## container.describe(key)
|
|
222
|
+
|
|
223
|
+
```typescript
|
|
224
|
+
describe(key: keyof T): ProviderInfo
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
Returns detailed information about a single provider.
|
|
228
|
+
|
|
229
|
+
```typescript
|
|
230
|
+
container.describe('userService');
|
|
231
|
+
// { key: 'userService', resolved: true, deps: ['userRepo', 'logger'], scope: 'singleton' }
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
If the key is not registered, returns `{ key, resolved: false, deps: [], scope: 'singleton' }`.
|
|
235
|
+
|
|
236
|
+
## container.health()
|
|
237
|
+
|
|
238
|
+
```typescript
|
|
239
|
+
health(): ContainerHealth
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
Returns container health status and warnings.
|
|
243
|
+
|
|
244
|
+
```typescript
|
|
245
|
+
interface ContainerHealth {
|
|
246
|
+
totalProviders: number;
|
|
247
|
+
resolved: string[];
|
|
248
|
+
unresolved: string[];
|
|
249
|
+
warnings: ContainerWarning[];
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
interface ContainerWarning {
|
|
253
|
+
type: 'scope_mismatch' | 'duplicate_key';
|
|
254
|
+
message: string;
|
|
255
|
+
details: Record<string, unknown>;
|
|
256
|
+
}
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
```typescript
|
|
260
|
+
container.health();
|
|
261
|
+
// {
|
|
262
|
+
// totalProviders: 4,
|
|
263
|
+
// resolved: ['db', 'logger'],
|
|
264
|
+
// unresolved: ['cache', 'userService'],
|
|
265
|
+
// warnings: [
|
|
266
|
+
// { type: 'scope_mismatch', message: "Singleton 'userService' depends on transient 'requestId'.", details: { singleton: 'userService', transient: 'requestId' } }
|
|
267
|
+
// ]
|
|
268
|
+
// }
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
## container.dispose()
|
|
272
|
+
|
|
273
|
+
```typescript
|
|
274
|
+
dispose(): Promise<void>
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
Disposes the container. Calls `onDestroy()` on all resolved instances that implement it, in **reverse resolution order** (LIFO). Clears the singleton cache after disposal.
|
|
278
|
+
|
|
279
|
+
```typescript
|
|
280
|
+
const container = createContainer({
|
|
281
|
+
db: () => new Database(), // resolved first
|
|
282
|
+
cache: () => new Redis(), // resolved second
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
container.db;
|
|
286
|
+
container.cache;
|
|
287
|
+
await container.dispose(); // calls cache.onDestroy() then db.onDestroy()
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
**GOTCHA**: After `dispose()`, accessing dependencies will re-resolve them (cache is cleared). This can cause unexpected behavior if you continue using the container after disposal.
|
|
291
|
+
|
|
292
|
+
## detectDuplicateKeys(...modules)
|
|
293
|
+
|
|
294
|
+
```typescript
|
|
295
|
+
function detectDuplicateKeys(...modules: Record<string, unknown>[]): string[]
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
Detects duplicate keys across multiple module objects. Returns an array of keys that appear in more than one module. Use this before spreading modules into `createContainer()` to catch accidental collisions.
|
|
299
|
+
|
|
300
|
+
```typescript
|
|
301
|
+
import { detectDuplicateKeys } from 'inwire';
|
|
302
|
+
|
|
303
|
+
const authModule = { logger: () => new AuthLogger(), auth: () => new AuthService() };
|
|
304
|
+
const userModule = { logger: () => new UserLogger(), user: () => new UserService() };
|
|
305
|
+
|
|
306
|
+
detectDuplicateKeys(authModule, userModule);
|
|
307
|
+
// ['logger'] — appears in both modules
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
## String(container)
|
|
311
|
+
|
|
312
|
+
Containers implement `toString()` for human-readable output:
|
|
313
|
+
|
|
314
|
+
```typescript
|
|
315
|
+
String(container);
|
|
316
|
+
// "Container { db -> [] (resolved), logger (pending) }"
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
Also works with `Symbol.toPrimitive` and `Symbol.toStringTag`.
|
|
320
|
+
|
|
321
|
+
## Lifecycle Hooks: OnInit / OnDestroy
|
|
322
|
+
|
|
323
|
+
```typescript
|
|
324
|
+
interface OnInit {
|
|
325
|
+
onInit(): void | Promise<void>;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
interface OnDestroy {
|
|
329
|
+
onDestroy(): void | Promise<void>;
|
|
330
|
+
}
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
Lifecycle hooks are **duck-typed** — no need to explicitly implement the interface. Any object with an `onInit` or `onDestroy` method will be detected automatically.
|
|
334
|
+
|
|
335
|
+
```typescript
|
|
336
|
+
import type { OnInit, OnDestroy } from 'inwire';
|
|
337
|
+
|
|
338
|
+
class Database implements OnInit, OnDestroy {
|
|
339
|
+
async onInit() { await this.connect(); }
|
|
340
|
+
async onDestroy() { await this.disconnect(); }
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
const container = createContainer({
|
|
344
|
+
db: () => new Database(),
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
// Use preload to properly await async onInit:
|
|
348
|
+
await container.preload('db');
|
|
349
|
+
|
|
350
|
+
// Later, dispose calls onDestroy in LIFO order:
|
|
351
|
+
await container.dispose();
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
**GOTCHA — CRITICAL**: During normal property access (`container.db`), `onInit()` IS called but NOT awaited. It runs as fire-and-forget. If `onInit()` returns a Promise:
|
|
355
|
+
- The Promise is caught silently (errors are swallowed)
|
|
356
|
+
- The instance is returned immediately, potentially before initialization completes
|
|
357
|
+
- Use `await container.preload('db')` to ensure `onInit()` completes and errors surface
|
|
358
|
+
|
|
359
|
+
This is by design: property access in JavaScript is synchronous, so the Proxy cannot await.
|
|
360
|
+
|
|
361
|
+
## Modules Pattern
|
|
362
|
+
|
|
363
|
+
Group related factories into plain objects and spread them into `createContainer()`:
|
|
364
|
+
|
|
365
|
+
```typescript
|
|
366
|
+
const dbModule = {
|
|
367
|
+
db: () => new Database(process.env.DB_URL!),
|
|
368
|
+
redis: () => new Redis(process.env.REDIS_URL!),
|
|
369
|
+
};
|
|
370
|
+
|
|
371
|
+
const serviceModule = {
|
|
372
|
+
userService: (c) => new UserService(c.db),
|
|
373
|
+
};
|
|
374
|
+
|
|
375
|
+
const container = createContainer({
|
|
376
|
+
...dbModule,
|
|
377
|
+
...serviceModule,
|
|
378
|
+
});
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
Use `satisfies DepsDefinition` on module objects for type checking without losing literal types:
|
|
382
|
+
|
|
383
|
+
```typescript
|
|
384
|
+
import type { DepsDefinition } from 'inwire';
|
|
385
|
+
|
|
386
|
+
const dbModule = {
|
|
387
|
+
db: () => new Database(),
|
|
388
|
+
} satisfies DepsDefinition;
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
## Test Overrides Pattern
|
|
392
|
+
|
|
393
|
+
Replace any dependency with a mock by spreading production deps and overriding:
|
|
394
|
+
|
|
395
|
+
```typescript
|
|
396
|
+
const container = createContainer({
|
|
397
|
+
...productionDeps,
|
|
398
|
+
db: () => new InMemoryDatabase(), // override
|
|
399
|
+
});
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
No special test API needed — last spread wins.
|
|
403
|
+
|
|
404
|
+
## Type Utilities
|
|
405
|
+
|
|
406
|
+
```typescript
|
|
407
|
+
type Factory<T = any> = (container: any) => T;
|
|
408
|
+
type DepsDefinition = Record<string, Factory>;
|
|
409
|
+
type ResolvedDeps<T extends DepsDefinition> = { readonly [K in keyof T]: ReturnType<T[K]> };
|
|
410
|
+
type Container<T extends Record<string, any> = Record<string, any>> = T & IContainer<T>;
|
|
411
|
+
interface ScopeOptions { name?: string }
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
## Error Reference
|
|
415
|
+
|
|
416
|
+
All errors extend `ContainerError`, which extends `Error`. Every error has:
|
|
417
|
+
- `hint: string` — actionable fix suggestion
|
|
418
|
+
- `details: Record<string, unknown>` — structured context for programmatic consumption
|
|
419
|
+
|
|
420
|
+
### ContainerConfigError
|
|
421
|
+
|
|
422
|
+
Thrown when a non-function value is passed in the deps definition.
|
|
423
|
+
|
|
424
|
+
```
|
|
425
|
+
'apiKey' must be a factory function, got string.
|
|
426
|
+
hint: "Wrap it: apiKey: () => 'sk-123'"
|
|
427
|
+
```
|
|
428
|
+
|
|
429
|
+
Constructor: `new ContainerConfigError(key: string, actualType: string)`
|
|
430
|
+
Details: `{ key, actualType }`
|
|
431
|
+
|
|
432
|
+
### ReservedKeyError
|
|
433
|
+
|
|
434
|
+
Thrown when a reserved container method name is used as a dependency key. Reserved keys: `scope`, `extend`, `preload`, `reset`, `inspect`, `describe`, `health`, `dispose`, `toString`.
|
|
435
|
+
|
|
436
|
+
```
|
|
437
|
+
'inspect' is a reserved container method.
|
|
438
|
+
hint: "Rename this dependency, e.g. 'inspectService' or 'myInspect'."
|
|
439
|
+
```
|
|
440
|
+
|
|
441
|
+
Constructor: `new ReservedKeyError(key: string, reserved: readonly string[])`
|
|
442
|
+
Details: `{ key, reserved }`
|
|
443
|
+
|
|
444
|
+
### ProviderNotFoundError
|
|
445
|
+
|
|
446
|
+
Thrown when a dependency cannot be found during resolution. Includes Levenshtein-based fuzzy suggestion if a similar key exists (>= 50% similarity).
|
|
447
|
+
|
|
448
|
+
```
|
|
449
|
+
Cannot resolve 'userServce': dependency 'userServce' not found.
|
|
450
|
+
Registered keys: [userService, logger, db]
|
|
451
|
+
Did you mean 'userService'?
|
|
452
|
+
hint: "Did you mean 'userService'? Or add 'userServce' to your container: ..."
|
|
453
|
+
```
|
|
454
|
+
|
|
455
|
+
Constructor: `new ProviderNotFoundError(key: string, chain: string[], registered: string[], suggestion?: string)`
|
|
456
|
+
Details: `{ key, chain, registered, suggestion }`
|
|
457
|
+
|
|
458
|
+
### CircularDependencyError
|
|
459
|
+
|
|
460
|
+
Thrown when a circular dependency is detected in the resolution chain.
|
|
461
|
+
|
|
462
|
+
```
|
|
463
|
+
Circular dependency detected while resolving 'authService'.
|
|
464
|
+
Cycle: authService -> userService -> authService
|
|
465
|
+
```
|
|
466
|
+
|
|
467
|
+
Constructor: `new CircularDependencyError(key: string, chain: string[])`
|
|
468
|
+
Details: `{ key, chain, cycle }`
|
|
469
|
+
|
|
470
|
+
### UndefinedReturnError
|
|
471
|
+
|
|
472
|
+
Thrown when a factory function returns `undefined`.
|
|
473
|
+
|
|
474
|
+
```
|
|
475
|
+
Factory 'db' returned undefined.
|
|
476
|
+
hint: "Your factory function returned undefined. Did you forget a return statement?"
|
|
477
|
+
```
|
|
478
|
+
|
|
479
|
+
Constructor: `new UndefinedReturnError(key: string, chain: string[])`
|
|
480
|
+
Details: `{ key, chain }`
|
|
481
|
+
|
|
482
|
+
### FactoryError
|
|
483
|
+
|
|
484
|
+
Thrown when a factory function throws an error during resolution. Wraps the original error.
|
|
485
|
+
|
|
486
|
+
```
|
|
487
|
+
Factory 'db' threw an error: "Connection refused"
|
|
488
|
+
hint: "Check the factory function for 'db'. The error occurred during instantiation."
|
|
489
|
+
```
|
|
490
|
+
|
|
491
|
+
Constructor: `new FactoryError(key: string, chain: string[], originalError: unknown)`
|
|
492
|
+
Details: `{ key, chain, originalError }` (originalError is the message string)
|
|
493
|
+
Additional property: `originalError: unknown` (the raw error object)
|
|
494
|
+
|
|
495
|
+
### ScopeMismatchWarning
|
|
496
|
+
|
|
497
|
+
Not an error — a warning emitted when a singleton depends on a transient. Surfaced via `container.health().warnings`.
|
|
498
|
+
|
|
499
|
+
```
|
|
500
|
+
Singleton 'userService' depends on transient 'requestId'.
|
|
501
|
+
hint: "The transient value was resolved once and is now frozen inside the singleton. ..."
|
|
502
|
+
```
|
|
503
|
+
|
|
504
|
+
Constructor: `new ScopeMismatchWarning(singletonKey: string, transientKey: string)`
|
|
505
|
+
Properties: `type: 'scope_mismatch'`, `message`, `hint`, `details: { singleton, transient }`
|
|
506
|
+
|
|
507
|
+
## Gotchas and Common Pitfalls
|
|
508
|
+
|
|
509
|
+
1. **Async onInit is fire-and-forget**: `onInit()` is called during property access but NOT awaited. Errors are swallowed. Use `await container.preload('key')` to properly await async initialization.
|
|
510
|
+
|
|
511
|
+
2. **Scope mismatch**: A singleton depending on a transient freezes the transient value. The singleton will always see the first resolved value. Check `container.health().warnings` for `scope_mismatch` warnings.
|
|
512
|
+
|
|
513
|
+
3. **Reserved keys**: `scope`, `extend`, `preload`, `reset`, `inspect`, `describe`, `health`, `dispose`, `toString` cannot be used as dependency keys. Using them throws `ReservedKeyError`.
|
|
514
|
+
|
|
515
|
+
4. **Undefined return**: Factories that return `undefined` (missing return statement, void function) throw `UndefinedReturnError`. Every factory must return a value.
|
|
516
|
+
|
|
517
|
+
5. **LIFO dispose order**: `dispose()` calls `onDestroy()` in reverse resolution order. If `db` was resolved before `cache`, `cache.onDestroy()` runs first.
|
|
518
|
+
|
|
519
|
+
6. **extend() shares cache**: `extend()` copies the singleton cache. Already-resolved singletons are shared. New resolutions in the extended container do NOT propagate back to the original.
|
|
520
|
+
|
|
521
|
+
7. **scope() vs extend()**: `scope()` creates a parent-child chain (child delegates unknown keys to parent). `extend()` creates a flat merged container. Use `scope()` for request-level isolation, `extend()` for additive composition.
|
|
522
|
+
|
|
523
|
+
8. **reset() is scope-local**: `reset()` only clears the cache of the container it's called on. In a scope, resetting a key does not affect the parent container's cache.
|
package/llms.txt
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# inwire
|
|
2
|
+
|
|
3
|
+
> Zero-ceremony dependency injection for TypeScript. Full inference, no decorators, no tokens.
|
|
4
|
+
> Proxy-based lazy singleton resolution. ~4.7KB gzipped, 0 runtime deps.
|
|
5
|
+
|
|
6
|
+
inwire uses factory functions `(container) => instance` as the single abstraction. Property access on the container triggers lazy resolution through a Proxy. Instances are cached as singletons by default. TypeScript infers the full container type from the factory definitions — no manual type wiring. Built-in introspection methods return serializable JSON, making the dependency graph directly consumable by LLMs and AI tooling.
|
|
7
|
+
|
|
8
|
+
## Core API
|
|
9
|
+
|
|
10
|
+
- `createContainer(defs)` — Creates a DI container from an object of factory functions. Returns a fully-typed Proxy.
|
|
11
|
+
- `transient(factory)` — Marks a factory as transient (new instance on every access, no caching).
|
|
12
|
+
- `detectDuplicateKeys(...modules)` — Detects keys that appear in more than one module object.
|
|
13
|
+
|
|
14
|
+
## Container Methods
|
|
15
|
+
|
|
16
|
+
- `.scope(extra, options?)` — Creates a child container with additional deps. Child inherits parent singletons. Pass `{ name }` for debugging/introspection.
|
|
17
|
+
- `.extend(extra)` — Returns a new container with merged factories. Shares existing singleton cache.
|
|
18
|
+
- `.preload(...keys)` — Eagerly resolves specific dependencies, or all if no keys given. Only way to await async `onInit()`.
|
|
19
|
+
- `.reset(...keys)` — Invalidates cached singletons, forcing re-creation on next access. Does not affect parent scopes.
|
|
20
|
+
- `.inspect()` — Returns the full dependency graph as `ContainerGraph` (serializable JSON).
|
|
21
|
+
- `.describe(key)` — Returns `ProviderInfo` for a single provider.
|
|
22
|
+
- `.health()` — Returns `ContainerHealth` with warnings (e.g. scope mismatches).
|
|
23
|
+
- `.dispose()` — Calls `onDestroy()` on all resolved instances in LIFO order.
|
|
24
|
+
|
|
25
|
+
## Types
|
|
26
|
+
|
|
27
|
+
- `Container<T>` — Resolved deps + container methods
|
|
28
|
+
- `Factory<T>` — `(container: any) => T`
|
|
29
|
+
- `DepsDefinition` — `Record<string, Factory>`
|
|
30
|
+
- `ResolvedDeps<T>` — Maps each key to its factory's return type
|
|
31
|
+
- `OnInit` — Interface: `onInit(): void | Promise<void>`
|
|
32
|
+
- `OnDestroy` — Interface: `onDestroy(): void | Promise<void>`
|
|
33
|
+
- `ScopeOptions` — `{ name?: string }` — options for `scope()`
|
|
34
|
+
- `ContainerGraph` — `{ name?: string, providers: Record<string, ProviderInfo> }`
|
|
35
|
+
- `ContainerHealth` — `{ totalProviders, resolved, unresolved, warnings }`
|
|
36
|
+
- `ContainerWarning` — `{ type: 'scope_mismatch' | 'duplicate_key', message, details }`
|
|
37
|
+
- `ProviderInfo` — `{ key, resolved, deps, scope: 'singleton' | 'transient' }`
|
|
38
|
+
|
|
39
|
+
## Errors
|
|
40
|
+
|
|
41
|
+
All errors extend `ContainerError` and include `hint` (actionable fix) and `details` (structured context).
|
|
42
|
+
|
|
43
|
+
- `ContainerConfigError` — Non-function value in deps definition
|
|
44
|
+
- `ReservedKeyError` — Reserved container method name used as dependency key (`reset` included)
|
|
45
|
+
- `ProviderNotFoundError` — Dependency not found during resolution (includes fuzzy suggestion)
|
|
46
|
+
- `CircularDependencyError` — Circular dependency detected in resolution chain
|
|
47
|
+
- `UndefinedReturnError` — Factory returned `undefined`
|
|
48
|
+
- `FactoryError` — Factory threw an error during resolution (wraps original error)
|
|
49
|
+
- `ScopeMismatchWarning` — Singleton depends on transient (warning, not error)
|
|
50
|
+
|
|
51
|
+
## Full Documentation
|
|
52
|
+
|
|
53
|
+
See `llms-full.txt` for complete API reference with signatures, examples, and gotchas.
|
package/package.json
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "inwire",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Zero-ceremony dependency injection for TypeScript. Full inference, no decorators, no tokens. Built-in introspection for AI tooling.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.cjs",
|
|
7
|
+
"module": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"import": {
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"default": "./dist/index.js"
|
|
14
|
+
},
|
|
15
|
+
"require": {
|
|
16
|
+
"types": "./dist/index.d.cts",
|
|
17
|
+
"default": "./dist/index.cjs"
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"files": [
|
|
22
|
+
"dist",
|
|
23
|
+
"llms.txt",
|
|
24
|
+
"llms-full.txt"
|
|
25
|
+
],
|
|
26
|
+
"scripts": {
|
|
27
|
+
"build": "tsup",
|
|
28
|
+
"test": "vitest run",
|
|
29
|
+
"test:watch": "vitest",
|
|
30
|
+
"test:coverage": "vitest run --coverage",
|
|
31
|
+
"lint": "tsc --noEmit",
|
|
32
|
+
"prepublishOnly": "npm run build"
|
|
33
|
+
},
|
|
34
|
+
"publishConfig": {
|
|
35
|
+
"access": "public"
|
|
36
|
+
},
|
|
37
|
+
"keywords": [
|
|
38
|
+
"dependency-injection",
|
|
39
|
+
"di",
|
|
40
|
+
"ioc",
|
|
41
|
+
"typescript",
|
|
42
|
+
"proxy",
|
|
43
|
+
"ai-first",
|
|
44
|
+
"clean-architecture"
|
|
45
|
+
],
|
|
46
|
+
"repository": {
|
|
47
|
+
"type": "git",
|
|
48
|
+
"url": "git+https://github.com/axelhamil/inwire.git"
|
|
49
|
+
},
|
|
50
|
+
"homepage": "https://github.com/axelhamil/inwire#readme",
|
|
51
|
+
"bugs": {
|
|
52
|
+
"url": "https://github.com/axelhamil/inwire/issues"
|
|
53
|
+
},
|
|
54
|
+
"license": "MIT",
|
|
55
|
+
"devDependencies": {
|
|
56
|
+
"@semantic-release/changelog": "^6.0.0",
|
|
57
|
+
"@semantic-release/git": "^10.0.0",
|
|
58
|
+
"@vitest/coverage-v8": "^3.2.4",
|
|
59
|
+
"semantic-release": "^24.0.0",
|
|
60
|
+
"tsup": "^8.0.0",
|
|
61
|
+
"typescript": "^5.4.0",
|
|
62
|
+
"vitest": "^3.0.0"
|
|
63
|
+
}
|
|
64
|
+
}
|