di-craft 0.0.20 → 0.0.22
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 +103 -501
- package/dist/container-l5DkUBUj.mjs +323 -0
- package/dist/container-l5DkUBUj.mjs.map +1 -0
- package/dist/hydration-BJ-gWd38.d.mts +2 -1
- package/dist/hydration-CLZgCNpJ.mjs +60 -0
- package/dist/hydration-CLZgCNpJ.mjs.map +1 -0
- package/dist/index.d.mts +2 -4
- package/dist/index.mjs +103 -1
- package/dist/index.mjs.map +1 -0
- package/dist/next/client.mjs +3 -1
- package/dist/next/server.d.mts +2 -1
- package/dist/next/server.mjs +53 -1
- package/dist/next/server.mjs.map +1 -0
- package/dist/types-BzLfq9VA.d.mts +2 -1
- package/package.json +20 -7
- package/dist/container-C6GcrdNn.mjs +0 -1
- package/dist/hydration-DsCFyhuJ.mjs +0 -1
package/README.md
CHANGED
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
|
|
7
7
|
<p align="center">
|
|
8
8
|
<b>A tiny, type-safe dependency injection container for TypeScript</b>
|
|
9
|
+
<br />
|
|
10
|
+
<span>Framework-agnostic core with an optional Next.js App Router / RSC adapter</span>
|
|
9
11
|
</p>
|
|
10
12
|
|
|
11
13
|
<p align="center">
|
|
@@ -17,35 +19,10 @@
|
|
|
17
19
|
</a>
|
|
18
20
|
</p>
|
|
19
21
|
|
|
20
|
-
##
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
- [Features](#features)
|
|
25
|
-
- [Install](#install)
|
|
26
|
-
- [Core concepts](#core-concepts)
|
|
27
|
-
- [Tokens](#tokens)
|
|
28
|
-
- [Providers](#providers)
|
|
29
|
-
- [Annotation-based class providers](#annotation-based-class-providers)
|
|
30
|
-
- [Optional dependencies](#optional-dependencies)
|
|
31
|
-
- [Container](#container)
|
|
32
|
-
- [Scopes](#scopes)
|
|
33
|
-
- [Disposal](#disposal)
|
|
34
|
-
- [Child containers](#child-containers)
|
|
35
|
-
- [Cycle detection](#cycle-detection)
|
|
36
|
-
- [Async dependencies](#async-dependencies)
|
|
37
|
-
- [Adapters](#adapters)
|
|
38
|
-
- [Next.js App Router](#nextjs-app-router)
|
|
39
|
-
- [Dependency injection vs service location](#dependency-injection-vs-service-location)
|
|
40
|
-
- [Error handling](#error-handling)
|
|
41
|
-
- [API reference](#api-reference)
|
|
42
|
-
- [License](#license)
|
|
43
|
-
|
|
44
|
-
## Quick start
|
|
45
|
-
|
|
46
|
-
Declare typed tokens, describe how each one is built with a provider, then create
|
|
47
|
-
a container and resolve from it. Dependencies are wired explicitly through the
|
|
48
|
-
`deps` map and resolved for you.
|
|
22
|
+
## Quick Start
|
|
23
|
+
|
|
24
|
+
Declare typed tokens, describe how each value is built, then resolve at the
|
|
25
|
+
composition root.
|
|
49
26
|
|
|
50
27
|
```ts
|
|
51
28
|
import {
|
|
@@ -56,15 +33,44 @@ import {
|
|
|
56
33
|
type Provider,
|
|
57
34
|
} from "di-craft";
|
|
58
35
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
36
|
+
type Config = {
|
|
37
|
+
readonly logPrefix: string;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
class Logger {
|
|
41
|
+
private readonly prefix: string;
|
|
42
|
+
|
|
43
|
+
constructor(prefix: string) {
|
|
44
|
+
this.prefix = prefix;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
info(message: string): string {
|
|
48
|
+
return `${this.prefix}: ${message}`;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
class UserService {
|
|
53
|
+
private readonly logger: Logger;
|
|
62
54
|
|
|
63
|
-
|
|
64
|
-
|
|
55
|
+
constructor(logger: Logger) {
|
|
56
|
+
this.logger = logger;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
list(): readonly string[] {
|
|
60
|
+
this.logger.info("list users");
|
|
61
|
+
return ["Ada", "Grace"];
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const CONFIG = createToken<Config>("CONFIG");
|
|
66
|
+
const LOGGER = createToken<Logger>("LOGGER");
|
|
67
|
+
const USERS = createToken<UserService>("USERS");
|
|
68
|
+
|
|
69
|
+
const providers: readonly Provider[] = [
|
|
70
|
+
provideValue(CONFIG, { logPrefix: "users" }),
|
|
65
71
|
provideFactory(LOGGER, {
|
|
66
72
|
deps: { config: CONFIG },
|
|
67
|
-
useFactory: ({ config }) => new Logger(config.
|
|
73
|
+
useFactory: ({ config }) => new Logger(config.logPrefix),
|
|
68
74
|
}),
|
|
69
75
|
provideFactory(USERS, {
|
|
70
76
|
deps: { logger: LOGGER },
|
|
@@ -73,30 +79,21 @@ const providers: Provider[] = [
|
|
|
73
79
|
];
|
|
74
80
|
|
|
75
81
|
const container = createContainer(providers);
|
|
76
|
-
|
|
77
|
-
const users = container.get(USERS); // UserService, fully typed
|
|
82
|
+
const users = container.get(USERS); // UserService
|
|
78
83
|
```
|
|
79
84
|
|
|
80
|
-
##
|
|
81
|
-
|
|
82
|
-
Dependency injection without hidden magic — no `reflect-metadata`, no runtime
|
|
83
|
-
type guessing, and no framework coupling. You work with just **tokens**,
|
|
84
|
-
**providers**, a **container**, **scopes**, and **cycle detection**. Standard
|
|
85
|
-
JavaScript decorators are available as optional sugar for class providers, but
|
|
86
|
-
they still use explicit tokens.
|
|
87
|
-
|
|
88
|
-
## Features
|
|
85
|
+
## Why di-craft
|
|
89
86
|
|
|
90
87
|
- Zero runtime dependencies
|
|
91
88
|
- Type-safe tokens and factories
|
|
92
89
|
- Optional `@Injectable` annotation for class providers
|
|
90
|
+
- Optional Next.js App Router / React Server Components adapter
|
|
93
91
|
- Optional dependencies via `optional()`
|
|
94
92
|
- Singleton, transient, and scoped lifetimes
|
|
95
|
-
- Hierarchical child containers
|
|
93
|
+
- Hierarchical child containers for request-like lifecycles
|
|
96
94
|
- Deterministic disposal with `onDispose` hooks
|
|
97
95
|
- Circular dependency detection
|
|
98
|
-
- Tree-shakable,
|
|
99
|
-
- ESM-only, ships with TypeScript declarations
|
|
96
|
+
- Tree-shakable, ESM-only, ships with TypeScript declarations
|
|
100
97
|
|
|
101
98
|
## Install
|
|
102
99
|
|
|
@@ -107,57 +104,36 @@ pnpm add di-craft
|
|
|
107
104
|
yarn add di-craft
|
|
108
105
|
```
|
|
109
106
|
|
|
110
|
-
Requires Node.js `>= 20`. This package is ESM-only
|
|
111
|
-
CommonJS code can load it with a dynamic `import()`.
|
|
112
|
-
|
|
113
|
-
## Core concepts
|
|
114
|
-
|
|
115
|
-
### Tokens
|
|
116
|
-
|
|
117
|
-
A token is a unique, type-carrying key. Identity is based on an internal `symbol`,
|
|
118
|
-
**not** on the name — two tokens with the same name are still different.
|
|
119
|
-
|
|
120
|
-
```ts
|
|
121
|
-
const PORT = createToken<number>("port");
|
|
122
|
-
|
|
123
|
-
PORT.name; // "port" — used only for error messages
|
|
124
|
-
```
|
|
125
|
-
|
|
126
|
-
The type argument flows everywhere: providers must produce a matching value, and
|
|
127
|
-
`container.get(PORT)` returns `number`.
|
|
107
|
+
Requires Node.js `>= 20`. This package is ESM-only.
|
|
128
108
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
A provider tells the container how to produce the value for a token.
|
|
132
|
-
|
|
133
|
-
`provideValue` — register an existing value:
|
|
109
|
+
## Core API
|
|
134
110
|
|
|
135
111
|
```ts
|
|
136
|
-
|
|
112
|
+
import {
|
|
113
|
+
Scopes,
|
|
114
|
+
createChildContainer,
|
|
115
|
+
createContainer,
|
|
116
|
+
createToken,
|
|
117
|
+
optional,
|
|
118
|
+
provideFactory,
|
|
119
|
+
provideValue,
|
|
120
|
+
} from "di-craft";
|
|
137
121
|
```
|
|
138
122
|
|
|
139
|
-
|
|
123
|
+
Core concepts are documented in [docs/core.md](./docs/core.md).
|
|
140
124
|
|
|
141
|
-
|
|
142
|
-
provideFactory(HTTP, {
|
|
143
|
-
deps: { config: CONFIG }, // optional, keyed map of tokens
|
|
144
|
-
scope: "singleton", // optional, defaults to "singleton"
|
|
145
|
-
useFactory: ({ config }) => new HttpClient(config.apiUrl),
|
|
146
|
-
});
|
|
147
|
-
```
|
|
148
|
-
|
|
149
|
-
The keys in `deps` become the keys of the object passed to `useFactory`, each
|
|
150
|
-
resolved to its token's type.
|
|
125
|
+
Typed examples are available in [examples/typed-docs](./examples/typed-docs):
|
|
151
126
|
|
|
152
|
-
|
|
127
|
+
- [basic container](./examples/typed-docs/core/basic.ts)
|
|
128
|
+
- [scopes and child containers](./examples/typed-docs/core/scopes.ts)
|
|
129
|
+
- [annotation-based providers](./examples/typed-docs/annotations/injectable.ts)
|
|
130
|
+
- [Next.js request scope](./examples/typed-docs/next/request-scope.ts)
|
|
131
|
+
- [Next.js state hydration](./examples/typed-docs/next/hydration.ts)
|
|
153
132
|
|
|
154
|
-
|
|
155
|
-
with standard JavaScript decorators, then turn the class into a normal provider
|
|
156
|
-
with `provideInjectable`.
|
|
133
|
+
## Annotation-Based Providers
|
|
157
134
|
|
|
158
|
-
`@Injectable
|
|
159
|
-
|
|
160
|
-
`onDispose` behave exactly like they do in `provideFactory`.
|
|
135
|
+
`@Injectable` lets class-based services describe their token, constructor
|
|
136
|
+
dependencies, scope, and disposal hook next to the class.
|
|
161
137
|
|
|
162
138
|
```ts
|
|
163
139
|
import {
|
|
@@ -165,27 +141,23 @@ import {
|
|
|
165
141
|
Scopes,
|
|
166
142
|
createContainer,
|
|
167
143
|
createToken,
|
|
168
|
-
optional,
|
|
169
144
|
provideInjectable,
|
|
170
145
|
provideValue,
|
|
171
146
|
} from "di-craft";
|
|
172
147
|
|
|
173
|
-
const
|
|
174
|
-
const
|
|
175
|
-
const USERS = createToken<UserService>("users");
|
|
148
|
+
const LOGGER = createToken<Logger>("LOGGER");
|
|
149
|
+
const USERS = createToken<UserService>("USERS");
|
|
176
150
|
|
|
177
151
|
@Injectable({
|
|
178
152
|
token: USERS,
|
|
179
|
-
deps: [LOGGER
|
|
153
|
+
deps: [LOGGER],
|
|
180
154
|
scope: Scopes.Scoped,
|
|
181
155
|
})
|
|
182
156
|
class UserService {
|
|
183
157
|
private readonly logger: Logger;
|
|
184
|
-
private readonly config: Config | undefined;
|
|
185
158
|
|
|
186
|
-
constructor(logger: Logger
|
|
159
|
+
constructor(logger: Logger) {
|
|
187
160
|
this.logger = logger;
|
|
188
|
-
this.config = config;
|
|
189
161
|
}
|
|
190
162
|
}
|
|
191
163
|
|
|
@@ -193,265 +165,16 @@ const container = createContainer([
|
|
|
193
165
|
provideValue(LOGGER, new Logger()),
|
|
194
166
|
provideInjectable(UserService),
|
|
195
167
|
]);
|
|
196
|
-
|
|
197
|
-
const users = container.get(USERS); // UserService
|
|
198
|
-
```
|
|
199
|
-
|
|
200
|
-
`@Injectable` is the only annotation needed for class injection. It produces a
|
|
201
|
-
regular factory provider internally, so all existing container behavior still
|
|
202
|
-
applies: optional dependencies, scopes, child containers, disposal hooks,
|
|
203
|
-
overrides, and cycle detection.
|
|
204
|
-
|
|
205
|
-
di-craft does not use `reflect-metadata` or parameter decorators. Constructor
|
|
206
|
-
types are erased by JavaScript at runtime, so dependency tokens stay explicit
|
|
207
|
-
instead of being guessed from TypeScript types.
|
|
208
|
-
|
|
209
|
-
### Optional dependencies
|
|
210
|
-
|
|
211
|
-
Wrap a token with `optional` to mark a dependency as not required. When no
|
|
212
|
-
provider for it is registered anywhere in the container chain, the factory
|
|
213
|
-
receives `undefined` instead of the resolution throwing `MissingProviderError`.
|
|
214
|
-
The inferred type is widened to `T | undefined`, so TypeScript forces you to
|
|
215
|
-
handle the absent case:
|
|
216
|
-
|
|
217
|
-
```ts
|
|
218
|
-
import { optional, provideFactory } from "di-craft";
|
|
219
|
-
|
|
220
|
-
provideFactory(USERS, {
|
|
221
|
-
deps: { logger: optional(LOGGER) }, // LOGGER may or may not be registered
|
|
222
|
-
useFactory: ({ logger }) => {
|
|
223
|
-
logger?.info("creating users service"); // logger: Logger | undefined
|
|
224
|
-
return new UsersService();
|
|
225
|
-
},
|
|
226
|
-
});
|
|
227
|
-
```
|
|
228
|
-
|
|
229
|
-
The same descriptor works at the top level — `optional` can be passed anywhere a
|
|
230
|
-
dependency is accepted, including `container.get`:
|
|
231
|
-
|
|
232
|
-
```ts
|
|
233
|
-
const logger = container.get(optional(LOGGER)); // Logger | undefined
|
|
234
|
-
```
|
|
235
|
-
|
|
236
|
-
Optional only affects the token itself: if a provider _is_ registered, it is
|
|
237
|
-
resolved normally and its own errors (cycles, missing nested deps) still surface.
|
|
238
|
-
|
|
239
|
-
### Container
|
|
240
|
-
|
|
241
|
-
The container holds your providers and resolves values on demand. Create one
|
|
242
|
-
from a list of providers (all optional), then add more, check, resolve, and
|
|
243
|
-
dispose:
|
|
244
|
-
|
|
245
|
-
- `register(provider, options?)` — add a provider at any time.
|
|
246
|
-
- `has(token)` — whether a provider for the token is registered.
|
|
247
|
-
- `get(token)` — resolve the value, building and caching it as its scope dictates.
|
|
248
|
-
Accepts `optional(token)` to get `undefined` instead of throwing when absent.
|
|
249
|
-
- `dispose()` — run `onDispose` hooks and release tracked instances owned by this
|
|
250
|
-
container.
|
|
251
|
-
|
|
252
|
-
```ts
|
|
253
|
-
const container = createContainer(providers); // providers are optional
|
|
254
|
-
|
|
255
|
-
container.register(provideValue(PORT, 3000)); // register more at any time
|
|
256
|
-
container.has(PORT); // true
|
|
257
|
-
container.get(PORT); // 3000
|
|
258
|
-
await container.dispose(); // clears tracked instances and awaits disposal hooks
|
|
259
|
-
```
|
|
260
|
-
|
|
261
|
-
Registering the same token twice throws `DuplicateProviderError`. To replace an
|
|
262
|
-
existing provider on purpose (handy for tests, mocks, and environment-specific
|
|
263
|
-
overrides), pass `{ allowOverride: true }`:
|
|
264
|
-
|
|
265
|
-
```ts
|
|
266
|
-
container.register(provideValue(API, fakeApi), { allowOverride: true });
|
|
267
|
-
```
|
|
268
|
-
|
|
269
|
-
Overriding a token whose value was already resolved as a singleton drops the
|
|
270
|
-
cached instance, so the next `get` rebuilds it from the new provider. If that
|
|
271
|
-
resolved instance has an `onDispose` hook, the override throws
|
|
272
|
-
`InvalidProviderError` instead of silently dropping it — call `dispose()` first
|
|
273
|
-
so the resource is released, then register the replacement.
|
|
274
|
-
|
|
275
|
-
### Scopes
|
|
276
|
-
|
|
277
|
-
| Scope | Behavior |
|
|
278
|
-
| --------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
279
|
-
| `singleton` (default) | The factory runs once; the same instance is returned every time. |
|
|
280
|
-
| `transient` | The factory runs on every `get`, producing a fresh instance. |
|
|
281
|
-
| `scoped` | One instance per container. In a child container each child gets its own instance, while the provider can still be declared once on the parent. |
|
|
282
|
-
|
|
283
|
-
Use the `Scopes` helper for autocompletion, or pass the plain string — both work:
|
|
284
|
-
|
|
285
|
-
```ts
|
|
286
|
-
import { Scopes, provideFactory } from "di-craft";
|
|
287
|
-
|
|
288
|
-
provideFactory(ID, {
|
|
289
|
-
scope: Scopes.Transient, // or scope: "transient"
|
|
290
|
-
useFactory: () => crypto.randomUUID(),
|
|
291
|
-
});
|
|
292
|
-
|
|
293
|
-
container.get(ID) !== container.get(ID); // true
|
|
294
|
-
```
|
|
295
|
-
|
|
296
|
-
A provider may only depend on dependencies that live **at least as long** as
|
|
297
|
-
itself, so a longer-lived instance never captures a shorter-lived one. A
|
|
298
|
-
transient may depend on anything; a scoped may depend on scoped or singleton; a
|
|
299
|
-
singleton may depend only on singletons (and values). Violating this throws
|
|
300
|
-
`InvalidProviderError` at resolution. A transient that depends on a singleton, for
|
|
301
|
-
example, reuses the shared singleton instance.
|
|
302
|
-
|
|
303
|
-
### Disposal
|
|
304
|
-
|
|
305
|
-
Factory providers can declare an `onDispose` hook to release resources (database
|
|
306
|
-
pools, sockets, timers, subscriptions). Calling `container.dispose()` runs the
|
|
307
|
-
hooks for every resolved cached instance owned by that container and releases the
|
|
308
|
-
container's tracked instances:
|
|
309
|
-
|
|
310
|
-
```ts
|
|
311
|
-
const DB = createToken<Pool>("db");
|
|
312
|
-
|
|
313
|
-
const container = createContainer([
|
|
314
|
-
provideFactory(DB, {
|
|
315
|
-
useFactory: () => createPool(url),
|
|
316
|
-
onDispose: (pool) => pool.end(), // may be sync or async
|
|
317
|
-
}),
|
|
318
|
-
]);
|
|
319
|
-
|
|
320
|
-
container.get(DB);
|
|
321
|
-
|
|
322
|
-
await container.dispose(); // clears tracked instances and awaits disposal hooks
|
|
323
|
-
```
|
|
324
|
-
|
|
325
|
-
Details:
|
|
326
|
-
|
|
327
|
-
- Hooks run in reverse creation order (dependents before their dependencies).
|
|
328
|
-
- `dispose()` returns a promise and awaits async hooks.
|
|
329
|
-
- Instances are removed from the cache before hooks run, making disposal
|
|
330
|
-
idempotent and re-entrancy safe.
|
|
331
|
-
- Only resolved cached instances owned by that container are disposed: singletons
|
|
332
|
-
owned by that container and scoped instances created for that container.
|
|
333
|
-
Transient and never-resolved instances are not tracked.
|
|
334
|
-
- `onDispose` is only meaningful for cached instances. Declaring it on a
|
|
335
|
-
`transient` provider throws `InvalidProviderError`, since transient instances
|
|
336
|
-
are never tracked and the hook could never run.
|
|
337
|
-
|
|
338
|
-
### Child containers
|
|
339
|
-
|
|
340
|
-
`createChildContainer(parent, providers?)` creates a child that inherits
|
|
341
|
-
everything from its parent but can add or override providers locally. This is the
|
|
342
|
-
typical pattern for per-request isolation on a server: shared services live on
|
|
343
|
-
the root, request-specific values live on a short-lived child.
|
|
344
|
-
|
|
345
|
-
```ts
|
|
346
|
-
const root = createContainer([
|
|
347
|
-
provideFactory(LOGGER, { useFactory: () => console }), // singleton, shared
|
|
348
|
-
provideFactory(HANDLER, {
|
|
349
|
-
scope: Scopes.Scoped, // one instance per child
|
|
350
|
-
deps: { request: REQUEST },
|
|
351
|
-
useFactory: ({ request }) => createHandler(request),
|
|
352
|
-
}),
|
|
353
|
-
]);
|
|
354
|
-
|
|
355
|
-
function handle(request: Request) {
|
|
356
|
-
const child = createChildContainer(root, [provideValue(REQUEST, request)]);
|
|
357
|
-
|
|
358
|
-
child.get(LOGGER); // same logger as the root and every other child
|
|
359
|
-
child.get(HANDLER); // a fresh handler, unique to this child
|
|
360
|
-
|
|
361
|
-
return child.dispose(); // release only this child's instances
|
|
362
|
-
}
|
|
363
|
-
```
|
|
364
|
-
|
|
365
|
-
How resolution works across the chain:
|
|
366
|
-
|
|
367
|
-
- A token is looked up in the child first, then walks up to the parent.
|
|
368
|
-
- `singleton` is cached on the container that **owns** the provider, so it is
|
|
369
|
-
shared by the whole subtree.
|
|
370
|
-
- `scoped` is cached on the **requesting** child, so each child gets its own
|
|
371
|
-
instance — even when the provider is declared once on the parent.
|
|
372
|
-
- A `scoped` provider resolves its dependencies from the requesting child, so it
|
|
373
|
-
can depend on values registered only in that child (like `REQUEST`).
|
|
374
|
-
- `dispose()` only releases the container it is called on; it does not cascade to
|
|
375
|
-
parents or children.
|
|
376
|
-
|
|
377
|
-
### Cycle detection
|
|
378
|
-
|
|
379
|
-
If providers form a dependency cycle, resolution throws `CircularDependencyError`
|
|
380
|
-
with the full path instead of overflowing the stack.
|
|
381
|
-
|
|
382
|
-
```ts
|
|
383
|
-
// A -> B -> A
|
|
384
|
-
container.get(A); // throws: Circular dependency detected: A -> B -> A
|
|
385
|
-
```
|
|
386
|
-
|
|
387
|
-
### Async dependencies
|
|
388
|
-
|
|
389
|
-
di-craft resolves synchronously by design — there is no `getAsync`, and async
|
|
390
|
-
never colors the rest of your graph. Asynchronous values are handled with one of
|
|
391
|
-
two patterns, which together cover the vast majority of cases.
|
|
392
|
-
|
|
393
|
-
**Resolve first, then register.** Do the async work at your composition root and
|
|
394
|
-
register the resolved value. Simplest and most common:
|
|
395
|
-
|
|
396
|
-
```ts
|
|
397
|
-
const db = await connectDatabase(config);
|
|
398
|
-
container.register(provideValue(DB, db));
|
|
399
168
|
```
|
|
400
169
|
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
```ts
|
|
406
|
-
const POOL = createToken<Promise<Pool>>("pool");
|
|
407
|
-
|
|
408
|
-
container.register(provideFactory(POOL, { useFactory: () => createPool() }));
|
|
170
|
+
Annotations are only syntax sugar over normal providers. There is no
|
|
171
|
+
`reflect-metadata`, parameter decorators, runtime type guessing, or global
|
|
172
|
+
container.
|
|
409
173
|
|
|
410
|
-
|
|
411
|
-
```
|
|
174
|
+
## Next.js App Router
|
|
412
175
|
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
```ts
|
|
416
|
-
provideFactory(USERS, {
|
|
417
|
-
deps: { pool: POOL },
|
|
418
|
-
useFactory: async ({ pool }) => new UsersRepo(await pool),
|
|
419
|
-
});
|
|
420
|
-
// USERS is now Token<Promise<UsersRepo>> — consumers await it too.
|
|
421
|
-
```
|
|
422
|
-
|
|
423
|
-
When using `Promise<T>` as the token value, disposal hooks receive the promise
|
|
424
|
-
itself. Await it inside `onDispose` if cleanup needs the resolved value:
|
|
425
|
-
|
|
426
|
-
```ts
|
|
427
|
-
const POOL = createToken<Promise<Pool>>("pool");
|
|
428
|
-
|
|
429
|
-
provideFactory(POOL, {
|
|
430
|
-
useFactory: () => createPool(),
|
|
431
|
-
onDispose: async (poolPromise) => {
|
|
432
|
-
const pool = await poolPromise;
|
|
433
|
-
await pool.end();
|
|
434
|
-
},
|
|
435
|
-
});
|
|
436
|
-
```
|
|
437
|
-
|
|
438
|
-
## Adapters
|
|
439
|
-
|
|
440
|
-
Adapters are optional framework integrations built around the core container.
|
|
441
|
-
They live behind subpath exports, so the root import stays framework-agnostic:
|
|
442
|
-
|
|
443
|
-
```ts
|
|
444
|
-
import { createContainer } from "di-craft"; // core only
|
|
445
|
-
```
|
|
446
|
-
|
|
447
|
-
### Next.js App Router
|
|
448
|
-
|
|
449
|
-
The Next adapter helps connect di-craft to the App Router request lifecycle
|
|
450
|
-
without making React or Next.js part of the core import.
|
|
451
|
-
|
|
452
|
-
Use `di-craft/next/server` from a server-only composition file. Pass React's
|
|
453
|
-
`cache` function so React/Next owns request memoization while di-craft owns only
|
|
454
|
-
the dependency graph:
|
|
176
|
+
The Next adapter lives behind subpath exports, so React and Next.js are not part
|
|
177
|
+
of the core import.
|
|
455
178
|
|
|
456
179
|
```ts
|
|
457
180
|
// app/di.server.ts
|
|
@@ -460,11 +183,7 @@ import { cache } from "react";
|
|
|
460
183
|
import { provideValue } from "di-craft";
|
|
461
184
|
import { createNextDi } from "di-craft/next/server";
|
|
462
185
|
|
|
463
|
-
export const {
|
|
464
|
-
getRequestContainer,
|
|
465
|
-
getRootContainer,
|
|
466
|
-
runWithRequestContainer,
|
|
467
|
-
} = createNextDi({
|
|
186
|
+
export const { getRequestContainer, runWithRequestContainer } = createNextDi({
|
|
468
187
|
cache,
|
|
469
188
|
providers,
|
|
470
189
|
requestProviders: () => [
|
|
@@ -473,7 +192,7 @@ export const {
|
|
|
473
192
|
});
|
|
474
193
|
```
|
|
475
194
|
|
|
476
|
-
|
|
195
|
+
Resolve dependencies in Server Components at the composition edge:
|
|
477
196
|
|
|
478
197
|
```ts
|
|
479
198
|
import { getRequestContainer } from "./di.server";
|
|
@@ -485,15 +204,11 @@ export default async function Page() {
|
|
|
485
204
|
}
|
|
486
205
|
```
|
|
487
206
|
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
Action, test, or job, use `runWithRequestContainer`. It creates a fresh child
|
|
492
|
-
container and disposes it in a `finally` block:
|
|
207
|
+
For Route Handlers, Server Actions, tests, or jobs where you own the lifecycle,
|
|
208
|
+
use `runWithRequestContainer`. It creates a fresh child container and disposes it
|
|
209
|
+
in a `finally` block.
|
|
493
210
|
|
|
494
211
|
```ts
|
|
495
|
-
import { runWithRequestContainer } from "./di.server";
|
|
496
|
-
|
|
497
212
|
export async function GET() {
|
|
498
213
|
return runWithRequestContainer({
|
|
499
214
|
run: async (container) => {
|
|
@@ -505,151 +220,38 @@ export async function GET() {
|
|
|
505
220
|
}
|
|
506
221
|
```
|
|
507
222
|
|
|
508
|
-
State hydration is explicit
|
|
509
|
-
`di-craft/next/server`; the client restores them with `di-craft/next/client`.
|
|
510
|
-
The DI container itself is never hydrated.
|
|
511
|
-
|
|
512
|
-
Import boundary-specific runtime helpers from their own subpath:
|
|
513
|
-
|
|
514
|
-
- `di-craft/next/server` — `createNextDi`, `dehydrate`, server-only adapter types.
|
|
515
|
-
- `di-craft/next/client` — `hydrate`, client-boundary hydration types.
|
|
516
|
-
- Shared hydration contracts like `Hydratable`, `HydrationSchema`, and
|
|
517
|
-
`HydrationSnapshot` are exported from both subpaths for convenience.
|
|
223
|
+
State hydration is explicit:
|
|
518
224
|
|
|
519
|
-
```
|
|
520
|
-
|
|
521
|
-
dehydrate,
|
|
522
|
-
type Hydratable,
|
|
523
|
-
type HydrationSchema,
|
|
524
|
-
} from "di-craft/next/server";
|
|
525
|
-
|
|
526
|
-
class UserState implements Hydratable<UserSnapshot> {
|
|
527
|
-
dehydrate(): UserSnapshot {
|
|
528
|
-
return { users: this.users };
|
|
529
|
-
}
|
|
530
|
-
|
|
531
|
-
hydrate(snapshot: UserSnapshot): void {
|
|
532
|
-
this.users = snapshot.users;
|
|
533
|
-
}
|
|
534
|
-
}
|
|
535
|
-
|
|
536
|
-
const hydration = {
|
|
537
|
-
user: USER_STATE,
|
|
538
|
-
} satisfies HydrationSchema;
|
|
539
|
-
const snapshot = dehydrate({
|
|
540
|
-
container: getRequestContainer(),
|
|
541
|
-
schema: hydration,
|
|
542
|
-
});
|
|
225
|
+
```txt
|
|
226
|
+
server DI container -> serializable snapshot -> client state
|
|
543
227
|
```
|
|
544
228
|
|
|
545
|
-
|
|
546
|
-
"use client";
|
|
547
|
-
|
|
548
|
-
import { hydrate } from "di-craft/next/client";
|
|
549
|
-
|
|
550
|
-
hydrate({
|
|
551
|
-
container: clientContainer,
|
|
552
|
-
schema: hydration,
|
|
553
|
-
snapshot,
|
|
554
|
-
});
|
|
555
|
-
```
|
|
556
|
-
|
|
557
|
-
## Dependency injection vs service location
|
|
558
|
-
|
|
559
|
-
di-craft is built for **dependency injection**: dependencies are declared up
|
|
560
|
-
front and handed to your code. The opposite is **service location**, where code
|
|
561
|
-
reaches into a container at runtime to pull what it needs, hiding its real
|
|
562
|
-
dependencies.
|
|
563
|
-
|
|
564
|
-
Two habits keep usage canonical: call `container.get()` only at the **composition
|
|
565
|
-
root** (entrypoint, framework hooks, route handlers), and never pass the
|
|
566
|
-
container into your classes or functions. di-craft enforces the key half for you
|
|
567
|
-
— **a factory only ever receives its declared `deps`, never the container** — so
|
|
568
|
-
a provider physically cannot locate arbitrary services.
|
|
569
|
-
|
|
570
|
-
```ts
|
|
571
|
-
// Dependency injection — deps are explicit, the class never sees the container.
|
|
572
|
-
provideFactory(USERS, {
|
|
573
|
-
deps: { repo: REPO, logger: LOGGER },
|
|
574
|
-
useFactory: ({ repo, logger }) => new UserService(repo, logger),
|
|
575
|
-
});
|
|
576
|
-
|
|
577
|
-
const users = container.get(USERS); // resolved at the root, then injected down
|
|
578
|
-
```
|
|
579
|
-
|
|
580
|
-
```ts
|
|
581
|
-
// Service location (anti-pattern) — the container is smuggled into domain code.
|
|
582
|
-
class UserService {
|
|
583
|
-
constructor(private container: Container) {}
|
|
584
|
-
|
|
585
|
-
list() {
|
|
586
|
-
const repo = this.container.get(REPO); // hidden, runtime-only dependency
|
|
587
|
-
}
|
|
588
|
-
}
|
|
589
|
-
```
|
|
590
|
-
|
|
591
|
-
The second form compiles, but it hides dependencies and defeats DI. No runtime
|
|
592
|
-
flag can forbid it — `get()` is the same call the composition root relies on — so
|
|
593
|
-
keep resolution at the edges by convention, or enforce it with a lint rule that
|
|
594
|
-
allows `.get()` only in your composition-root files.
|
|
229
|
+
The DI container itself is never hydrated.
|
|
595
230
|
|
|
596
|
-
|
|
231
|
+
Runtime subpaths:
|
|
597
232
|
|
|
598
|
-
|
|
599
|
-
|
|
233
|
+
| Export | Description |
|
|
234
|
+
| ---------------------- | ------------------------------------------------------ |
|
|
235
|
+
| `di-craft/next/server` | `createNextDi`, `dehydrate`, server-side adapter types |
|
|
236
|
+
| `di-craft/next/client` | `hydrate`, client-boundary hydration types |
|
|
600
237
|
|
|
601
|
-
|
|
602
|
-
import { DiError, MissingProviderError } from "di-craft";
|
|
238
|
+
## API Reference
|
|
603
239
|
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
```
|
|
616
|
-
|
|
617
|
-
| Error | Thrown when |
|
|
618
|
-
| ------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
619
|
-
| `MissingProviderError` | A token is resolved but no provider is registered. |
|
|
620
|
-
| `DuplicateProviderError` | A token is registered more than once. |
|
|
621
|
-
| `CircularDependencyError` | Providers form a dependency cycle. |
|
|
622
|
-
| `InvalidDependencyError` | A declared dependency token is missing/undefined. |
|
|
623
|
-
| `InvalidProviderError` | A provider is misconfigured (`onDispose` on a transient, or a dependency with a shorter lifetime than its consumer) or an override would drop a live disposable instance. |
|
|
624
|
-
|
|
625
|
-
## API reference
|
|
626
|
-
|
|
627
|
-
| Export | Description |
|
|
628
|
-
| ------------------------------------------ | --------------------------------------------------------------------------- |
|
|
629
|
-
| `createToken<T>(name)` | Create a unique, typed token. |
|
|
630
|
-
| `provideValue(token, value)` | Provider that returns an existing value. |
|
|
631
|
-
| `provideFactory(token, options)` | Provider that builds a value via a factory. |
|
|
632
|
-
| `@Injectable(options)` | Mark a class as a token-backed injectable provider. |
|
|
633
|
-
| `provideInjectable(class)` | Create a factory provider from an injectable class. |
|
|
634
|
-
| `optional(token)` | Mark a dependency as optional (resolves to `undefined` when absent). |
|
|
635
|
-
| `createContainer(providers?)` | Create a container, optionally seeded with providers. |
|
|
636
|
-
| `createChildContainer(parent, providers?)` | Create a child container that inherits from `parent`. |
|
|
637
|
-
| `Scopes` | Object of scope values (`Scopes.Singleton`, `Scopes.Transient`, `Scopes.Scoped`). |
|
|
638
|
-
|
|
639
|
-
Exported types: `Container`, `Token`, `Provider`, `ValueProvider`,
|
|
640
|
-
`FactoryProvider`, `Dependency`, `OptionalDependency`, `Scope`, `DisposeHook`,
|
|
641
|
-
`RegisterOptions`.
|
|
240
|
+
| Export | Description |
|
|
241
|
+
| ------------------------------------------ | ------------------------------------------------------------------- |
|
|
242
|
+
| `createToken<T>(name)` | Create a unique, typed token |
|
|
243
|
+
| `provideValue(token, value)` | Register an existing value |
|
|
244
|
+
| `provideFactory(token, options)` | Register a lazy factory with optional dependencies, scope, disposal |
|
|
245
|
+
| `@Injectable(options)` | Mark a class as a token-backed injectable provider |
|
|
246
|
+
| `provideInjectable(class)` | Create a factory provider from an injectable class |
|
|
247
|
+
| `optional(token)` | Mark a dependency as optional |
|
|
248
|
+
| `createContainer(providers?)` | Create a container |
|
|
249
|
+
| `createChildContainer(parent, providers?)` | Create a child container that inherits from `parent` |
|
|
250
|
+
| `Scopes` | `Singleton`, `Transient`, and `Scoped` scope constants |
|
|
642
251
|
|
|
643
252
|
Exported errors: `DiError`, `MissingProviderError`, `DuplicateProviderError`,
|
|
644
253
|
`CircularDependencyError`, `InvalidDependencyError`, `InvalidProviderError`.
|
|
645
254
|
|
|
646
|
-
Subpath exports:
|
|
647
|
-
|
|
648
|
-
| Export | Description |
|
|
649
|
-
| --------------------------- | ------------------------------------------------------- |
|
|
650
|
-
| `di-craft/next/server` | Next.js server adapter for request-scoped containers. |
|
|
651
|
-
| `di-craft/next/client` | Client-boundary helpers for restoring state snapshots. |
|
|
652
|
-
|
|
653
255
|
## License
|
|
654
256
|
|
|
655
257
|
[MIT](./LICENSE)
|