@codefast/di 0.11.0 → 0.13.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.
Files changed (79) hide show
  1. package/CHANGELOG.md +151 -0
  2. package/README.md +128 -31
  3. package/dist/container/{binding-builders.d.ts → binding-chain.d.ts} +21 -28
  4. package/dist/container/{binding-builders.js → binding-chain.js} +53 -163
  5. package/dist/container/container.d.ts +11 -3
  6. package/dist/container/container.js +176 -102
  7. package/dist/core/binding-builders.d.ts +117 -0
  8. package/dist/core/binding-declaration.d.ts +9 -2
  9. package/dist/core/binding-declaration.js +5 -4
  10. package/dist/core/binding.d.ts +21 -149
  11. package/dist/core/binding.js +15 -25
  12. package/dist/core/module.d.ts +1 -1
  13. package/dist/core/registry.d.ts +9 -24
  14. package/dist/core/registry.js +30 -155
  15. package/dist/core/tag.js +1 -1
  16. package/dist/decorators/inject.d.ts +3 -1
  17. package/dist/decorators/inject.js +5 -5
  18. package/dist/decorators/injectable.js +2 -2
  19. package/dist/decorators/lifecycle.d.ts +20 -0
  20. package/dist/decorators/{lifecycle-decorators.js → lifecycle.js} +7 -3
  21. package/dist/decorators/{decorator-metadata.js → metadata-record.js} +1 -1
  22. package/dist/{errors/errors.d.ts → errors.d.ts} +29 -19
  23. package/dist/{errors/errors.js → errors.js} +43 -25
  24. package/dist/index.d.ts +12 -10
  25. package/dist/index.js +6 -6
  26. package/dist/injection/descriptor.d.ts +1 -1
  27. package/dist/introspection/dependency-graph.d.ts +1 -1
  28. package/dist/introspection/dependency-graph.js +3 -4
  29. package/dist/introspection/explanation.d.ts +101 -0
  30. package/dist/introspection/explanation.js +169 -0
  31. package/dist/introspection/inspector.d.ts +9 -2
  32. package/dist/introspection/inspector.js +25 -20
  33. package/dist/lifecycle/{lifecycle-manager.d.ts → hooks.d.ts} +11 -2
  34. package/dist/lifecycle/{lifecycle-manager.js → hooks.js} +51 -28
  35. package/dist/lifecycle/{scope-manager.d.ts → scopes.d.ts} +23 -6
  36. package/dist/lifecycle/{scope-manager.js → scopes.js} +93 -20
  37. package/dist/metadata/{metadata-keys.d.ts → keys.d.ts} +1 -1
  38. package/dist/metadata/{metadata-keys.js → keys.js} +1 -1
  39. package/dist/metadata/{metadata-reader-token.d.ts → reader-token.d.ts} +1 -1
  40. package/dist/metadata/{symbol-metadata-reader.d.ts → symbol-reader.d.ts} +1 -1
  41. package/dist/metadata/{symbol-metadata-reader.js → symbol-reader.js} +1 -1
  42. package/dist/metadata/{metadata-types.d.ts → types.d.ts} +1 -1
  43. package/dist/metadata/types.js +1 -0
  44. package/dist/metadata/verify.d.ts +43 -0
  45. package/dist/metadata/verify.js +133 -0
  46. package/dist/metadata/{verifying-metadata-reader.d.ts → verifying-reader.d.ts} +1 -1
  47. package/dist/metadata/{verifying-metadata-reader.js → verifying-reader.js} +2 -2
  48. package/dist/resolution/cache/activation-need.d.ts +1 -1
  49. package/dist/resolution/cache/activation-need.js +5 -1
  50. package/dist/resolution/cache/class-introspector.d.ts +27 -36
  51. package/dist/resolution/cache/class-introspector.js +61 -159
  52. package/dist/resolution/cache/{binding-lookup-cache.d.ts → lookup.d.ts} +7 -2
  53. package/dist/resolution/cache/{binding-lookup-cache.js → lookup.js} +40 -8
  54. package/dist/resolution/context.d.ts +1 -1
  55. package/dist/resolution/context.js +1 -1
  56. package/dist/resolution/{path/resolution-path.d.ts → path.d.ts} +12 -2
  57. package/dist/resolution/{path/resolution-path.js → path.js} +23 -3
  58. package/dist/resolution/plan/{plan-codegen.d.ts → codegen.d.ts} +2 -4
  59. package/dist/resolution/plan/{plan-codegen.js → codegen.js} +2 -4
  60. package/dist/resolution/plan/{instantiation-plan.d.ts → compiler.d.ts} +1 -1
  61. package/dist/resolution/plan/{instantiation-plan.js → compiler.js} +4 -4
  62. package/dist/resolution/resolver.d.ts +19 -7
  63. package/dist/resolution/resolver.js +343 -125
  64. package/dist/resolution/select/{binding-select.d.ts → candidates.d.ts} +30 -0
  65. package/dist/resolution/select/{binding-select.js → candidates.js} +51 -10
  66. package/dist/resolution/select/constraints.js +1 -1
  67. package/package.json +80 -72
  68. package/dist/core/binding-scope.d.ts +0 -12
  69. package/dist/core/binding-scope.js +0 -12
  70. package/dist/decorators/lifecycle-decorators.d.ts +0 -14
  71. /package/dist/{ambient/active-container.d.ts → ambient-container.d.ts} +0 -0
  72. /package/dist/{ambient/active-container.js → ambient-container.js} +0 -0
  73. /package/dist/{metadata/metadata-types.js → core/binding-builders.js} +0 -0
  74. /package/dist/decorators/{decorator-metadata.d.ts → metadata-record.d.ts} +0 -0
  75. /package/dist/injection/{resolve-options.d.ts → dependency-slot.d.ts} +0 -0
  76. /package/dist/injection/{resolve-options.js → dependency-slot.js} +0 -0
  77. /package/dist/{errors → introspection}/diagnostics.d.ts +0 -0
  78. /package/dist/{errors → introspection}/diagnostics.js +0 -0
  79. /package/dist/metadata/{metadata-reader-token.js → reader-token.js} +0 -0
