katagami 1.1.0 → 2.0.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.zh-TW.md DELETED
@@ -1,438 +0,0 @@
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 不依賴裝飾器或元資料機制,開箱即用於任何建置工具。而就像型版能適應不同的織物與技法,Katagami 也能跨越 TypeScript 與 JavaScript、類別令牌與 PropertyKey 令牌——以混合方式實現嚴格、可組合的 DI。
12
-
13
- ## 特性
14
-
15
- | 特性 | 說明 |
16
- | --------------- | ----------------------------------------------------------------------------- |
17
- | 完整的型別推斷 | 型別隨方法鏈累積;解析未註冊的令牌會產生編譯時錯誤 |
18
- | 三種生命週期 | Singleton、Transient 和 Scoped(支援子容器) |
19
- | 非同步工廠 | 回傳 Promise 的工廠會被型別系統自動追蹤 |
20
- | 循環依賴偵測 | 包含完整循環路徑的清晰錯誤訊息 |
21
- | Disposable 支援 | TC39 顯式資源管理(`Symbol.dispose` / `Symbol.asyncDispose` / `await using`) |
22
- | 捕獲依賴防護 | Singleton/Transient 工廠無法存取 Scoped 令牌;在編譯時捕獲 |
23
- | 選擇性解析 | `tryResolve` 對未註冊令牌回傳 `undefined` 而非拋出例外 |
24
- | 混合令牌策略 | 類別令牌提供嚴格的型別安全,PropertyKey 令牌提供彈性 |
25
- | 介面型別映射 | 向 `createContainer<T>()` 傳入介面,實現與註冊順序無關的註冊 |
26
- | 零依賴 | 無需裝飾器、無需 reflect-metadata、無需 polyfill |
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
- 無執行時依賴,無 polyfill。無需將 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` 非同步令牌時,回傳型別是 `Promise<V>` 而非 `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',即使 '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` 不同,它對未註冊的令牌回傳 `undefined` 而不是拋出 `ContainerError`:
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 容器。傳入介面作為 `T` 以定義 PropertyKey 令牌的型別映射。傳入 `ScopedT` 以定義 Scoped PropertyKey 令牌的獨立型別映射(與 `T` 一樣與註冊順序無關)。
395
-
396
- ### `container.registerSingleton(token, factory)`
397
-
398
- 將工廠註冊為 Singleton。實例在首次 `resolve` 時建立並快取。回傳容器以支援方法鏈。
399
-
400
- ### `container.registerTransient(token, factory)`
401
-
402
- 將工廠註冊為 Transient。每次 `resolve` 都會建立新實例。回傳容器以支援方法鏈。
403
-
404
- ### `container.registerScoped(token, factory)`
405
-
406
- 將工廠註冊為 Scoped。在作用域內,實例在首次 `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]()`。冪等——後續呼叫為空操作。銷毀後,`resolve()` 和 `createScope()` 將拋出 `ContainerError`。
427
-
428
- ### `ContainerError`
429
-
430
- 用於容器故障的錯誤類別,例如解析未註冊的令牌、循環依賴或對已銷毀容器/作用域的操作。
431
-
432
- ### `Resolver`
433
-
434
- 表示傳遞給工廠回呼的解析器的型別匯出。當你需要為接受解析器參數的函式添加型別時很有用。
435
-
436
- ## 授權條款
437
-
438
- MIT