@husky-di/decorator 1.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.md ADDED
@@ -0,0 +1,238 @@
1
+ # @husky-di/decorator
2
+
3
+ `@husky-di/decorator` 是 husky-di 的装饰器支持包,提供了基于 TypeScript 装饰器的依赖注入功能。
4
+
5
+ ## 概述
6
+
7
+ 该包专门为偏好使用装饰器语法的开发者设计,提供了简洁直观的依赖注入方式。**仅支持 TypeScript 装饰器**,不支持 ES 装饰器。
8
+
9
+ ### 为什么仅支持 TypeScript 装饰器?
10
+
11
+ husky-di 的设计理念是**仅支持构造函数注入**,而 ES 装饰器的规范中**没有设计参数注入器**。
12
+
13
+ ## 安装
14
+
15
+ ```bash
16
+ pnpm add @husky-di/decorator
17
+ ```
18
+
19
+ ## 核心装饰器
20
+
21
+ ### @injectable()
22
+
23
+ 标记一个类为可注入,使其能够被依赖注入容器管理。
24
+
25
+ > 背后的原因是因为只有应用了装饰器,typescript 编译器才会触发元数据的标记。
26
+
27
+ ```typescript
28
+ import { injectable } from "@husky-di/decorator";
29
+
30
+ @injectable()
31
+ class UserService {
32
+ constructor() {}
33
+ }
34
+ ```
35
+
36
+ ### @inject()
37
+
38
+ 用于构造函数参数注入,支持多种注入选项。
39
+
40
+ ```typescript
41
+ import { inject, injectable } from "@husky-di/decorator";
42
+
43
+ @injectable()
44
+ class LoggerService {
45
+ log(message: string) {
46
+ console.log(message);
47
+ }
48
+ }
49
+
50
+ @injectable()
51
+ class UserService {
52
+ constructor(@inject(LoggerService) private logger: LoggerService) {}
53
+ }
54
+ ```
55
+
56
+ ## 注入选项
57
+
58
+ `@inject()` 装饰器支持多种注入选项:
59
+
60
+ ### 动态注入 (dynamic)
61
+
62
+ 获取服务的动态引用,支持延迟解析:
63
+
64
+ ```typescript
65
+ @injectable()
66
+ class ConfigService {
67
+ getValue(key: string) {
68
+ return `config-${key}`;
69
+ }
70
+ }
71
+
72
+ @injectable()
73
+ class CacheService {
74
+ constructor(
75
+ @inject(ConfigService, { dynamic: true })
76
+ private configRef: Ref<ConfigService>
77
+ ) {}
78
+
79
+ get(key: string) {
80
+ const config = this.configRef.current;
81
+ return `cached-${config.getValue(key)}`;
82
+ }
83
+ }
84
+ ```
85
+
86
+ ### 引用注入 (ref)
87
+
88
+ 获取服务的引用对象:
89
+
90
+ ```typescript
91
+ @injectable()
92
+ class ApiService {
93
+ constructor(
94
+ @inject(CacheService, { ref: true })
95
+ public cacheRef: Ref<CacheService>
96
+ ) {}
97
+ }
98
+ ```
99
+
100
+ ### 可选注入 (optional)
101
+
102
+ 支持可选依赖,当依赖不存在时不会抛出错误:
103
+
104
+ ```typescript
105
+ @injectable()
106
+ class TestService {
107
+ constructor(
108
+ @inject(ExistingService, { optional: true })
109
+ public service: ExistingService
110
+ ) {}
111
+ }
112
+ ```
113
+
114
+ ## 中间件集成
115
+
116
+ 装饰器包提供了 `decoratorMiddleware` 中间件,需要注册到全局中间件中:
117
+
118
+ ```typescript
119
+ import { globalMiddleware } from "@husky-di/core";
120
+ import { decoratorMiddleware } from "@husky-di/decorator";
121
+
122
+ // 注册装饰器中间件
123
+ globalMiddleware.use(decoratorMiddleware);
124
+ ```
125
+
126
+ ## 完整示例
127
+
128
+ ```typescript
129
+ import "reflect-metadata";
130
+ import { createContainer, globalMiddleware } from "@husky-di/core";
131
+ import { decoratorMiddleware, inject, injectable } from "@husky-di/decorator";
132
+
133
+ // 注册装饰器中间件
134
+ globalMiddleware.use(decoratorMiddleware);
135
+
136
+ // 创建容器
137
+ const container = createContainer();
138
+
139
+ // 定义服务
140
+ @injectable()
141
+ class LoggerService {
142
+ log(message: string) {
143
+ return `Logged: ${message}`;
144
+ }
145
+ }
146
+
147
+ @injectable()
148
+ class DatabaseService {
149
+ constructor(@inject(LoggerService) private logger: LoggerService) {}
150
+
151
+ query(sql: string) {
152
+ return this.logger.log(`Executing: ${sql}`);
153
+ }
154
+ }
155
+
156
+ @injectable()
157
+ class UserService {
158
+ constructor(
159
+ @inject(DatabaseService) private db: DatabaseService,
160
+ @inject(LoggerService) private logger: LoggerService
161
+ ) {}
162
+
163
+ getUser(id: string) {
164
+ return this.db.query(`SELECT * FROM users WHERE id = ${id}`);
165
+ }
166
+ }
167
+
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"
172
+ ```
173
+
174
+ ## 错误处理
175
+
176
+ ### 常见错误
177
+
178
+ 1. **重复使用 @injectable()**
179
+
180
+ ```typescript
181
+ // ❌ 错误:不能对同一个类使用两次 @injectable()
182
+ @injectable()
183
+ @injectable()
184
+ class TestService {}
185
+ ```
186
+
187
+ 2. **注入非可注入类**
188
+
189
+ ```typescript
190
+ // ❌ 错误:依赖的类没有使用 @injectable()
191
+ class NonInjectableService {}
192
+
193
+ @injectable()
194
+ class TestService {
195
+ constructor(@inject(NonInjectableService) dep: NonInjectableService) {}
196
+ }
197
+ ```
198
+
199
+ 3. **循环依赖**
200
+
201
+ ```typescript
202
+ // ❌ 错误:检测到循环依赖
203
+ @injectable()
204
+ class ServiceA {
205
+ constructor(@inject(ServiceB) public serviceB: ServiceB) {}
206
+ }
207
+
208
+ @injectable()
209
+ class ServiceB {
210
+ constructor(@inject(ServiceA) public serviceA: ServiceA) {}
211
+ }
212
+ ```
213
+
214
+ ## 设计原理
215
+
216
+ 装饰器包使用 TypeScript 的 `reflect-metadata`或其他提供 Reflect API 的库来存储和管理注入元数据:
217
+
218
+ - `@injectable()` 收集构造函数参数的注入信息
219
+ - `@inject()` 为特定参数位置设置注入配置
220
+ - `decoratorMiddleware` 在实例化时读取元数据并执行注入
221
+
222
+ ## 最佳实践
223
+
224
+ 1. **始终使用 @injectable() 标记可注入类**
225
+ 2. **为所有依赖参数使用 @inject() 装饰器**
226
+ 3. **合理使用注入选项(dynamic、ref、optional)**
227
+ 4. **避免循环依赖,必要时使用 ref 选项**
228
+ 5. **在应用启动时注册 decoratorMiddleware**
229
+
230
+ ## 注意事项
231
+
232
+ - 需要启用 TypeScript 的装饰器支持
233
+
234
+ > 建议在 tsconfig.json 中启用 `experimentalDecorators` 和 `emitDecoratorMetadata` 选项
235
+
236
+ - 需要引入 `reflect-metadata` 包或其他提供 Reflect API 的库
237
+ - `@inject()` 仅支持构造函数注入,不支持属性注入 (可以 `@husky-di/core` 中的 `resolve` 方法来实现属性注入)
238
+ - 装饰器中间件需要全局注册,这样才会在每个容器中都生效
@@ -0,0 +1,8 @@
1
+ /**
2
+ * @overview
3
+ * @author AEPKILL
4
+ * @created 2021-10-03 20:55:17
5
+ */
6
+ export declare const ParamsMetadataKeyConst = "design:paramtypes";
7
+ export declare const InjectionMetadataKeyConst = "husky-di.injection-metadata";
8
+ export declare const InjectionParamsMetadataKeyConst = "husky-di.injection-params-metadata";
@@ -0,0 +1,9 @@
1
+ /**
2
+ * @overview
3
+ * @author AEPKILL
4
+ * @created 2021-10-03 21:08:27
5
+ */
6
+ import type { ServiceIdentifier } from "@husky-di/core";
7
+ import type { InjectionMetadata } from "../types/injection-metadata.type";
8
+ export type InjectOptions<T> = Omit<InjectionMetadata<T>, "serviceIdentifier">;
9
+ export declare const inject: <T>(serviceIdentifier: ServiceIdentifier<T>, options?: InjectOptions<T>) => ParameterDecorator;
@@ -0,0 +1,10 @@
1
+ /**
2
+ * @overview
3
+ * @author AEPKILL
4
+ * @created 2022-03-11 16:02:58
5
+ */
6
+ /**
7
+ * @description
8
+ * markup a class as a injectable class
9
+ */
10
+ export declare const injectable: () => ClassDecorator;
@@ -0,0 +1,7 @@
1
+ /**
2
+ * @overview
3
+ * @author AEPKILL
4
+ * @created 2021-10-03 21:16:18
5
+ */
6
+ import type { InjectionMetadata } from "../types/injection-metadata.type";
7
+ export declare const tagged: <T>(metadata: InjectionMetadata<T>) => ParameterDecorator;
package/dist/index.cjs ADDED
@@ -0,0 +1,99 @@
1
+ "use strict";
2
+ var __webpack_require__ = {};
3
+ (()=>{
4
+ __webpack_require__.d = (exports1, definition)=>{
5
+ for(var key in definition)if (__webpack_require__.o(definition, key) && !__webpack_require__.o(exports1, key)) Object.defineProperty(exports1, key, {
6
+ enumerable: true,
7
+ get: definition[key]
8
+ });
9
+ };
10
+ })();
11
+ (()=>{
12
+ __webpack_require__.o = (obj, prop)=>Object.prototype.hasOwnProperty.call(obj, prop);
13
+ })();
14
+ (()=>{
15
+ __webpack_require__.r = (exports1)=>{
16
+ if ('undefined' != typeof Symbol && Symbol.toStringTag) Object.defineProperty(exports1, Symbol.toStringTag, {
17
+ value: 'Module'
18
+ });
19
+ Object.defineProperty(exports1, '__esModule', {
20
+ value: true
21
+ });
22
+ };
23
+ })();
24
+ var __webpack_exports__ = {};
25
+ __webpack_require__.r(__webpack_exports__);
26
+ __webpack_require__.d(__webpack_exports__, {
27
+ inject: ()=>inject,
28
+ tagged: ()=>tagged,
29
+ decoratorMiddleware: ()=>decoratorMiddleware,
30
+ injectable: ()=>injectable
31
+ });
32
+ const ParamsMetadataKeyConst = "design:paramtypes";
33
+ const InjectionMetadataKeyConst = "husky-di.injection-metadata";
34
+ const tagged = (metadata)=>(target, _propertyKey, parameterIndex)=>{
35
+ const parametersMetadata = Reflect.getMetadata(InjectionMetadataKeyConst, target) || [];
36
+ parametersMetadata[parameterIndex] = metadata;
37
+ Reflect.defineMetadata(InjectionMetadataKeyConst, parametersMetadata, target);
38
+ };
39
+ const inject = (serviceIdentifier, options)=>tagged({
40
+ ...options,
41
+ serviceIdentifier
42
+ });
43
+ const InjectionMetadataMap = "undefined" == typeof WeakMap ? Map : WeakMap;
44
+ const injectionMetadataMap = new InjectionMetadataMap();
45
+ const injectable = ()=>(target)=>{
46
+ if (injectionMetadataMap.has(target)) throw new Error(`can't use "@injectable()" decorate class "${target.name}" twice`);
47
+ const parametersServiceIdentifiers = Reflect.getMetadata(ParamsMetadataKeyConst, target) || [];
48
+ const parametersMetadata = Reflect.getMetadata(InjectionMetadataKeyConst, target) || [];
49
+ const metadata = [];
50
+ const parametersMetadataLength = Math.max(parametersServiceIdentifiers.length, parametersMetadata.length);
51
+ for(let index = 0; index < parametersMetadataLength; index++){
52
+ if (void 0 !== parametersMetadata[index]) {
53
+ metadata.push(parametersMetadata[index]);
54
+ continue;
55
+ }
56
+ const serviceIdentifier = parametersServiceIdentifiers[index];
57
+ if ("function" != typeof serviceIdentifier) throw new Error(`only can inject class type in constructor "${target.name}" parameter #${index}`);
58
+ metadata.push({
59
+ serviceIdentifier
60
+ });
61
+ }
62
+ injectionMetadataMap.set(target, metadata);
63
+ };
64
+ const core_namespaceObject = require("@husky-di/core");
65
+ const decoratorMiddleware = {
66
+ name: Symbol("DecoratorMiddleware"),
67
+ executor: (params, next)=>{
68
+ const { registration, resolveRecord } = params;
69
+ if (registration.type !== core_namespaceObject.RegistrationTypeEnum["class"]) return next(params);
70
+ const provider = registration.provider;
71
+ const parametersMetadata = injectionMetadataMap.get(provider);
72
+ if (!parametersMetadata) throw new core_namespaceObject.ResolveException(`The class ${provider.name} has no injection metadata, please make sure the class is decorated with @Injectable().`, resolveRecord);
73
+ const parameters = parametersMetadata.map((metadata, index)=>{
74
+ const { serviceIdentifier, ...options } = metadata;
75
+ resolveRecord._internalStashCurrent();
76
+ resolveRecord.addRecordNode({
77
+ type: core_namespaceObject.ResolveRecordTypeEnum.message,
78
+ message: `Resolve parameter #${index} of constructor "${provider.name}"`
79
+ });
80
+ const instance = (0, core_namespaceObject.resolve)(serviceIdentifier, options);
81
+ resolveRecord._internalRestoreCurrent();
82
+ return instance;
83
+ });
84
+ return new provider(...parameters);
85
+ }
86
+ };
87
+ exports.decoratorMiddleware = __webpack_exports__.decoratorMiddleware;
88
+ exports.inject = __webpack_exports__.inject;
89
+ exports.injectable = __webpack_exports__.injectable;
90
+ exports.tagged = __webpack_exports__.tagged;
91
+ for(var __webpack_i__ in __webpack_exports__)if (-1 === [
92
+ "decoratorMiddleware",
93
+ "inject",
94
+ "injectable",
95
+ "tagged"
96
+ ].indexOf(__webpack_i__)) exports[__webpack_i__] = __webpack_exports__[__webpack_i__];
97
+ Object.defineProperty(exports, '__esModule', {
98
+ value: true
99
+ });
@@ -0,0 +1,10 @@
1
+ /**
2
+ * @overview
3
+ * @author AEPKILL
4
+ * @created 2025-08-08 21:00:35
5
+ */
6
+ export * from "./decorators/inject.decorator";
7
+ export * from "./decorators/injectable.decorator";
8
+ export * from "./decorators/tagged.decorator";
9
+ export { decoratorMiddleware } from "./middlewares/decorator.middleware";
10
+ export * from "./types/injection-metadata.type";
package/dist/index.js ADDED
@@ -0,0 +1,56 @@
1
+ import { RegistrationTypeEnum, ResolveException, ResolveRecordTypeEnum, resolve } from "@husky-di/core";
2
+ const ParamsMetadataKeyConst = "design:paramtypes";
3
+ const InjectionMetadataKeyConst = "husky-di.injection-metadata";
4
+ const tagged = (metadata)=>(target, _propertyKey, parameterIndex)=>{
5
+ const parametersMetadata = Reflect.getMetadata(InjectionMetadataKeyConst, target) || [];
6
+ parametersMetadata[parameterIndex] = metadata;
7
+ Reflect.defineMetadata(InjectionMetadataKeyConst, parametersMetadata, target);
8
+ };
9
+ const inject = (serviceIdentifier, options)=>tagged({
10
+ ...options,
11
+ serviceIdentifier
12
+ });
13
+ const InjectionMetadataMap = "undefined" == typeof WeakMap ? Map : WeakMap;
14
+ const injectionMetadataMap = new InjectionMetadataMap();
15
+ const injectable = ()=>(target)=>{
16
+ if (injectionMetadataMap.has(target)) throw new Error(`can't use "@injectable()" decorate class "${target.name}" twice`);
17
+ const parametersServiceIdentifiers = Reflect.getMetadata(ParamsMetadataKeyConst, target) || [];
18
+ const parametersMetadata = Reflect.getMetadata(InjectionMetadataKeyConst, target) || [];
19
+ const metadata = [];
20
+ const parametersMetadataLength = Math.max(parametersServiceIdentifiers.length, parametersMetadata.length);
21
+ for(let index = 0; index < parametersMetadataLength; index++){
22
+ if (void 0 !== parametersMetadata[index]) {
23
+ metadata.push(parametersMetadata[index]);
24
+ continue;
25
+ }
26
+ const serviceIdentifier = parametersServiceIdentifiers[index];
27
+ if ("function" != typeof serviceIdentifier) throw new Error(`only can inject class type in constructor "${target.name}" parameter #${index}`);
28
+ metadata.push({
29
+ serviceIdentifier
30
+ });
31
+ }
32
+ injectionMetadataMap.set(target, metadata);
33
+ };
34
+ const decoratorMiddleware = {
35
+ name: Symbol("DecoratorMiddleware"),
36
+ executor: (params, next)=>{
37
+ const { registration, resolveRecord } = params;
38
+ if (registration.type !== RegistrationTypeEnum["class"]) return next(params);
39
+ const provider = registration.provider;
40
+ const parametersMetadata = injectionMetadataMap.get(provider);
41
+ if (!parametersMetadata) throw new ResolveException(`The class ${provider.name} has no injection metadata, please make sure the class is decorated with @Injectable().`, resolveRecord);
42
+ const parameters = parametersMetadata.map((metadata, index)=>{
43
+ const { serviceIdentifier, ...options } = metadata;
44
+ resolveRecord._internalStashCurrent();
45
+ resolveRecord.addRecordNode({
46
+ type: ResolveRecordTypeEnum.message,
47
+ message: `Resolve parameter #${index} of constructor "${provider.name}"`
48
+ });
49
+ const instance = resolve(serviceIdentifier, options);
50
+ resolveRecord._internalRestoreCurrent();
51
+ return instance;
52
+ });
53
+ return new provider(...parameters);
54
+ }
55
+ };
56
+ export { decoratorMiddleware, inject, injectable, tagged };
@@ -0,0 +1,8 @@
1
+ /**
2
+ * @overview
3
+ * @author AEPKILL
4
+ * @created 2025-08-06 22:51:38
5
+ */
6
+ /** biome-ignore-all lint/suspicious/noExplicitAny: need to use any to avoid type errors */
7
+ import { type ResolveMiddleware } from "@husky-di/core";
8
+ export declare const decoratorMiddleware: ResolveMiddleware<any, any>;
@@ -0,0 +1,12 @@
1
+ /**
2
+ * @overview
3
+ * @author AEPKILL
4
+ * @created 2025-08-06 21:47:22
5
+ */
6
+ import type { Constructor } from "@husky-di/core";
7
+ import type { InjectionMetadata } from "../types/injection-metadata.type";
8
+ export declare const injectionMetadataMap: {
9
+ get<T>(target: Constructor<T>): InjectionMetadata<T>[];
10
+ has<T>(target: Constructor<T>): boolean;
11
+ set<T>(target: Constructor<T>, metadata: InjectionMetadata<T>[]): void;
12
+ };
@@ -0,0 +1,9 @@
1
+ /**
2
+ * @overview
3
+ * @author AEPKILL
4
+ * @created 2023-05-24 10:47:34
5
+ */
6
+ import type { ResolveOptions, ServiceIdentifier } from "@husky-di/core";
7
+ export type InjectionMetadata<T> = ResolveOptions<T> & {
8
+ serviceIdentifier: ServiceIdentifier<T>;
9
+ };
package/package.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "@husky-di/decorator",
3
+ "version": "1.0.0",
4
+ "type": "module",
5
+ "exports": {
6
+ ".": {
7
+ "types": "./dist/index.d.ts",
8
+ "import": "./dist/index.js",
9
+ "require": "./dist/index.cjs"
10
+ }
11
+ },
12
+ "main": "./dist/index.cjs",
13
+ "types": "./dist/index.d.ts",
14
+ "files": [
15
+ "dist"
16
+ ],
17
+ "dependencies": {
18
+ "@husky-di/core": "1.0.0"
19
+ },
20
+ "devDependencies": {
21
+ "@biomejs/biome": "2.1.3",
22
+ "@rslib/core": "^0.11.2",
23
+ "@types/node": "^22.17.0",
24
+ "reflect-metadata": "^0.2.2",
25
+ "typescript": "^5.9.2",
26
+ "vitest": "^3.2.4"
27
+ },
28
+ "peerDependencies": {
29
+ "reflect-metadata": "^0.2.2"
30
+ },
31
+ "publishConfig": {
32
+ "access": "public"
33
+ },
34
+ "scripts": {
35
+ "build": "rslib build",
36
+ "check": "biome check --write",
37
+ "dev": "rslib build --watch",
38
+ "format": "biome format --write",
39
+ "test": "vitest run"
40
+ }
41
+ }