@husky-di/decorator 1.2.1 → 1.3.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
@@ -1,238 +1,463 @@
1
1
  # @husky-di/decorator
2
2
 
3
- `@husky-di/decorator` husky-di 的装饰器支持包,提供了基于 TypeScript 装饰器的依赖注入功能。
3
+ `@husky-di/decorator` adds decorator support to husky-di.
4
+ It translates TypeScript decorator metadata into `@husky-di/core` resolution behavior, so you can declare dependencies directly on constructor parameters.
4
5
 
5
- ## 概述
6
+ You can think of it as a syntax layer on top of `core`:
6
7
 
7
- 该包专门为偏好使用装饰器语法的开发者设计,提供了简洁直观的依赖注入方式。**仅支持 TypeScript 装饰器**,不支持 ES 装饰器。
8
+ - it makes dependency declaration feel more natural
9
+ - it does not replace the container itself, and lifecycle, middleware, `ref`, and `dynamic` still come from `@husky-di/core`
8
10
 
9
- ### 为什么仅支持 TypeScript 装饰器?
11
+ This package currently supports TypeScript experimental decorators only, not ES decorators.
12
+ The reason is straightforward: husky-di depends on parameter decorators for constructor injection, and ES decorators do not provide that capability.
10
13
 
11
- husky-di 的设计理念是**仅支持构造函数注入**,而 ES 装饰器的规范中**没有设计参数注入器**。
14
+ ## Is This The Right Package?
12
15
 
13
- ## 安装
16
+ This package is a good fit when:
14
17
 
15
- ```bash
16
- pnpm add @husky-di/decorator
17
- ```
18
+ - you want dependencies to live directly on constructor parameters
19
+ - you do not want to hand-write wiring for every class
20
+ - you are comfortable using TypeScript experimental decorators and `reflect-metadata`
18
21
 
19
- ## 核心装饰器
22
+ If what you really want is:
20
23
 
21
- ### @injectable()
24
+ - a low-level container with no decorator dependency:
25
+ see `../core/README.md`
26
+ - module import/export boundaries:
27
+ pair it with `../module/README.md`
22
28
 
23
- 标记一个类为可注入,使其能够被依赖注入容器管理。
29
+ ## What You Get
24
30
 
25
- > 背后的原因是因为只有应用了装饰器,typescript 编译器才会触发元数据的标记。
31
+ - `@injectable()` to mark classes as instantiable through decorator-aware resolution
32
+ - `@inject()` to declare service identifiers and resolve options for constructor parameters
33
+ - `@tagged()` as the low-level metadata decorator for custom abstractions
34
+ - `decoratorMiddleware` to read injection metadata during class resolution
35
+ - stable error exports such as `DecoratorException` and `DecoratorErrorCodeEnum`
26
36
 
27
- ```typescript
28
- import { injectable } from "@husky-di/decorator";
37
+ ## Installation
29
38
 
30
- @injectable()
31
- class UserService {
32
- constructor() {}
39
+ ```bash
40
+ pnpm add @husky-di/core @husky-di/decorator reflect-metadata
41
+ ```
42
+
43
+ `reflect-metadata` is a peer dependency.
44
+ At runtime, you need to load it first or provide a compatible Reflect metadata implementation.
45
+
46
+ ## TypeScript Configuration
47
+
48
+ Enable TypeScript experimental decorators and metadata emission:
49
+
50
+ ```json
51
+ {
52
+ "compilerOptions": {
53
+ "experimentalDecorators": true,
54
+ "emitDecoratorMetadata": true
55
+ }
33
56
  }
34
57
  ```
35
58
 
36
- ### @inject()
59
+ ## Quick Start
37
60
 
38
- 用于构造函数参数注入,支持多种注入选项。
61
+ The example below shows the most common setup: register the middleware once, then declare dependencies directly on constructor parameters.
39
62
 
40
63
  ```typescript
