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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 inwire contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,465 @@
1
+ # inwire
2
+
3
+ Zero-ceremony dependency injection for TypeScript. Full inference, no decorators, no tokens. Built-in introspection for AI tooling and debugging.
4
+
5
+ [![Bundle size](https://img.shields.io/bundlephobia/minzip/inwire)](https://bundlephobia.com/package/inwire)
6
+ [![Dependencies](https://img.shields.io/librariesio/release/npm/inwire)](https://www.npmjs.com/package/inwire)
7
+ [![Tests](https://img.shields.io/badge/tests-119%20passing-brightgreen)](https://github.com)
8
+ [![Speed](https://img.shields.io/badge/tests-%3C30ms-brightgreen)](https://github.com)
9
+ [![Coverage](https://img.shields.io/badge/coverage-%E2%89%A590%25-brightgreen)](./vitest.config.ts)
10
+ [![TypeScript](https://img.shields.io/badge/TypeScript-strict-blue)](https://www.typescriptlang.org/)
11
+ [![License](https://img.shields.io/badge/license-MIT-green)](./LICENSE)
12
+
13
+ ## Install
14
+
15
+ ```bash
16
+ npm/pnpm i inwire
17
+ ```
18
+
19
+ ## Quick Start
20
+
21
+ ```typescript
22
+ import { createContainer } from 'inwire';
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
+ });
30
+
31
+ container.userService; // lazy, singleton, fully typed
32
+ ```
33
+
34
+ That's it. Every dependency is a factory function `(container) => instance`. Access a property, get a singleton. TypeScript infers everything.
35
+
36
+ ## ⚠️ Important: Async Lifecycle
37
+
38
+ 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
+
40
+ **`preload()` is the only way to safely initialize async services.**
41
+
42
+ ```typescript
43
+ class Database implements OnInit {
44
+ async onInit() { await this.connect(); }
45
+ }
46
+
47
+ const container = createContainer({
48
+ db: () => new Database(),
49
+ });
50
+
51
+ // ❌ BAD — onInit() fires but is NOT awaited, errors are lost
52
+ container.db;
53
+
54
+ // ✅ GOOD — onInit() is awaited, errors surface immediately
55
+ await container.preload('db');
56
+ container.db; // safe to use, fully initialized
57
+ ```
58
+
59
+ ## Why use a DI container?
60
+
61
+ Most JS/TS projects wire dependencies through direct imports. A DI container gives you three things imports don't:
62
+
63
+ - **Testability** — swap any dependency for a mock at creation time, no monkey-patching or `jest.mock`
64
+ - **Decoupling** — program against interfaces, not concrete imports; swap implementations without touching consumers
65
+ - **Visibility** — inspect the full dependency graph at runtime, catch scope mismatches, and monitor container health
66
+
67
+ ## Features
68
+
69
+ ### Lazy Singletons (default)
70
+
71
+ Factories run on first access and the result is cached forever. No eager init, no manual wiring.
72
+
73
+ ```typescript
74
+ const container = createContainer({
75
+ db: () => new Database(process.env.DB_URL!),
76
+ });
77
+
78
+ container.db; // creates Database
79
+ container.db; // same instance (cached)
80
+ ```
81
+
82
+ ### Dependency Inversion
83
+
84
+ Annotate the return type to program against an interface:
85
+
86
+ ```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
+ ```
95
+
96
+ ### Modules = Spread Objects
97
+
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
+ });
114
+ ```
115
+
116
+ ### Test Overrides
117
+
118
+ Replace any dependency with a mock at container creation:
119
+
120
+ ```typescript
121
+ const container = createContainer({
122
+ ...productionDeps,
123
+ db: () => new InMemoryDatabase(), // override
124
+ });
125
+ ```
126
+
127
+ ### Scopes
128
+
129
+ Create child containers for request-level isolation. The child inherits all parent singletons and adds its own:
130
+
131
+ ```typescript
132
+ const container = createContainer({
133
+ logger: () => new LoggerService(),
134
+ db: () => new Database(),
135
+ });
136
+
137
+ // Per-request child container
138
+ const request = container.scope({
139
+ requestId: () => crypto.randomUUID(),
140
+ currentUser: () => getCurrentUser(),
141
+ });
142
+
143
+ request.requestId; // scoped singleton (unique to this child)
144
+ request.logger; // inherited from parent
145
+ ```
146
+
147
+ #### Named Scopes
148
+
149
+ Pass an options object to name a scope for debugging and introspection:
150
+
151
+ ```typescript
152
+ const request = container.scope(
153
+ { requestId: () => crypto.randomUUID() },
154
+ { name: 'request-123' },
155
+ );
156
+
157
+ String(request); // "Scope(request-123) { requestId (pending) }"
158
+ request.inspect().name; // "request-123"
159
+ ```
160
+
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
+ ### Lifecycle (onInit / onDestroy / dispose)
178
+
179
+ Implement `onInit()` for post-creation setup and `onDestroy()` for cleanup:
180
+
181
+ ```typescript
182
+ import type { OnInit, OnDestroy } from 'inwire';
183
+
184
+ class Database implements OnInit, OnDestroy {
185
+ async onInit() { await this.connect(); }
186
+ async onDestroy() { await this.disconnect(); }
187
+ }
188
+
189
+ const container = createContainer({
190
+ db: () => new Database(),
191
+ });
192
+
193
+ container.db; // resolves + calls onInit()
194
+ await container.dispose(); // calls onDestroy() on all resolved instances (LIFO order)
195
+ ```
196
+
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
+ ### Extend
200
+
201
+ Add dependencies to an existing container without mutating it. Existing singletons are shared:
202
+
203
+ ```typescript
204
+ const base = createContainer({
205
+ logger: () => new LoggerService(),
206
+ });
207
+
208
+ const extended = base.extend({
209
+ db: (c) => new Database(c.logger),
210
+ });
211
+
212
+ extended.logger; // shared singleton from base
213
+ extended.db; // new dependency
214
+ ```
215
+
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.
217
+
218
+ ### Preload
219
+
220
+ Eagerly resolve specific dependencies at startup (warm-up):
221
+
222
+ ```typescript
223
+ const container = createContainer({
224
+ db: () => new Database(),
225
+ cache: () => new Redis(),
226
+ logger: () => new LoggerService(),
227
+ });
228
+
229
+ await container.preload('db', 'cache');
230
+ // db and cache are now resolved, logger is still lazy
231
+
232
+ await container.preload();
233
+ // resolve ALL dependencies at once
234
+ ```
235
+
236
+ **This is how you safely initialize async services.** See [⚠️ Important: Async Lifecycle](#️-important-async-lifecycle) above.
237
+
238
+ ### Reset
239
+
240
+ Invalidate cached singletons to force re-creation on next access:
241
+
242
+ ```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
251
+
252
+ // Other singletons are untouched
253
+ // Reset in a scope does not affect the parent
254
+ ```
255
+
256
+ ### Introspection
257
+
258
+ Built-in tools for debugging and AI analysis:
259
+
260
+ ```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' }
268
+
269
+ // Health check
270
+ container.health();
271
+ // { totalProviders: 4, resolved: ['db', 'logger'], unresolved: ['cache'], warnings: [] }
272
+
273
+ // Human-readable string
274
+ String(container);
275
+ // "Container { db -> [] (resolved), logger (pending) }"
276
+ ```
277
+
278
+ Feed the graph to an LLM or diagnostic tool:
279
+
280
+ ```typescript
281
+ const graph = JSON.stringify(container.inspect(), null, 2);
282
+
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
+ });
287
+ ```
288
+
289
+ ### Smart Errors
290
+
291
+ 7 error types, each with `hint`, `details`, and actionable suggestions:
292
+
293
+ ```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
+ // Reserved key
300
+ createContainer({ inspect: () => 'foo' });
301
+ // ReservedKeyError: 'inspect' is a reserved container method.
302
+ // hint: "Rename this dependency, e.g. 'inspectService' or 'myInspect'."
303
+
304
+ // Missing dependency with fuzzy suggestion
305
+ container.userServce; // typo
306
+ // ProviderNotFoundError: Cannot resolve 'userServce': dependency 'userServce' not found.
307
+ // hint: "Did you mean 'userService'?"
308
+
309
+ // 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"
319
+ ```
320
+
321
+ ### Scope Mismatch Detection
322
+
323
+ Warns when a singleton depends on a transient (the transient value gets frozen inside the singleton):
324
+
325
+ ```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;
332
+ // [{ type: 'scope_mismatch', message: "Singleton 'userService' depends on transient 'requestId'." }]
333
+ ```
334
+
335
+ ### Fuzzy Key Suggestion
336
+
337
+ When a key is not found, Levenshtein-based matching suggests the closest registered key (>= 50% similarity):
338
+
339
+ ```typescript
340
+ container.userServce;
341
+ // ProviderNotFoundError: Did you mean 'userService'?
342
+ ```
343
+
344
+ ### Duplicate Key Detection
345
+
346
+ Detect accidental key collisions when spreading modules:
347
+
348
+ ```typescript
349
+ import { detectDuplicateKeys } from 'inwire';
350
+
351
+ const duplicates = detectDuplicateKeys(authModule, userModule, dbModule);
352
+ // ['logger'] — appears in more than one module
353
+ ```
354
+
355
+ ## Comparison
356
+
357
+ | Feature | inwire | awilix | tsyringe | Inversify | NestJS |
358
+ |---|---|---|---|---|---|
359
+ | Decorators required | No | No | Yes | Yes | Yes |
360
+ | Tokens/symbols | No | No | Yes | Yes | Yes |
361
+ | Full TS inference | Yes | No (manual Cradle) | Partial | Partial | Partial |
362
+ | Lazy singletons | Default | Default | Manual | Manual | Manual |
363
+ | Scoped containers | `.scope()` | `.createScope()` | `.createChildContainer()` | `.createChild()` | Module scope |
364
+ | Lifecycle hooks | `onInit`/`onDestroy` | `dispose()` | `beforeResolution`/`afterResolution` | No | `onModuleInit`/`onModuleDestroy` |
365
+ | Introspection | Built-in JSON graph | `.registrations` | `isRegistered()` | No | DevTools |
366
+ | 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 |
369
+
370
+ ## Architecture
371
+
372
+ ```
373
+ src/
374
+ index.ts # barrel export
375
+ domain/
376
+ types.ts # interfaces, types, RESERVED_KEYS
377
+ errors.ts # 7 error classes + ScopeMismatchWarning
378
+ lifecycle.ts # OnInit / OnDestroy interfaces
379
+ validation.ts # Validator, detectDuplicateKeys, Levenshtein
380
+ infrastructure/
381
+ proxy-handler.ts # Resolver (Proxy handler, cache, cycle detection)
382
+ transient.ts # transient() marker
383
+ application/
384
+ create-container.ts # createContainer, buildContainerProxy
385
+ scope.ts # createScope (child containers)
386
+ introspection.ts # inspect, describe, health, toString
387
+ ```
388
+
389
+ ## LLM / AI Integration
390
+
391
+ This package ships with [llms.txt](https://llmstxt.org/) files for AI-assisted development:
392
+
393
+ - **`llms.txt`** — Concise index following the llms.txt standard
394
+ - **`llms-full.txt`** — Complete API reference optimized for LLM context windows
395
+
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
+ Compatible with [Context7](https://context7.com/) and any tool that supports the llms.txt standard.
403
+
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
+ ## API Reference
411
+
412
+ ### Functions
413
+
414
+ | Export | Description |
415
+ |---|---|
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 |
419
+
420
+ ### Container Methods
421
+
422
+ | Method | Description |
423
+ |---|---|
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 |
432
+
433
+ ### Types
434
+
435
+ | Export | Description |
436
+ |---|---|
437
+ | `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` |
441
+ | `OnInit` | Interface with `onInit(): void \| Promise<void>` |
442
+ | `OnDestroy` | Interface with `onDestroy(): void \| Promise<void>` |
443
+ | `ContainerGraph` | Return type of `inspect()` |
444
+ | `ContainerHealth` | Return type of `health()` |
445
+ | `ContainerWarning` | Warning object (`scope_mismatch` \| `duplicate_key`) |
446
+ | `ProviderInfo` | Return type of `describe()` |
447
+ | `IContainer<T>` | Container methods interface |
448
+ | `ScopeOptions` | Options for `scope()` (`{ name?: string }`) |
449
+
450
+ ### Errors
451
+
452
+ | Export | Thrown when |
453
+ |---|---|
454
+ | `ContainerError` | Base class for all errors |
455
+ | `ContainerConfigError` | Non-function value in deps definition |
456
+ | `ReservedKeyError` | Reserved key used as dependency name |
457
+ | `ProviderNotFoundError` | Dependency not found during resolution |
458
+ | `CircularDependencyError` | Circular dependency detected |
459
+ | `UndefinedReturnError` | Factory returned `undefined` |
460
+ | `FactoryError` | Factory threw during resolution |
461
+ | `ScopeMismatchWarning` | Singleton depends on transient |
462
+
463
+ ## License
464
+
465
+ MIT