di-craft 0.0.14 → 0.0.16
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 +111 -61
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,21 @@
|
|
|
1
|
-
|
|
1
|
+
<h1 align="center">di-craft</h1>
|
|
2
|
+
|
|
3
|
+
<p align="center">
|
|
4
|
+
<img src="./assets/logo.png" alt="di-craft" width="200" />
|
|
5
|
+
</p>
|
|
6
|
+
|
|
7
|
+
<p align="center">
|
|
8
|
+
<b>A tiny, type-safe dependency injection container for TypeScript</b>
|
|
9
|
+
</p>
|
|
10
|
+
|
|
11
|
+
<p align="center">
|
|
12
|
+
<img alt="NPM Version" src="https://img.shields.io/npm/v/di-craft?style=flat-square&color=%2364d4c1&link=https%3A%2F%2Fwww.npmjs.com%2Fpackage%2Fdi-craft">
|
|
13
|
+
<img alt="NPM package gzipped size" src="https://img.shields.io/bundlejs/size/di-craft?style=flat-square&label=gzip&color=%2364d4c1">
|
|
14
|
+
</p>
|
|
15
|
+
|
|
16
|
+
> [!NOTE]
|
|
17
|
+
> This README was generated with a bit of AI help — don't believe everything you see 🙂
|
|
2
18
|
|
|
3
|
-
A tiny, type-safe dependency injection container for TypeScript.
|
|
4
19
|
|
|
5
20
|
## Contents
|
|
6
21
|
|
|
@@ -93,7 +108,8 @@ CommonJS code can load it with a dynamic `import()`.
|
|
|
93
108
|
|
|
94
109
|
### Tokens
|
|
95
110
|
|
|
96
|
-
A token is a unique, type-carrying key. Identity is based on an internal `symbol`,
|
|
111
|
+
A token is a unique, type-carrying key. Identity is based on an internal `symbol`,
|
|
112
|
+
**not** on the name — two tokens with the same name are still different.
|
|
97
113
|
|
|
98
114
|
```ts
|
|
99
115
|
const PORT = createToken<number>("port");
|
|
@@ -101,7 +117,8 @@ const PORT = createToken<number>("port");
|
|
|
101
117
|
PORT.name; // "port" — used only for error messages
|
|
102
118
|
```
|
|
103
119
|
|
|
104
|
-
The type argument flows everywhere: providers must produce a matching value, and
|
|
120
|
+
The type argument flows everywhere: providers must produce a matching value, and
|
|
121
|
+
`container.get(PORT)` returns `number`.
|
|
105
122
|
|
|
106
123
|
### Providers
|
|
107
124
|
|
|
@@ -117,13 +134,14 @@ provideValue(PORT, 3000);
|
|
|
117
134
|
|
|
118
135
|
```ts
|
|
119
136
|
provideFactory(HTTP, {
|
|
120
|
-
deps: { config: CONFIG },
|
|
121
|
-
scope: "singleton",
|
|
137
|
+
deps: { config: CONFIG }, // optional, keyed map of tokens
|
|
138
|
+
scope: "singleton", // optional, defaults to "singleton"
|
|
122
139
|
useFactory: ({ config }) => new HttpClient(config.apiUrl),
|
|
123
140
|
});
|
|
124
141
|
```
|
|
125
142
|
|
|
126
|
-
The keys in `deps` become the keys of the object passed to `useFactory`, each
|
|
143
|
+
The keys in `deps` become the keys of the object passed to `useFactory`, each
|
|
144
|
+
resolved to its token's type.
|
|
127
145
|
|
|
128
146
|
### Optional dependencies
|
|
129
147
|
|
|
@@ -153,8 +171,7 @@ const logger = container.get(optional(LOGGER)); // Logger | undefined
|
|
|
153
171
|
```
|
|
154
172
|
|
|
155
173
|
Optional only affects the token itself: if a provider _is_ registered, it is
|
|
156
|
-
resolved normally and its own errors (cycles, missing nested deps) still
|
|
157
|
-
surface.
|
|
174
|
+
resolved normally and its own errors (cycles, missing nested deps) still surface.
|
|
158
175
|
|
|
159
176
|
### Container
|
|
160
177
|
|
|
@@ -164,16 +181,18 @@ dispose:
|
|
|
164
181
|
|
|
165
182
|
- `register(provider, options?)` — add a provider at any time.
|
|
166
183
|
- `has(token)` — whether a provider for the token is registered.
|
|
167
|
-
- `get(token)` — resolve the value, building and caching it as its scope dictates.
|
|
168
|
-
|
|
184
|
+
- `get(token)` — resolve the value, building and caching it as its scope dictates.
|
|
185
|
+
Accepts `optional(token)` to get `undefined` instead of throwing when absent.
|
|
186
|
+
- `dispose()` — run `onDispose` hooks and release tracked instances owned by this
|
|
187
|
+
container.
|
|
169
188
|
|
|
170
189
|
```ts
|
|
171
190
|
const container = createContainer(providers); // providers are optional
|
|
172
191
|
|
|
173
192
|
container.register(provideValue(PORT, 3000)); // register more at any time
|
|
174
|
-
container.has(PORT);
|
|
175
|
-
container.get(PORT);
|
|
176
|
-
await container.dispose();
|
|
193
|
+
container.has(PORT); // true
|
|
194
|
+
container.get(PORT); // 3000
|
|
195
|
+
await container.dispose(); // clears tracked instances and awaits disposal hooks
|
|
177
196
|
```
|
|
178
197
|
|
|
179
198
|
Registering the same token twice throws `DuplicateProviderError`. To replace an
|
|
@@ -192,11 +211,11 @@ so the resource is released, then register the replacement.
|
|
|
192
211
|
|
|
193
212
|
### Scopes
|
|
194
213
|
|
|
195
|
-
| Scope
|
|
196
|
-
|
|
|
197
|
-
| `singleton` (default)
|
|
198
|
-
| `transient`
|
|
199
|
-
| `scoped`
|
|
214
|
+
| Scope | Behavior |
|
|
215
|
+
| --------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
216
|
+
| `singleton` (default) | The factory runs once; the same instance is returned every time. |
|
|
217
|
+
| `transient` | The factory runs on every `get`, producing a fresh instance. |
|
|
218
|
+
| `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. |
|
|
200
219
|
|
|
201
220
|
Use the `Scopes` helper for autocompletion, or pass the plain string — both work:
|
|
202
221
|
|
|
@@ -222,7 +241,8 @@ example, reuses the shared singleton instance.
|
|
|
222
241
|
|
|
223
242
|
Factory providers can declare an `onDispose` hook to release resources (database
|
|
224
243
|
pools, sockets, timers, subscriptions). Calling `container.dispose()` runs the
|
|
225
|
-
hooks for every resolved
|
|
244
|
+
hooks for every resolved cached instance owned by that container and releases the
|
|
245
|
+
container's tracked instances:
|
|
226
246
|
|
|
227
247
|
```ts
|
|
228
248
|
const DB = createToken<Pool>("db");
|
|
@@ -236,16 +256,21 @@ const container = createContainer([
|
|
|
236
256
|
|
|
237
257
|
container.get(DB);
|
|
238
258
|
|
|
239
|
-
await container.dispose(); //
|
|
259
|
+
await container.dispose(); // clears tracked instances and awaits disposal hooks
|
|
240
260
|
```
|
|
241
261
|
|
|
242
262
|
Details:
|
|
243
263
|
|
|
244
264
|
- Hooks run in reverse creation order (dependents before their dependencies).
|
|
245
265
|
- `dispose()` returns a promise and awaits async hooks.
|
|
246
|
-
-
|
|
247
|
-
|
|
248
|
-
-
|
|
266
|
+
- Instances are removed from the cache before hooks run, making disposal
|
|
267
|
+
idempotent and re-entrancy safe.
|
|
268
|
+
- Only resolved cached instances owned by that container are disposed: singletons
|
|
269
|
+
owned by that container and scoped instances created for that container.
|
|
270
|
+
Transient and never-resolved instances are not tracked.
|
|
271
|
+
- `onDispose` is only meaningful for cached instances. Declaring it on a
|
|
272
|
+
`transient` provider throws `InvalidProviderError`, since transient instances
|
|
273
|
+
are never tracked and the hook could never run.
|
|
249
274
|
|
|
250
275
|
### Child containers
|
|
251
276
|
|
|
@@ -277,14 +302,19 @@ function handle(request: Request) {
|
|
|
277
302
|
How resolution works across the chain:
|
|
278
303
|
|
|
279
304
|
- A token is looked up in the child first, then walks up to the parent.
|
|
280
|
-
- `singleton` is cached on the container that **owns** the provider, so it is
|
|
281
|
-
|
|
282
|
-
-
|
|
283
|
-
|
|
305
|
+
- `singleton` is cached on the container that **owns** the provider, so it is
|
|
306
|
+
shared by the whole subtree.
|
|
307
|
+
- `scoped` is cached on the **requesting** child, so each child gets its own
|
|
308
|
+
instance — even when the provider is declared once on the parent.
|
|
309
|
+
- A `scoped` provider resolves its dependencies from the requesting child, so it
|
|
310
|
+
can depend on values registered only in that child (like `REQUEST`).
|
|
311
|
+
- `dispose()` only releases the container it is called on; it does not cascade to
|
|
312
|
+
parents or children.
|
|
284
313
|
|
|
285
314
|
### Cycle detection
|
|
286
315
|
|
|
287
|
-
If providers form a dependency cycle, resolution throws `CircularDependencyError`
|
|
316
|
+
If providers form a dependency cycle, resolution throws `CircularDependencyError`
|
|
317
|
+
with the full path instead of overflowing the stack.
|
|
288
318
|
|
|
289
319
|
```ts
|
|
290
320
|
// A -> B -> A
|
|
@@ -327,17 +357,33 @@ provideFactory(USERS, {
|
|
|
327
357
|
// USERS is now Token<Promise<UsersRepo>> — consumers await it too.
|
|
328
358
|
```
|
|
329
359
|
|
|
360
|
+
When using `Promise<T>` as the token value, disposal hooks receive the promise
|
|
361
|
+
itself. Await it inside `onDispose` if cleanup needs the resolved value:
|
|
362
|
+
|
|
363
|
+
```ts
|
|
364
|
+
const POOL = createToken<Promise<Pool>>("pool");
|
|
365
|
+
|
|
366
|
+
provideFactory(POOL, {
|
|
367
|
+
useFactory: () => createPool(),
|
|
368
|
+
onDispose: async (poolPromise) => {
|
|
369
|
+
const pool = await poolPromise;
|
|
370
|
+
await pool.end();
|
|
371
|
+
},
|
|
372
|
+
});
|
|
373
|
+
```
|
|
374
|
+
|
|
330
375
|
## Dependency injection vs service location
|
|
331
376
|
|
|
332
|
-
di-craft is built for **dependency injection**: dependencies are declared up
|
|
333
|
-
and handed to your code. The opposite is **service location**, where code
|
|
334
|
-
into a container at runtime to pull what it needs, hiding its real
|
|
377
|
+
di-craft is built for **dependency injection**: dependencies are declared up
|
|
378
|
+
front and handed to your code. The opposite is **service location**, where code
|
|
379
|
+
reaches into a container at runtime to pull what it needs, hiding its real
|
|
380
|
+
dependencies.
|
|
335
381
|
|
|
336
382
|
Two habits keep usage canonical: call `container.get()` only at the **composition
|
|
337
|
-
root** (entrypoint, framework hooks, route handlers), and never pass the
|
|
338
|
-
into your classes or functions. di-craft enforces the key half for you
|
|
339
|
-
factory only ever receives its declared `deps`, never the container** — so
|
|
340
|
-
provider physically cannot locate arbitrary services.
|
|
383
|
+
root** (entrypoint, framework hooks, route handlers), and never pass the
|
|
384
|
+
container into your classes or functions. di-craft enforces the key half for you
|
|
385
|
+
— **a factory only ever receives its declared `deps`, never the container** — so
|
|
386
|
+
a provider physically cannot locate arbitrary services.
|
|
341
387
|
|
|
342
388
|
```ts
|
|
343
389
|
// Dependency injection — deps are explicit, the class never sees the container.
|
|
@@ -360,14 +406,15 @@ class UserService {
|
|
|
360
406
|
}
|
|
361
407
|
```
|
|
362
408
|
|
|
363
|
-
The second form compiles, but it hides dependencies and defeats DI. No runtime
|
|
364
|
-
can forbid it — `get()` is the same call the composition root relies on — so
|
|
365
|
-
resolution at the edges by convention, or enforce it with a lint rule that
|
|
366
|
-
`.get()` only in your composition-root files.
|
|
409
|
+
The second form compiles, but it hides dependencies and defeats DI. No runtime
|
|
410
|
+
flag can forbid it — `get()` is the same call the composition root relies on — so
|
|
411
|
+
keep resolution at the edges by convention, or enforce it with a lint rule that
|
|
412
|
+
allows `.get()` only in your composition-root files.
|
|
367
413
|
|
|
368
414
|
## Error handling
|
|
369
415
|
|
|
370
|
-
All errors extend the shared `DiError` base class, so you can catch any container
|
|
416
|
+
All errors extend the shared `DiError` base class, so you can catch any container
|
|
417
|
+
error with a single check:
|
|
371
418
|
|
|
372
419
|
```ts
|
|
373
420
|
import { DiError, MissingProviderError } from "di-craft";
|
|
@@ -385,29 +432,32 @@ try {
|
|
|
385
432
|
}
|
|
386
433
|
```
|
|
387
434
|
|
|
388
|
-
| Error
|
|
389
|
-
|
|
|
390
|
-
| `MissingProviderError`
|
|
391
|
-
| `DuplicateProviderError`
|
|
392
|
-
| `CircularDependencyError`
|
|
393
|
-
| `InvalidDependencyError`
|
|
394
|
-
| `InvalidProviderError`
|
|
435
|
+
| Error | Thrown when |
|
|
436
|
+
| ------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
437
|
+
| `MissingProviderError` | A token is resolved but no provider is registered. |
|
|
438
|
+
| `DuplicateProviderError` | A token is registered more than once. |
|
|
439
|
+
| `CircularDependencyError` | Providers form a dependency cycle. |
|
|
440
|
+
| `InvalidDependencyError` | A declared dependency token is missing/undefined. |
|
|
441
|
+
| `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. |
|
|
395
442
|
|
|
396
443
|
## API reference
|
|
397
444
|
|
|
398
|
-
| Export
|
|
399
|
-
|
|
|
400
|
-
| `createToken<T>(name)`
|
|
401
|
-
| `provideValue(token, value)`
|
|
402
|
-
| `provideFactory(token, options)`
|
|
403
|
-
| `optional(token)`
|
|
404
|
-
| `createContainer(providers?)`
|
|
405
|
-
| `createChildContainer(parent, providers?)` | Create a child container that inherits from `parent`.
|
|
406
|
-
| `Scopes`
|
|
407
|
-
|
|
408
|
-
Exported types: `Container`, `Token`, `Provider`, `ValueProvider`,
|
|
409
|
-
|
|
410
|
-
|
|
445
|
+
| Export | Description |
|
|
446
|
+
| ------------------------------------------ | --------------------------------------------------------------------------- |
|
|
447
|
+
| `createToken<T>(name)` | Create a unique, typed token. |
|
|
448
|
+
| `provideValue(token, value)` | Provider that returns an existing value. |
|
|
449
|
+
| `provideFactory(token, options)` | Provider that builds a value via a factory. |
|
|
450
|
+
| `optional(token)` | Mark a dependency as optional (resolves to `undefined` when absent). |
|
|
451
|
+
| `createContainer(providers?)` | Create a container, optionally seeded with providers. |
|
|
452
|
+
| `createChildContainer(parent, providers?)` | Create a child container that inherits from `parent`. |
|
|
453
|
+
| `Scopes` | Object of scope values (`Scopes.Singleton`, `Scopes.Transient`, `Scopes.Scoped`). |
|
|
454
|
+
|
|
455
|
+
Exported types: `Container`, `Token`, `Provider`, `ValueProvider`,
|
|
456
|
+
`FactoryProvider`, `Dependency`, `OptionalDependency`, `Scope`, `DisposeHook`,
|
|
457
|
+
`RegisterOptions`.
|
|
458
|
+
|
|
459
|
+
Exported errors: `DiError`, `MissingProviderError`, `DuplicateProviderError`,
|
|
460
|
+
`CircularDependencyError`, `InvalidDependencyError`, `InvalidProviderError`.
|
|
411
461
|
|
|
412
462
|
## License
|
|
413
463
|
|