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