katagami 1.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.ja.md ADDED
@@ -0,0 +1,438 @@
1
+ [English](./README.md) | [日本語](./README.ja.md) | [한국어](./README.ko.md) | [繁體中文](./README.zh-TW.md) | [简体中文](./README.zh-CN.md) | [Español](./README.es.md) | [Deutsch](./README.de.md) | [Français](./README.fr.md)
2
+
3
+ # Katagami
4
+
5
+ 軽量な TypeScript DI コンテナ。完全な型推論をサポートします。
6
+
7
+ [![npm version](https://img.shields.io/npm/v/katagami)](https://www.npmjs.com/package/katagami)
8
+ [![license](https://img.shields.io/npm/l/katagami)](https://github.com/hiroiku/katagami/blob/master/LICENSE)
9
+ [![bundle size](https://img.shields.io/bundlephobia/minzip/katagami)](https://bundlephobia.com/package/katagami)
10
+
11
+ > 名前の由来は「型紙」— 伝統的な染色で精密な模様を布に写し取るための型抜き紙です。複数の型紙を重ねて精緻な文様を構成するように、メソッドチェーンの各呼び出しで型が蓄積されます。型紙は紙と刷毛さえあれば成立し、大掛かりな装置を必要としません。Katagami もデコレータやメタデータの仕組みに依存せず、どのビルドツールでもそのまま動作します。そして、異なる生地や技法に対応する型紙のように、Katagami は TypeScript と JavaScript、クラストークンと PropertyKey トークンに対応する、ハイブリッドで厳密な DI を実現します。
12
+
13
+ ## 特徴
14
+
15
+ | 機能 | 説明 |
16
+ | ------------------------ | --------------------------------------------------------------------------------------------- |
17
+ | 完全な型推論 | メソッドチェーンで型が蓄積され、未登録トークンの解決はコンパイル時エラーになる |
18
+ | 3 つのライフタイム | Singleton、Transient、Scoped(子コンテナ対応) |
19
+ | 非同期ファクトリ | Promise を返すファクトリは型システムが自動的に追跡 |
20
+ | 循環依存の検出 | 循環パスの全体を含む明確なエラーメッセージ |
21
+ | Disposable サポート | TC39 Explicit Resource Management(`Symbol.dispose` / `Symbol.asyncDispose` / `await using`) |
22
+ | キャプティブ依存の防止 | Singleton/Transient のファクトリから Scoped トークンへのアクセスをコンパイル時に防止 |
23
+ | オプショナル解決 | `tryResolve` は未登録トークンでスローせず `undefined` を返す |
24
+ | ハイブリッドトークン戦略 | クラストークンで厳密な型安全性、PropertyKey トークンで柔軟性 |
25
+ | インターフェース型マップ | `createContainer<T>()` にインターフェースを渡して登録順序非依存に |
26
+ | ゼロ依存 | デコレータ不要、reflect-metadata 不要、ポリフィル不要 |
27
+
28
+ ## インストール
29
+
30
+ ```bash
31
+ npm install katagami
32
+ ```
33
+
34
+ ## クイックスタート
35
+
36
+ ```ts
37
+ import { createContainer } from 'katagami';
38
+
39
+ class Logger {
40
+ log(msg: string) {
41
+ console.log(msg);
42
+ }
43
+ }
44
+
45
+ class UserService {
46
+ constructor(private logger: Logger) {}
47
+ greet(name: string) {
48
+ this.logger.log(`Hello, ${name}`);
49
+ }
50
+ }
51
+
52
+ const container = createContainer()
53
+ .registerSingleton(Logger, () => new Logger())
54
+ .registerSingleton(UserService, r => new UserService(r.resolve(Logger)));
55
+
56
+ const userService = container.resolve(UserService);
57
+ // ^? UserService(完全に推論される)
58
+ userService.greet('world');
59
+ ```
60
+
61
+ ## なぜ Katagami なのか
62
+
63
+ 多くの TypeScript DI コンテナはデコレータ、reflect-metadata、または文字列ベースのトークンに依存しており、ツールの互換性・型安全性・バンドルサイズのいずれかにトレードオフが生じます。Katagami は異なるアプローチを採用しています。
64
+
65
+ ### デコレータ不要、reflect-metadata 不要
66
+
67
+ デコレータベースの DI には `experimentalDecorators` と `emitDecoratorMetadata` コンパイラオプションが必要です。esbuild や Vite(デフォルト設定)などのモダンなビルドツールは `emitDecoratorMetadata` をサポートしておらず、TC39 標準デコレータ提案にもデザインタイム型情報の自動出力に相当する機能は含まれていません。Katagami はこれらに一切依存しないため、どのビルドツールでもそのまま動作します。
68
+
69
+ ### クラストークンによる完全な型推論
70
+
71
+ 文字列トークンの DI では、トークンと型の手動マッピングを維持する必要があります。パラメータ名によるマッチングは、ミニファイ時に壊れます。Katagami はクラスを直接トークンとして使用するため、`resolve` は正しい戻り値の型(同期または `Promise`)を自動的に推論します。追加のアノテーションは不要です。
72
+
73
+ ### メソッドチェーンによる型の蓄積
74
+
75
+ `register` を呼び出すたびに型が蓄積されます。ファクトリ内では、リゾルバはチェーンのその時点までに登録されたトークンのみを受け付けます。未登録のトークンを解決しようとすると、実行時エラーではなくコンパイル時エラーになります。
76
+
77
+ ### ハイブリッドトークン戦略
78
+
79
+ クラストークンでは、メソッドチェーンによる厳密な順序依存の型安全性が得られます。一方、サービス群を先に定義して任意の順序で登録したい場合もあるでしょう。`createContainer<T>()` にインターフェースを渡して PropertyKey トークンを使えば、型マップは作成時に固定されるため、登録順序は問題になりません。
80
+
81
+ ### ゼロ依存
82
+
83
+ ランタイム依存なし、ポリフィルなし。reflect-metadata(未ミニファイで約 50 KB)をバンドルに追加する必要はありません。
84
+
85
+ ## ガイド
86
+
87
+ ### Singleton と Transient
88
+
89
+ Singleton は最初の `resolve` でインスタンスを作成しキャッシュします。Transient は毎回新しいインスタンスを作成します。
90
+
91
+ ```ts
92
+ import { createContainer } from 'katagami';
93
+
94
+ class Database {
95
+ constructor(public id = Math.random()) {}
96
+ }
97
+
98
+ class RequestHandler {
99
+ constructor(public id = Math.random()) {}
100
+ }
101
+
102
+ const container = createContainer()
103
+ .registerSingleton(Database, () => new Database())
104
+ .registerTransient(RequestHandler, () => new RequestHandler());
105
+
106
+ // Singleton — 常に同じインスタンス
107
+ container.resolve(Database) === container.resolve(Database); // true
108
+
109
+ // Transient — 毎回新しいインスタンス
110
+ container.resolve(RequestHandler) === container.resolve(RequestHandler); // false
111
+ ```
112
+
113
+ ### Scoped ライフタイムと子コンテナ
114
+
115
+ Scoped 登録はスコープ内では Singleton のように振る舞いますが、新しいスコープでは新しいインスタンスを生成します。`createScope()` で子コンテナを作成します。Scoped トークンはルートコンテナからは解決できません。
116
+
117
+ ```ts
118
+ import { createContainer } from 'katagami';
119
+
120
+ class DbPool {
121
+ constructor(public name = 'main') {}
122
+ }
123
+
124
+ class RequestContext {
125
+ constructor(public id = Math.random()) {}
126
+ }
127
+
128
+ const root = createContainer()
129
+ .registerSingleton(DbPool, () => new DbPool())
130
+ .registerScoped(RequestContext, () => new RequestContext());
131
+
132
+ // リクエストごとにスコープを作成
133
+ const scope1 = root.createScope();
134
+ const scope2 = root.createScope();
135
+
136
+ // Scoped — 同じスコープ内では同一、スコープ間では別インスタンス
137
+ scope1.resolve(RequestContext) === scope1.resolve(RequestContext); // true
138
+ scope1.resolve(RequestContext) === scope2.resolve(RequestContext); // false
139
+
140
+ // Singleton — すべてのスコープで共有
141
+ scope1.resolve(DbPool) === scope2.resolve(DbPool); // true
142
+ ```
143
+
144
+ スコープはネストも可能です。ネストされたスコープは独自の Scoped インスタンスキャッシュを持ちつつ、Singleton は親と共有します:
145
+
146
+ ```ts
147
+ const parentScope = root.createScope();
148
+ const childScope = parentScope.createScope();
149
+
150
+ // ネストされたスコープはそれぞれ独自の Scoped インスタンスを持つ
151
+ parentScope.resolve(RequestContext) === childScope.resolve(RequestContext); // false
152
+
153
+ // Singleton は引き続き共有
154
+ parentScope.resolve(DbPool) === childScope.resolve(DbPool); // true
155
+ ```
156
+
157
+ ### 非同期ファクトリ
158
+
159
+ `Promise` を返すファクトリは型システムによって自動的に追跡されます。非同期トークンを `resolve` すると、戻り値の型は `V` ではなく `Promise<V>` になります:
160
+
161
+ ```ts
162
+ import { createContainer } from 'katagami';
163
+
164
+ class Database {
165
+ constructor(public connected: boolean) {}
166
+ }
167
+
168
+ class Logger {
169
+ log(msg: string) {
170
+ console.log(msg);
171
+ }
172
+ }
173
+
174
+ const container = createContainer()
175
+ .registerSingleton(Logger, () => new Logger())
176
+ .registerSingleton(Database, async () => {
177
+ await new Promise(r => setTimeout(r, 100)); // 非同期初期化をシミュレート
178
+ return new Database(true);
179
+ });
180
+
181
+ const logger = container.resolve(Logger);
182
+ // ^? Logger
183
+
184
+ const db = await container.resolve(Database);
185
+ // ^? Promise<Database>(await 後 → Database)
186
+ db.connected; // true
187
+ ```
188
+
189
+ 非同期ファクトリは同期・非同期両方の登録に依存できます:
190
+
191
+ ```ts
192
+ const container = createContainer()
193
+ .registerSingleton(Logger, () => new Logger())
194
+ .registerSingleton(Database, async r => {
195
+ const logger = r.resolve(Logger); // 同期 → Logger
196
+ logger.log('接続中...');
197
+ return new Database(true);
198
+ });
199
+ ```
200
+
201
+ ### 循環依存の検出
202
+
203
+ Katagami は解決中のトークンを追跡します。循環依存が検出されると、循環パスの全体を含む `ContainerError` がスローされます:
204
+
205
+ ```ts
206
+ import { createContainer } from 'katagami';
207
+
208
+ class ServiceA {
209
+ constructor(public b: ServiceB) {}
210
+ }
211
+
212
+ class ServiceB {
213
+ constructor(public a: ServiceA) {}
214
+ }
215
+
216
+ const container = createContainer()
217
+ .registerSingleton(ServiceA, r => new ServiceA(r.resolve(ServiceB)))
218
+ .registerSingleton(ServiceB, r => new ServiceB(r.resolve(ServiceA)));
219
+
220
+ container.resolve(ServiceA);
221
+ // ContainerError: Circular dependency detected: ServiceA -> ServiceB -> ServiceA
222
+ ```
223
+
224
+ 間接的な循環も検出されます:
225
+
226
+ ```
227
+ ContainerError: Circular dependency detected: ServiceX -> ServiceY -> ServiceZ -> ServiceX
228
+ ```
229
+
230
+ ### Disposable サポート
231
+
232
+ `Container` と `Scope` は `AsyncDisposable` を実装しています。破棄時に、管理対象のインスタンスを逆順(LIFO)で走査し、`[Symbol.asyncDispose]()` または `[Symbol.dispose]()` を自動的に呼び出します。
233
+
234
+ ```ts
235
+ import { createContainer } from 'katagami';
236
+
237
+ class Connection {
238
+ async [Symbol.asyncDispose]() {
239
+ console.log('Connection closed');
240
+ }
241
+ }
242
+
243
+ // 手動で破棄
244
+ const container = createContainer().registerSingleton(Connection, () => new Connection());
245
+
246
+ container.resolve(Connection);
247
+ await container[Symbol.asyncDispose]();
248
+ // => "Connection closed"
249
+ ```
250
+
251
+ `await using` を使うと、ブロックの終了時にスコープが自動的に破棄されます:
252
+
253
+ ```ts
254
+ const root = createContainer()
255
+ .registerSingleton(DbPool, () => new DbPool())
256
+ .registerScoped(Connection, () => new Connection());
257
+
258
+ {
259
+ await using scope = root.createScope();
260
+ const conn = scope.resolve(Connection);
261
+ // ... conn を使用 ...
262
+ } // ここでスコープが破棄される — Connection はクリーンアップされ、DbPool はされない
263
+ ```
264
+
265
+ スコープの破棄は Scoped インスタンスのみに影響します。Singleton インスタンスはルートコンテナに所有されており、コンテナ自体が破棄されたときに破棄されます。
266
+
267
+ ### インターフェース型マップ
268
+
269
+ `createContainer<T>()` にインターフェースを渡すと、PropertyKey トークンはチェーンによる蓄積ではなく、インターフェースから型が決定されます。これにより、トークンを任意の順序で登録・解決できます:
270
+
271
+ ```ts
272
+ import { createContainer } from 'katagami';
273
+
274
+ class Logger {
275
+ log(msg: string) {
276
+ console.log(msg);
277
+ }
278
+ }
279
+
280
+ interface Services {
281
+ logger: Logger;
282
+ greeting: string;
283
+ }
284
+
285
+ const container = createContainer<Services>()
286
+ // 'greeting' は 'logger' より先に登録されていても参照できる
287
+ .registerSingleton('greeting', r => {
288
+ r.resolve('logger').log('greeting を構築中...');
289
+ return 'Hello!';
290
+ })
291
+ .registerSingleton('logger', () => new Logger());
292
+
293
+ const greeting = container.resolve('greeting');
294
+ // ^? string
295
+ ```
296
+
297
+ ### ハイブリッドトークン戦略
298
+
299
+ 両方のアプローチを組み合わせることもできます — クラストークンで順序依存の型安全性を、PropertyKey トークンで順序非依存の柔軟性を活用できます:
300
+
301
+ ```ts
302
+ const container = createContainer<Services>()
303
+ .registerSingleton(Logger, () => new Logger())
304
+ .registerSingleton('logger', () => new Logger())
305
+ .registerSingleton('greeting', r => {
306
+ r.resolve(Logger).log('greeting を構築中...');
307
+ return 'Hello!';
308
+ });
309
+ ```
310
+
311
+ ### キャプティブ依存の防止
312
+
313
+ 「キャプティブ依存」とは、長いライフタイムのサービス(Singleton や Transient)が短いライフタイムのサービス(Scoped)を捕獲し、意図したスコープを超えて保持してしまう問題です。Katagami はこれをコンパイル時に防止します — Singleton と Transient のファクトリには、Scoped トークンを含まないリゾルバのみが渡されます:
314
+
315
+ ```ts
316
+ import { createContainer } from 'katagami';
317
+
318
+ class DbPool {}
319
+ class RequestContext {}
320
+
321
+ const container = createContainer()
322
+ .registerScoped(RequestContext, () => new RequestContext())
323
+ // @ts-expect-error — Singleton のファクトリは Scoped トークンを解決できない
324
+ .registerSingleton(DbPool, r => new DbPool(r.resolve(RequestContext)));
325
+ ```
326
+
327
+ 一方、Scoped のファクトリは Scoped トークンと非 Scoped トークンの両方を解決できます:
328
+
329
+ ```ts
330
+ const container = createContainer()
331
+ .registerSingleton(DbPool, () => new DbPool())
332
+ .registerScoped(RequestContext, r => {
333
+ r.resolve(DbPool); // OK — Scoped のファクトリは Singleton トークンを解決できる
334
+ return new RequestContext();
335
+ });
336
+ ```
337
+
338
+ ### オプショナル解決(tryResolve)
339
+
340
+ オプショナル依存を扱いたい場合や、エラーをスローせずにトークンが登録されているかを確認したい場合は、`tryResolve` を使用します。`resolve` と異なり、未登録トークンで `ContainerError` をスローする代わりに `undefined` を返します:
341
+
342
+ ```ts
343
+ import { createContainer } from 'katagami';
344
+
345
+ class Logger {
346
+ log(msg: string) {
347
+ console.log(msg);
348
+ }
349
+ }
350
+
351
+ class Analytics {
352
+ track(event: string) {
353
+ console.log(`Track: ${event}`);
354
+ }
355
+ }
356
+
357
+ const container = createContainer().registerSingleton(Logger, () => new Logger());
358
+
359
+ // resolve は未登録トークンでスロー
360
+ container.resolve(Analytics); // ContainerError: Token "Analytics" is not registered.
361
+
362
+ // tryResolve は未登録トークンで undefined を返す
363
+ const analytics = container.tryResolve(Analytics);
364
+ // ^? Analytics | undefined
365
+ if (analytics) {
366
+ analytics.track('event');
367
+ }
368
+ ```
369
+
370
+ `tryResolve` はファクトリ内のオプショナル依存に特に便利です。`resolve` と異なり、未登録トークンでもコンパイル時エラーになりません:
371
+
372
+ ```ts
373
+ const container = createContainer()
374
+ .registerSingleton(Logger, () => new Logger())
375
+ .registerSingleton('UserService', r => {
376
+ const logger = r.tryResolve(Logger); // オプショナル依存
377
+ const analytics = r.tryResolve(Analytics); // Analytics は未登録だがコンパイルエラーにならない
378
+
379
+ return {
380
+ greet(name: string) {
381
+ logger?.log(`Hello, ${name}`);
382
+ analytics?.track('user_greeted');
383
+ },
384
+ };
385
+ });
386
+ ```
387
+
388
+ `tryResolve` は循環依存や破棄済みコンテナ/スコープの操作では従来通り `ContainerError` をスローします — `undefined` を返すのは未登録トークンの場合のみです。
389
+
390
+ ## API
391
+
392
+ ### `createContainer<T, ScopedT>()`
393
+
394
+ 新しい DI コンテナを作成します。PropertyKey トークンの型マップを定義するには、インターフェースを `T` として渡します。Scoped な PropertyKey トークンの型マップを定義するには `ScopedT` を渡します(`T` と同様に登録順序非依存)。
395
+
396
+ ### `container.registerSingleton(token, factory)`
397
+
398
+ ファクトリをシングルトンとして登録します。インスタンスは最初の `resolve` 時に作成され、以降はキャッシュされます。メソッドチェーン用にコンテナを返します。
399
+
400
+ ### `container.registerTransient(token, factory)`
401
+
402
+ ファクトリをトランジェントとして登録します。`resolve` のたびに新しいインスタンスが作成されます。メソッドチェーン用にコンテナを返します。
403
+
404
+ ### `container.registerScoped(token, factory)`
405
+
406
+ ファクトリをスコープ付きとして登録します。スコープ内では最初の `resolve` でインスタンスが作成され、そのスコープ内でキャッシュされます。各スコープは独自のキャッシュを持ちます。Scoped トークンはルートコンテナからは解決できません。メソッドチェーン用にコンテナを返します。
407
+
408
+ ### `container.resolve(token)`
409
+
410
+ 指定されたトークンのインスタンスを解決して返します。トークンが未登録の場合、または循環依存が検出された場合に `ContainerError` をスローします。
411
+
412
+ ### `container.tryResolve(token)` / `scope.tryResolve(token)`
413
+
414
+ 指定されたトークンのインスタンスの解決を試みます。トークンが未登録の場合、スローせず `undefined` を返します。循環依存や破棄済みコンテナ/スコープの操作では従来通り `ContainerError` をスローします。
415
+
416
+ ### `container.createScope()`
417
+
418
+ 新しい `Scope`(子コンテナ)を作成します。スコープは親のすべての登録を継承します。Singleton インスタンスは親と共有され、Scoped インスタンスはスコープ内でローカルになります。
419
+
420
+ ### `Scope`
421
+
422
+ `createScope()` で作成されるスコープ付き子コンテナです。`resolve(token)`、`tryResolve(token)`、`createScope()`(ネストスコープ用)、`[Symbol.asyncDispose]()` を提供します。
423
+
424
+ ### `container[Symbol.asyncDispose]()` / `scope[Symbol.asyncDispose]()`
425
+
426
+ 管理対象のインスタンスを逆順(LIFO)で破棄します。各インスタンスの `[Symbol.asyncDispose]()` または `[Symbol.dispose]()` を呼び出します。冪等 — 2 回目以降の呼び出しは何もしません。破棄後は `resolve()` と `createScope()` が `ContainerError` をスローします。
427
+
428
+ ### `ContainerError`
429
+
430
+ 未登録トークンの解決、循環依存、破棄済みコンテナ/スコープの操作など、コンテナの障害時にスローされるエラークラスです。
431
+
432
+ ### `Resolver`
433
+
434
+ ファクトリコールバックに渡されるリゾルバを表す型エクスポートです。リゾルバを引数に取る関数を型付けする際に使用できます。
435
+
436
+ ## ライセンス
437
+
438
+ MIT