inwire 2.2.1 → 2.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -301,6 +301,101 @@ const full = withDb.module((b) =>
301
301
 
302
302
  `module()` uses the builder internally for typed `c`, then delegates to `extend()`. Works on `scope()` and `extend()` results too.
303
303
 
304
+ #### Deriving the container shape (Zod-style)
305
+
306
+ You don't need to maintain a manual interface for the container's full shape. Derive it from the container itself, exactly like `z.infer<typeof schema>`:
307
+
308
+ ```typescript
309
+ // container.ts
310
+ import { container } from 'inwire';
311
+ import { authModule } from './modules/auth.module';
312
+ import { billingModule } from './modules/billing.module';
313
+ import { persistenceModule } from './modules/persistence.module';
314
+
315
+ export const di = container()
316
+ .addModule(persistenceModule)
317
+ .addModule(authModule)
318
+ .addModule(billingModule)
319
+ .build();
320
+
321
+ // Single source of truth — derived, never written by hand.
322
+ // Use this as the type for handlers, controllers, etc.
323
+ export type Di = typeof di;
324
+ ```
325
+
326
+ > Note: `Di` is your own type alias, not the global `AppDeps` interface inwire exports for the Pinia-style pattern below. They serve different purposes — `Di` is consumed by your code; `AppDeps` augments inwire's typing.
327
+
328
+ Each module declares only the contracts it consumes via `<TDeps>` — and those contracts are the **interfaces you already have** in `domain/` or `contracts/` (Clean Architecture, DDD):
329
+
330
+ ```typescript
331
+ // modules/auth.module.ts
332
+ import { defineModule } from 'inwire';
333
+ import type { IUserRepository } from '../contracts/IUserRepository';
334
+ import type { IAuthProvider } from '../contracts/IAuthProvider';
335
+ import { BetterAuthProvider } from '../infrastructure/BetterAuthProvider';
336
+ import { SignInUseCase } from '../application/SignInUseCase';
337
+
338
+ export const authModule = defineModule<{ IUserRepository: IUserRepository }>()((b) =>
339
+ b
340
+ .add('IAuthProvider', (): IAuthProvider => new BetterAuthProvider())
341
+ .add('SignInUseCase', (c) => new SignInUseCase(c.IUserRepository, c.IAuthProvider)),
342
+ );
343
+ ```
344
+
345
+ Why this is the right pattern:
346
+ - **No shape interface to maintain.** Add a binding anywhere → `Di` grows automatically. Remove one → it shrinks.
347
+ - **No `declare module` augmentation.** No global state, no import side-effects.
348
+ - **Local prerequisites.** A module's `<TDeps>` is its API contract — three lines max, exactly what it needs.
349
+ - **Cross-module references work at build time.** When `authModule` registers `SignInUseCase` that needs `IUserRepository` (provided by `persistenceModule`), the prerequisite check at `addModule()` time enforces the order.
350
+
351
+ See [examples/05-zod-style-typing.ts](examples/05-zod-style-typing.ts) for a full walk-through with three modules and a derived `Di`.
352
+
353
+ #### Cross-module forward references (Pinia-style)
354
+
355
+ When a module needs to consume a binding **provided by another module loaded later**, the local `<TDeps>` pattern can't help — the prerequisite would have to list everything the module sees, defeating the locality. For that case, inwire exposes an augmentable global interface, exactly like Pinia's `PiniaCustomProperties` or Vue's `ComponentCustomProperties`:
356
+
357
+ ```typescript
358
+ import 'inwire';
359
+
360
+ declare module 'inwire' {
361
+ interface AppDeps {
362
+ IUserRepository: IUserRepository;
363
+ SignInUseCase: SignInUseCase;
364
+ }
365
+ }
366
+ ```
367
+
368
+ Each module file augments `AppDeps` with the bindings **it provides**. When you call `defineModule()` *without* a `<TDeps>` generic, `c` is typed as the global `AppDeps` — so `c.X` resolves transparently across modules, regardless of declaration order:
369
+
370
+ ```typescript
371
+ // modules/auth.module.ts
372
+ declare module 'inwire' {
373
+ interface AppDeps {
374
+ IAuthProvider: IAuthProvider;
375
+ SignInUseCase: SignInUseCase;
376
+ }
377
+ }
378
+
379
+ export const authModule = defineModule()((b) =>
380
+ b
381
+ .add('IAuthProvider', (): IAuthProvider => new BetterAuthProvider())
382
+ .add('SignInUseCase', (c) => new SignInUseCase(c.IUserRepository, c.IAuthProvider)),
383
+ // ^^^^^^^^^^^^^^^^^^^^
384
+ // provided by another module — type-checked via AppDeps
385
+ );
386
+ ```
387
+
388
+ Trade-off vs `defineModule<TDeps>()`:
389
+
390
+ | Pattern | You declare | Cross-module forward ref |
391
+ |---|---|---|
392
+ | `defineModule<TDeps>()` | what the module **consumes** (inputs) | no — must be already added |
393
+ | `defineModule()` + `declare module` | what the module **adds** (outputs) | yes — order-independent |
394
+
395
+ Both patterns coexist. Mix freely — the explicit `<TDeps>` always overrides the global mode for that module. Why two? Because not every project needs the global augmentation, and not every module needs a tight prerequisite list. Pick the one that feels lighter for the file you're writing.
396
+
397
+ See [examples/06-pinia-augmentation.ts](examples/06-pinia-augmentation.ts) for a full walk-through with two modules cross-referencing each other.
398
+
304
399
  ### Preload
305
400
 
306
401
  ```typescript
@@ -382,6 +477,8 @@ detectDuplicateKeys(authModule, userModule);
382
477
  | [02-modular-testing.ts](examples/02-modular-testing.ts) | `npm run example:test` | Free mode, instance values, test overrides, extend + transient |
383
478
  | [03-plugin-system.ts](examples/03-plugin-system.ts) | `npm run example:plugin` | Extend chain, scoped jobs, health, JSON graph for LLM |
384
479
  | [04-modules.ts](examples/04-modules.ts) | `npm run example:modules` | addModule, module() post-build, typed reusable modules |
480
+ | [05-zod-style-typing.ts](examples/05-zod-style-typing.ts) | `npm run example:typing` | `type AppDeps = typeof di` pattern, Clean Arch contracts, no manual interface |
481
+ | [06-pinia-augmentation.ts](examples/06-pinia-augmentation.ts) | `npm run example:pinia` | Cross-module forward references via `declare module 'inwire'`, order-independent typing |
385
482
 
386
483
  ## Architecture
387
484
 
package/dist/index.d.mts CHANGED
@@ -188,6 +188,28 @@ type Factory<T = unknown> = (container: unknown) => T;
188
188
  * Reserved method names on the container that cannot be used as dependency keys.
189
189
  */
190
190
  declare const RESERVED_KEYS: readonly ["scope", "extend", "module", "preload", "reset", "inspect", "describe", "health", "dispose", "toString"];
191
+ /**
192
+ * Global, augmentable interface describing the application's dependency shape.
193
+ *
194
+ * Empty by default. Each module file augments it with the bindings IT provides,
195
+ * enabling **cross-module forward references** in factories — `c.X` resolves
196
+ * even when `X` is added by another module loaded later.
197
+ *
198
+ * @example Augment from a module file:
199
+ * ```typescript
200
+ * declare module 'inwire' {
201
+ * interface AppDeps {
202
+ * IUserRepository: IUserRepository;
203
+ * SignInUseCase: SignInUseCase;
204
+ * }
205
+ * }
206
+ * ```
207
+ *
208
+ * When `defineModule()` is called without an explicit `<TDeps>` generic, the
209
+ * builder's `c` parameter is typed as `AppDeps` — the union of every module's
210
+ * augmentations. TypeScript merges these declarations across files.
211
+ */
212
+ interface AppDeps {}
191
213
  /**
192
214
  * Options for creating a scoped container.
193
215
  */
@@ -351,6 +373,12 @@ interface ContainerWarning {
351
373
  }
352
374
  //#endregion
353
375
  //#region src/application/container-builder.d.ts
376
+ /**
377
+ * `T` with the keys of `U` overridden by `U` (no duplicate-key intersection).
378
+ * Required to avoid `A & A → never` when classes have private members and the
379
+ * same key is declared in both `T` and `U` (e.g. global AppDeps + module add).
380
+ */
381
+ type Override<T, U> = Omit<T, keyof U> & U;
354
382
  /**
355
383
  * Fluent builder that constructs a typed DI container incrementally.
356
384
  *
@@ -369,16 +397,22 @@ declare class ContainerBuilder<TContract extends Record<string, any> = Record<st
369
397
  * Convention: `typeof value === 'function'` → factory. Otherwise → instance (wrapped in `() => value`).
370
398
  * To register a function as a value: `add('fn', () => myFunction)`.
371
399
  */
372
- 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>>;
400
+ 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, Override<TBuilt, Record<K, V>>>;
373
401
  /**
374
402
  * Registers a transient dependency (new instance on every access).
375
403
  */
376
- 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>>;
404
+ 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, Override<TBuilt, Record<K, V>>>;
377
405
  /**
378
406
  * Applies a module — a function that chains `.add()` calls on this builder.
379
- * `c` in the module's factories is fully typed with all previously registered deps.
407
+ *
408
+ * `TDepsM` (the module's expected prereqs) is inferred independently from the
409
+ * builder's current `TBuilt`. Prereq satisfaction is NOT enforced at the type
410
+ * level on purpose: in global mode (`defineModule()` typed against `AppDeps`)
411
+ * the prereq surface is the full app, never the partial builder state. The
412
+ * runtime guarantees correctness via `ProviderNotFoundError` if a key is
413
+ * missing at resolution time.
380
414
  */
381
- addModule<TNew extends Record<string, any>>(module: (builder: ContainerBuilder<TContract, TBuilt>) => ContainerBuilder<TContract, TNew>): ContainerBuilder<TContract, TBuilt & TNew>;
415
+ addModule<TDepsM extends Record<string, any>, TNew extends Record<string, any>>(module: (builder: ContainerBuilder<TContract, TDepsM>) => ContainerBuilder<TContract, TNew>): ContainerBuilder<TContract, Override<TBuilt, TNew>>;
382
416
  /**
383
417
  * Merges a standalone builder into this one. All factories of `other` are copied.
384
418
  * The accumulated type becomes `TBuilt & TOther`, so subsequent factories can
@@ -393,7 +427,7 @@ declare class ContainerBuilder<TContract extends Record<string, any> = Record<st
393
427
  * Cross-builder dependencies are resolved at build time. Reserved keys throw.
394
428
  * Duplicate keys override silently — same semantics as `.add()` over an existing key.
395
429
  */
396
- merge<TOther extends Record<string, unknown>>(other: ContainerBuilder<Record<string, unknown>, TOther>): ContainerBuilder<TContract, TBuilt & TOther>;
430
+ merge<TOther extends Record<string, unknown>>(other: ContainerBuilder<Record<string, unknown>, TOther>): ContainerBuilder<TContract, Override<TBuilt, TOther>>;
397
431
  /**
398
432
  * Returns the accumulated factories as a plain record.
399
433
  * @internal Used by `module()` on the container and `merge()` on builders.
@@ -437,29 +471,49 @@ declare function container<T extends Record<string, any> = Record<string, unknow
437
471
  *
438
472
  * Use {@link defineModule} to build a `Module` with strong inference.
439
473
  */
440
- type Module<TDeps extends Record<string, any> = {}, TBuilt extends Record<string, any> = TDeps> = (builder: ContainerBuilder<Record<string, unknown>, TDeps>) => ContainerBuilder<Record<string, unknown>, TBuilt>;
474
+ type Module<TDeps extends Record<string, any> = AppDeps, TBuilt extends Record<string, any> = TDeps> = (builder: ContainerBuilder<Record<string, unknown>, TDeps>) => ContainerBuilder<Record<string, unknown>, TBuilt>;
441
475
  /**
442
- * Defines a reusable, strongly-typed module without importing the host's deps interface.
476
+ * Defines a reusable, strongly-typed module.
443
477
  *
444
- * Pattern: `defineModule<Prerequisites>()(builder => builder.add(...))`.
445
- * Prerequisites are declared **locally**, not pulled from a global `AppDeps`.
446
- * The output type is **inferred** from the chained `.add()` calls — no manual signature.
478
+ * Two modes, picked by whether you pass `<TDeps>` explicitly:
447
479
  *
448
- * @example
480
+ * - **Global mode** (`defineModule()`, no generic): `c` is typed as `AppDeps`,
481
+ * the augmentable global interface. Each module file augments `AppDeps` with
482
+ * what it provides via `declare module 'inwire' { interface AppDeps { … } }`.
483
+ * Cross-module forward references work transparently — `c.X` resolves even
484
+ * when `X` is added by another module.
485
+ * - **Local mode** (`defineModule<TDeps>()`): `c` is typed as `TDeps`, declared
486
+ * locally inline. No global augmentation needed. Use when the module's
487
+ * prerequisites are a tight, fixed surface.
488
+ *
489
+ * The output type is always **inferred** from the chained `.add()` calls.
490
+ *
491
+ * @example Global mode (Pinia-style):
449
492
  * ```typescript
450
- * import { defineModule } from 'inwire';
493
+ * declare module 'inwire' {
494
+ * interface AppDeps {
495
+ * IUserRepository: IUserRepository;
496
+ * SignInUseCase: SignInUseCase;
497
+ * }
498
+ * }
451
499
  *
500
+ * export const authModule = defineModule()((b) =>
501
+ * b
502
+ * .add('IUserRepository', () => new DrizzleUserRepository())
503
+ * .add('SignInUseCase', (c) => new SignInUseCase(c.IUserRepository, c.IAuthProvider)),
504
+ * // ^^^^^^^^^^^^^^^
505
+ * // provided by another module — typed via AppDeps
506
+ * );
507
+ * ```
508
+ *
509
+ * @example Local mode (explicit prerequisites):
510
+ * ```typescript
452
511
  * const billingModule = defineModule<{ eventBus: EventBus }>()((b) =>
453
512
  * b.add('subscribeUseCase', (c) => new SubscribeUseCase(c.eventBus)),
454
513
  * );
455
- *
456
- * const di = container()
457
- * .add('eventBus', () => new EventBus())
458
- * .addModule(billingModule)
459
- * .build();
460
514
  * ```
461
515
  */
462
- declare function defineModule<TDeps extends Record<string, any> = {}>(): <TBuilt extends Record<string, any>>(fn: (builder: ContainerBuilder<Record<string, unknown>, TDeps>) => ContainerBuilder<Record<string, unknown>, TBuilt>) => Module<TDeps, TBuilt>;
516
+ declare function defineModule<TDeps extends Record<string, any> = AppDeps>(): <TBuilt extends Record<string, any>>(fn: (builder: ContainerBuilder<Record<string, unknown>, TDeps>) => ContainerBuilder<Record<string, unknown>, TBuilt>) => Module<TDeps, TBuilt>;
463
517
  /**
464
518
  * Extracts the prerequisite deps of a `Module`.
465
519
  */
@@ -525,7 +579,7 @@ declare function detectDuplicateKeys(...modules: Record<string, unknown>[]): str
525
579
  * app.requestId; // 'def-456' (different!)
526
580
  * ```
527
581
  */
528
- declare function transient<T>(factory: Factory<T>): Factory<T>;
582
+ declare function transient<T, C = unknown>(factory: (container: C) => T): (container: C) => T;
529
583
  //#endregion
530
- export { AsyncInitErrorWarning, CircularDependencyError, type Container, ContainerBuilder, ContainerConfigError, ContainerError, type ContainerGraph, type ContainerHealth, type ContainerWarning, type Factory, FactoryError, type IContainer, type InferModuleBuilt, type InferModuleDeps, type Module, type OnDestroy, type OnInit, type ProviderInfo, ProviderNotFoundError, ReservedKeyError, ScopeMismatchWarning, type ScopeOptions, UndefinedReturnError, container, defineModule, detectDuplicateKeys, transient };
584
+ export { type AppDeps, AsyncInitErrorWarning, CircularDependencyError, type Container, ContainerBuilder, ContainerConfigError, ContainerError, type ContainerGraph, type ContainerHealth, type ContainerWarning, type Factory, FactoryError, type IContainer, type InferModuleBuilt, type InferModuleDeps, type Module, type OnDestroy, type OnInit, type ProviderInfo, ProviderNotFoundError, ReservedKeyError, ScopeMismatchWarning, type ScopeOptions, UndefinedReturnError, container, defineModule, detectDuplicateKeys, transient };
531
585
  //# sourceMappingURL=index.d.mts.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "inwire",
3
- "version": "2.2.1",
3
+ "version": "2.3.1",
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",
@@ -20,16 +20,18 @@
20
20
  "test": "vitest run",
21
21
  "test:watch": "vitest",
22
22
  "test:coverage": "vitest run --coverage",
23
- "typecheck": "tsc --noEmit",
23
+ "typecheck": "tsc --noEmit && tsc --noEmit -p examples/tsconfig.json",
24
24
  "lint": "biome check",
25
25
  "lint:fix": "biome check --fix",
26
26
  "format": "biome format --write",
27
27
  "format:check": "biome format",
28
- "check": "biome check && tsc --noEmit",
28
+ "check": "biome check && pnpm typecheck",
29
29
  "example:web": "tsx examples/01-web-service.ts",
30
30
  "example:test": "tsx examples/02-modular-testing.ts",
31
31
  "example:plugin": "tsx examples/03-plugin-system.ts",
32
32
  "example:modules": "tsx examples/04-modules.ts",
33
+ "example:typing": "tsx examples/05-zod-style-typing.ts",
34
+ "example:pinia": "tsx examples/06-pinia-augmentation.ts",
33
35
  "prepublishOnly": "npm run build"
34
36
  },
35
37
  "publishConfig": {