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.
Files changed (2) hide show
  1. package/README.md +115 -61
  2. package/package.json +9 -5
package/README.md CHANGED
@@ -1,6 +1,25 @@
1
- # di-craft
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`, **not** on the name — two tokens with the same name are still different.
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 `container.get(PORT)` returns `number`.
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 }, // optional, keyed map of tokens
121
- scope: "singleton", // optional, defaults to "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 resolved to its token's type.
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. Accepts `optional(token)` to get `undefined` instead of throwing when absent.
168
- - `dispose()` run `onDispose` hooks and release resolved instances.
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); // true
175
- container.get(PORT); // 3000
176
- await container.dispose(); // release resolved singletons
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 | Behavior |
196
- | ---------------------- | ---------------------------------------------------------------- |
197
- | `singleton` (default) | The factory runs once; the same instance is returned every time. |
198
- | `transient` | The factory runs on every `get`, producing a fresh instance. |
199
- | `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. |
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 singleton and clears the cache:
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(); // awaits async hooks, then clears instances
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
- - It is idempotent calling it again is a no-op.
247
- - Only resolved singletons (and scoped instances on their container) are disposed; never-resolved instances are not tracked.
248
- - `onDispose` is only meaningful for cached instances. Declaring it on a `transient` provider throws `InvalidProviderError`, since transient instances are never tracked and the hook could never run.
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 shared by the whole subtree.
281
- - `scoped` is cached on the **requesting** child, so each child gets its own instance — even when the provider is declared once on the parent.
282
- - A `scoped` provider resolves its dependencies from the requesting child, so it can depend on values registered only in that child (like `REQUEST`).
283
- - `dispose()` only releases the container it is called on; it does not cascade to parents or children.
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` with the full path instead of overflowing the stack.
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 front
333
- and handed to your code. The opposite is **service location**, where code reaches
334
- into a container at runtime to pull what it needs, hiding its real dependencies.
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 container
338
- into your classes or functions. di-craft enforces the key half for you — **a
339
- factory only ever receives its declared `deps`, never the container** — so a
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 flag
364
- can forbid it — `get()` is the same call the composition root relies on — so keep
365
- resolution at the edges by convention, or enforce it with a lint rule that allows
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 error with a single check:
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 | Thrown when |
389
- | -------------------------- | -------------------------------------------------------- |
390
- | `MissingProviderError` | A token is resolved but no provider is registered. |
391
- | `DuplicateProviderError` | A token is registered more than once. |
392
- | `CircularDependencyError` | Providers form a dependency cycle. |
393
- | `InvalidDependencyError` | A declared dependency token is missing/undefined. |
394
- | `InvalidProviderError` | A provider is misconfigured (`onDispose` on a transient, or a dependency that outlives its consumer) or an override would drop a live disposable instance. |
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 | Description |
399
- | ----------------------- | ---------------------------------------------------------- |
400
- | `createToken<T>(name)` | Create a unique, typed token. |
401
- | `provideValue(token, value)` | Provider that returns an existing value. |
402
- | `provideFactory(token, options)` | Provider that builds a value via a factory. |
403
- | `optional(token)` | Mark a dependency as optional (resolves to `undefined` when absent). |
404
- | `createContainer(providers?)` | Create a container, optionally seeded with providers. |
405
- | `createChildContainer(parent, providers?)` | Create a child container that inherits from `parent`. |
406
- | `Scopes` | Object of scope values (`Scopes.Singleton`, `Scopes.Transient`, `Scopes.Scoped`). |
407
-
408
- Exported types: `Container`, `Token`, `Provider`, `ValueProvider`, `FactoryProvider`, `Dependency`, `OptionalDependency`, `Scope`, `DisposeHook`, `RegisterOptions`.
409
-
410
- Exported errors: `DiError`, `MissingProviderError`, `DuplicateProviderError`, `CircularDependencyError`, `InvalidDependencyError`, `InvalidProviderError`.
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.15",
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-of-control",
49
+ "inversion of control",
50
+ "ioc container",
48
51
  "typescript",
49
- "type-safe",
50
52
  "container",
51
- "scoped",
53
+ "zero dependencies",
54
+ "lightweight",
55
+ "tiny",
52
56
  "tree-shakable"
53
57
  ],
54
58
  "homepage": "https://github.com/bezmen-e/di-craft",