@codefast/di 0.3.13 → 0.3.14-canary.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.
Files changed (53) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/README.md +270 -234
  3. package/dist/binding-select.d.mts +17 -6
  4. package/dist/binding-select.mjs +17 -6
  5. package/dist/binding.d.mts +167 -34
  6. package/dist/binding.mjs +111 -14
  7. package/dist/constraints.d.mts +18 -3
  8. package/dist/constraints.mjs +18 -3
  9. package/dist/container.d.mts +85 -35
  10. package/dist/container.mjs +140 -6
  11. package/dist/decorators/inject.d.mts +40 -9
  12. package/dist/decorators/inject.mjs +50 -11
  13. package/dist/decorators/injectable.d.mts +2 -1
  14. package/dist/decorators/injectable.mjs +14 -2
  15. package/dist/decorators/lifecycle-decorators.d.mts +16 -4
  16. package/dist/decorators/lifecycle-decorators.mjs +16 -4
  17. package/dist/dependency-graph.d.mts +36 -13
  18. package/dist/dependency-graph.mjs +42 -8
  19. package/dist/errors.d.mts +132 -21
  20. package/dist/errors.mjs +126 -18
  21. package/dist/graph-adapters/cytoscape.d.mts +10 -0
  22. package/dist/graph-adapters/cytoscape.mjs +40 -0
  23. package/dist/graph-adapters/dot.d.mts +9 -0
  24. package/dist/graph-adapters/dot.mjs +97 -0
  25. package/dist/graph-adapters/reactflow.d.mts +10 -0
  26. package/dist/graph-adapters/reactflow.mjs +80 -0
  27. package/dist/graph-adapters/types.d.mts +91 -0
  28. package/dist/graph-adapters/types.mjs +1 -0
  29. package/dist/index.d.mts +2 -3
  30. package/dist/index.mjs +2 -2
  31. package/dist/inspector.d.mts +42 -40
  32. package/dist/inspector.mjs +18 -169
  33. package/dist/lifecycle.d.mts +28 -6
  34. package/dist/lifecycle.mjs +29 -10
  35. package/dist/metadata/metadata-keys.d.mts +17 -6
  36. package/dist/metadata/metadata-keys.mjs +17 -6
  37. package/dist/metadata/metadata-types.d.mts +42 -18
  38. package/dist/metadata/param-registry.mjs +6 -0
  39. package/dist/metadata/symbol-metadata-reader.d.mts +20 -3
  40. package/dist/metadata/symbol-metadata-reader.mjs +23 -4
  41. package/dist/module.d.mts +46 -2
  42. package/dist/module.mjs +19 -0
  43. package/dist/registry.d.mts +39 -8
  44. package/dist/registry.mjs +39 -8
  45. package/dist/resolver.d.mts +107 -12
  46. package/dist/resolver.mjs +134 -37
  47. package/dist/scope-validation.d.mts +3 -2
  48. package/dist/scope-validation.mjs +3 -2
  49. package/dist/scope.d.mts +38 -6
  50. package/dist/scope.mjs +42 -13
  51. package/dist/token.d.mts +9 -2
  52. package/dist/token.mjs +7 -1
  53. package/package.json +18 -2
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @codefast/di
2
2
 
3
- Lightweight, type-safe dependency injection for TypeScript — powered by TC39 Stage 3 decorators and zero runtime reflection.
3
+ Type-safe, ESM-only dependency injection for modern TypeScript — built on TC39 Stage 3 decorators with no runtime reflection.
4
4
 
