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