41
- import { inject, injectable } from "@husky-di/decorator";
64
+ import "reflect-metadata";
65
+ import {
66
+ createContainer,
67
+ createServiceIdentifier,
68
+ globalMiddleware,
69
+ } from "@husky-di/core";
70
+ import {
71
+ decoratorMiddleware,
72
+ inject,
73
+ injectable,
74
+ } from "@husky-di/decorator";
75
+
76
+ interface Logger {
77
+ log(message: string): void;
78
+ }
79
+
80
+ const ILogger = createServiceIdentifier<Logger>("ILogger");
42
81
 
43
82
  @injectable()
44
- class LoggerService {
83
+ class ConsoleLogger implements Logger {
45
84
  log(message: string) {
46
- console.log(message);
85
+ console.log(`[log] ${message}`);
47
86
  }
48
87
  }
49
88
 
50
89
  @injectable()
51
90
  class UserService {
52
- constructor(@inject(LoggerService) private logger: LoggerService) {}
91
+ constructor(@inject(ILogger) private readonly logger: Logger) {}
92
+
93
+ getUser(id: string) {
94
+ this.logger.log(`load user: ${id}`);
95
+ return { id, name: "Ada" };
96
+ }
53
97
  }
54
- ```
55
98
 
56
- ## 注入选项
99
+ globalMiddleware.use(decoratorMiddleware);
100
+
101
+ const container = createContainer("AppContainer");
102
+ container.register(ILogger, { useClass: ConsoleLogger });
103
+
104
+ const userService = container.resolve(UserService);
105
+ console.log(userService.getUser("u-1"));
106
+ ```
57
107
 
58
- `@inject()` 装饰器支持多种注入选项:
108
+ In this example:
59
109
 
60
- ### 动态注入 (dynamic)
110
+ - `decoratorMiddleware` reads constructor parameter metadata
111
+ - `@inject(ILogger)` maps an interface dependency to a runtime-visible service identifier
61
112
 
62
- 获取服务的动态引用,支持延迟解析:
113
+ With plain `@husky-di/core`, a common alternative is to resolve the dependency inside the class directly:
63
114
 
64
115
  ```typescript
65
- @injectable()
66
- class ConfigService {
67
- getValue(key: string) {
68
- return `config-${key}`;
116
+ import { resolve } from "@husky-di/core";
117
+
118
+ class UserService {
119
+ private readonly logger = resolve(ILogger);
120
+
121
+ getUser(id: string) {
122
+ this.logger.log(`load user: ${id}`);
123
+ return { id, name: "Ada" };
69
124
  }
70
125
  }
126
+ ```
127
+
128
+ ## Adding It To An Existing Project
129
+
130
+ ### Register Middleware Globally
131
+
132
+ This is the recommended option for most applications:
133
+
134
+ ```typescript
135
+ import { globalMiddleware } from "@husky-di/core";
136
+ import { decoratorMiddleware } from "@husky-di/decorator";
137
+
138
+ globalMiddleware.use(decoratorMiddleware);
139
+ ```
140
+
141
+ That enables decorator-based constructor injection for all containers.
142
+
143
+ ### Register Middleware Locally
144
+
145
+ If you only want decorator support in one container, register it locally instead:
146
+
147
+ ```typescript
148
+ const container = createContainer("FeatureContainer");
149
+ container.use(decoratorMiddleware);
150
+ ```
151
+
152
+ ## Main APIs
153
+
154
+ ### `@injectable()`
155
+
156
+ Marks a class as instantiable by the decorator middleware and merges its parameter metadata into the internal metadata store.
157
+
158
+ ```typescript
159
+ import { injectable } from "@husky-di/decorator";
71
160
 
72
161
  @injectable()
73
- class CacheService {
74
- constructor(
75
- @inject(ConfigService, { dynamic: true })
76
- private configRef: Ref<ConfigService>
77
- ) {}
162
+ class UserService {}
163
+ ```
78
164
 
79
- get(key: string) {
80
- const config = this.configRef.current;
81
- return `cached-${config.getValue(key)}`;
82
- }
165
+ Key points:
166
+
167
+ - the same class cannot be decorated with `@injectable()` more than once
168
+ - parameters without explicit `@inject()` are inferred from `design:paramtypes`
169
+ - if inference resolves to a primitive instead of a class, resolution fails immediately
170
+
171
+ ### `@inject()`
172
+
173
+ Explicitly declares the service identifier for a constructor parameter.
174
+
175
+ ```typescript
176
+ @injectable()
177
+ class UserService {
178
+ constructor(@inject(ILogger) private readonly logger: Logger) {}
83
179
  }
84
180
  ```
85
181
 
86
- ### 引用注入 (ref)
182
+ The supported service identifier kinds match `core`:
183
+
184
+ - class constructor
185
+ - `symbol`
186
+ - `string`
87
187
 
88
- 获取服务的引用对象:
188
+ ### `@tagged()`
189
+
190
+ `@tagged()` is the lower-level metadata decorator.
191
+ It accepts the full `InjectionMetadata` object directly.
89
192
 
90
193
  ```typescript
194
+ import { tagged, injectable } from "@husky-di/decorator";
195
+
91
196
  @injectable()
92
- class ApiService {
197
+ class UserService {
93
198
  constructor(
94
- @inject(CacheService, { ref: true })
95
- public cacheRef: Ref<CacheService>
199
+ @tagged({ serviceIdentifier: ILogger, optional: true })
200
+ private readonly logger?: Logger
96
201
  ) {}
97
202
  }
98
203
  ```
99
204
 
100
- ### 可选注入 (optional)
205
+ This is useful when:
206
+
207
+ - you want to build a domain-specific custom decorator
208
+ - you want full control over the metadata instead of the `@inject()` shorthand
209
+
210
+ ## Cases Where You Should Explicitly Use `@inject()`
101
211
 
102
- 支持可选依赖,当依赖不存在时不会抛出错误:
212
+ ### Interface Types
213
+
214
+ Interfaces do not exist at runtime, so you must provide a runtime-visible service identifier explicitly.
103
215
 
104
216
  ```typescript
217
+ interface Logger {
218
+ log(message: string): void;
219
+ }
220
+
221
+ const ILogger = createServiceIdentifier<Logger>("ILogger");
222
+
105
223
  @injectable()
106
- class TestService {
107
- constructor(
108
- @inject(ExistingService, { optional: true })
109
- public service: ExistingService
110
- ) {}
224
+ class UserService {
225
+ constructor(@inject(ILogger) private readonly logger: Logger) {}
111
226
  }
112
227
  ```
113
228
 
114
- ## 中间件集成
229
+ ### Primitive Types
115
230
 
116
- 装饰器包提供了 `decoratorMiddleware` 中间件,需要注册到全局中间件中:
231
+ Primitive types such as `string`, `number`, and `boolean` also need an explicit identifier.
117
232
 
118
233
  ```typescript
119
- import { globalMiddleware } from "@husky-di/core";
120
- import { decoratorMiddleware } from "@husky-di/decorator";
234
+ const API_BASE_URL = Symbol("API_BASE_URL");
121
235
 
122
- // 注册装饰器中间件
123
- globalMiddleware.use(decoratorMiddleware);
236
+ @injectable()
237
+ class ApiClient {
238
+ constructor(@inject(API_BASE_URL) private readonly baseUrl: string) {}
239
+ }
124
240
  ```
125
241
 
126
- ## 完整示例
242
+ ### When You Want To Override Inference
127
243
 
128
- ```typescript
129
- import "reflect-metadata";
130
- import { createContainer, globalMiddleware } from "@husky-di/core";
131
- import { decoratorMiddleware, inject, injectable } from "@husky-di/decorator";
244
+ Even if the parameter type is a class, you should still write `@inject()` or `@tagged()` when you want to:
132
245
 
133
- // 注册装饰器中间件
134
- globalMiddleware.use(decoratorMiddleware);
246
+ - use a different token
247
+ - enable `optional`
248
+ - enable `ref`
249
+ - enable `dynamic`
250
+ - change the `core.resolve()` container scope
251
+
252
+ ## Injection Options
253
+
254
+ The decorator layer supports the same resolve options as `core.resolve()`.
255
+
256
+ ### `optional`
135
257
 
136
- // 创建容器
137
- const container = createContainer();
258
+ Return `undefined` instead of throwing when the dependency is missing.
138
259
 
139
- // 定义服务
260
+ ```typescript
140
261
  @injectable()
141
- class LoggerService {
142
- log(message: string) {
143
- return `Logged: ${message}`;
144
- }
262
+ class UserService {
263
+ constructor(
264
+ @inject("auditLogger", { optional: true })
265
+ private readonly auditLogger?: { log(message: string): void }
266
+ ) {}
145
267
  }
268
+ ```
269
+
270
+ ### `ref`
271
+
272
+ Return a lazy reference, which is useful for deferred access or partially breaking circular dependencies.
273
+
274
+ ```typescript
275
+ import type { Ref } from "@husky-di/core";
146
276
 
147
277
  @injectable()
148
- class DatabaseService {
149
- constructor(@inject(LoggerService) private logger: LoggerService) {}
278
+ class UserService {
279
+ constructor(
280
+ @inject(ILogger, { ref: true })
281
+ private readonly loggerRef: Ref<Logger>
282
+ ) {}
150
283
 
151
- query(sql: string) {
152
- return this.logger.log(`Executing: ${sql}`);
284
+ run() {
285
+ this.loggerRef.current.log("run");
153
286
  }
154
287
  }
288
+ ```
289
+
290
+ ### `dynamic`
291
+
292
+ Return a dynamic reference whose `.current` value is re-resolved on every access.
293
+
294
+ ```typescript
295
+ import type { Ref } from "@husky-di/core";
155
296
 
156
297
  @injectable()
157
298
  class UserService {
158
299
  constructor(
159
- @inject(DatabaseService) private db: DatabaseService,
160
- @inject(LoggerService) private logger: LoggerService
300
+ @inject(ILogger, { dynamic: true })
301
+ private readonly loggerRef: Ref<Logger>
161
302
  ) {}
303
+ }
304
+ ```
162
305
 
163
- getUser(id: string) {
164
- return this.db.query(`SELECT * FROM users WHERE id = ${id}`);
165
- }
306
+ Prefer `ref` unless you specifically need to re-run resolution every time the value is read.
307
+
308
+ ### `scope`
309
+
310
+ Choose which `core.resolve()` container perspective the decorator should use for
311
+ that parameter.
312
+
313
+ ```typescript
314
+ import { ResolveContainerScopeEnum } from "@husky-di/core";
315
+
316
+ @injectable()
317
+ class DatabaseConsumer {
318
+ constructor(
319
+ @inject(IDatabaseOptions, { scope: ResolveContainerScopeEnum.origin })
320
+ private readonly options: { baseURL: string }
321
+ ) {}
166
322
  }
323
+ ```
167
324
 
168
- // 使用服务
169
- const userService = container.resolve(UserService);
170
- const result = userService.getUser("123");
171
- console.log(result); // "Logged: Executing: SELECT * FROM users WHERE id = 123"
325
+ This is useful when a parent-provided class should consume child-container
326
+ overrides for a specific constructor parameter.
327
+
328
+ ## When Automatic Inference Works
329
+
330
+ ### Cases Where Type Inference Is Enough
331
+
332
+ If the parameter itself is a class, and that class is also marked with `@injectable()`, you can omit `@inject()`:
333
+
334
+ ```typescript
335
+ @injectable()
336
+ class LoggerService {}
337
+
338
+ @injectable()
339
+ class UserService {
340
+ constructor(private readonly logger: LoggerService) {}
341
+ }
172
342
  ```
173
343
 
174
- ## 错误处理
344
+ ### Cases Where You Should Not Rely On Inference
345
+
346
+ Do not rely on automatic inference in these cases:
175
347
 
176
- ### 常见错误
348
+ - the parameter type is an interface
349
+ - the parameter type is a primitive
350
+ - you need `optional`
351
+ - you need `ref`
352
+ - you need `dynamic`
353
+ - you want to bind the parameter to a different token than its runtime class
177
354
 
178
- 1. **重复使用 @injectable()**
355
+ ## Relationship To `core`
179
356
 
180
- ```typescript
181
- // 错误:不能对同一个类使用两次 @injectable()
182
- @injectable()
183
- @injectable()
184
- class TestService {}
185
- ```
357
+ `@husky-di/decorator` does not replace `core`.
358
+ It is a syntax layer built on top of it.
186
359
 
187
- 2. **注入非可注入类**
360
+ You still keep using:
188
361
 
189
- ```typescript
190
- // 错误:依赖的类没有使用 @injectable()
191
- class NonInjectableService {}
362
+ - `createContainer()`
363
+ - `createServiceIdentifier()`
364
+ - `LifecycleEnum`
365
+ - `globalMiddleware`
366
+ - `resolve()` / `ref` / `dynamic`
192
367
 
193
- @injectable()
194
- class TestService {
195
- constructor(@inject(NonInjectableService) dep: NonInjectableService) {}
196
- }
197
- ```
368
+ The decorator middleware only participates in the class-instantiation phase.
369
+ Registrations such as `useValue`, `useFactory`, and `useAlias` still follow normal `core` rules.
198
370
 
199
- 3. **循环依赖**
371
+ ## Common Pitfalls
200
372
 
201
- ```typescript
202
- // 错误:检测到循环依赖
203
- @injectable()
204
- class ServiceA {
205
- constructor(@inject(ServiceB) public serviceB: ServiceB) {}
206
- }
373
+ ### Forgetting To Register `decoratorMiddleware`
207
374
 
208
- @injectable()
209
- class ServiceB {
210
- constructor(@inject(ServiceA) public serviceA: ServiceA) {}
211
- }
212
- ```
375
+ If the middleware is not registered, the container does not read decorator metadata.
213
376
 
214
- ## 设计原理
377
+ ### Forgetting To Import `reflect-metadata`
215
378
 
216
- 装饰器包使用 TypeScript 的 `reflect-metadata`或其他提供 Reflect API 的库来存储和管理注入元数据:
379
+ Without Reflect metadata at runtime, the implementation cannot access `design:paramtypes`.
217
380
 
218
- - `@injectable()` 收集构造函数参数的注入信息
219
- - `@inject()` 为特定参数位置设置注入配置
220
- - `decoratorMiddleware` 在实例化时读取元数据并执行注入
381
+ ### Using Interfaces Or Primitives Without `@inject()`
221
382
 
222
- ## 最佳实践
383
+ That leaves the metadata incomplete or points inference at the wrong runtime identifier.
223
384
 
224
- 1. **始终使用 @injectable() 标记可注入类**
225
- 2. **为所有依赖参数使用 @inject() 装饰器**
226
- 3. **合理使用注入选项(dynamic、ref、optional)**
227
- 4. **避免循环依赖,必要时使用 ref 选项**
228
- 5. **在应用启动时注册 decoratorMiddleware**
385
+ ### Applying `@injectable()` Twice To The Same Class
229
386
 
230
- ## 注意事项
387
+ This throws `E_DUPLICATE_INJECTABLE`.
231
388
 
232
- - 需要启用 TypeScript 的装饰器支持
389
+ ### Using `dynamic` And `ref` Together
233
390
 
234
- > 建议在 tsconfig.json 中启用 `experimentalDecorators` `emitDecoratorMetadata` 选项
391
+ These options are mutually exclusive and throw `E_CONFLICTING_OPTIONS`.
235
392
 
236
- - 需要引入 `reflect-metadata` 包或其他提供 Reflect API 的库
237
- - `@inject()` 仅支持构造函数注入,不支持属性注入 (可以 `@husky-di/core` 中的 `resolve` 方法来实现属性注入)
238
- - 装饰器中间件需要全局注册,这样才会在每个容器中都生效
393
+ ## Complete Example
394
+
395
+ ```typescript
396
+ import "reflect-metadata";
397
+ import {
398
+ createContainer,
399
+ createServiceIdentifier,
400
+ globalMiddleware,
401
+ type Ref,
402
+ } from "@husky-di/core";
403
+ import {
404
+ decoratorMiddleware,
405
+ inject,
406
+ injectable,
407
+ } from "@husky-di/decorator";
408
+
409
+ interface Config {
410
+ apiBaseUrl: string;
411
+ }
412
+
413
+ interface Logger {
414
+ log(message: string): void;
415
+ }
416
+
417
+ const IConfig = createServiceIdentifier<Config>("IConfig");
418
+ const ILogger = createServiceIdentifier<Logger>("ILogger");
419
+
420
+ @injectable()
421
+ class ConsoleLogger implements Logger {
422
+ log(message: string) {
423
+ console.log(message);
424
+ }
425
+ }
426
+
427
+ @injectable()
428
+ class ApiClient {
429
+ constructor(
430
+ @inject(IConfig) private readonly config: Config,
431
+ @inject(ILogger, { ref: true }) private readonly loggerRef: Ref<Logger>
432
+ ) {}
433
+
434
+ getUser(id: string) {
435
+ this.loggerRef.current.log(`GET ${this.config.apiBaseUrl}/users/${id}`);
436
+ return { id, name: "Ada" };
437
+ }
438
+ }
439
+
440
+ globalMiddleware.use(decoratorMiddleware);
441
+
442
+ const container = createContainer("AppContainer");
443
+ container.register(IConfig, {
444
+ useValue: { apiBaseUrl: "https://api.example.com" },
445
+ });
446
+ container.register(ILogger, { useClass: ConsoleLogger });
447
+
448
+ const apiClient = container.resolve(ApiClient);
449
+ console.log(apiClient.getUser("u-1"));
450
+ ```
451
+
452
+ ## Related Docs
453
+
454
+ - container and resolution model: `../core/README.md`
455
+ - decorator behavior specification: `./docs/SPECIFICATION.md`
456
+ - module system: `../module/README.md`
457
+
458
+ ## Local Development
459
+
460
+ ```bash
461
+ pnpm build
462
+ pnpm test
463
+ ```
@@ -3,7 +3,7 @@
3
3
  * @author AEPKILL
4
4
  * @created 2023-05-24 10:47:34
5
5
  */
6
- import type { ResolveOptions, ServiceIdentifier } from "@husky-di/core";
7
- export type InjectionMetadata<T> = ResolveOptions<T> & {
6
+ import type { ResolveHelperOptions, ServiceIdentifier } from "@husky-di/core";
7
+ export type InjectionMetadata<T> = ResolveHelperOptions<T> & {
8
8
  serviceIdentifier: ServiceIdentifier<T>;
9
9
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@husky-di/decorator",
3
- "version": "1.2.1",
3
+ "version": "1.3.0",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {
@@ -15,7 +15,7 @@
15
15
  "dist"
16
16
  ],
17
17
  "dependencies": {
18
- "@husky-di/core": "1.2.1"
18
+ "@husky-di/core": "1.3.0"
19
19
  },
20
20
  "devDependencies": {
21
21
  "@biomejs/biome": "2.4.15",