5
5
  [![CI](https://github.com/codefastlabs/codefast/actions/workflows/release.yml/badge.svg)](https://github.com/codefastlabs/codefast/actions/workflows/release.yml)
6
6
  [![npm version](https://img.shields.io/npm/v/@codefast/di.svg)](https://www.npmjs.com/package/@codefast/di)
@@ -9,27 +9,33 @@ Lightweight, type-safe dependency injection for TypeScript — powered by TC39 S
9
9
 
10
10
  ## Table of Contents
11
11
 
12
- - [Overview](#overview)
13
- - [Installation](#installation)
12
+ - [Why @codefast/di](#why-codefastdi)
14
13
  - [Requirements](#requirements)
15
- - [Core Concepts](#core-concepts)
14
+ - [Installation](#installation)
16
15
  - [Quick Start](#quick-start)
16
+ - [Core Concepts](#core-concepts)
17
17
  - [Tokens](#tokens)
18
- - [Binding API](#binding-api)
19
- - [Binding strategies](#binding-strategies)
18
+ - [Bindings](#bindings)
19
+ - [Strategies](#strategies)
20
20
  - [Scopes](#scopes)
21
- - [Lifecycle hooks](#lifecycle-hooks)
22
21
  - [Constraints](#constraints)
22
+ - [Lifecycle hooks](#lifecycle-hooks)
23
23
  - [Decorators](#decorators)
24
- - [Container API](#container-api)
24
+ - [`@injectable`](#injectable)
25
+ - [`inject` / `optional` / `injectAll`](#inject--optional--injectall)
26
+ - [Accessor injection](#accessor-injection)
27
+ - [`@postConstruct` / `@preDestroy`](#postconstruct--predestroy)
28
+ - [Auto-registration](#auto-registration)
29
+ - [Container](#container)
25
30
  - [Resolution](#resolution)
26
31
  - [Async resolution](#async-resolution)
32
+ - [Rebinding and unbinding](#rebinding-and-unbinding)
27
33
  - [Child containers](#child-containers)
28
34
  - [Validation](#validation)
29
35
  - [Introspection](#introspection)
30
- - [Disposable resources](#disposable-resources)
36
+ - [Disposal](#disposal)
31
37
  - [Modules](#modules)
32
- - [Error handling](#error-handling)
38
+ - [Errors](#errors)
33
39
  - [Package exports](#package-exports)
34
40
  - [Contributing](#contributing)
35
41
  - [License](#license)
@@ -37,31 +43,17 @@ Lightweight, type-safe dependency injection for TypeScript — powered by TC39 S
37
43
 
38
44
  ---
39
45
 
40
- ## Overview
41
-
42
- `@codefast/di` is a small **ESM-only** IoC container for TypeScript applications.
43
-
44
- **Key features:**
45
-
46
- - **Type-safe tokens** — `Token<Value>` ensures `resolve()` returns the exact type you registered; no accidental `get<WrongType>(…)` at compile time.
47
- - **TC39 Stage 3 decorators** — `@injectable`, `@inject`, `@optional`, `@singleton`, `@scoped` write metadata via `Symbol.metadata`. No `reflect-metadata`, no `experimentalDecorators`.
48
- - **Fluent binding API** — constants, classes, sync and async factories, scopes, tags, named hints, constraint predicates, and lifecycle hooks.
49
- - **Module system** — organize registrations into reusable `Module` / `AsyncModule` units.
50
- - **Scope safety** — detects singleton → transient/scoped captive dependencies in development.
51
- - **Async-first** — parallel inflight deduplication for async singleton initialization.
52
- - **Tree-shakeable subpaths** — import only the surface you need.
53
-
54
- ---
55
-
56
- ## Installation
46
+ ## Why @codefast/di
57
47
 
58
- ```bash
59
- # pnpm
60
- pnpm add @codefast/di
48
+ `@codefast/di` is a small IoC container designed for applications that compile to ESM and want strong typing without metadata reflection tricks.
61
49
 
62
- # npm
63
- npm install @codefast/di
64
- ```
50
+ - **Typed tokens.** `Token<Value>` flows through every `bind → resolve` path; the return type of `resolve()` is the one you registered.
51
+ - **Native Stage 3 decorators.** `@injectable`, `inject`, `optional`, `@postConstruct`, `@preDestroy` write to `Symbol.metadata`. No `reflect-metadata`, no `experimentalDecorators`.
52
+ - **Fluent binding API.** Constants, classes, sync/async factories, aliases, named/tagged/predicate constraints, activation + deactivation hooks.
53
+ - **Module system.** `Module` and `AsyncModule` bundle bindings into reusable units that can be loaded, unloaded, and re-used across containers.
54
+ - **Scope safety.** Detects captive dependencies (a singleton depending on a scoped or transient binding) during development and test.
55
+ - **Async-first.** Dedupes in-flight async singleton construction and supports `await using` for automatic cleanup.
56
+ - **Tree-shakeable subpaths.** Import only the surface you need.
65
57
 
66
58
  ---
67
59
 
@@ -72,7 +64,7 @@ npm install @codefast/di
72
64
  | Node.js | `>= 22.0.0` |
73
65
  | TypeScript | `>= 5.2` (Stage 3 decorators) |
74
66
 
75
- Enable native decorators in `tsconfig.json` (do **not** enable `experimentalDecorators`):
67
+ Enable native decorators in `tsconfig.json` — do **not** enable `experimentalDecorators`:
76
68
 
77
69
  ```json
78
70
  {
@@ -86,16 +78,15 @@ Enable native decorators in `tsconfig.json` (do **not** enable `experimentalDeco
86
78
 
87
79
  ---
88
80
 
89
- ## Core Concepts
81
+ ## Installation
90
82
 
91
- | Concept | Description |
92
- | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
93
- | **Token** | Branded key that pairs a name string with a TypeScript type. Tokens use reference equality — pass the same object to `bind` and `inject`. |
94
- | **Binding** | Associates a token (or constructor) with a value strategy (constant, class, factory, alias) and optional scope, constraints, and lifecycle hooks. |
95
- | **Container** | Stores bindings in a `BindingRegistry` and resolves them on demand via a `DependencyResolver`. |
96
- | **Module** | An immutable bundle of bindings. Loaded into any container and unloaded cleanly. |
97
- | **Scope** | Controls instance lifetime: `singleton` (one per container), `scoped` (one per child), or `transient` (new each time). |
98
- | **Decorator metadata** | `@injectable([…])` writes constructor dependency descriptors into `Symbol.metadata`, which the container reads at resolution time. |
83
+ ```bash
84
+ pnpm add @codefast/di
85
+ # or
86
+ npm install @codefast/di
87
+ # or
88
+ yarn add @codefast/di
89
+ ```
99
90
 
100
91
  ---
101
92
 
@@ -104,15 +95,13 @@ Enable native decorators in `tsconfig.json` (do **not** enable `experimentalDeco
104
95
  ```typescript
105
96
  import { Container, inject, injectable, token } from "@codefast/di";
106
97
 
107
- // 1. Define an interface and a token for it
108
98
  interface Logger {
109
99
  log(message: string): void;
110
100
  }
111
101
 
112
102
  const LoggerToken = token<Logger>("Logger");
113
103
 
114
- // 2. Implement and annotate the class
115
- @injectable([inject(LoggerToken)])
104
+ @injectable([LoggerToken])
116
105
  class AppService {
117
106
  constructor(private readonly logger: Logger) {}
118
107
 
@@ -121,22 +110,32 @@ class AppService {
121
110
  }
122
111
  }
123
112
 
124
- // 3. Create a container and register bindings
125
113
  const container = Container.create();
126
114
 
127
115
  container.bind(LoggerToken).toConstantValue({ log: console.log });
128
116
  container.bind(AppService).toSelf().singleton();
129
117
 
130
- // 4. Resolve and use
131
- const app = container.resolve(AppService);
132
- app.run(); // "App started"
118
+ container.resolve(AppService).run(); // "App started"
133
119
  ```
134
120
 
121
+ The `@injectable([LoggerToken])` call lists constructor dependencies in order. Wrap an entry with `inject(token, options)` or `optional(token, options)` when you need a named/tagged hint or optional semantics.
122
+
135
123
  ---
136
124
 
137
- ## Tokens
125
+ ## Core Concepts
126
+
127
+ | Concept | Description |
128
+ | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
129
+ | **Token** | Branded identifier pairing a name string with a TypeScript type. Tokens compare by reference — always reuse the same `const`. |
130
+ | **Binding** | Associates a token (or constructor) with a value strategy (constant / class / factory / alias), a scope, optional constraints, and lifecycle hooks. |
131
+ | **Container** | Holds a `BindingRegistry` and a `ScopeManager`; resolves bindings through the `DependencyResolver`. Supports child containers and async disposal. |
132
+ | **Scope** | Instance lifetime: `singleton` (one per root container, shared with children), `scoped` (one per child container), `transient` (new on every resolve). |
133
+ | **Module** | Reusable bundle of bindings. Loaded once per container; loading the same module twice is a no-op. |
134
+ | **Metadata** | `@injectable([...])` writes constructor descriptors into `Symbol.metadata`, which the container reads at resolution time. |
138
135
 
139
- A `Token<Value>` is a branded object that acts as a type-safe registry key.
136
+ ---
137
+
138
+ ## Tokens
140
139
 
141
140
  ```typescript
142
141
  import { token } from "@codefast/di";
@@ -145,50 +144,44 @@ const DbToken = token<Database>("Database");
145
144
  const CacheToken = token<Cache>("Cache");
146
145
  ```
147
146
 
148
- The type parameter flows through the binding and resolution chain — `container.resolve(DbToken)` returns `Database`, not `unknown`.
149
-
150
- > **Token identity** — tokens use reference equality. Always import and reuse the same `const` value for both `bind()` and `inject()`. Creating two tokens with the same name produces two distinct keys.
151
-
152
- You can also use a constructor directly as a key:
147
+ - The type parameter flows through the binding and resolution chain.
148
+ - Tokens use reference equality; two tokens with the same name are two distinct keys.
149
+ - A class constructor can itself be a key:
153
150
 
154
151
  ```typescript
155
- container.bind(MyService).toSelf();
156
- container.resolve(MyService); // returns MyService
152
+ container.bind(UserService).toSelf();
153
+ container.resolve(UserService); // returns UserService
157
154
  ```
158
155
 
159
156
  ---
160
157
 
161
- ## Binding API
158
+ ## Bindings
162
159
 
163
- ### Binding strategies
160
+ Start with `container.bind(key)` and chain a strategy, then optionally a scope, constraints, and hooks.
164
161
 
165
- Start a binding with `container.bind(key)` and chain a strategy method:
162
+ ### Strategies
166
163
 
167
- | Method | Description |
168
- | ---------------------------- | ---------------------------------------------------------------------- |
169
- | `.toConstantValue(value)` | Bind to a fixed value. Always treated as singleton. |
170
- | `.toSelf()` | Bind a constructor to itself. Reads `@injectable()` metadata. |
171
- | `.to(Constructor)` | Bind a token to a class constructor. |
172
- | `.toDynamic(factory)` | Bind to a synchronous factory `(ctx: ResolutionContext) => Value`. |
173
- | `.toDynamicAsync(factory)` | Bind to an async factory `(ctx: ResolutionContext) => Promise<Value>`. |
174
- | `.toResolved(factory, deps)` | Bind to a factory with an explicit typed dependency array. |
175
- | `.toAlias(targetToken)` | Redirect this token to another token. |
164
+ | Method | Description |
165
+ | ---------------------------- | --------------------------------------------------------------------------- |
166
+ | `.toConstantValue(value)` | Bind to a fixed value. Always treated as a singleton. |
167
+ | `.toSelf()` | Bind a constructor to itself. Reads `@injectable()` metadata. |
168
+ | `.to(Constructor)` | Bind a token to a class constructor. |
169
+ | `.toDynamic(factory)` | Bind to a sync factory `(ctx: ResolutionContext) => Value`. |
170
+ | `.toDynamicAsync(factory)` | Bind to an async factory `(ctx: ResolutionContext) => Promise<Value>`. |
171
+ | `.toResolved(factory, deps)` | Bind to a factory whose dependency array is checked against the deps types. |
172
+ | `.toAlias(targetToken)` | Redirect resolution to another token; the alias adopts the target's scope. |
176
173
 
177
174
  ```typescript
178
- // Constant
179
175
  container.bind(AppConfigToken).toConstantValue({ port: 3000 });
180
176
 
181
- // Class
182
177
  container.bind(UserRepository).toSelf().singleton();
183
178
  container.bind(UserServiceToken).to(UserService).transient();
184
179
 
185
- // Factory — receive resolution context to resolve nested deps
186
180
  container.bind(DbToken).toDynamic((ctx) => {
187
181
  const config = ctx.resolve(AppConfigToken);
188
182
  return new Database(config.dbUrl);
189
183
  });
190
184
 
191
- // Async factory
192
185
  container.bind(DbToken).toDynamicAsync(async (ctx) => {
193
186
  const config = ctx.resolve(AppConfigToken);
194
187
  const db = new Database(config.dbUrl);
@@ -196,19 +189,20 @@ container.bind(DbToken).toDynamicAsync(async (ctx) => {
196
189
  return db;
197
190
  });
198
191
 
199
- // Alias
192
+ container
193
+ .bind(UserServiceToken)
194
+ .toResolved((repo, cfg) => new UserService(repo, cfg), [UserRepository, AppConfigToken] as const);
195
+
200
196
  container.bind(LegacyServiceToken).toAlias(NewServiceToken);
201
197
  ```
202
198
 
203
199
  ### Scopes
204
200
 
205
- After setting the strategy, chain a scope method:
206
-
207
- | Method | Lifetime |
208
- | -------------- | --------------------------------------------------------------------------- |
209
- | `.singleton()` | One instance per container. Created once, reused forever. |
210
- | `.transient()` | New instance on every `resolve()`. Default for most bindings. |
211
- | `.scoped()` | One instance per child container scope. Useful for request-scoped services. |
201
+ | Method | Lifetime |
202
+ | -------------- | -------------------------------------------------------------------------- |
203
+ | `.singleton()` | One instance per root container, shared with descendants. |
204
+ | `.scoped()` | One instance per child container. Useful for request-scoped services. |
205
+ | `.transient()` | New instance on every resolution. Default when no scope method is chained. |
212
206
 
213
207
  ```typescript
214
208
  container.bind(DatabaseToken).toDynamic(createDb).singleton();
@@ -216,75 +210,74 @@ container.bind(RequestContextToken).toSelf().scoped();
216
210
  container.bind(QueryBuilderToken).toSelf().transient();
217
211
  ```
218
212
 
219
- ### Lifecycle hooks
220
-
221
- Attach activation and deactivation callbacks to any binding:
222
-
223
- ```typescript
224
- container
225
- .bind(DbToken)
226
- .toDynamicAsync(async (ctx) => new Database(ctx.resolve(ConfigToken)))
227
- .singleton()
228
- .onActivation(async (instance, ctx) => {
229
- await instance.connect();
230
- return instance; // must return the instance
231
- })
232
- .onDeactivation(async (instance) => {
233
- await instance.disconnect();
234
- });
235
- ```
236
-
237
- `onActivation` runs after the instance is created. It must return the (optionally modified) instance.
238
- `onDeactivation` runs when `container.dispose()` is called.
239
-
240
213
  ### Constraints
241
214
 
242
- Constraints let multiple bindings share the same token and select one based on resolution context.
215
+ Multiple bindings can share the same token. A constraint picks the right one at resolution time.
243
216
 
244
- **Named bindings** — disambiguate with a string name:
217
+ **Named**
245
218
 
246
219
  ```typescript
247
- container.bind(LoggerToken).toConstantValue(fileLogger).whenNamed("file");
248
- container.bind(LoggerToken).toConstantValue(consoleLogger).whenNamed("console");
220
+ container.bind(LoggerToken).whenNamed("file").toConstantValue(fileLogger);
221
+ container.bind(LoggerToken).whenNamed("console").toConstantValue(consoleLogger);
249
222
 
250
- // Resolve by name
251
223
  container.resolve(LoggerToken, { name: "file" });
252
224
  ```
253
225
 
254
- **Tagged bindings** — disambiguate with a tag + value pair:
226
+ **Tagged** — the hint is a tuple `[tag, value]`:
255
227
 
256
228
  ```typescript
257
- container.bind(StorageToken).to(S3Storage).whenTagged("provider", "s3");
258
- container.bind(StorageToken).to(LocalStorage).whenTagged("provider", "local");
229
+ container.bind(StorageToken).whenTagged("provider", "s3").to(S3Storage);
230
+ container.bind(StorageToken).whenTagged("provider", "local").to(LocalStorage);
259
231
 
260
- container.resolve(StorageToken, { tag: "provider", tagValue: "s3" });
232
+ container.resolve(StorageToken, { tag: ["provider", "s3"] });
261
233
  ```
262
234
 
263
- **Custom predicate** — inspect the full resolution context:
235
+ **Predicate** — inspect the full resolution graph:
264
236
 
265
237
  ```typescript
266
- import { whenParentIs } from "@codefast/di";
238
+ import { whenAnyAncestorIs, whenParentIs, whenTargetTagged } from "@codefast/di";
267
239
 
268
- container.bind(LoggerToken).toConstantValue(verboseLogger).when(whenParentIs(DiagnosticsService));
240
+ container.bind(LoggerToken).when(whenParentIs(DiagnosticsService)).toConstantValue(verboseLogger);
269
241
  ```
270
242
 
271
- Built-in predicate helpers:
243
+ Built-in predicates:
244
+
245
+ | Helper | Matches when … |
246
+ | ------------------------------ | --------------------------------------------------------------------------- |
247
+ | `whenParentIs(key)` | the direct parent binding was registered for `key`. |
248
+ | `whenAnyAncestorIs(key)` | any ancestor binding on the materialization stack was registered for `key`. |
249
+ | `whenTargetTagged(tag, value)` | the immediate parent binding carries `tag` with `value`. |
250
+
251
+ For anything else, pass a custom `(ctx: ConstraintContext) => boolean` to `.when(predicate)`.
252
+
253
+ ### Lifecycle hooks
254
+
255
+ ```typescript
256
+ container
257
+ .bind(DbToken)
258
+ .toDynamicAsync(async (ctx) => new Database(ctx.resolve(ConfigToken)))
259
+ .singleton()
260
+ .onActivation(async (ctx, instance) => {
261
+ await instance.connect();
262
+ return instance;
263
+ })
264
+ .onDeactivation(async (instance) => {
265
+ await instance.disconnect();
266
+ });
267
+ ```
272
268
 
273
- | Helper | Description |
274
- | ------------------------------ | ------------------------------------------------------------------ |
275
- | `whenParentIs(key)` | Matches when the direct parent binding is for the given key. |
276
- | `whenAnyAncestorIs(key)` | Matches when any ancestor binding is for the given key. |
277
- | `whenTargetTagged(tag, value)` | Matches when the immediate parent was injected with the given tag. |
269
+ - `onActivation(ctx, instance)` runs after construction (and after `@postConstruct`). It must return the instance (possibly wrapped).
270
+ - `onDeactivation(instance)` runs when the instance is evicted: `container.dispose()`, `unbind`, `rebind`, or `unload`.
278
271
 
279
272
  ---
280
273
 
281
274
  ## Decorators
282
275
 
283
- Decorators use TC39 Stage 3 syntax and write metadata to `Symbol.metadata`.
276
+ All decorators use TC39 Stage 3 syntax and write to `Symbol.metadata`.
284
277
 
285
- ### `@injectable(deps)`
278
+ ### `@injectable`
286
279
 
287
- Registers constructor dependency metadata. Every dependency must be listed.
280
+ Registers constructor dependencies. The array length must match the constructor arity; a mismatch throws `InternalError` at class-definition time.
288
281
 
289
282
  ```typescript
290
283
  import { inject, injectable, optional, token } from "@codefast/di";
@@ -292,7 +285,7 @@ import { inject, injectable, optional, token } from "@codefast/di";
292
285
  const ConfigToken = token<AppConfig>("AppConfig");
293
286
  const CacheToken = token<Cache>("Cache");
294
287
 
295
- @injectable([inject(ConfigToken), optional(CacheToken)])
288
+ @injectable([ConfigToken, optional(CacheToken)])
296
289
  class AppService {
297
290
  constructor(
298
291
  private readonly config: AppConfig,
@@ -301,124 +294,166 @@ class AppService {
301
294
  }
302
295
  ```
303
296
 
304
- - `inject(token, options?)` — required dependency. Throws `TokenNotBoundError` if absent.
305
- - `optional(token, options?)` — optional dependency. Resolves to `undefined` if not bound.
297
+ Each entry is either a plain `Token` / `Constructor` or an `InjectionDescriptor` produced by `inject()` / `optional()` / `injectAll()`.
306
298
 
307
- Both accept `InjectOptions` for named or tagged resolution hints:
299
+ ### `inject` / `optional` / `injectAll`
308
300
 
309
301
  ```typescript
310
302
  inject(LoggerToken, { name: "console" });
311
- inject(StorageToken, { tag: "provider", tagValue: "s3" });
303
+ inject(StorageToken, { tag: ["provider", "s3"] });
304
+ optional(CacheToken);
305
+ injectAll(PluginToken);
306
+ ```
307
+
308
+ - `inject(token, options?)` — required. Throws `TokenNotBoundError` if the dependency is missing.
309
+ - `optional(token, options?)` — optional. Resolves to `undefined` when unbound.
310
+ - `injectAll(token, options?)` — resolves every matching binding into an array (`Value[]`), applying `name`/`tag` filters when provided.
311
+
312
+ ### Accessor injection
313
+
314
+ `inject` doubles as a TC39 accessor-field decorator for post-construction property injection:
315
+
316
+ ```typescript
317
+ @injectable([])
318
+ class Controller {
319
+ @inject(LoggerToken) accessor logger!: Logger;
320
+ }
312
321
  ```
313
322
 
314
- ### `@singleton()` and `@scoped()`
323
+ The container injects the accessor after construction, so it does not count toward the constructor arity declared in `@injectable([])`.
324
+
325
+ ### `@postConstruct` / `@preDestroy`
315
326
 
316
- Scope hints on the class itself. The container reads these when you call `.to(Constructor)` without chaining an explicit scope method.
327
+ Method decorators that hook into the instance lifecycle. Order:
328
+ `construct → @postConstruct → onActivation → cache … onDeactivation → @preDestroy`.
317
329
 
318
330
  ```typescript
319
- @singleton()
320
- @injectable([inject(DbToken)])
331
+ import { postConstruct, preDestroy } from "@codefast/di";
332
+
333
+ @injectable([DbToken])
321
334
  class UserRepository {
322
335
  constructor(private readonly db: Database) {}
336
+
337
+ @postConstruct()
338
+ async init(): Promise<void> {
339
+ await this.db.warmCache();
340
+ }
341
+
342
+ @preDestroy()
343
+ async shutdown(): Promise<void> {
344
+ await this.db.flush();
345
+ }
323
346
  }
347
+ ```
348
+
349
+ Only one of each decorator is allowed per class.
350
+
351
+ ### Auto-registration
324
352
 
325
- // No .singleton() needed — decorator provides the hint
326
- container.bind(UserRepository).toSelf();
353
+ Pass `autoRegister` to `@injectable` to have the class register itself in a module-scoped list:
354
+
355
+ ```typescript
356
+ @injectable([DbToken], { autoRegister: true, scope: "singleton" })
357
+ class UserRepository {
358
+ constructor(private readonly db: Database) {}
359
+ }
360
+
361
+ // Bind every auto-registered class at once
362
+ container.loadAutoRegistered();
327
363
  ```
328
364
 
329
- `@singleton()` and `@scoped()` cannot be combined on the same class.
365
+ `scope` defaults to `"transient"`. `getAutoRegistered()` returns the list if you prefer to iterate manually.
330
366
 
331
367
  ---
332
368
 
333
- ## Container API
369
+ ## Container
334
370
 
335
371
  ### Resolution
336
372
 
337
373
  ```typescript
338
374
  const container = Container.create();
339
375
 
340
- // Single binding
341
- const service = container.resolve(ServiceToken);
342
-
343
- // Named or tagged hint
344
- const logger = container.resolve(LoggerToken, { name: "console" });
376
+ container.resolve(ServiceToken);
377
+ container.resolveOptional(CacheToken); // undefined when unbound
378
+ container.resolveAll(HandlerToken); // all matching multi-bindings
345
379
 
346
- // All matching bindings
347
- const handlers = container.resolveAll(HandlerToken);
380
+ container.resolve(LoggerToken, { name: "console" });
381
+ container.resolve(StorageToken, { tag: ["provider", "s3"] });
348
382
 
349
- // Optional — returns undefined if not bound
350
- const cache = container.resolveOptional(CacheToken);
383
+ container.has(CacheToken);
384
+ container.has(LoggerToken, { name: "console" });
351
385
  ```
352
386
 
353
- Check binding existence before resolving:
354
-
355
- ```typescript
356
- if (container.has(CacheToken)) {
357
- const cache = container.resolve(CacheToken);
358
- }
359
- ```
387
+ `resolveAll()` and `ctx.resolveAll()` preserve the current resolution context (path + parent/ancestors stack), so `when(...)` predicates and scope checks behave the same as `resolve()`.
360
388
 
361
389
  ### Async resolution
362
390
 
363
- Use `resolveAsync` / `resolveAllAsync` / `resolveOptionalAsync` when any dependency in the chain uses `toDynamicAsync`:
391
+ Use the `*Async` variants when any binding in the resolution chain uses `toDynamicAsync`, async `onActivation`, or async `@postConstruct`. Mixing async into a sync resolve throws `AsyncResolutionError`.
364
392
 
365
393
  ```typescript
366
394
  const db = await container.resolveAsync(DbToken);
367
395
  const handlers = await container.resolveAllAsync(HandlerToken);
396
+ // resolveOptional is sync-only — for async optional lookups, check `has` then `resolveAsync`
397
+ const cache = container.has(CacheToken) ? await container.resolveAsync(CacheToken) : undefined;
398
+
399
+ // Eagerly construct every singleton binding
400
+ await container.initializeAsync();
368
401
  ```
369
402
 
370
403
  ### Rebinding and unbinding
371
404
 
372
405
  ```typescript
373
- // Replace all bindings for a token
374
406
  container.rebind(LoggerToken).toConstantValue(testLogger);
375
407
 
376
- // Remove bindings (sync)
377
- container.unbind(CacheToken);
378
-
379
- // Remove with deactivation hooks (async)
380
- await container.unbindAsync(CacheToken);
408
+ container.unbind(CacheToken); // sync deactivation hooks only
409
+ await container.unbindAsync(CacheToken); // awaits async deactivation
381
410
  ```
382
411
 
383
412
  ### Child containers
384
413
 
385
- Child containers share the parent's singleton cache but maintain an isolated scoped cache — ideal for per-request isolation.
414
+ Child containers fall through to the parent's bindings and share the parent's singleton cache, but have their own scoped cache.
386
415
 
387
416
  ```typescript
388
417
  const requestContainer = container.createChild();
389
418
 
390
419
  requestContainer.bind(RequestContextToken).toConstantValue(req).scoped();
391
-
392
420
  const service = requestContainer.resolve(RequestScopedService);
393
421
 
394
- // Clean up scoped instances
395
- await requestContainer.dispose();
422
+ await requestContainer.dispose(); // releases scoped instances
396
423
  ```
397
424
 
398
425
  ### Validation
399
426
 
400
- `validate()` checks for captive dependency violations (a singleton depending on a transient or scoped binding). In development and test environments this runs automatically after `load()` and the first `resolve()`.
427
+ `validate()` statically checks that no singleton binding depends on a `scoped` or `transient` binding (a captive dependency). In development and test environments the container runs `validate()` at most once after registry changes, so most scope violations surface without an explicit call.
401
428
 
402
429
  ```typescript
403
- container.validate(); // throws ScopeViolationError if invalid
430
+ container.validate(); // throws ScopeViolationError on the first violation
404
431
  ```
405
432
 
433
+ Control the environment heuristic via `NODE_ENV` — see `isDevelopmentOrTestEnvironment` in `@codefast/di/environment`.
434
+
406
435
  ### Introspection
407
436
 
408
437
  ```typescript
409
- // Snapshot of all registered bindings
410
- const snapshot = container.inspect();
438
+ import { toCytoscapeGraph } from "@codefast/di/graph-adapters/cytoscape";
439
+ import { toDotGraph } from "@codefast/di/graph-adapters/dot";
440
+ import { toReactFlowGraph } from "@codefast/di/graph-adapters/reactflow";
411
441
 
412
- // Graphviz DOT format for dependency graph visualization
413
- const dot = container.generateDependencyGraphDot({ hideInternals: true });
442
+ const snapshot = container.inspect();
443
+ const json = container.generateDependencyGraph({ hideInternals: true });
444
+ const dot = toDotGraph(json);
414
445
 
415
- // JSON format
416
- const graph = container.generateDependencyGraphJson();
446
+ // Adapters are pure converters from the canonical JSON graph.
447
+ const cytoscape = toCytoscapeGraph(json);
448
+ const reactflow = toReactFlowGraph(json);
417
449
  ```
418
450
 
419
- ### Disposable resources
451
+ `generateDependencyGraph` always returns the canonical typed `ContainerGraphJson` (`nodes` + `edges`).
452
+ Keep visualization adapters (`toDotGraph`, `toCytoscapeGraph`, `toReactFlowGraph`, or your own converters) outside container/inspector core APIs and import them from direct subpaths under `@codefast/di/graph-adapters/*`.
453
+
454
+ ### Disposal
420
455
 
421
- `Container` implements `AsyncDisposable`, enabling `await using` syntax:
456
+ `Container` implements `AsyncDisposable`, so `await using` runs `dispose()` automatically:
422
457
 
423
458
  ```typescript
424
459
  {
@@ -426,26 +461,26 @@ const graph = container.generateDependencyGraphJson();
426
461
  container.bind(DbToken).toDynamicAsync(connectDb).singleton().onDeactivation(disconnectDb);
427
462
 
428
463
  const db = await container.resolveAsync(DbToken);
429
- // ...
430
- } // container.dispose() called automatically — runs all deactivation hooks
464
+ // …
465
+ } // dispose() runs all deactivation hooks
431
466
  ```
432
467
 
433
- Or call explicitly:
434
-
435
- ```typescript
436
- await container.dispose();
437
- ```
468
+ Sync `using` is intentionally rejected: calling `Symbol.dispose` throws. Use `await using` or `await container.dispose()`.
438
469
 
439
470
  ---
440
471
 
441
472
  ## Modules
442
473
 
443
- Modules group related bindings into reusable, composable units.
474
+ Modules bundle related bindings into reusable units. A module holds no runtime state and can be loaded into any number of containers.
444
475
 
445
- ### Synchronous modules
476
+ Inside module setup, binding semantics depend on whether a disambiguator is present before `to*()`:
477
+
478
+ - `api.bind(Token).to*(...)` uses last-wins for that token.
479
+ - `api.bind(Token).whenNamed(...)` / `.whenTagged(...)` / `.when(...)` **before** `to*()` appends another multi-binding entry.
480
+ - `api.bind(Token).to*(...).whenNamed(...)` updates only that single built binding; it does not switch later lines into append mode.
446
481
 
447
482
  ```typescript
448
- import { Module } from "@codefast/di";
483
+ import { Container, Module } from "@codefast/di";
449
484
 
450
485
  const InfraModule = Module.create("Infra", (api) => {
451
486
  api.bind(LoggerToken).toConstantValue(console);
@@ -453,7 +488,7 @@ const InfraModule = Module.create("Infra", (api) => {
453
488
  });
454
489
 
455
490
  const AppModule = Module.create("App", (api) => {
456
- api.import(InfraModule); // load dependency module
491
+ api.import(InfraModule);
457
492
  api.bind(UserRepository).toSelf().singleton();
458
493
  api.bind(UserServiceToken).to(UserService).transient();
459
494
  });
@@ -461,11 +496,9 @@ const AppModule = Module.create("App", (api) => {
461
496
  const container = Container.fromModules(AppModule);
462
497
  ```
463
498
 
464
- ### Async modules
499
+ Async modules may `await` during setup (e.g. dynamic config):
465
500
 
466
501
  ```typescript
467
- import { Module } from "@codefast/di";
468
-
469
502
  const DbModule = Module.createAsync("Database", async (api) => {
470
503
  const config = await fetchRemoteConfig();
471
504
  api.bind(DbToken).toConstantValue(await Database.connect(config.dbUrl));
@@ -474,44 +507,45 @@ const DbModule = Module.createAsync("Database", async (api) => {
474
507
  const container = await Container.fromModulesAsync(DbModule, AppModule);
475
508
  ```
476
509
 
477
- `Module.createAsync` returns an `AsyncModule`. Use `Container.fromModulesAsync` (or `container.loadAsync`) to load async modules.
478
-
479
- ### Loading and unloading
510
+ Load and unload on an existing container:
480
511
 
481
512
  ```typescript
482
- // Load into existing container
483
513
  container.load(InfraModule, AppModule);
484
514
  await container.loadAsync(DbModule);
485
515
 
486
- // Remove and run deactivation hooks
487
516
  container.unload(AppModule);
488
517
  await container.unloadAsync(DbModule);
489
518
  ```
490
519
 
520
+ Re-loading a module already present is a no-op. Circular imports between modules throw `CircularDependencyError`.
521
+
491
522
  ---
492
523
 
493
- ## Error handling
524
+ ## Errors
494
525
 
495
- All errors extend `DiError` and expose a stable `code` property for `switch` / `instanceof` checks.
526
+ All errors extend `DiError` and expose a stable `code` property.
496
527
 
497
- | Error class | `code` | When thrown |
498
- | ------------------------- | ----------------------- | --------------------------------------------------- |
499
- | `TokenNotBoundError` | `"TOKEN_NOT_BOUND"` | `resolve()` called for an unregistered token |
500
- | `NoMatchingBindingError` | `"NO_MATCHING_BINDING"` | Name/tag hint matches no binding |
501
- | `CircularDependencyError` | `"CIRCULAR_DEPENDENCY"` | Circular dependency detected during resolution |
502
- | `MissingMetadataError` | `"MISSING_METADATA"` | Class binding lacks `@injectable()` metadata |
503
- | `AsyncModuleLoadError` | `"ASYNC_MODULE_LOAD"` | `load()` (sync) called with an `AsyncModule` |
504
- | `AsyncResolutionError` | `"ASYNC_RESOLUTION"` | Async dependency encountered during sync resolution |
505
- | `ScopeViolationError` | `"SCOPE_VIOLATION"` | Singleton depends on transient/scoped binding |
528
+ | Error class | `code` | Thrown when |
529
+ | ------------------------- | ----------------------- | ----------------------------------------------------------- |
530
+ | `TokenNotBoundError` | `"TOKEN_NOT_BOUND"` | A required token has no binding |
531
+ | `NoMatchingBindingError` | `"NO_MATCHING_BINDING"` | A name/tag/predicate hint matches no registered binding |
532
+ | `CircularDependencyError` | `"CIRCULAR_DEPENDENCY"` | A cycle is detected in dependency or module resolution |
533
+ | `MissingMetadataError` | `"MISSING_METADATA"` | A class binding is missing `@injectable()` metadata |
534
+ | `AsyncModuleLoadError` | `"ASYNC_MODULE_LOAD"` | Sync `load()` is called with an `AsyncModule` |
535
+ | `AsyncResolutionError` | `"ASYNC_RESOLUTION"` | An async binding is reached during a sync `resolve()` |
536
+ | `ScopeViolationError` | `"SCOPE_VIOLATION"` | A singleton depends on a scoped or transient binding |
537
+ | `InternalError` | `"INTERNAL"` | Invariant violations (should never surface in correct code) |
506
538
 
507
539
  ```typescript
508
- import { DiError, TokenNotBoundError, ScopeViolationError } from "@codefast/di";
540
+ import { DiError, ScopeViolationError, TokenNotBoundError } from "@codefast/di";
509
541
 
510
542
  try {
511
- const service = container.resolve(ServiceToken);
543
+ container.resolve(ServiceToken);
512
544
  } catch (error) {
513
545
  if (error instanceof TokenNotBoundError) {
514
546
  console.error(`Not registered: ${error.tokenName}`);
547
+ } else if (error instanceof ScopeViolationError) {
548
+ console.error(`Scope violation: ${error.message}`);
515
549
  } else if (error instanceof DiError) {
516
550
  console.error(`DI error [${error.code}]: ${error.message}`);
517
551
  }
@@ -522,31 +556,30 @@ try {
522
556
 
523
557
  ## Package exports
524
558
 
525
- The root entry `@codefast/di` re-exports the complete public API. Subpath exports are available for fine-grained imports and tree-shaking:
526
-
527
- | Subpath | Contents |
528
- | ------------------------------------ | ------------------------------------------------------------ |
529
- | `@codefast/di` | Full public façade — everything below, combined |
530
- | `@codefast/di/container` | `Container` interface, `DefaultContainer`, and related types |
531
- | `@codefast/di/token` | `token()`, `Token<Value>`, `TokenValue` |
532
- | `@codefast/di/binding` | `BindingBuilder`, `bind()`, binding type definitions |
533
- | `@codefast/di/binding-select` | `filterMatchingBindings`, `selectBindingForRegistry` |
534
- | `@codefast/di/module` | `Module`, `AsyncModule`, module builder types |
535
- | `@codefast/di/decorators` | `@injectable`, `@singleton`, `@scoped`, `inject`, `optional` |
536
- | `@codefast/di/decorators/inject` | `inject`, `optional`, `isInjectionDescriptor` |
537
- | `@codefast/di/decorators/injectable` | `@injectable` |
538
- | `@codefast/di/decorators/singleton` | `@singleton` |
539
- | `@codefast/di/decorators/scoped` | `@scoped` |
540
- | `@codefast/di/registry` | `BindingRegistry`, `registryKeyLabel` |
541
- | `@codefast/di/resolver` | Resolution internals |
542
- | `@codefast/di/scope` | `ScopeManager`, scope cache management |
543
- | `@codefast/di/scope-validation` | `validateScopeRules` |
544
- | `@codefast/di/lifecycle` | `runActivation`, `runActivationAsync`, activation types |
545
- | `@codefast/di/constraints` | `whenParentIs`, `whenAnyAncestorIs`, `whenTargetTagged` |
546
- | `@codefast/di/dependency-graph` | `collectStaticDependencyEdges`, `listResolvedDependencies` |
547
- | `@codefast/di/inspector` | `ContainerInspector`, `ContainerSnapshot` |
548
- | `@codefast/di/errors` | Complete `DiError` hierarchy |
549
- | `@codefast/di/environment` | `isDevelopmentOrTestEnvironment`, `isProductionEnvironment` |
559
+ The root entry re-exports the full public API. Subpath exports are provided for fine-grained imports and bundler tree-shaking.
560
+
561
+ | Subpath | Contents |
562
+ | ---------------------------------------------- | ---------------------------------------------------------------- |
563
+ | `@codefast/di` | Public façade — tokens, `Container`, modules, decorators, errors |
564
+ | `@codefast/di/container` | `Container` interface and related types |
565
+ | `@codefast/di/token` | `token()`, `Token<Value>`, `TokenValue` |
566
+ | `@codefast/di/binding` | `BindingBuilder`, binding type definitions |
567
+ | `@codefast/di/binding-select` | Binding selection internals (`filterMatchingBindings`, …) |
568
+ | `@codefast/di/module` | `Module`, `AsyncModule`, module builders |
569
+ | `@codefast/di/decorators/inject` | `inject`, `optional`, `injectAll`, `isInjectionDescriptor` |
570
+ | `@codefast/di/decorators/injectable` | `@injectable`, `getAutoRegistered` |
571
+ | `@codefast/di/decorators/lifecycle-decorators` | `@postConstruct`, `@preDestroy` |
572
+ | `@codefast/di/constraints` | `whenParentIs`, `whenAnyAncestorIs`, `whenTargetTagged` |
573
+ | `@codefast/di/registry` | `BindingRegistry` |
574
+ | `@codefast/di/resolver` | `DependencyResolver` internals |
575
+ | `@codefast/di/scope` | `ScopeManager` |
576
+ | `@codefast/di/scope-validation` | `validateScopeRules` |
577
+ | `@codefast/di/lifecycle` | Activation/deactivation runners |
578
+ | `@codefast/di/dependency-graph` | Dependency-edge collection helpers |
579
+ | `@codefast/di/inspector` | `ContainerInspector`, snapshot + graph types |
580
+ | `@codefast/di/errors` | Full `DiError` hierarchy |
581
+ | `@codefast/di/environment` | `isDevelopmentOrTestEnvironment`, `isProductionEnvironment` |
582
+ | `@codefast/di/metadata/*` | Metadata keys, types, readers, parameter registry |
550
583
 
551
584
  See `package.json → exports` for the authoritative list.
552
585
 
@@ -559,14 +592,17 @@ This package lives in the [Codefast monorepo](https://github.com/codefastlabs/co
559
592
  ```bash
560
593
  pnpm --filter @codefast/di build
561
594
  pnpm --filter @codefast/di test
595
+ pnpm --filter @codefast/di check-types
562
596
  ```
563
597
 
564
598
  ---
565
599
 
566
600
  ## License
567
601
 
568
- [MIT](https://opensource.org/licenses/MIT) — see `license` in [`package.json`](./package.json).
602
+ [MIT](https://opensource.org/licenses/MIT) — see [`package.json`](./package.json).
603
+
604
+ ---
569
605
 
570
606
  ## Changelog
571
607
 
572
- Version history is published on [npm](https://www.npmjs.com/package/@codefast/di?activeTab=versions) with each release.
608
+ See [CHANGELOG.md](./CHANGELOG.md) for the full version history. Releases are also published on [npm](https://www.npmjs.com/package/@codefast/di?activeTab=versions).