katagami 2.0.0 → 2.1.0

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 CHANGED
@@ -8,22 +8,24 @@ Lightweight TypeScript DI container with full type inference.
8
8
  [![license](https://img.shields.io/npm/l/katagami)](https://github.com/hiroiku/katagami/blob/master/LICENSE)
9
9
  [![bundle size](https://img.shields.io/bundlephobia/minzip/katagami)](https://bundlephobia.com/package/katagami)
10
10
 
11
- > The name comes from 型紙 _(katagami)_ — precision stencil paper used in traditional Japanese dyeing to transfer exact patterns onto fabric. Multiple stencils are layered to compose intricate designs, just as types accumulate through each method-chain call. A stencil needs only paper and a brush, no elaborate machinery likewise, Katagami requires no decorators or metadata mechanisms and works with any build tool out of the box. And like stencils that work across different fabrics and techniques, Katagami adapts across TypeScript and JavaScript, class tokens and PropertyKey tokens a hybrid approach to strict, composable DI.
11
+ > The name comes from 型紙 _(katagami)_ — precision stencil paper used in traditional Japanese dyeing to transfer exact patterns onto fabric. Multiple stencils are layered to compose intricate designs, just as types accumulate through each method-chain call. Each stencil is a self-contained piece chosen only for the current work, the rest left behind just as subpath exports ensure only the code you use enters your bundle. The cut pattern determines exactly where dye passes and where it is blocked, much like Katagami's type system catches misuse at compile time, not at runtime. And a stencil needs only paper and a brush, no elaborate machinery — likewise, Katagami requires no decorators or metadata mechanisms and works with any build tool out of the box.
12
12
 
13
13
  ## Features
14
14
 
15
- | Feature | Description |
16
- | ----------------------------- | -------------------------------------------------------------------------------------------- |
17
- | Full type inference | Types accumulate through method chaining; unregistered tokens are compile-time errors |
18
- | Three lifetimes | Singleton, Transient, and Scoped with child containers |
19
- | Async factories | Promise-returning factories are automatically tracked by the type system |
20
- | Circular dependency detection | Clear error messages with the full cycle path |
21
- | Disposable support | TC39 Explicit Resource Management (`Symbol.dispose` / `Symbol.asyncDispose` / `await using`) |
22
- | Captive dependency prevention | Singleton/Transient factories cannot access scoped tokens; caught at compile time |
23
- | Optional resolution | `tryResolve` returns `undefined` for unregistered tokens instead of throwing |
24
- | Hybrid token strategy | Class tokens for strict type safety, PropertyKey tokens for flexibility |
25
- | Interface type map | Pass an interface to `createContainer<T>()` for order-independent registration |
26
- | Zero dependencies | No decorators, no reflect-metadata, no polyfills |
15
+ | Feature | Description |
16
+ | ----------------------------- | ---------------------------------------------------------------------------------------------------------- |
17
+ | Zero dependencies | No decorators, no reflect-metadata, no polyfills works with any bundler out of the box |
18
+ | Full type inference | Types accumulate through method chaining; unregistered tokens are compile-time errors |
19
+ | Tree-shakeable | Subpath exports (`katagami/scope`, `katagami/disposable`) and `sideEffects: false` for minimal bundle size |
20
+ | Captive dependency prevention | Singleton/Transient factories cannot access scoped tokens; caught at compile time |
21
+ | Hybrid token strategy | Class tokens for strict type safety, PropertyKey tokens for flexibility |
22
+ | Interface type map | Pass an interface to `createContainer<T>()` for order-independent registration |
23
+ | Three lifetimes | Singleton, Transient, and Scoped with child containers |
24
+ | Disposable support | TC39 Explicit Resource Management (`Symbol.dispose` / `Symbol.asyncDispose` / `await using`) |
25
+ | Module composition | Containers can be composed via `use()` to group and reuse registrations |
26
+ | Async factories | Promise-returning factories are automatically tracked by the type system |
27
+ | Circular dependency detection | Clear error messages with the full cycle path |
28
+ | Optional resolution | `tryResolve` returns `undefined` for unregistered tokens instead of throwing |
27
29
 
28
30
  ## Install
29
31
 
@@ -66,6 +68,19 @@ Most TypeScript DI containers rely on decorators, reflect-metadata, or string-ba
66
68
 
67
69
  Decorator-based DI requires `experimentalDecorators` and `emitDecoratorMetadata` compiler options. Modern build tools such as esbuild and Vite (default configuration) do not support `emitDecoratorMetadata`, and the TC39 standard decorators proposal does not include an equivalent for automatic type metadata emission. Katagami depends on none of these — it works with any build tool out of the box.
68
70
 
71
+ ### Tree-shakeable
72
+
73
+ Katagami is split into subpath exports. Import only what you use — `katagami/scope` and `katagami/disposable` are completely eliminated from the bundle if not imported. Combined with `sideEffects: false`, bundlers can remove every unused byte.
74
+
75
+ ```ts
76
+ // Core only — scope and disposable are not included in the bundle
77
+ import { createContainer } from 'katagami';
78
+
79
+ // Import only what you need
80
+ import { createScope } from 'katagami/scope';
81
+ import { disposable } from 'katagami/disposable';
82
+ ```
83
+
69
84
  ### Full type inference from class tokens
70
85
 
71
86
  String-token DI forces you to maintain manual token-to-type mappings. Parameter-name matching breaks under minification. Katagami uses classes directly as tokens, so `resolve` automatically infers the correct return type — synchronous or `Promise` — with no extra annotations.
@@ -155,6 +170,53 @@ parentScope.resolve(RequestContext) === childScope.resolve(RequestContext); // f
155
170
  parentScope.resolve(DbPool) === childScope.resolve(DbPool); // true
156
171
  ```
157
172
 
173
+ ### Module Composition
174
+
175
+ Group related registrations into a reusable module by creating a container with `createContainer()`, then apply it to another container with `use()`. Only registration entries are copied — singleton instance caches are not shared.
176
+
177
+ ```ts
178
+ import { createContainer } from 'katagami';
179
+
180
+ class AuthService {
181
+ authenticate() {
182
+ return true;
183
+ }
184
+ }
185
+
186
+ class TokenService {
187
+ issue() {
188
+ return 'token';
189
+ }
190
+ }
191
+
192
+ class UserService {
193
+ constructor(private auth: AuthService, private tokens: TokenService) {}
194
+ }
195
+
196
+ // Define a reusable module
197
+ const authModule = createContainer()
198
+ .registerSingleton(AuthService, () => new AuthService())
199
+ .registerSingleton(TokenService, () => new TokenService());
200
+
201
+ // Compose modules
202
+ const container = createContainer()
203
+ .use(authModule)
204
+ .registerSingleton(UserService, r => new UserService(r.resolve(AuthService), r.resolve(TokenService)));
205
+ ```
206
+
207
+ Modules can also compose other modules:
208
+
209
+ ```ts
210
+ const infraModule = createContainer().registerSingleton(AuthService, () => new AuthService());
211
+
212
+ const appModule = createContainer()
213
+ .use(infraModule)
214
+ .registerSingleton(UserService, r => new UserService(r.resolve(AuthService), r.resolve(TokenService)));
215
+
216
+ // appModule includes both AuthService and UserService
217
+ const container = createContainer().use(appModule);
218
+ ```
219
+
158
220
  ### Async Factories
159
221
 
160
222
  Factories that return a `Promise` are automatically tracked by the type system. When you `resolve` an async token, the return type is `Promise<V>` instead of `V`:
@@ -243,9 +305,7 @@ class Connection {
243
305
  }
244
306
 
245
307
  // Manual disposal
246
- const container = disposable(
247
- createContainer().registerSingleton(Connection, () => new Connection()),
248
- );
308
+ const container = disposable(createContainer().registerSingleton(Connection, () => new Connection()));
249
309
 
250
310
  container.resolve(Connection);
251
311
  await container[Symbol.asyncDispose]();
@@ -271,6 +331,28 @@ const root = createContainer()
271
331
 
272
332
  Scope disposal only affects scoped instances. Singleton instances are owned by the root container and are disposed when the container itself is disposed.
273
333
 
334
+ The `disposable()` wrapper also narrows the returned type so that registration methods (`registerSingleton`, `registerTransient`, `registerScoped`, `use`) are removed at the type level. This prevents accidental registration on a potentially-disposed container:
335
+
336
+ ```ts
337
+ const container = disposable(createContainer().registerSingleton(Connection, () => new Connection()));
338
+
339
+ container.resolve(Connection); // OK
340
+ container.registerSingleton(/* ... */); // Compile-time error
341
+ ```
342
+
343
+ ### Tree Shaking
344
+
345
+ Katagami uses subpath exports to split functionality into independent entry points. If you only need the core container, `katagami/scope` and `katagami/disposable` are completely excluded from the bundle. The package declares `sideEffects: false`, so bundlers can safely eliminate any unused code.
346
+
347
+ ```ts
348
+ // Core only — scope and disposable are not included in the bundle
349
+ import { createContainer } from 'katagami';
350
+
351
+ // Import only what you need
352
+ import { createScope } from 'katagami/scope';
353
+ import { disposable } from 'katagami/disposable';
354
+ ```
355
+
274
356
  ### Interface Type Map
275
357
 
276
358
  When you pass an interface to `createContainer<T>()`, PropertyKey tokens are typed from the interface rather than accumulated through chaining. This means you can register and resolve tokens in any order:
@@ -400,23 +482,27 @@ const container = createContainer()
400
482
 
401
483
  Creates a new DI container. Pass an interface as `T` to define the type map for PropertyKey tokens. Pass `ScopedT` to define a separate type map for scoped PropertyKey tokens (order-independent, just like `T`).
402
484
 
403
- ### `container.registerSingleton(token, factory)`
485
+ ### `Container.prototype.registerSingleton(token, factory)`
404
486
 
405
487
  Registers a factory as a singleton. The instance is created on the first `resolve` and cached thereafter. Returns the container for method chaining.
406
488
 
407
- ### `container.registerTransient(token, factory)`
489
+ ### `Container.prototype.registerTransient(token, factory)`
408
490
 
409
491
  Registers a factory as transient. A new instance is created on every `resolve`. Returns the container for method chaining.
410
492
 
411
- ### `container.registerScoped(token, factory)`
493
+ ### `Container.prototype.registerScoped(token, factory)`
412
494
 
413
495
  Registers a factory as scoped. Within a scope, the instance is created on the first `resolve` and cached for that scope. Each scope maintains its own cache. Scoped tokens cannot be resolved from the root container. Returns the container for method chaining.
414
496
 
415
- ### `container.resolve(token)`
497
+ ### `Container.prototype.use(source)`
498
+
499
+ Copies all registrations from `source` (another `Container`) into this container. Only factory and lifetime entries are copied — singleton instance caches are not shared. Returns the container for method chaining.
500
+
501
+ ### `Container.prototype.resolve(token)`
416
502
 
417
503
  Resolves and returns the instance for the given token. Throws `ContainerError` if the token is not registered or if a circular dependency is detected.
418
504
 
419
- ### `container.tryResolve(token)` / `scope.tryResolve(token)`
505
+ ### `Container.prototype.tryResolve(token)`
420
506
 
421
507
  Attempts to resolve the instance for the given token. Returns `undefined` if the token is not registered, instead of throwing. Still throws `ContainerError` for circular dependencies or operations on disposed containers/scopes.
422
508
 
@@ -424,19 +510,27 @@ Attempts to resolve the instance for the given token. Returns `undefined` if the
424
510
 
425
511
  Creates a new `Scope` (child container) from a `Container` or an existing `Scope`. The scope inherits all registrations from the source. Singleton instances are shared with the parent, while scoped instances are local to the scope.
426
512
 
427
- ### `Scope`
513
+ ### `class Scope`
514
+
515
+ A scoped child container created by `createScope()`.
516
+
517
+ ### `Scope.prototype.resolve(token)`
518
+
519
+ Resolves and returns the instance for the given token. Behaves the same as `Container.prototype.resolve`, but can also resolve scoped tokens.
520
+
521
+ ### `Scope.prototype.tryResolve(token)`
428
522
 
429
- A scoped child container created by `createScope()`. Provides `resolve(token)` and `tryResolve(token)`.
523
+ Attempts to resolve the instance for the given token. Returns `undefined` if the token is not registered, instead of throwing. Still throws `ContainerError` for circular dependencies or operations on disposed scopes.
430
524
 
431
525
  ### `disposable(container)` — `katagami/disposable`
432
526
 
433
- Attaches `[Symbol.asyncDispose]` to a `Container` or `Scope`, enabling `await using` syntax. Disposes all owned instances in reverse creation order (LIFO). Calls `[Symbol.asyncDispose]()` or `[Symbol.dispose]()` on each instance that implements them. Idempotent — subsequent calls are no-ops. After disposal, `resolve()` and `createScope()` will throw `ContainerError`.
527
+ Attaches `[Symbol.asyncDispose]` to a `Container` or `Scope`, enabling `await using` syntax. Disposes all owned instances in reverse creation order (LIFO). Calls `[Symbol.asyncDispose]()` or `[Symbol.dispose]()` on each instance that implements them. Idempotent — subsequent calls are no-ops. After disposal, `resolve()` and `createScope()` will throw `ContainerError`. The returned type is narrowed to `DisposableContainer` or `DisposableScope`, which only expose `resolve` and `tryResolve` — registration methods are excluded at the type level.
434
528
 
435
- ### `ContainerError`
529
+ ### `class ContainerError`
436
530
 
437
531
  Error class thrown for container failures such as resolving an unregistered token, circular dependencies, or operations on a disposed container/scope.
438
532
 
439
- ### `Resolver`
533
+ ### `type Resolver`
440
534
 
441
535
  Type export representing the resolver passed to factory callbacks. Useful when you need to type a function that accepts a resolver parameter.
442
536
 
@@ -83,6 +83,16 @@ export declare class Container<T = Record<never, never>, Sync extends AbstractCo
83
83
  registerScoped<V>(token: AbstractConstructor<V>, factory: (resolver: Resolver<T & ScopedT, Sync | ScopedSync, Async | ScopedAsync>) => V): Container<T, Sync, Async, ScopedT, ScopedSync | AbstractConstructor<V>, ScopedAsync>;
84
84
  registerScoped<K extends PropertyKey, V>(token: K, factory: (resolver: Resolver<T & ScopedT, Sync | ScopedSync, Async | ScopedAsync>) => V): Container<T, Sync, Async, Record<K, V> & ScopedT, ScopedSync, ScopedAsync>;
85
85
  registerScoped<V>(token: unknown, factory: (resolver: Resolver<T & ScopedT, Sync | ScopedSync, Async | ScopedAsync>) => V): Container<T, Sync, Async, ScopedT, ScopedSync, ScopedAsync>;
86
+ /**
87
+ * Apply all registrations from another container (module) to this container.
88
+ *
89
+ * Copies only registration entries (factory + lifetime). Singleton instance caches
90
+ * are not shared — each container manages its own.
91
+ *
92
+ * @param source A container whose registrations will be copied into this container
93
+ * @returns The container for method chaining
94
+ */
95
+ use<MT, MSync extends AbstractConstructor, MAsync extends AbstractConstructor, MScopedT, MScopedSync extends AbstractConstructor, MScopedAsync extends AbstractConstructor>(source: Container<MT, MSync, MAsync, MScopedT, MScopedSync, MScopedAsync>): Container<T & MT, Sync | MSync, Async | MAsync, ScopedT & MScopedT, ScopedSync | MScopedSync, ScopedAsync | MScopedAsync>;
86
96
  /**
87
97
  * Resolve an instance for the given token.
88
98
  *
@@ -1,4 +1,39 @@
1
+ import type { Container } from '../container';
1
2
  import { type ContainerInternals, INTERNALS } from '../internal';
3
+ import type { AbstractConstructor, Resolver } from '../resolver';
4
+ import type { Scope } from '../scope';
5
+ /**
6
+ * A container wrapped with `disposable()`.
7
+ *
8
+ * Only `resolve` and `tryResolve` are available at the type level.
9
+ * Registration methods (`registerSingleton`, `registerTransient`, `registerScoped`, `use`)
10
+ * are excluded, preventing accidental registration on a potentially-disposed container.
11
+ *
12
+ * @template T PropertyKey-based token type map
13
+ * @template Sync Union of registered sync class constructors
14
+ * @template Async Union of registered async class constructors
15
+ * @template ScopedT PropertyKey-based token type map for scoped registrations
16
+ * @template ScopedSync Union of scoped sync class constructors
17
+ * @template ScopedAsync Union of scoped async class constructors
18
+ */
19
+ export interface DisposableContainer<T = Record<never, never>, Sync extends AbstractConstructor = never, Async extends AbstractConstructor = never, _ScopedT = Record<never, never>, _ScopedSync extends AbstractConstructor = never, _ScopedAsync extends AbstractConstructor = never> extends Resolver<T, Sync, Async>, AsyncDisposable {
20
+ readonly [INTERNALS]: ContainerInternals;
21
+ }
22
+ /**
23
+ * A scope wrapped with `disposable()`.
24
+ *
25
+ * Only `resolve` and `tryResolve` are available at the type level.
26
+ *
27
+ * @template T PropertyKey-based token type map
28
+ * @template Sync Union of registered sync class constructors
29
+ * @template Async Union of registered async class constructors
30
+ * @template ScopedT PropertyKey-based token type map for scoped registrations
31
+ * @template ScopedSync Union of scoped sync class constructors
32
+ * @template ScopedAsync Union of scoped async class constructors
33
+ */
34
+ export interface DisposableScope<T = Record<never, never>, Sync extends AbstractConstructor = never, Async extends AbstractConstructor = never, ScopedT = Record<never, never>, ScopedSync extends AbstractConstructor = never, ScopedAsync extends AbstractConstructor = never> extends Resolver<T & ScopedT, Sync | ScopedSync, Async | ScopedAsync>, AsyncDisposable {
35
+ readonly [INTERNALS]: ContainerInternals;
36
+ }
2
37
  /**
3
38
  * Add async disposal capability to a container or scope.
4
39
  *
@@ -6,8 +41,11 @@ import { type ContainerInternals, INTERNALS } from '../internal';
6
41
  * Disposes owned instances in reverse creation order (LIFO), calling
7
42
  * `[Symbol.asyncDispose]()` or `[Symbol.dispose]()` on each instance that implements them.
8
43
  *
44
+ * The returned type is narrowed to only expose `resolve` and `tryResolve`,
45
+ * preventing registration methods from being called on a potentially-disposed container.
46
+ *
9
47
  * @param container A Container or Scope to make disposable
10
- * @returns The same object with `AsyncDisposable` capability added
48
+ * @returns The same object with `AsyncDisposable` capability added and registration methods removed from the type
11
49
  *
12
50
  * @example
13
51
  * ```ts
@@ -19,6 +57,5 @@ import { type ContainerInternals, INTERNALS } from '../internal';
19
57
  * );
20
58
  * ```
21
59
  */
22
- export declare function disposable<C extends {
23
- readonly [INTERNALS]: ContainerInternals;
24
- }>(container: C): C & AsyncDisposable;
60
+ export declare function disposable<T, Sync extends AbstractConstructor, Async extends AbstractConstructor, ScopedT, ScopedSync extends AbstractConstructor, ScopedAsync extends AbstractConstructor>(container: Container<T, Sync, Async, ScopedT, ScopedSync, ScopedAsync>): DisposableContainer<T, Sync, Async, ScopedT, ScopedSync, ScopedAsync>;
61
+ export declare function disposable<T, Sync extends AbstractConstructor, Async extends AbstractConstructor, ScopedT, ScopedSync extends AbstractConstructor, ScopedAsync extends AbstractConstructor>(scope: Scope<T, Sync, Async, ScopedT, ScopedSync, ScopedAsync>): DisposableScope<T, Sync, Async, ScopedT, ScopedSync, ScopedAsync>;
package/dist/index.cjs CHANGED
@@ -105,6 +105,12 @@ class Container {
105
105
  registerScoped(token, factory) {
106
106
  return this.addRegistration(token, factory, "scoped");
107
107
  }
108
+ use(source) {
109
+ for (const [token, registration] of source[INTERNALS].registrations) {
110
+ this.registrations.set(token, registration);
111
+ }
112
+ return this;
113
+ }
108
114
  resolve(token) {
109
115
  return this.resolveToken(token, true);
110
116
  }
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  export { Container, createContainer } from './container';
2
- export type { disposable } from './disposable';
2
+ export type { DisposableContainer, DisposableScope, disposable } from './disposable';
3
3
  export { ContainerError } from './error';
4
4
  export type { Resolver } from './resolver';
5
5
  export type { createScope, Scope } from './scope';
package/dist/index.js CHANGED
@@ -41,6 +41,12 @@ class Container {
41
41
  registerScoped(token, factory) {
42
42
  return this.addRegistration(token, factory, "scoped");
43
43
  }
44
+ use(source) {
45
+ for (const [token, registration] of source[INTERNALS].registrations) {
46
+ this.registrations.set(token, registration);
47
+ }
48
+ return this;
49
+ }
44
50
  resolve(token) {
45
51
  return this.resolveToken(token, true);
46
52
  }
@@ -1,18 +1,21 @@
1
1
  import type { Container } from '../container';
2
+ import type { DisposableContainer, DisposableScope } from '../disposable';
2
3
  import { type ContainerInternals, INTERNALS } from '../internal';
3
4
  import type { AbstractConstructor, Registration } from '../resolver';
4
5
  /**
5
- * Create a new scope (child container) from a Container or an existing Scope.
6
+ * Create a new scope (child container) from a Container, Scope, or their disposable variants.
6
7
  *
7
8
  * The scope inherits all registrations from the source.
8
9
  * Singleton instances are shared with the parent, while scoped instances are local to the scope.
9
10
  *
10
- * @param source A Container or Scope to create a child scope from
11
+ * @param source A Container, Scope, DisposableContainer, or DisposableScope to create a child scope from
11
12
  * @returns A new Scope instance
12
13
  * @throws ContainerError if the source has been disposed
13
14
  */
14
15
  export declare function createScope<T, Sync extends AbstractConstructor, Async extends AbstractConstructor, ScopedT, ScopedSync extends AbstractConstructor, ScopedAsync extends AbstractConstructor>(source: Container<T, Sync, Async, ScopedT, ScopedSync, ScopedAsync>): Scope<T, Sync, Async, ScopedT, ScopedSync, ScopedAsync>;
15
16
  export declare function createScope<T, Sync extends AbstractConstructor, Async extends AbstractConstructor, ScopedT, ScopedSync extends AbstractConstructor, ScopedAsync extends AbstractConstructor>(source: Scope<T, Sync, Async, ScopedT, ScopedSync, ScopedAsync>): Scope<T, Sync, Async, ScopedT, ScopedSync, ScopedAsync>;
17
+ export declare function createScope<T, Sync extends AbstractConstructor, Async extends AbstractConstructor, ScopedT, ScopedSync extends AbstractConstructor, ScopedAsync extends AbstractConstructor>(source: DisposableContainer<T, Sync, Async, ScopedT, ScopedSync, ScopedAsync>): Scope<T, Sync, Async, ScopedT, ScopedSync, ScopedAsync>;
18
+ export declare function createScope<T, Sync extends AbstractConstructor, Async extends AbstractConstructor, ScopedT, ScopedSync extends AbstractConstructor, ScopedAsync extends AbstractConstructor>(source: DisposableScope<T, Sync, Async, ScopedT, ScopedSync, ScopedAsync>): Scope<T, Sync, Async, ScopedT, ScopedSync, ScopedAsync>;
16
19
  /**
17
20
  * Scoped child container.
18
21
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "katagami",
3
- "version": "2.0.0",
3
+ "version": "2.1.0",
4
4
  "description": "Lightweight DI container for TypeScript and JavaScript — full type inference, no decorators, no reflect-metadata, hybrid class & PropertyKey tokens.",
5
5
  "license": "MIT",
6
6
  "type": "module",