package/CHANGELOG.md CHANGED
@@ -1,5 +1,156 @@
1
1
  # @codefast/di
2
2
 
3
+ ## 0.13.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [#1022](https://github.com/codefastlabs/codefast/pull/1022) **Breaking:** `Container` no longer declares `[Symbol.dispose]`, and `SyncDisposalNotSupportedError` is removed. A
8
+ deactivation hook may be async, so a container disposes through `await using` or `dispose()` only, and a synchronous
9
+ `using` on one is now a compile error instead of a `SyncDisposalNotSupportedError` at runtime. Replace `using` with
10
+ `await using`, and drop any `instanceof SyncDisposalNotSupportedError` check.
11
+
12
+ - [#1024](https://github.com/codefastlabs/codefast/pull/1024) **Breaking:** `@postConstruct()` and `@preDestroy()` now reject a private (`#name`) method, as they already rejected a
13
+ static or symbol-keyed one. The lifecycle manager calls a hook by name on the instance, so a private hook passed
14
+ `validate()` and then failed at resolve with an `InvalidMetadataError` that blamed the metadata reader, or at
15
+ `dispose()` for `@preDestroy()`. Decorating one now throws the new `PrivateLifecycleMethodError` when the class is
16
+ defined. The decorators' types also accept only a public, instance, string-named method, and `@inject` only an instance
17
+ accessor, so TypeScript reports all of these at the decoration site (TS1240, TS1241). To run private code from a hook,
18
+ call it from a public one.
19
+
20
+ - [#1022](https://github.com/codefastlabs/codefast/pull/1022) `TokenNotBoundError` and `NoMatchingBindingError` now carry `path`: the token names from the outermost request down to
21
+ the one that missed, alias hops included. A miss below the token asked for ends its message with that path —
22
+ `Path: UserService → UserRepository → Database → app:Logger` — so a deep graph says which consumer needed the binding; a
23
+ miss on the token asked for keeps its message as it was. Every lane reports the same path: sync and async, generated
24
+ plans, factory bodies, and `explain()` for an ancestor that selects nothing.
25
+
26
+ - [#1022](https://github.com/codefastlabs/codefast/pull/1022) **Breaking:** a `scoped` instance is now deactivated with the child container that cached it. Disposing that child runs
27
+ the binding's `onDeactivation`, the container-level `onDeactivation` hooks of the container that owns the binding, and
28
+ `@preDestroy()` for every scoped instance it cached, latest first and before its own singletons; unbinding a scoped
29
+ binding on the child that cached it deactivates that instance too. `.scoped()` now offers `onDeactivation`, and
30
+ `Module.fromBindings` accepts `onDeactivation` with `scope: "scoped"`, so per-request resources — a transaction, a
31
+ connection — can be released where they are declared. `validate()` no longer reports a deactivation hook on a
32
+ scoped-only token as unreachable. Code that relied on a scoped instance never being torn down, or that closed one by
33
+ hand after `dispose()`, should drop the manual close.
34
+
35
+ - [#1022](https://github.com/codefastlabs/codefast/pull/1022) **Breaking:** `validate()` now throws the error `resolve()` would for a singleton graph that cannot resolve — a required
36
+ dependency nothing binds (`TokenNotBoundError`), a slot nothing matches (`NoMatchingBindingError`), or a cycle
37
+ (`CircularDependencyError`) — with the path from that singleton down to the problem. A parent-owned singleton reached
38
+ from a child is now read from the parent's chain, as `resolve` reads it. It still leaves alone what it cannot decide: a
39
+ transient or scoped consumer, whose dependencies a child container may bind, and a request only a `when()` candidate
40
+ matches. A container that passed `validate()` while holding such a graph now fails it; bind the missing dependency, or
41
+ mark it `optional()` when it may be absent.
42
+
43
+ ## 0.12.0
44
+
45
+ ### Minor Changes
46
+
47
+ - [#1010](https://github.com/codefastlabs/codefast/pull/1010) **Breaking:** `resolveAsync`, `resolveOptionalAsync` and `resolveAllAsync` on a disposed container — or on a child of
48
+ one — now return a promise rejected with `DisposedContainerError` instead of throwing it, and `resolveAllAsync(token)`
49
+ likewise rejects when a `when()` predicate throws or an alias cycle is followed, so every async method reports its
50
+ failure through the promise it returns. Code that awaits these calls, or chains `.catch()`, behaves as before or better;
51
+ only a caller that wrapped the call itself in `try`/`catch` without awaiting it no longer sees the error there, and
52
+ should await it instead.
53
+
54
+ - [#995](https://github.com/codefastlabs/codefast/pull/995) Add `container.explain(token, options)`, which answers why a request selects the binding it does without instantiating
55
+ anything. It reads the container chain the way `resolve` does and decides by the same rules, so its answer is the
56
+ engine's own. It reports each registry it read with every candidate and its verdict (`eligible`, `slot-mismatch`,
57
+ `predicate-refused`, `collection-member`), the rule that decided (`sole-candidate`, `sole-predicate`, `most-criteria`,
58
+ `default-alias`, `ambiguous`), the binding the request ends on after every alias, and an `outcome` naming the error
59
+ `resolve` would throw when it ends on none. `when()` predicates run as they would in `resolve`, and `options.ancestors`
60
+ explains a request made from inside other resolutions, so a predicate that reads the parent sees the one it would.
61
+
62
+ - [#999](https://github.com/codefastlabs/codefast/pull/999) **Breaking:** a binding's slot is declared before its strategy —
63
+ `bind(Logger).whenNamed("file").to(FileLogger).singleton()`. `when()`, `whenNamed()`, `whenTagged()`, `whenDefault()`
64
+ and `many()` now come before `to*()` and are refused after it with `ChainAlreadyRegisteredError`, so a binding registers
65
+ once, in its final slot, and no later step moves it, displaces something else or brings back what it displaced. Scope
66
+ and lifecycle hooks stay after `to*()`. To migrate, move each chain's slot steps in front of its `to*()` call.
67
+
68
+ The registry no longer parks the bindings a re-slot displaced or restores them, so a bind with a name or a tag costs a
69
+ quarter to two fifths less. `BindingBuilder`, `ConstantBindingBuilder` and `AliasBindingBuilder` lose their `Names` type
70
+ parameter, since the slot is settled before they exist.
71
+
72
+ - [#1005](https://github.com/codefastlabs/codefast/pull/1005) **Breaking for deep subpath imports only** — the root entry `@codefast/di` is unchanged. `src/` now follows three rules,
73
+ and the published subpaths follow `src/` with no exception: a directory names a family of two or more modules and a lone
74
+ module sits flat at its layer; a directory carries the topic and a file its role, so no file repeats its directory's
75
+ name; and the exports map is the source tree, so the `strip` that kept the introspection modules at flat specifiers is
76
+ gone.
77
+
78
+ Moves: the `MetadataReader` verification leaves `resolution/cache/class-introspector` for `metadata/verify` — it was the
79
+ one value import pointing up from `metadata/` into `resolution/`; the `RESOLUTION_DIAGNOSTICS` channel leaves `errors/`
80
+ for `introspection/diagnostics`; the fluent chain's contract leaves `core/binding` for `core/binding-builders`, and the
81
+ class implementing it is `container/binding-chain`; `effectiveBindingScope` folds into `core/binding`.
82
+
83
+ Renamed specifiers: `./errors/errors` → `./errors`; `./errors/diagnostics` → `./introspection/diagnostics`;
84
+ `./ambient/active-container` → `./ambient-container`; `./container/binding-builders` → `./container/binding-chain`;
85
+ `./core/binding-scope` → gone (`effectiveBindingScope` is in `./core/binding`); `./injection/resolve-options` →
86
+ `./injection/dependency-slot`;
87
+ `./metadata/{metadata-keys,metadata-types,metadata-reader-token,symbol-metadata-reader,verifying-metadata-reader}` →
88
+ `./metadata/{keys,types,reader-token,symbol-reader,verifying-reader}`; `./lifecycle/{lifecycle-manager,scope-manager}` →
89
+ `./lifecycle/{hooks,scopes}`; `./decorators/{lifecycle-decorators,decorator-metadata}` →
90
+ `./decorators/{lifecycle,metadata-record}`; `./resolution/path/resolution-path` → `./resolution/path`;
91
+ `./resolution/plan/{instantiation-plan,plan-codegen}` → `./resolution/plan/{compiler,codegen}`;
92
+ `./resolution/select/binding-select` → `./resolution/select/candidates`; `./resolution/cache/binding-lookup-cache` →
93
+ `./resolution/cache/lookup`; `./{inspector,explanation,dependency-graph,graph-adapters/*}` →
94
+ `./introspection/{inspector,explanation,dependency-graph,graph-adapters/*}`. New: `./core/binding-builders`,
95
+ `./metadata/verify`.
96
+
97
+ `@codefast/di-testing` follows the one specifier it imports, `metadata/verifying-reader`.
98
+
99
+ ### Patch Changes
100
+
101
+ - [#996](https://github.com/codefastlabs/codefast/pull/996) A request with criteria that matches no slot is fast again when the container holds no alias: `resolveOptional()` of a
102
+ missing tagged slot no longer probes for a default-slot alias to forward to in a registry that never held one.
103
+
104
+ - [#1009](https://github.com/codefastlabs/codefast/pull/1009) `resolveAsync` no longer rejects with `AsyncResolutionError` when a later sibling reads a singleton or scoped binding
105
+ synchronously — a `toDynamic` factory's `ctx.resolve`, an `@inject` accessor, a top-level `resolve` — while the async
106
+ lane is still materialising it from an earlier sibling. A level of the async lane now yields only where a dependency or
107
+ hook really awaits, so an instance built without yielding is cached before the next sibling starts, and only a
108
+ materialisation that is genuinely pending is published in flight.
109
+
110
+ - [#994](https://github.com/codefastlabs/codefast/pull/994) A scope refinement costs less at bind time. `singleton()`, `transient()` and `scoped()` now do nothing when the scope
111
+ does not change, and release only what exists when it does, so `bind(T).to(X).singleton()` straight after registration
112
+ no longer calls into the scope manager.
113
+
114
+ - [#997](https://github.com/codefastlabs/codefast/pull/997) Every read on a child container is cheaper. The child's check that no ancestor was disposed is now one epoch compare
115
+ that V8 inlines into each entry point, the walk up the chain runs only after some container was disposed, and a new
116
+ child starts confirmed live, so a per-request child skips the walk on its first read.
117
+
118
+ - [#994](https://github.com/codefastlabs/codefast/pull/994) Resolving a class binding for the first time is cheaper. The metadata cache now keeps one record per class, which a cold
119
+ resolve looks up once instead of reading a separate map for each fact, and a class with no constructor parameters is
120
+ built without allocating an empty argument list. This mostly speeds up containers that bind and resolve many class
121
+ singletons at startup.
122
+
123
+ - [#1001](https://github.com/codefastlabs/codefast/pull/1001) The first resolve of a class binding is cheaper. The class's parameter list, and the check that a subclass is not
124
+ dropping its base's declared dependencies, are settled once per class instead of once per instantiation, and the class
125
+ asked about last is answered without a map lookup, so a hundred singletons of one class bind and materialize about a
126
+ fifth faster.
127
+
128
+ - [#994](https://github.com/codefastlabs/codefast/pull/994) A root-level `resolveAll()` read in a loop over the same token, as an event bus does for every event, is cheaper: the
129
+ lookup cache keeps the last collection in front of its map, so a repeat read skips the map lookup.
130
+
131
+ - [#997](https://github.com/codefastlabs/codefast/pull/997) A container that reads a collection once no longer builds a value list it never reads again. A root-level `resolveAll`
132
+ keeps its stable value list from the second read on, the way an instantiation plan compiles on repeat, so a fresh
133
+ container per request pays only for the members it resolves.
134
+
135
+ - [#996](https://github.com/codefastlabs/codefast/pull/996) A resolve that carries options on a root container gets back much of the speed 0.11.0 took from it. The check that
136
+ refuses a disposed chain grew in that release enough that V8 stopped inlining the lookup behind that entry point; a live
137
+ root now pays two field reads for it, and only a child reads the dispose epoch.
138
+
139
+ - [#998](https://github.com/codefastlabs/codefast/pull/998) A root container's `resolve(token, options)` no longer pays for the check a child makes that no ancestor was disposed.
140
+ Each container is created with the `resolve` of its role, so the root's never carries the child's path and the rule that
141
+ a disposed ancestor refuses its descendants stays as it was.
142
+
143
+ - [#994](https://github.com/codefastlabs/codefast/pull/994) A scoped binding whose scope changes stops handing children the instance they cached before the change. A binding
144
+ registered on a parent, resolved from a child, then refined `scoped()` → `transient()` → `scoped()` gave that child its
145
+ old scoped instance back, because only the registering container's cache was cleared. Every container now files a scoped
146
+ instance under a key the binding replaces when it leaves `scoped`, so the cached instance is never found again.
147
+
148
+ - [#1003](https://github.com/codefastlabs/codefast/pull/1003) A teardown reads a class's `@preDestroy` methods once for a run of bindings of that class, instead of once per binding,
149
+ and names the token only when a hook fails.
150
+
151
+ - [#1002](https://github.com/codefastlabs/codefast/pull/1002) A rebind that owes no teardown allocates nothing for the binding it displaces, and a collection read skips its scan for
152
+ dangling aliases in a chain that never held one.
153
+
3
154
  ## 0.11.0
4
155
 
5
156
  ### Minor Changes
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @codefast/di
2
2
 
3
- Lightweight, type-safe dependency injection for TypeScript applications, built on TC39 Stage 3 decorators with no
3
+ Type-safe dependency injection for TypeScript applications, with zero dependencies, built on standard decorators with no
4
4
  `reflect-metadata` and no `experimentalDecorators`.
5
5
 
6
6
  [![npm version](https://img.shields.io/npm/v/@codefast/di)](https://www.npmjs.com/package/@codefast/di)
@@ -20,12 +20,12 @@ its keep as the dependency graph grows.
20
20
 
21
21
  - **Typed tokens.** `Token<Value>` flows through every `bind → resolve` path, so `resolve()` returns exactly the type
22
22
  you registered.
23
- - **Native Stage 3 decorators.** `@injectable`, `inject`, `optional`, `injectAll`, `@postConstruct`, and `@preDestroy`
24
- declare dependencies explicitly — no runtime reflection.
23
+ - **Standard decorators.** `@injectable`, `inject`, `optional`, `injectAll`, `@postConstruct`, and `@preDestroy` declare
24
+ dependencies explicitly — no runtime reflection.
25
25
  - **Fluent bindings.** Constants, classes, sync and async factories, aliases, named/tagged/predicate constraints, and
26
26
  lifecycle hooks compose in one invariant chain order.
27
27
  - **Scopes with validation.** Choose `singleton`, `scoped`, or `transient`, and call `validate()` to catch captive
28
- dependencies before the first request.
28
+ dependencies, missing bindings and cycles before the first request.
29
29
  - **Modules and introspection.** Bundle bindings into reusable modules, and inspect a container or render its dependency
30
30
  graph as DOT, Mermaid, Cytoscape, or React Flow.
31
31
 
@@ -36,7 +36,7 @@ pnpm add @codefast/di
36
36
  ```
37
37
 
38
38
  `@codefast/di` runs on Node.js 24 or later and in Chrome and Edge 136, Firefox 136, or Safari 18.4 or later
39
- ([support policy](../../SUPPORT.md#browsers)). It requires TypeScript 7 or later, with native Stage 3 decorators. Leave
39
+ ([support policy](../../SUPPORT.md#browsers)). It requires TypeScript 7 or later, with standard decorators. Leave
40
40
  `experimentalDecorators` off — it's off by default. Its declarations use explicit resource management, which no numbered
41
41
  `lib` declares yet, so your program needs those types: `@types/node` 24 or later loads them, and any other program adds
42
42
  `ESNext.Disposable` to `lib` once its runtime ships explicit resource management. A browser program that targets Safari,
@@ -44,6 +44,67 @@ which has not shipped it, keeps `skipLibCheck` on and calls `dispose()` instead
44
44
  published on 0.x and versioned on its own track: breaking changes ship as minor versions, so pin the minor version when
45
45
  you need stability.
46
46
 
47
+ ### Toolchain
48
+
49
+ No JavaScript engine runs decorators natively yet — the TC39 proposal went back to Stage 2.7 in May 2026 — so a
50
+ decorated class always passes through a transform that lowers it. What that takes depends on the toolchain:
51
+
52
+ | Toolchain | What it needs |
53
+ | ------------------------- | ----------------------------------------------------------------------------------------------------------- |
54
+ | tsx | Nothing: it lowers standard decorators for the Node it runs on. |
55
+ | esbuild | A `target` below `esnext`, such as `es2024` or `node24`: at its default, `esnext`, decorators pass through. |
56
+ | Vite 8, and Vitest on it | The Babel decorators plugin: their Oxc transform lowers only `experimentalDecorators`. |
57
+ | SWC 1.15.21 or later | `jsc.parser.decorators: true` and `jsc.transform.decoratorVersion: "2023-11"`. |
58
+ | `tsc`, then Node directly | A `target` below `ESNext`, and `Symbol.metadata` installed before any decorated class is defined. |
59
+
60
+ For Vite and Vitest, add `@babel/core`, `@babel/plugin-proposal-decorators` and `@rolldown/plugin-babel` as dev
61
+ dependencies, then the decorators preset to the config — `vitest.config.ts` takes the same one:
62
+
63
+ ```ts
64
+ // vite.config.ts
65
+ import babel, { defineRolldownBabelPreset } from "@rolldown/plugin-babel";
66
+ import { defineConfig } from "vite";
67
+
68
+ export default defineConfig({
69
+ plugins: [
70
+ babel({
71
+ presets: [
72
+ defineRolldownBabelPreset({
73
+ preset: () => ({ plugins: [["@babel/plugin-proposal-decorators", { version: "2023-11" }]] }),
74
+ // Every decorator holds an `@`, so Babel skips only files that cannot hold one.
75
+ rolldown: { filter: { code: "@" } },
76
+ }),
77
+ ],
78
+ }),
79
+ ],
80
+ });
81
+ ```
82
+
83
+ The filter is what keeps Babel off the files without decorators; the plugin applies it only when every preset carries
84
+ one and `plugins` is left empty. Keep it the plain `"@"`: a narrower pattern can miss a legal position such as
85
+ `export @injectable() class`, and a missed file ships its decorators raw. Beside other presets, list this one last,
86
+ since Babel runs presets last to first.
87
+
88
+ > **Warning.** Without the plugin the decorators pass through untouched, and nothing flags it at build time:
89
+ > `vite build` succeeds, then the bundle throws `SyntaxError: Invalid or unexpected token` as it loads — the same error
90
+ > Vitest reports for a test file that imports a decorated class.
91
+
92
+ `tsc` leaves decorators in its output at `target: "ESNext"`. At any lower target it lowers them, but on a runtime
93
+ without `Symbol.metadata` it hands them no metadata object, which every decorator here reports as
94
+ `MissingDecoratorMetadataError`. Install the symbol in a module the entry point imports first:
95
+
96
+ ```ts
97
+ (Symbol as { metadata?: symbol }).metadata ??= Symbol.for("Symbol.metadata");
98
+ ```
99
+
100
+ Not every toolchain lowers them yet. Node's own type stripping rejects decorators; Next.js cannot select the `2023-11`
101
+ version (vercel/next.js#96328 adds it); and Bun 1.4.2 and earlier inject the wrong values, silently, into a module that
102
+ holds two decorated classes (oven-sh/bun#28316).
103
+
104
+ [The specification → tsconfig setup](./spec/decorators.md#tsconfig-setup) covers the compiler options. And
105
+ [decorators](#decorators) are optional: an app wired through `toResolved`, `toDynamic` and `Module.fromBindings` needs
106
+ none of this.
107
+
47
108
  ## Quick start
48
109
 
49
110
  ```ts
@@ -98,9 +159,9 @@ A class constructor works as a key too: `container.bind(UserService).toSelf()`,
98
159
 
99
160
  ## Bindings
100
161
 
101
- A binding describes how the container produces a value for a token. You declare it as a chain, in one fixed order: a
102
- strategy (the `to*` step, which produces the value), then optional constraints, a scope, and lifecycle hooks. Only
103
- `container.bind(key).to*(…)` is required; everything after it is optional.
162
+ A binding describes how the container produces a value for a token. You declare it as a chain, in one fixed order:
163
+ optional [constraints](#constraints), then a strategy (the `to*` step, which produces the value and registers the
164
+ binding), then an optional scope and lifecycle hooks. Only `container.bind(key).to*(…)` is required.
104
165
 
105
166
  | Strategy | Produces |
106
167
  | --------------------------------- | ----------------------------------------------------------------- |
@@ -134,7 +195,7 @@ no scope produces a new instance every time.
134
195
  | Scope | Lifetime |
135
196
  | -------------- | ---------------------------------------------------------------------------------------- |
136
197
  | `.singleton()` | One instance for the container that owns the binding; children resolve the same instance |
137
- | `.scoped()` | One instance per child container — resolving from a container with no child scope throws |
198
+ | `.scoped()` | One instance per child container, deactivated with it — resolving outside a child throws |
138
199
  | `.transient()` | A new instance on every resolution — the default |
139
200
 
140
201
  ```ts
@@ -148,9 +209,10 @@ container.bind(RequestContextToken).toSelf().scoped();
148
209
  ### Lifecycle hooks
149
210
 
150
211
  A lifecycle hook runs your code as an instance is created or torn down. `.onActivation(fn)` runs right after an instance
151
- is created, and may replace it; `.onDeactivation(fn)` runs when the owning container is disposed or the binding is
152
- unbound. Both are also available container-wide, through `container.onActivation(token, fn)` and
153
- `container.onDeactivation(token, fn)`:
212
+ is created, and may replace it; `.onDeactivation(fn)` runs as the instance's lifetime ends — a singleton's when its
213
+ container is disposed or the binding is unbound, a scoped instance's when the child container that cached it is
214
+ disposed. A transient instance has no deactivation. Both hooks are also available container-wide, through
215
+ `container.onActivation(token, fn)` and `container.onDeactivation(token, fn)`:
154
216
 
155
217
  ```ts
156
218
  container
@@ -169,8 +231,8 @@ constraints is its slot — think of a slot as a labelled variant of the token.
169
231
  `{ name }`:
170
232
 
171
233
  ```ts
172
- container.bind(LoggerToken).toConstantValue(consoleLogger).whenNamed("console");
173
- container.bind(LoggerToken).toConstantValue(fileLogger).whenNamed("file");
234
+ container.bind(LoggerToken).whenNamed("console").toConstantValue(consoleLogger);
235
+ container.bind(LoggerToken).whenNamed("file").toConstantValue(fileLogger);
174
236
 
175
237
  container.resolve(LoggerToken, { name: "file" }); // → fileLogger
176
238
  ```
@@ -180,7 +242,7 @@ Declare the names on the token and they become checked, completable literals at
180
242
  ```ts
181
243
  const LoggerToken = token<Logger, "console" | "file">("app:Logger");
182
244
 
183
- container.bind(LoggerToken).toConstantValue(fileLogger).whenNamed("file");
245
+ container.bind(LoggerToken).whenNamed("file").toConstantValue(fileLogger);
184
246
  container.resolve(LoggerToken, { name: "file" }); // { name: "fiel" } is a compile error
185
247
  ```
186
248
 
@@ -193,7 +255,7 @@ import { tag } from "@codefast/di";
193
255
 
194
256
  const Provider = tag<"s3" | "gcs">("app:provider");
195
257
 
196
- container.bind(StorageToken).to(S3Storage).whenTagged(Provider.of("s3"));
258
+ container.bind(StorageToken).whenTagged(Provider.of("s3")).to(S3Storage);
197
259
  container.resolve(StorageToken, { tag: Provider.of("s3") }); // → S3Storage
198
260
  ```
199
261
 
@@ -212,6 +274,19 @@ container.resolve(StorageToken, { tag: Provider.of("s3") }); // → S3Storage
212
274
  `injectAll` alike. Chain `.whenTagged(...)` once per criterion a slot carries, and request several at once with
213
275
  `{ tags: [...] }`.
214
276
 
277
+ **One binding per slot; a collection with `many()`.** Binding a token again in a slot it already fills replaces the
278
+ earlier binding — `bind` is last-wins per slot, so `resolveAll` then finds only the newer one. To register several
279
+ implementations for `resolveAll` and `injectAll` to gather, mark each one a collection member with `.many()`:
280
+
281
+ ```ts
282
+ container.bind(PluginToken).many().to(AuditPlugin);
283
+ container.bind(PluginToken).many().to(MetricsPlugin);
284
+
285
+ container.resolveAll(PluginToken); // → an AuditPlugin and a MetricsPlugin, in registration order
286
+ ```
287
+
288
+ A member keeps the default slot and is never what a single `resolve` selects.
289
+
215
290
  **Predicate constraints — graph-aware selection.** When a slot isn't enough — when you need to choose based on _who_ is
216
291
  resolving — pass a predicate to `.when(ctx => boolean)`. It runs at resolve time, after slot matching. These ready-made
217
292
  predicates ship from the root entry:
@@ -230,7 +305,7 @@ predicates ship from the root entry:
230
305
  | `whenAnyAncestorTaggedAll(criteria)` | some ancestor's slot carries all criteria in the array |
231
306
 
232
307
  For the exact matching and most-specific-wins rules, see
233
- [`SPEC.md` → Slots and last-wins](./SPEC.md#slots-and-last-wins--the-exact-definition).
308
+ [the specification → Slots and last-wins](./spec/binding.md#slots-and-last-wins--the-exact-definition).
234
309
 
235
310
  ## Decorators
236
311
 
@@ -252,7 +327,7 @@ class UserRepository {
252
327
  constructor(
253
328
  private readonly db: Database,
254
329
  private readonly cache: Cache | undefined,
255
- private readonly plugins: Array<Plugin>,
330
+ private readonly plugins: ReadonlyArray<Plugin>,
256
331
  private readonly audit: Logger,
257
332
  ) {}
258
333
 
@@ -349,17 +424,21 @@ parent's singletons, while each `scoped` binding gets a fresh instance per child
349
424
 
350
425
  A captive dependency is a long-lived binding that holds a shorter-lived one — a `singleton` that depends on a `scoped`
351
426
  or `transient` binding — which silently freezes that dependency for the singleton's whole life. `validate()` fails fast
352
- on captive dependencies, and on constraints no request can satisfy, before the first resolve.
427
+ on captive dependencies, and on constraints no request can satisfy, before the first resolve. It also throws the error
428
+ `resolve()` would for a singleton graph that cannot resolve: a required dependency nothing binds, a slot nothing
429
+ matches, or a cycle.
353
430
 
354
431
  ```ts
355
- container.validate(); // throws ScopeViolationError on the first violation
432
+ container.validate(); // throws on the first violation — ScopeViolationError, TokenNotBoundError, …
356
433
  ```
357
434
 
435
+ A transient or scoped consumer's dependencies are left to `resolve()`, because a child container may still bind them.
436
+
358
437
  ### Disposal
359
438
 
360
439
  `Container` implements `AsyncDisposable`, so `await using` runs every deactivation hook automatically as the block
361
- exits. Synchronous `using` isn't supported — `onDeactivation` may be async — and `Symbol.dispose` throws
362
- `SyncDisposalNotSupportedError`.
440
+ exits, and disposing a child deactivates the scoped instances it cached. Synchronous `using` is a compile error: a
441
+ deactivation hook may be async, so the container implements `Symbol.asyncDispose` only.
363
442
 
364
443
  ### Introspection
365
444
 
@@ -367,6 +446,18 @@ exits. Synchronous `using` isn't supported — `onDeactivation` may be async —
367
446
  token's bindings; and `container.generateDependencyGraph()` returns a JSON graph. Adapters render that graph for common
368
447
  viewers: `toDotGraph`, `toMermaidGraph`, `toCytoscapeGraph`, and `toReactFlowGraph`.
369
448
 
449
+ `container.explain(token, options)` answers why a request selects the binding it does, without instantiating anything:
450
+ every candidate each registry offered and what selection made of it, the rule that decided, and the binding it ends on.
451
+ It evaluates `when()` predicates the way `resolve` does, and `options.ancestors` explains a request made from inside
452
+ other resolutions, so a predicate that reads the parent sees the one it would:
453
+
454
+ ```ts
455
+ const explanation = container.explain(Logger, { ancestors: [Settlement] });
456
+
457
+ explanation.selected?.id; // the binding `resolve` would answer with
458
+ explanation.steps[0]?.rule; // "sole-predicate": the only candidate with a `when()` beat the rest
459
+ ```
460
+
370
461
  ## Modules
371
462
 
372
463
  A module is a reusable, stateless bundle of related bindings. Group them once, then load them into any container.
@@ -463,31 +554,37 @@ Every error extends `DiError` and carries a stable `code`, so you can branch on
463
554
  | `MissingContainerContextError` | `MISSING_CONTAINER_CONTEXT` | An `@inject` accessor initializes with no container open |
464
555
  | `DisposedContainerError` | `DISPOSED_CONTAINER` | A disposed container is used |
465
556
 
557
+ A miss below the token you asked for names its path: `TokenNotBoundError` and `NoMatchingBindingError` carry `path`, and
558
+ the message ends with it — `Path: UserService → UserRepository → app:Logger`.
559
+
466
560
  The full taxonomy — including `MissingMetadataError`, `InvalidMetadataError`, `RebindUnboundTokenError`,
467
561
  `AsyncModuleLoadError`, `InvalidBindingDeclarationError`, and the rest — is exported from the root entry and from
468
- `@codefast/di/errors/errors`.
562
+ `@codefast/di/errors`.
469
563
 
470
564
  ## Subpath exports
471
565
 
472
566
  Prefer the root entry: it re-exports the whole public API. Reach for a subpath only to trim a bundle, or to pull in
473
567
  something the root doesn't surface.
474
568
 
475
- Every module is also published as a subpath that mirrors the source layout: the model under `@codefast/di/core/*`
476
- (`core/token`, `core/tag`, `core/module`, …), errors under `errors/*`, the runtime under `container/*`, `injection/*`,
477
- `lifecycle/*`, and `resolution/*` (for example `@codefast/di/resolution/select/constraints`), and decorators and
478
- metadata under `decorators/*` and `metadata/*`. Introspection ships at flat specifiers: `@codefast/di/inspector`,
479
- `@codefast/di/dependency-graph`, and `@codefast/di/graph-adapters/{dot,mermaid,cytoscape,reactflow}`.
569
+ Every module is also published as a subpath that is its source path, with no exception: the model under
570
+ `@codefast/di/core/*` (`core/token`, `core/tag`, `core/module`, …), the error taxonomy at `@codefast/di/errors`, the
571
+ runtime under `container/*`, `injection/*`, `lifecycle/*`, and `resolution/*` (for example
572
+ `@codefast/di/resolution/select/constraints`), decorators and metadata under `decorators/*` and `metadata/*`, and
573
+ introspection under `introspection/*`: `@codefast/di/introspection/inspector`, `@codefast/di/introspection/explanation`,
574
+ `@codefast/di/introspection/dependency-graph`, and
575
+ `@codefast/di/introspection/graph-adapters/{dot,mermaid,cytoscape,reactflow}`.
480
576
 
481
577
  ## Benchmarks
482
578
 
483
579
  A first-party benchmark suite lives in the monorepo, at [`benchmarks/di`](../../benchmarks/di). It runs the same
484
- workloads through `@codefast/di`, InversifyJS, Awilix, and tsyringe, and its `RESULTS.md` ledger records the numbers
485
- alongside the method that produced them. Run it yourself rather than taking any figure on faith.
580
+ workloads through `@codefast/di`, InversifyJS, Awilix, tsyringe, Brandi, ditox, and injection-js, and its `RESULTS.md`
581
+ ledger records the numbers alongside the method that produced them. Run it yourself rather than taking any figure on
582
+ faith.
486
583
 
487
584
  ## Documentation
488
585
 
489
586
  - [Rendered docs on codefastlabs.com](https://codefastlabs.com/docs/di)
490
- - [`SPEC.md`](./SPEC.md) — the behavioural contract: public API, semantics, and errors.
587
+ - [`spec/`](./spec/README.md) — the behavioural contract: public API, semantics, and errors, one document per API area.
491
588
  - [`ARCHITECTURE.md`](./ARCHITECTURE.md) — the internal shape and the invariants the hot paths depend on.
492
589
  - [`DECISIONS.md`](./DECISIONS.md) — why the API looks like this, and what it did not take from InversifyJS v8.
493
590
  - [`examples/`](./examples/README.md) — runnable examples from basic tokens to a multi-file Ports & Adapters app.
@@ -3,17 +3,18 @@
3
3
  *
4
4
  * @remarks One object plays every role: each builder step, the record the registry stores, and the
5
5
  * binding the resolver reads. The binding fields are declared first and in a fixed order, so every
6
- * binding in the process shares one hidden class; the return type of each step is what pins the
7
- * chain's order, so no runtime check has to.
6
+ * binding in the process shares one hidden class; the return type of each step pins the chain's
7
+ * order, slot before strategy, so the binding registers once and is never re-slotted.
8
8
  */
9
- import type { AliasBindingBuilder, Binding, BindingBuilder, BindingSlot, BindToBuilder, ConstantBindingBuilder, ScopedBindingBuilder, SingletonBindingBuilder, SingletonLifecycleBuilder, TransientBindingBuilder } from "#core/binding";
9
+ import type { Binding, BindingSlot } from "#core/binding";
10
+ import type { AliasBindingBuilder, BindingBuilder, BindToBuilder, ConstantBindingBuilder, ScopedBindingBuilder, SingletonBindingBuilder, SingletonLifecycleBuilder, TransientBindingBuilder } from "#core/binding-builders";
10
11
  import type { DeclaredBinding } from "#core/binding-declaration";
11
12
  import type { BindingRegistry } from "#core/registry";
12
13
  import type { BindingTag } from "#core/tag";
13
14
  import type { Token } from "#core/token";
14
15
  import type { ActivationHandler, BindingConstraint, BindingIdentifier, BindingKind, BindingScope, Constructor, DeactivationHandler, ResolutionContext, ResolutionFrame } from "#core/types";
15
16
  import type { InjectableDependency, InjectionDescriptor, ResolvedDependencyValue } from "#injection/descriptor";
16
- import type { ScopeManager } from "#lifecycle/scope-manager";
17
+ import type { ScopeManager } from "#lifecycle/scopes";
17
18
  /**
18
19
  * Where a chain registers, and on whose behalf.
19
20
  *
@@ -26,24 +27,22 @@ export interface BindingRegistration {
26
27
  readonly registry: BindingRegistry;
27
28
  readonly scope: ScopeManager;
28
29
  readonly moduleBindingIds: Array<BindingIdentifier> | undefined;
29
- /** Runs for a binding this registration displaces, instead of parking it for a later restore. */
30
+ /** Runs for a binding this registration displaces, deactivating it on the spot. */
30
31
  readonly deactivateDisplaced?: ((binding: Binding) => void) | undefined;
31
- /** Notes a parked displaced binding so the container can still tear it down if no restore reclaims it. */
32
+ /** Notes a displaced binding so the container can still tear it down, when nothing deactivates it on the spot. */
32
33
  readonly onDisplaced?: ((binding: Binding) => void) | undefined;
33
- /** Undoes {@link BindingRegistration.onDisplaced} when a refinement restores the binding to the registry. */
34
- readonly onRestored?: ((binding: Binding) => void) | undefined;
35
34
  }
36
35
  /**
37
36
  * The one builder behind `bind()` and every `to*()` return type — and the binding it registers.
38
37
  *
39
38
  * @remarks The binding fields come first, in the order every binding shares, and the chain's own
40
- * bookkeeping follows as private fields; `to*()` fills the fields in place and hands this object to
41
- * the registry, so a plain bind is one allocation. Refinements write the registered object: a scope
42
- * or hook in place, a slot or predicate through the registry so its indexes follow.
39
+ * bookkeeping follows as a private field. The slot steps write the fields before anything registers;
40
+ * `to*()` fills the strategy in place and hands this object to the registry once, so a bind is one
41
+ * allocation and one add. A scope or hook afterwards writes the registered object in place.
43
42
  *
44
43
  * @since 0.5.0-canary.8
45
44
  */
46
- export declare class BindingChain<Value, Names extends string = string> implements AliasBindingBuilder<Names>, BindingBuilder<Value, Names>, BindToBuilder<Value, Names>, ConstantBindingBuilder<Value, Names>, ScopedBindingBuilder<Value>, SingletonBindingBuilder<Value>, SingletonLifecycleBuilder<Value>, TransientBindingBuilder<Value> {
45
+ export declare class BindingChain<Value, Names extends string = string> implements AliasBindingBuilder, BindingBuilder<Value>, BindToBuilder<Value, Names>, ConstantBindingBuilder<Value>, ScopedBindingBuilder<Value>, SingletonBindingBuilder<Value>, SingletonLifecycleBuilder<Value>, TransientBindingBuilder<Value> {
47
46
  #private;
48
47
  kind: BindingKind;
49
48
  readonly identifier: BindingIdentifier;
@@ -51,8 +50,8 @@ export declare class BindingChain<Value, Names extends string = string> implemen
51
50
  frame: ResolutionFrame | undefined;
52
51
  rootContext: ResolutionContext | undefined;
53
52
  activationStamp: number;
54
- registrationOrder: number;
55
53
  instance: unknown;
54
+ scopedCacheKey: BindingIdentifier;
56
55
  readonly token: Token<Value, Names> | Constructor<Value>;
57
56
  slot: BindingSlot;
58
57
  predicate: BindingConstraint | undefined;
@@ -65,26 +64,20 @@ export declare class BindingChain<Value, Names extends string = string> implemen
65
64
  activationHook: ActivationHandler<Value> | undefined;
66
65
  deactivationHook: DeactivationHandler<Value> | undefined;
67
66
  constructor(token: Token<Value, Names> | Constructor<Value>, registration: BindingRegistration);
68
- /**
69
- * Registers a declared module's bindings in list order, each a chain of its own that no step
70
- * will ever refine.
71
- *
72
- * @remarks A displaced binding is handled as `to*()` handles it, never parked: nothing refines the
73
- * chain, so nothing could restore it.
74
- */
67
+ /** Registers a declared module's bindings in list order, each a chain of its own, handled as `to*()` handles one. */
75
68
  static registerDeclared(declarations: ReadonlyArray<DeclaredBinding>, registration: BindingRegistration): void;
76
- to(type: Constructor<Value>): BindingBuilder<Value, Names>;
77
- toSelf(): BindingBuilder<Value, Names>;
78
- toConstantValue(value: Value): ConstantBindingBuilder<Value, Names>;
79
- toDynamic(factory: (ctx: ResolutionContext) => Value): BindingBuilder<Value, Names>;
80
- toDynamicAsync(factory: (ctx: ResolutionContext) => Promise<Value>): BindingBuilder<Value, Names>;
69
+ to(type: Constructor<Value>): BindingBuilder<Value>;
70
+ toSelf(): BindingBuilder<Value>;
71
+ toConstantValue(value: Value): ConstantBindingBuilder<Value>;
72
+ toDynamic(factory: (ctx: ResolutionContext) => Value): BindingBuilder<Value>;
73
+ toDynamicAsync(factory: (ctx: ResolutionContext) => Promise<Value>): BindingBuilder<Value>;
81
74
  toResolved<const Deps extends ReadonlyArray<InjectableDependency>>(factory: (...args: {
82
75
  [K in keyof Deps]: ResolvedDependencyValue<NoInfer<Deps>[K]>;
83
- }) => Value, deps: Deps): BindingBuilder<Value, Names>;
76
+ }) => Value, deps: Deps): BindingBuilder<Value>;
84
77
  toResolvedAsync<const Deps extends ReadonlyArray<InjectableDependency>>(factory: (...args: {
85
78
  [K in keyof Deps]: ResolvedDependencyValue<NoInfer<Deps>[K]>;
86
- }) => Promise<Value>, deps: Deps): BindingBuilder<Value, Names>;
87
- toAlias(target: Token<Value> | Constructor<Value>): AliasBindingBuilder<Names>;
79
+ }) => Promise<Value>, deps: Deps): BindingBuilder<Value>;
80
+ toAlias(target: Token<Value> | Constructor<Value>): AliasBindingBuilder;
88
81
  when(predicate: BindingConstraint): this;
89
82
  whenNamed(name: Names): this;
90
83
  whenTagged(criterion: BindingTag): this;