inwire 2.2.0 → 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 +97 -0
- package/dist/index.d.mts +57 -15
- package/package.json +3 -1
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
|
*/
|
|
@@ -378,7 +400,7 @@ declare class ContainerBuilder<TContract extends Record<string, any> = Record<st
|
|
|
378
400
|
* Applies a module — a function that chains `.add()` calls on this builder.
|
|
379
401
|
* `c` in the module's factories is fully typed with all previously registered deps.
|
|
380
402
|
*/
|
|
381
|
-
addModule<TNew extends Record<string, any>>(module: (builder: ContainerBuilder<TContract, TBuilt>) => ContainerBuilder<TContract, TNew>): ContainerBuilder<TContract, TNew>;
|
|
403
|
+
addModule<TNew extends Record<string, any>>(module: (builder: ContainerBuilder<TContract, TBuilt>) => ContainerBuilder<TContract, TNew>): ContainerBuilder<TContract, TBuilt & TNew>;
|
|
382
404
|
/**
|
|
383
405
|
* Merges a standalone builder into this one. All factories of `other` are copied.
|
|
384
406
|
* The accumulated type becomes `TBuilt & TOther`, so subsequent factories can
|
|
@@ -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> =
|
|
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
|
|
464
|
+
* Defines a reusable, strongly-typed module.
|
|
443
465
|
*
|
|
444
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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> =
|
|
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.
|
|
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": {
|