inwire 2.2.1 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -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
  */
@@ -437,29 +459,49 @@ declare function container<T extends Record<string, any> = Record<string, unknow
437
459
  *
438
460
  * Use {@link defineModule} to build a `Module` with strong inference.
439
461
  */
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>;
462
+ 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
463
  /**
442
- * Defines a reusable, strongly-typed module without importing the host's deps interface.
464
+ * Defines a reusable, strongly-typed module.
443
465
  *
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.
466
+ * Two modes, picked by whether you pass `<TDeps>` explicitly:
447
467
  *
448
- * @example
468
+ * - **Global mode** (`defineModule()`, no generic): `c` is typed as `AppDeps`,
469
+ * the augmentable global interface. Each module file augments `AppDeps` with
470
+ * what it provides via `declare module 'inwire' { interface AppDeps { … } }`.
471
+ * Cross-module forward references work transparently — `c.X` resolves even
472
+ * when `X` is added by another module.
473
+ * - **Local mode** (`defineModule<TDeps>()`): `c` is typed as `TDeps`, declared
474
+ * locally inline. No global augmentation needed. Use when the module's
475
+ * prerequisites are a tight, fixed surface.
476
+ *
477
+ * The output type is always **inferred** from the chained `.add()` calls.
478
+ *
479
+ * @example Global mode (Pinia-style):
449
480
  * ```typescript
450
- * import { defineModule } from 'inwire';
481
+ * declare module 'inwire' {
482
+ * interface AppDeps {
483
+ * IUserRepository: IUserRepository;
484
+ * SignInUseCase: SignInUseCase;
485
+ * }
486
+ * }
487
+ *
488
+ * export const authModule = defineModule()((b) =>
489
+ * b
490
+ * .add('IUserRepository', () => new DrizzleUserRepository())
491
+ * .add('SignInUseCase', (c) => new SignInUseCase(c.IUserRepository, c.IAuthProvider)),
492
+ * // ^^^^^^^^^^^^^^^
493
+ * // provided by another module — typed via AppDeps
494
+ * );
495
+ * ```
451
496
  *
497
+ * @example Local mode (explicit prerequisites):
498
+ * ```typescript
452
499
  * const billingModule = defineModule<{ eventBus: EventBus }>()((b) =>
453
500
  * b.add('subscribeUseCase', (c) => new SubscribeUseCase(c.eventBus)),
454
501
  * );
455
- *
456
- * const di = container()
457
- * .add('eventBus', () => new EventBus())
458
- * .addModule(billingModule)
459
- * .build();
460
502
  * ```
461
503
  */
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>;
504
+ 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
505
  /**
464
506
  * Extracts the prerequisite deps of a `Module`.
465
507
  */
@@ -527,5 +569,5 @@ declare function detectDuplicateKeys(...modules: Record<string, unknown>[]): str
527
569
  */
528
570
  declare function transient<T>(factory: Factory<T>): Factory<T>;
529
571
  //#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 };
572
+ 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
573
  //# 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.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",
@@ -30,6 +30,8 @@
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": {