@keq-request/nestjs 5.0.0-beta.12 → 5.0.0-beta.13

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.
@@ -1,10 +1,10 @@
1
1
 
2
- > @keq-request/nestjs@5.0.0-beta.12 build /home/runner/work/keq/keq/packages/nestjs
2
+ > @keq-request/nestjs@5.0.0-beta.13 build /home/runner/work/keq/keq/packages/nestjs
3
3
  > tsdown
4
4
 
5
5
  ℹ tsdown v0.21.10 powered by rolldown v1.0.0-rc.17
6
6
  ℹ config file: /home/runner/work/keq/keq/packages/nestjs/tsdown.config.ts
7
- (node:2656) [MODULE_TYPELESS_PACKAGE_JSON] Warning: Module type of file:///home/runner/work/keq/keq/packages/nestjs/tsdown.config.ts?no-cache=9597eb0e-a7b3-4070-b281-a70050e6a072 is not specified and it doesn't parse as CommonJS.
7
+ (node:2744) [MODULE_TYPELESS_PACKAGE_JSON] Warning: Module type of file:///home/runner/work/keq/keq/packages/nestjs/tsdown.config.ts?no-cache=63d674a9-ded8-4584-ad0b-ff9b06a87e93 is not specified and it doesn't parse as CommonJS.
8
8
  Reparsing as ES module because module syntax was detected. This incurs a performance overhead.
9
9
  To eliminate this warning, add "type": "module" to /home/runner/work/keq/keq/packages/nestjs/package.json.
10
10
  (Use `node --trace-warnings ...` to show where the warning was created)
@@ -12,25 +12,25 @@ To eliminate this warning, add "type": "module" to /home/runner/work/keq/keq/pac
12
12
  ℹ target: node20
13
13
  ℹ tsconfig: tsconfig.lib.json
14
14
  ℹ Build start
15
- ℹ [CJS] dist/index.js 6.68 kB │ gzip: 2.21 kB
16
- ℹ [CJS] dist/index.js.map 9.02 kB │ gzip: 2.82 kB
17
- ℹ [CJS] 2 files, total: 15.70 kB
15
+ ℹ [CJS] dist/index.js 10.52 kB │ gzip: 3.95 kB
16
+ ℹ [CJS] dist/index.js.map 15.05 kB │ gzip: 5.17 kB
17
+ ℹ [CJS] 2 files, total: 25.57 kB
18
18
  [PLUGIN_TIMINGS] Warning: Your build spent significant time in plugin `tsdown:deps`. See https://rolldown.rs/options/checks#plugintimings for more details.
19
19
 
20
- ℹ [CJS] dist/index.d.ts.map 1.03 kB │ gzip: 0.44 kB
21
- ℹ [CJS] dist/index.d.ts 1.92 kB │ gzip: 0.67 kB
20
+ ℹ [ESM] dist/index.mjs  9.19 kB │ gzip: 3.46 kB
21
+ ℹ [ESM] dist/index.mjs.map 14.98 kB │ gzip: 5.14 kB
22
+ ℹ [ESM] dist/index.d.mts.map  1.08 kB │ gzip: 0.47 kB
23
+ ℹ [ESM] dist/index.d.mts  2.96 kB │ gzip: 1.38 kB
24
+ ℹ [ESM] 4 files, total: 28.22 kB
25
+ [PLUGIN_TIMINGS] Warning: Your build spent significant time in plugin `rolldown-plugin-dts:generate`. See https://rolldown.rs/options/checks#plugintimings for more details.
26
+
27
+ ✔ Build complete in 5612ms
28
+ ℹ [CJS] dist/index.d.ts.map 1.08 kB │ gzip: 0.47 kB
29
+ ℹ [CJS] dist/index.d.ts 2.96 kB │ gzip: 1.37 kB
30
+ ℹ [CJS] 2 files, total: 4.04 kB
22
31
  [PLUGIN_TIMINGS] Warning: Your build spent significant time in plugins. Here is a breakdown:
23
- - rolldown-plugin-dts:generate (63%)
24
- ℹ [CJS] 2 files, total: 2.96 kB
25
- ✔ Build complete in 5744ms
26
- - tsdown:deps (31%)
32
+ - rolldown-plugin-dts:generate (48%)
33
+ - tsdown:deps (43%)
27
34
  See https://rolldown.rs/options/checks#plugintimings for more details.
28
35
 
29
- ℹ [ESM] dist/index.mjs 5.39 kB │ gzip: 1.77 kB
30
- ℹ [ESM] dist/index.mjs.map 8.95 kB │ gzip: 2.79 kB
31
- ℹ [ESM] dist/index.d.mts.map 1.03 kB │ gzip: 0.45 kB
32
- ℹ [ESM] dist/index.d.mts 1.93 kB │ gzip: 0.67 kB
33
- ℹ [ESM] 4 files, total: 17.30 kB
34
- [PLUGIN_TIMINGS] Warning: Your build spent significant time in plugin `rolldown-plugin-dts:generate`. See https://rolldown.rs/options/checks#plugintimings for more details.
35
-
36
- ✔ Build complete in 5755ms
36
+ ✔ Build complete in 5614ms
package/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # @keq-request/nestjs
2
2
 
3
+ ## 5.0.0-beta.13
4
+
5
+ ### Patch Changes
6
+
7
+ - 59f4773: **Fix:** ensure `configureKeqMiddleware()` works correctly in pnpm monorepos and validate module imports at startup.
8
+
9
+ - Fix middleware not being applied when pnpm creates multiple `@keq-request/nestjs` instances due to peerDependency version differences.
10
+ - Detect missing `KeqModule` imports in modules that implement `configureKeqMiddleware()` at startup, throwing a clear error instead of silently skipping middleware.
11
+ - Prevent middleware from being applied more than once when multiple `KeqModule` instances exist.
12
+ - Throw a descriptive error when a NestJS middleware class passed to `consumer.apply()` is not registered as a provider.
13
+ - keq@5.0.0-beta.13
14
+
3
15
  ## 5.0.0-beta.12
4
16
 
5
17
  ### Minor Changes
@@ -0,0 +1,155 @@
1
+ import 'reflect-metadata'
2
+ import { expect, test } from '@jest/globals'
3
+ import { Module } from '@nestjs/common'
4
+ import type { ModulesContainer } from '@nestjs/core'
5
+ import { KeqModule, validateConfigureKeqMiddlewareImports } from '../src/keq.module.js'
6
+
7
+ /**
8
+ * 模拟 NestJS Module wrapper 的运行时结构。
9
+ * NestJS 在扫描阶段会将 DynamicModule 的 imports 解析为 Set<Module>,
10
+ * 因此使用 imports Set 而非静态 Reflect.getMetadata 来校验。
11
+ */
12
+ function createMockModule(metatype: unknown, importsModules: Array<{ metatype: unknown }> = []) {
13
+ return {
14
+ metatype,
15
+ imports: new Set(importsModules),
16
+ }
17
+ }
18
+
19
+ function mockModulesContainer(
20
+ modules: Array<ReturnType<typeof createMockModule>>,
21
+ ): ModulesContainer {
22
+ const map = new Map()
23
+ for (const mod of modules) {
24
+ map.set(Symbol('module'), mod)
25
+ }
26
+ return map as unknown as ModulesContainer
27
+ }
28
+
29
+
30
+ test('throws when configureKeqMiddleware is declared without KeqModule in imports', () => {
31
+ @Module({ imports: [] })
32
+ class BadModule {
33
+ configureKeqMiddleware(): void {}
34
+ }
35
+
36
+ const container = mockModulesContainer([
37
+ createMockModule(KeqModule),
38
+ createMockModule(BadModule),
39
+ ])
40
+
41
+ expect(() => validateConfigureKeqMiddlewareImports(container)).toThrow(
42
+ /\[KeqModule\] 'BadModule' implements configureKeqMiddleware\(\)/,
43
+ )
44
+ })
45
+
46
+ test('passes when configureKeqMiddleware is declared with KeqModule in imports', () => {
47
+ @Module({ imports: [KeqModule] })
48
+ class GoodModule {
49
+ configureKeqMiddleware(): void {}
50
+ }
51
+
52
+ const keqModuleWrapper = createMockModule(KeqModule)
53
+
54
+ const container = mockModulesContainer([
55
+ keqModuleWrapper,
56
+ createMockModule(GoodModule, [keqModuleWrapper]),
57
+ ])
58
+
59
+ expect(() => validateConfigureKeqMiddlewareImports(container)).not.toThrow()
60
+ })
61
+
62
+ test('passes when module does not implement configureKeqMiddleware', () => {
63
+ @Module({ imports: [] })
64
+ class PlainModule {}
65
+
66
+ const container = mockModulesContainer([
67
+ createMockModule(PlainModule),
68
+ ])
69
+
70
+ expect(() => validateConfigureKeqMiddlewareImports(container)).not.toThrow()
71
+ })
72
+
73
+ test('passes when KeqModule is imported as DynamicModule', () => {
74
+ @Module({ imports: [{ module: KeqModule }] })
75
+ class DynamicImportModule {
76
+ configureKeqMiddleware(): void {}
77
+ }
78
+
79
+ // NestJS 在扫描阶段解析 DynamicModule,将 KeqModule wrapper 加入 imports Set
80
+ const keqModuleWrapper = createMockModule(KeqModule)
81
+
82
+ const container = mockModulesContainer([
83
+ keqModuleWrapper,
84
+ createMockModule(DynamicImportModule, [keqModuleWrapper]),
85
+ ])
86
+
87
+ expect(() => validateConfigureKeqMiddlewareImports(container)).not.toThrow()
88
+ })
89
+
90
+ test('passes when KeqModule is imported via forwardRef', () => {
91
+ const { forwardRef } = require('@nestjs/common')
92
+ @Module({ imports: [forwardRef(() => KeqModule)] })
93
+ class ForwardRefModule {
94
+ configureKeqMiddleware(): void {}
95
+ }
96
+
97
+ // NestJS 在扫描阶段解析 forwardRef,将 KeqModule wrapper 加入 imports Set
98
+ const keqModuleWrapper = createMockModule(KeqModule)
99
+
100
+ const container = mockModulesContainer([
101
+ keqModuleWrapper,
102
+ createMockModule(ForwardRefModule, [keqModuleWrapper]),
103
+ ])
104
+
105
+ expect(() => validateConfigureKeqMiddlewareImports(container)).not.toThrow()
106
+ })
107
+
108
+ test('passes when metatype is null', () => {
109
+ const container = mockModulesContainer([
110
+ createMockModule(null as unknown as undefined),
111
+ ])
112
+
113
+ expect(() => validateConfigureKeqMiddlewareImports(container)).not.toThrow()
114
+ })
115
+
116
+ test('throws when module has configureKeqMiddleware but KeqModule is not in container', () => {
117
+ // KeqModule 未在 ModulesContainer 中注册时,任何实现了 configureKeqMiddleware
118
+ // 的模块都无法通过校验,因为缺少 KeqModule 来注册中间件
119
+ @Module({ imports: [] })
120
+ class SomeModule {
121
+ configureKeqMiddleware(): void {}
122
+ }
123
+
124
+ const container = mockModulesContainer([
125
+ createMockModule(SomeModule),
126
+ ])
127
+
128
+ expect(() => validateConfigureKeqMiddlewareImports(container)).toThrow(
129
+ /\[KeqModule\] 'SomeModule'/,
130
+ )
131
+ })
132
+
133
+ test('throws for one module while another is valid', () => {
134
+ @Module({ imports: [] })
135
+ class BadModule {
136
+ configureKeqMiddleware(): void {}
137
+ }
138
+
139
+ @Module({ imports: [KeqModule] })
140
+ class GoodModule {
141
+ configureKeqMiddleware(): void {}
142
+ }
143
+
144
+ const keqModuleWrapper = createMockModule(KeqModule)
145
+
146
+ const container = mockModulesContainer([
147
+ keqModuleWrapper,
148
+ createMockModule(GoodModule, [keqModuleWrapper]),
149
+ createMockModule(BadModule),
150
+ ])
151
+
152
+ expect(() => validateConfigureKeqMiddlewareImports(container)).toThrow(
153
+ /'BadModule'/,
154
+ )
155
+ })
package/dist/index.d.mts CHANGED
@@ -1,13 +1,29 @@
1
1
  import { OnModuleInit, Type } from "@nestjs/common";
2
- import { ModuleRef, ModulesContainer } from "@nestjs/core";
2
+ import { ModulesContainer } from "@nestjs/core";
3
3
  import { KeqExecutionContext, KeqMiddleware, KeqNext, KeqRequest } from "keq";
4
4
 
5
5
  //#region src/keq.module.d.ts
6
+ /**
7
+ * 在 NestJS 模块初始化早期阶段校验 `configureKeqMiddleware` 的实现模块
8
+ * 是否显式导入了 `KeqModule`。
9
+ *
10
+ * NestJS 初始化分为两个严格串行的阶段:
11
+ * 1. 全量扫描 — 所有模块 wrapper 加入 ModulesContainer,metatype 已设置,
12
+ * DynamicModule 的 imports 已被解析并写入 moduleWrapper.imports Set
13
+ * 2. 实例创建 — 按依赖顺序调用构造函数 → onModuleInit
14
+ *
15
+ * 此校验在阶段 2 开头(构造函数中)执行,早于所有 onModuleInit,
16
+ * 确保中间件缺失问题在启动期就被捕获,而非在业务模块 onModuleInit 发请求时静默失效。
17
+ *
18
+ * 使用运行时 imports Set 而非 Reflect.getMetadata('imports', metatype) 的原因:
19
+ * - DynamicModule 通过 register() 返回的 imports 不在静态装饰器元数据中
20
+ * - 从 ModulesContainer 获取 KeqModule metatype 可避免 pnpm 重复包实例导致的 === 比对失败
21
+ */
22
+ declare function validateConfigureKeqMiddlewareImports(modulesContainer: ModulesContainer): void;
6
23
  declare class KeqModule implements OnModuleInit {
7
24
  private readonly modulesContainer;
8
- private readonly moduleRef;
9
25
  private readonly keqRequest;
10
- constructor(modulesContainer: ModulesContainer, moduleRef: ModuleRef, keqRequest: KeqRequest);
26
+ constructor(modulesContainer: ModulesContainer, keqRequest: KeqRequest);
11
27
  onModuleInit(): void;
12
28
  }
13
29
  //#endregion
@@ -27,7 +43,7 @@ interface KeqRouteInfo {
27
43
  method?: string;
28
44
  pathname?: string;
29
45
  }
30
- interface KeqModuleClass {
46
+ interface KeqModuleClass extends Type<any> {
31
47
  readonly KEQ_REQUEST: symbol;
32
48
  }
33
49
  interface KeqMiddlewareConfigProxy {
@@ -49,5 +65,5 @@ interface KeqModuleOptions {
49
65
  isolate?: boolean;
50
66
  }
51
67
  //#endregion
52
- export { KEQ_ROUTES, type KeqMiddlewareConfigProxy, type KeqMiddlewareConsumer, type KeqMiddlewareModule, KeqModule, type KeqModuleClass, type KeqModuleOptions, type KeqNestMiddleware, type KeqRouteInfo };
68
+ export { KEQ_ROUTES, type KeqMiddlewareConfigProxy, type KeqMiddlewareConsumer, type KeqMiddlewareModule, KeqModule, type KeqModuleClass, type KeqModuleOptions, type KeqNestMiddleware, type KeqRouteInfo, validateConfigureKeqMiddlewareImports };
53
69
  //# sourceMappingURL=index.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.mts","names":[],"sources":["../src/keq.module.ts","../src/constants.ts","../src/interfaces/keq-nest-middleware.interface.ts","../src/interfaces/keq-middleware-consumer.interface.ts","../src/interfaces/keq-middleware-module.interface.ts","../src/interfaces/keq-module-options.interface.ts"],"mappings":";;;;;cAYa,SAAA,YAAqB,YAAA;EAAA,iBAEb,gBAAA;EAAA,iBACA,SAAA;EAAA,iBACA,UAAA;cAFA,gBAAA,EAAkB,gBAAA,EAClB,SAAA,EAAW,SAAA,EACX,UAAA,EAAY,UAAA;EAG/B,YAAA,CAAA;AAAA;;;cCnBW,UAAA;EAAA,SAEH,GAAA;AAAA;;;UCCO,iBAAA;EACf,GAAA,CAAI,GAAA,EAAK,mBAAA,EAAqB,IAAA,EAAM,OAAA,UAAiB,WAAA;AAAA;;;UCEtC,YAAA;EACf,IAAA;EACA,MAAA;EACA,QAAA;AAAA;AAAA,UAGe,cAAA;EAAA,SACN,WAAA;AAAA;AAAA,UAGM,wBAAA;EACf,OAAA,IAAW,MAAA,EAAQ,YAAA,KAAiB,wBAAA;EACpC,SAAA,IAAa,MAAA,EAAQ,KAAA,QAAa,UAAA,CAAW,GAAA,GAAM,YAAA,GAAe,cAAA,IAAkB,qBAAA;AAAA;AAAA,UAGrE,qBAAA;EACf,KAAA,IAAS,WAAA,EAAa,KAAA,CAAM,IAAA,CAAK,iBAAA,IAAqB,aAAA,IAAiB,wBAAA;AAAA;;;UCnBxD,mBAAA;EACf,sBAAA,CAAuB,QAAA,EAAU,qBAAA;AAAA;;;UCDlB,gBAAA;EACf,WAAA,GAAc,aAAA;EACd,OAAA;AAAA"}
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../src/keq.module.ts","../src/constants.ts","../src/interfaces/keq-nest-middleware.interface.ts","../src/interfaces/keq-middleware-consumer.interface.ts","../src/interfaces/keq-middleware-module.interface.ts","../src/interfaces/keq-module-options.interface.ts"],"mappings":";;;;;;;AAoCA;;;;;AAkCA;;;;;;;;;iBAlCgB,qCAAA,CAAsC,gBAAA,EAAkB,gBAAA;AAAA,cAuC3D,SAAA,YAAqB,YAAA;EAAA,iBAEb,gBAAA;EAAA,iBACA,UAAA;cADA,gBAAA,EAAkB,gBAAA,EAClB,UAAA,EAAY,UAAA;EAK/B,YAAA,CAAA;AAAA;;;cCnFW,UAAA;EAAA,SAEH,GAAA;AAAA;;;UCCO,iBAAA;EACf,GAAA,CAAI,GAAA,EAAK,mBAAA,EAAqB,IAAA,EAAM,OAAA,UAAiB,WAAA;AAAA;;;UCEtC,YAAA;EACf,IAAA;EACA,MAAA;EACA,QAAA;AAAA;AAAA,UAGe,cAAA,SAAuB,IAAA;EAAA,SAC7B,WAAA;AAAA;AAAA,UAGM,wBAAA;EACf,OAAA,IAAW,MAAA,EAAQ,YAAA,KAAiB,wBAAA;EACpC,SAAA,IAAa,MAAA,EAAQ,KAAA,QAAa,UAAA,CAAW,GAAA,GAAM,YAAA,GAAe,cAAA,IAAkB,qBAAA;AAAA;AAAA,UAGrE,qBAAA;EACf,KAAA,IAAS,WAAA,EAAa,KAAA,CAAM,IAAA,CAAK,iBAAA,IAAqB,aAAA,IAAiB,wBAAA;AAAA;;;UCnBxD,mBAAA;EACf,sBAAA,CAAuB,QAAA,EAAU,qBAAA;AAAA;;;UCDlB,gBAAA;EACf,WAAA,GAAc,aAAA;EACd,OAAA;AAAA"}
package/dist/index.d.ts CHANGED
@@ -1,13 +1,29 @@
1
1
  import { OnModuleInit, Type } from "@nestjs/common";
2
- import { ModuleRef, ModulesContainer } from "@nestjs/core";
2
+ import { ModulesContainer } from "@nestjs/core";
3
3
  import { KeqExecutionContext, KeqMiddleware, KeqNext, KeqRequest } from "keq";
4
4
 
5
5
  //#region src/keq.module.d.ts
6
+ /**
7
+ * 在 NestJS 模块初始化早期阶段校验 `configureKeqMiddleware` 的实现模块
8
+ * 是否显式导入了 `KeqModule`。
9
+ *
10
+ * NestJS 初始化分为两个严格串行的阶段:
11
+ * 1. 全量扫描 — 所有模块 wrapper 加入 ModulesContainer,metatype 已设置,
12
+ * DynamicModule 的 imports 已被解析并写入 moduleWrapper.imports Set
13
+ * 2. 实例创建 — 按依赖顺序调用构造函数 → onModuleInit
14
+ *
15
+ * 此校验在阶段 2 开头(构造函数中)执行,早于所有 onModuleInit,
16
+ * 确保中间件缺失问题在启动期就被捕获,而非在业务模块 onModuleInit 发请求时静默失效。
17
+ *
18
+ * 使用运行时 imports Set 而非 Reflect.getMetadata('imports', metatype) 的原因:
19
+ * - DynamicModule 通过 register() 返回的 imports 不在静态装饰器元数据中
20
+ * - 从 ModulesContainer 获取 KeqModule metatype 可避免 pnpm 重复包实例导致的 === 比对失败
21
+ */
22
+ declare function validateConfigureKeqMiddlewareImports(modulesContainer: ModulesContainer): void;
6
23
  declare class KeqModule implements OnModuleInit {
7
24
  private readonly modulesContainer;
8
- private readonly moduleRef;
9
25
  private readonly keqRequest;
10
- constructor(modulesContainer: ModulesContainer, moduleRef: ModuleRef, keqRequest: KeqRequest);
26
+ constructor(modulesContainer: ModulesContainer, keqRequest: KeqRequest);
11
27
  onModuleInit(): void;
12
28
  }
13
29
  //#endregion
@@ -27,7 +43,7 @@ interface KeqRouteInfo {
27
43
  method?: string;
28
44
  pathname?: string;
29
45
  }
30
- interface KeqModuleClass {
46
+ interface KeqModuleClass extends Type<any> {
31
47
  readonly KEQ_REQUEST: symbol;
32
48
  }
33
49
  interface KeqMiddlewareConfigProxy {
@@ -49,5 +65,5 @@ interface KeqModuleOptions {
49
65
  isolate?: boolean;
50
66
  }
51
67
  //#endregion
52
- export { KEQ_ROUTES, type KeqMiddlewareConfigProxy, type KeqMiddlewareConsumer, type KeqMiddlewareModule, KeqModule, type KeqModuleClass, type KeqModuleOptions, type KeqNestMiddleware, type KeqRouteInfo };
68
+ export { KEQ_ROUTES, type KeqMiddlewareConfigProxy, type KeqMiddlewareConsumer, type KeqMiddlewareModule, KeqModule, type KeqModuleClass, type KeqModuleOptions, type KeqNestMiddleware, type KeqRouteInfo, validateConfigureKeqMiddlewareImports };
53
69
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","names":[],"sources":["../src/keq.module.ts","../src/constants.ts","../src/interfaces/keq-nest-middleware.interface.ts","../src/interfaces/keq-middleware-consumer.interface.ts","../src/interfaces/keq-middleware-module.interface.ts","../src/interfaces/keq-module-options.interface.ts"],"mappings":";;;;;cAYa,SAAA,YAAqB,YAAA;EAAA,iBAEb,gBAAA;EAAA,iBACA,SAAA;EAAA,iBACA,UAAA;cAFA,gBAAA,EAAkB,gBAAA,EAClB,SAAA,EAAW,SAAA,EACX,UAAA,EAAY,UAAA;EAG/B,YAAA,CAAA;AAAA;;;cCnBW,UAAA;EAAA,SAEH,GAAA;AAAA;;;UCCO,iBAAA;EACf,GAAA,CAAI,GAAA,EAAK,mBAAA,EAAqB,IAAA,EAAM,OAAA,UAAiB,WAAA;AAAA;;;UCEtC,YAAA;EACf,IAAA;EACA,MAAA;EACA,QAAA;AAAA;AAAA,UAGe,cAAA;EAAA,SACN,WAAA;AAAA;AAAA,UAGM,wBAAA;EACf,OAAA,IAAW,MAAA,EAAQ,YAAA,KAAiB,wBAAA;EACpC,SAAA,IAAa,MAAA,EAAQ,KAAA,QAAa,UAAA,CAAW,GAAA,GAAM,YAAA,GAAe,cAAA,IAAkB,qBAAA;AAAA;AAAA,UAGrE,qBAAA;EACf,KAAA,IAAS,WAAA,EAAa,KAAA,CAAM,IAAA,CAAK,iBAAA,IAAqB,aAAA,IAAiB,wBAAA;AAAA;;;UCnBxD,mBAAA;EACf,sBAAA,CAAuB,QAAA,EAAU,qBAAA;AAAA;;;UCDlB,gBAAA;EACf,WAAA,GAAc,aAAA;EACd,OAAA;AAAA"}
1
+ {"version":3,"file":"index.d.ts","names":[],"sources":["../src/keq.module.ts","../src/constants.ts","../src/interfaces/keq-nest-middleware.interface.ts","../src/interfaces/keq-middleware-consumer.interface.ts","../src/interfaces/keq-middleware-module.interface.ts","../src/interfaces/keq-module-options.interface.ts"],"mappings":";;;;;;;AAoCA;;;;;AAkCA;;;;;;;;;iBAlCgB,qCAAA,CAAsC,gBAAA,EAAkB,gBAAA;AAAA,cAuC3D,SAAA,YAAqB,YAAA;EAAA,iBAEb,gBAAA;EAAA,iBACA,UAAA;cADA,gBAAA,EAAkB,gBAAA,EAClB,UAAA,EAAY,UAAA;EAK/B,YAAA,CAAA;AAAA;;;cCnFW,UAAA;EAAA,SAEH,GAAA;AAAA;;;UCCO,iBAAA;EACf,GAAA,CAAI,GAAA,EAAK,mBAAA,EAAqB,IAAA,EAAM,OAAA,UAAiB,WAAA;AAAA;;;UCEtC,YAAA;EACf,IAAA;EACA,MAAA;EACA,QAAA;AAAA;AAAA,UAGe,cAAA,SAAuB,IAAA;EAAA,SAC7B,WAAA;AAAA;AAAA,UAGM,wBAAA;EACf,OAAA,IAAW,MAAA,EAAQ,YAAA,KAAiB,wBAAA;EACpC,SAAA,IAAa,MAAA,EAAQ,KAAA,QAAa,UAAA,CAAW,GAAA,GAAM,YAAA,GAAe,cAAA,IAAkB,qBAAA;AAAA;AAAA,UAGrE,qBAAA;EACf,KAAA,IAAS,WAAA,EAAa,KAAA,CAAM,IAAA,CAAK,iBAAA,IAAqB,aAAA,IAAiB,wBAAA;AAAA;;;UCnBxD,mBAAA;EACf,sBAAA,CAAuB,QAAA,EAAU,qBAAA;AAAA;;;UCDlB,gBAAA;EACf,WAAA,GAAc,aAAA;EACd,OAAA;AAAA"}
package/dist/index.js CHANGED
@@ -52,9 +52,9 @@ var KeqMiddlewareConsumerImpl = class {
52
52
  };
53
53
  return proxy;
54
54
  }
55
- applyTo(keqRequest, moduleRef) {
55
+ applyTo(keqRequest, modulesContainer) {
56
56
  for (const entry of this.entries) {
57
- let resolved = entry.middlewares.map((m) => this.resolveMiddleware(m, moduleRef));
57
+ let resolved = entry.middlewares.map((m) => this.resolveMiddleware(m, modulesContainer));
58
58
  if (entry.excludedRouteInfos && entry.excludedRouteInfos.length > 0) {
59
59
  const excludedRoutes = entry.excludedRouteInfos.map((info) => this.buildRoute(info));
60
60
  resolved = resolved.map((mw) => this.wrapWithExclusion(mw, excludedRoutes));
@@ -63,7 +63,7 @@ var KeqMiddlewareConsumerImpl = class {
63
63
  else for (const routeTarget of entry.routes) {
64
64
  if (routeTarget === KEQ_ROUTES.ALL) continue;
65
65
  if (this.isKeqModuleClass(routeTarget)) {
66
- const targetRequest = moduleRef.get(routeTarget.KEQ_REQUEST, { strict: false });
66
+ const targetRequest = this.findInModulesContainer(routeTarget, modulesContainer);
67
67
  if (targetRequest) for (const mw of resolved) targetRequest.use(mw);
68
68
  } else {
69
69
  const route = this.buildRoute(routeTarget);
@@ -78,15 +78,41 @@ var KeqMiddlewareConsumerImpl = class {
78
78
  isNestMiddlewareClass(m) {
79
79
  return typeof m === "function" && m.prototype !== void 0 && "use" in m.prototype;
80
80
  }
81
- resolveMiddleware(m, moduleRef) {
81
+ findMiddlewareInstance(middlewareClass, modulesContainer) {
82
+ for (const [, moduleWrapper] of modulesContainer) {
83
+ const provider = moduleWrapper.providers.get(middlewareClass);
84
+ if (provider?.instance) return provider.instance;
85
+ }
86
+ }
87
+ resolveMiddleware(m, modulesContainer) {
82
88
  if (this.isNestMiddlewareClass(m)) {
83
- const instance = moduleRef.get(m, { strict: false });
89
+ const instance = this.findMiddlewareInstance(m, modulesContainer);
90
+ if (!instance) throw new Error(`[KeqModule] Nest middleware class '${m.name}' is not registered as a provider in any module. Ensure the middleware class is added to the providers array of a @Module() decorator.`);
84
91
  return async (ctx, next) => {
85
92
  await instance.use(ctx, next);
86
93
  };
87
94
  }
88
95
  return m;
89
96
  }
97
+ /**
98
+ * 从 ModulesContainer 中查找目标 KeqModule 的 KeqRequest 实例
99
+ *
100
+ * 通过 metatype 精确匹配模块,从模块 providers 中直接获取 KeqRequest。
101
+ * 不依赖 ModuleRef 的作用域机制,直接从 NestJS 全局模块容器中查找。
102
+ *
103
+ * @param routeTarget - 目标 KeqModule 类,必须具有 KEQ_REQUEST symbol 属性
104
+ * @param modulesContainer - NestJS 全局模块容器
105
+ * @returns KeqRequest 实例,未找到时返回 undefined
106
+ */
107
+ findInModulesContainer(routeTarget, modulesContainer) {
108
+ const token = routeTarget.KEQ_REQUEST;
109
+ for (const [, moduleWrapper] of modulesContainer) {
110
+ if (moduleWrapper.metatype !== routeTarget) continue;
111
+ const provider = moduleWrapper.providers.get(token);
112
+ if (!provider) continue;
113
+ if (provider.instance) return provider.instance;
114
+ }
115
+ }
90
116
  buildRoute(info) {
91
117
  const routes = [];
92
118
  if (info.host) {
@@ -136,21 +162,63 @@ function __decorate(decorators, target, key, desc) {
136
162
  }
137
163
  //#endregion
138
164
  //#region src/keq.module.ts
139
- var _ref, _ref2, _ref3;
165
+ var _ref, _ref2;
166
+ /**
167
+ * 全局标记,用于跨 KeqModule 实例去重
168
+ *
169
+ * 当 pnpm 等包管理器因为 peerDependencies 版本差异生成多个
170
+ * `@keq-request/nestjs` 的 `.pnpm` 目录时,Node.js 会将它们视为不同模块,
171
+ * 产生多个 `KeqModule` 类引用,导致 NestJS 创建多个 `KeqModule` 实例。
172
+ *
173
+ * 每个实例的 `onModuleInit()` 都会遍历 `ModulesContainer` 并调用所有
174
+ * 实现了 `configureKeqMiddleware` 的模块。通过 `Symbol.for()`(全局 Symbol
175
+ * 注册表)在 `moduleWrapper.metatype` 上做标记,确保同一个模块类
176
+ * 只被处理一次,无论有多少个 `KeqModule` 实例。
177
+ */
178
+ const CONFIGURED_SYMBOL = Symbol.for("__keq_middleware_configured__");
179
+ /**
180
+ * 在 NestJS 模块初始化早期阶段校验 `configureKeqMiddleware` 的实现模块
181
+ * 是否显式导入了 `KeqModule`。
182
+ *
183
+ * NestJS 初始化分为两个严格串行的阶段:
184
+ * 1. 全量扫描 — 所有模块 wrapper 加入 ModulesContainer,metatype 已设置,
185
+ * DynamicModule 的 imports 已被解析并写入 moduleWrapper.imports Set
186
+ * 2. 实例创建 — 按依赖顺序调用构造函数 → onModuleInit
187
+ *
188
+ * 此校验在阶段 2 开头(构造函数中)执行,早于所有 onModuleInit,
189
+ * 确保中间件缺失问题在启动期就被捕获,而非在业务模块 onModuleInit 发请求时静默失效。
190
+ *
191
+ * 使用运行时 imports Set 而非 Reflect.getMetadata('imports', metatype) 的原因:
192
+ * - DynamicModule 通过 register() 返回的 imports 不在静态装饰器元数据中
193
+ * - 从 ModulesContainer 获取 KeqModule metatype 可避免 pnpm 重复包实例导致的 === 比对失败
194
+ */
195
+ function validateConfigureKeqMiddlewareImports(modulesContainer) {
196
+ let keqModuleMetatype;
197
+ for (const [, moduleWrapper] of modulesContainer) if (moduleWrapper.metatype?.name === "KeqModule") {
198
+ keqModuleMetatype = moduleWrapper.metatype;
199
+ break;
200
+ }
201
+ for (const [, moduleWrapper] of modulesContainer) {
202
+ const metatype = moduleWrapper.metatype;
203
+ if (typeof metatype?.prototype?.configureKeqMiddleware !== "function") continue;
204
+ if (![...moduleWrapper.imports].some((importedModule) => importedModule.metatype === keqModuleMetatype)) throw new Error(`[KeqModule] '${metatype.name}' implements configureKeqMiddleware() but does not explicitly import KeqModule. Add 'imports: [KeqModule]' to its @Module() decorator to ensure middleware is registered before the module's onModuleInit() runs.`);
205
+ }
206
+ }
140
207
  let KeqModule = class KeqModule {
141
- constructor(modulesContainer, moduleRef, keqRequest) {
208
+ constructor(modulesContainer, keqRequest) {
142
209
  this.modulesContainer = modulesContainer;
143
- this.moduleRef = moduleRef;
144
210
  this.keqRequest = keqRequest;
211
+ validateConfigureKeqMiddlewareImports(modulesContainer);
145
212
  }
146
213
  onModuleInit() {
147
214
  for (const [, moduleWrapper] of this.modulesContainer) {
148
215
  const instance = moduleWrapper.instance;
149
- if (hasConfigureKeqMiddleware(instance)) {
150
- const consumer = new KeqMiddlewareConsumerImpl();
151
- instance.configureKeqMiddleware(consumer);
152
- consumer.applyTo(this.keqRequest, this.moduleRef);
153
- }
216
+ if (!hasConfigureKeqMiddleware(instance)) continue;
217
+ if (moduleWrapper.metatype[CONFIGURED_SYMBOL]) continue;
218
+ moduleWrapper.metatype[CONFIGURED_SYMBOL] = true;
219
+ const consumer = new KeqMiddlewareConsumerImpl();
220
+ instance.configureKeqMiddleware(consumer);
221
+ consumer.applyTo(this.keqRequest, this.modulesContainer);
154
222
  }
155
223
  }
156
224
  };
@@ -163,11 +231,7 @@ KeqModule = __decorate([
163
231
  }],
164
232
  exports: [keq.KeqRequest]
165
233
  }),
166
- __decorateMetadata("design:paramtypes", [
167
- typeof (_ref = typeof _nestjs_core.ModulesContainer !== "undefined" && _nestjs_core.ModulesContainer) === "function" ? _ref : Object,
168
- typeof (_ref2 = typeof _nestjs_core.ModuleRef !== "undefined" && _nestjs_core.ModuleRef) === "function" ? _ref2 : Object,
169
- typeof (_ref3 = typeof keq.KeqRequest !== "undefined" && keq.KeqRequest) === "function" ? _ref3 : Object
170
- ])
234
+ __decorateMetadata("design:paramtypes", [typeof (_ref = typeof _nestjs_core.ModulesContainer !== "undefined" && _nestjs_core.ModulesContainer) === "function" ? _ref : Object, typeof (_ref2 = typeof keq.KeqRequest !== "undefined" && keq.KeqRequest) === "function" ? _ref2 : Object])
171
235
  ], KeqModule);
172
236
  //#endregion
173
237
  exports.KEQ_ROUTES = KEQ_ROUTES;
@@ -177,5 +241,6 @@ Object.defineProperty(exports, "KeqModule", {
177
241
  return KeqModule;
178
242
  }
179
243
  });
244
+ exports.validateConfigureKeqMiddlewareImports = validateConfigureKeqMiddlewareImports;
180
245
 
181
246
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":["KeqRequest"],"sources":["../src/constants.ts","../src/keq-middleware-consumer.ts","../src/utils/has-configure-keq-middleware.ts","../src/keq.module.ts"],"sourcesContent":["export const KEQ_ROUTES = {\n ALL: Symbol('KEQ_ROUTES.ALL'),\n} as const\n","import type { Type } from '@nestjs/common'\nimport type { ModuleRef } from '@nestjs/core'\nimport type { KeqExecutionContext, KeqMiddleware, KeqRoute } from 'keq'\nimport { composeRoute, KeqRequest } from 'keq'\nimport picomatch from 'picomatch'\nimport { KEQ_ROUTES } from './constants.js'\nimport type { KeqMiddlewareConfigProxy, KeqMiddlewareConsumer, KeqModuleClass, KeqRouteInfo } from './interfaces/keq-middleware-consumer.interface.js'\nimport type { KeqNestMiddleware } from './interfaces/keq-nest-middleware.interface.js'\n\n\ninterface MiddlewareEntry {\n middlewares: Array<Type<KeqNestMiddleware> | KeqMiddleware>\n routes: Array<typeof KEQ_ROUTES.ALL | KeqRouteInfo | KeqModuleClass>\n excludedRouteInfos?: KeqRouteInfo[]\n}\n\nexport class KeqMiddlewareConsumerImpl implements KeqMiddlewareConsumer {\n private entries: MiddlewareEntry[] = []\n\n apply(...middlewares: Array<Type<KeqNestMiddleware> | KeqMiddleware>): KeqMiddlewareConfigProxy {\n const entry: MiddlewareEntry = { middlewares, routes: [] }\n const excludedRouteInfos: KeqRouteInfo[] = []\n this.entries.push(entry)\n\n const proxy: KeqMiddlewareConfigProxy = {\n exclude: (...routes) => {\n excludedRouteInfos.push(...routes)\n return proxy\n },\n forRoutes: (...routes) => {\n entry.routes = routes\n if (excludedRouteInfos.length > 0) {\n entry.excludedRouteInfos = excludedRouteInfos\n }\n return this\n },\n }\n return proxy\n }\n\n applyTo(keqRequest: KeqRequest, moduleRef: ModuleRef): void {\n for (const entry of this.entries) {\n let resolved = entry.middlewares.map((m) => this.resolveMiddleware(m, moduleRef))\n\n if (entry.excludedRouteInfos && entry.excludedRouteInfos.length > 0) {\n const excludedRoutes = entry.excludedRouteInfos.map((info) => this.buildRoute(info))\n resolved = resolved.map((mw) => this.wrapWithExclusion(mw, excludedRoutes))\n }\n\n const isGlobal = entry.routes.length === 0\n || entry.routes.some((r) => r === KEQ_ROUTES.ALL)\n\n if (isGlobal) {\n for (const mw of resolved) keqRequest.use(mw)\n } else {\n for (const routeTarget of entry.routes) {\n if (routeTarget === KEQ_ROUTES.ALL) continue\n\n if (this.isKeqModuleClass(routeTarget)) {\n const targetRequest = moduleRef.get<KeqRequest>(routeTarget.KEQ_REQUEST, { strict: false })\n if (targetRequest) {\n for (const mw of resolved) targetRequest.use(mw)\n }\n } else {\n const route = this.buildRoute(routeTarget as KeqRouteInfo)\n keqRequest.useRouter().route(route, resolved)\n }\n }\n }\n }\n }\n\n private isKeqModuleClass(route: unknown): route is KeqModuleClass {\n return (\n typeof route === 'function'\n && 'KEQ_REQUEST' in route\n && typeof (route as Record<string, unknown>).KEQ_REQUEST === 'symbol'\n )\n }\n\n private isNestMiddlewareClass(m: Type<KeqNestMiddleware> | KeqMiddleware): m is Type<KeqNestMiddleware> {\n return typeof m === 'function' && m.prototype !== undefined && 'use' in m.prototype\n }\n\n private resolveMiddleware(m: Type<KeqNestMiddleware> | KeqMiddleware, moduleRef: ModuleRef): KeqMiddleware {\n if (this.isNestMiddlewareClass(m)) {\n const instance = moduleRef.get<KeqNestMiddleware>(m, { strict: false })\n return async (ctx, next) => {\n await instance.use(ctx, next)\n }\n }\n return m\n }\n\n private buildRoute(info: KeqRouteInfo): KeqRoute {\n const routes: KeqRoute[] = []\n if (info.host) {\n const host = info.host\n routes.push((ctx) => ctx.request.url.host === host)\n }\n if (info.method) {\n const method = info.method.toLowerCase()\n routes.push((ctx) => ctx.request.method.toLowerCase() === method)\n }\n if (info.pathname) {\n const isMatch: (test: string) => boolean = picomatch(info.pathname)\n routes.push((ctx) => isMatch(ctx.request.url.pathname))\n }\n if (routes.length === 1) return routes[0]\n return composeRoute(routes)\n }\n\n private wrapWithExclusion(middleware: KeqMiddleware, excludedRoutes: KeqRoute[]): KeqMiddleware {\n const excluded: KeqMiddleware = async (ctx, next) => {\n if (await this.checkExcluded(ctx, excludedRoutes)) {\n return next()\n }\n return middleware(ctx, next)\n }\n return excluded\n }\n\n private async checkExcluded(ctx: KeqExecutionContext, excludedRoutes: KeqRoute[]): Promise<boolean> {\n for (const route of excludedRoutes) {\n if (await route(ctx)) return true\n }\n return false\n }\n}\n","import type { KeqMiddlewareModule } from '../interfaces/keq-middleware-module.interface.js'\n\n\nexport function hasConfigureKeqMiddleware(instance: unknown): instance is KeqMiddlewareModule {\n return (\n instance !== null\n && instance !== undefined\n && typeof (instance as Record<string, unknown>).configureKeqMiddleware === 'function'\n )\n}\n","import { Global, Module, OnModuleInit } from '@nestjs/common'\nimport { ModulesContainer, ModuleRef } from '@nestjs/core'\nimport { KeqRequest } from 'keq'\nimport { KeqMiddlewareConsumerImpl } from './keq-middleware-consumer.js'\nimport { hasConfigureKeqMiddleware } from './utils/has-configure-keq-middleware.js'\n\n\n@Global()\n@Module({\n providers: [{ provide: KeqRequest, useFactory: () => new KeqRequest() }],\n exports: [KeqRequest],\n})\nexport class KeqModule implements OnModuleInit {\n constructor(\n private readonly modulesContainer: ModulesContainer,\n private readonly moduleRef: ModuleRef,\n private readonly keqRequest: KeqRequest,\n ) {}\n\n onModuleInit(): void {\n for (const [, moduleWrapper] of this.modulesContainer) {\n const instance = moduleWrapper.instance\n if (hasConfigureKeqMiddleware(instance)) {\n const consumer = new KeqMiddlewareConsumerImpl()\n instance.configureKeqMiddleware(consumer)\n consumer.applyTo(this.keqRequest, this.moduleRef)\n }\n }\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","names":["KeqRequest"],"sources":["../src/constants.ts","../src/keq-middleware-consumer.ts","../src/utils/has-configure-keq-middleware.ts","../src/keq.module.ts"],"sourcesContent":["export const KEQ_ROUTES = {\n ALL: Symbol('KEQ_ROUTES.ALL'),\n} as const\n","import type { Type } from '@nestjs/common'\nimport type { ModulesContainer } from '@nestjs/core'\nimport type { KeqExecutionContext, KeqMiddleware, KeqRoute } from 'keq'\nimport { composeRoute, KeqRequest } from 'keq'\nimport picomatch from 'picomatch'\nimport { KEQ_ROUTES } from './constants.js'\nimport type { KeqMiddlewareConfigProxy, KeqMiddlewareConsumer, KeqModuleClass, KeqRouteInfo } from './interfaces/keq-middleware-consumer.interface.js'\nimport type { KeqNestMiddleware } from './interfaces/keq-nest-middleware.interface.js'\n\n\ninterface MiddlewareEntry {\n middlewares: Array<Type<KeqNestMiddleware> | KeqMiddleware>\n routes: Array<typeof KEQ_ROUTES.ALL | KeqRouteInfo | KeqModuleClass>\n excludedRouteInfos?: KeqRouteInfo[]\n}\n\nexport class KeqMiddlewareConsumerImpl implements KeqMiddlewareConsumer {\n private entries: MiddlewareEntry[] = []\n\n apply(...middlewares: Array<Type<KeqNestMiddleware> | KeqMiddleware>): KeqMiddlewareConfigProxy {\n const entry: MiddlewareEntry = { middlewares, routes: [] }\n const excludedRouteInfos: KeqRouteInfo[] = []\n this.entries.push(entry)\n\n const proxy: KeqMiddlewareConfigProxy = {\n exclude: (...routes) => {\n excludedRouteInfos.push(...routes)\n return proxy\n },\n forRoutes: (...routes) => {\n entry.routes = routes\n if (excludedRouteInfos.length > 0) {\n entry.excludedRouteInfos = excludedRouteInfos\n }\n return this\n },\n }\n return proxy\n }\n\n applyTo(keqRequest: KeqRequest, modulesContainer: ModulesContainer): void {\n for (const entry of this.entries) {\n let resolved = entry.middlewares.map((m) => this.resolveMiddleware(m, modulesContainer))\n\n if (entry.excludedRouteInfos && entry.excludedRouteInfos.length > 0) {\n const excludedRoutes = entry.excludedRouteInfos.map((info) => this.buildRoute(info))\n resolved = resolved.map((mw) => this.wrapWithExclusion(mw, excludedRoutes))\n }\n\n const isGlobal = entry.routes.length === 0\n || entry.routes.some((r) => r === KEQ_ROUTES.ALL)\n\n if (isGlobal) {\n for (const mw of resolved) keqRequest.use(mw)\n } else {\n for (const routeTarget of entry.routes) {\n if (routeTarget === KEQ_ROUTES.ALL) continue\n\n if (this.isKeqModuleClass(routeTarget)) {\n const targetRequest = this.findInModulesContainer(routeTarget, modulesContainer)\n if (targetRequest) {\n for (const mw of resolved) targetRequest.use(mw)\n }\n } else {\n const route = this.buildRoute(routeTarget as KeqRouteInfo)\n keqRequest.useRouter().route(route, resolved)\n }\n }\n }\n }\n }\n\n private isKeqModuleClass(route: unknown): route is KeqModuleClass {\n return (\n typeof route === 'function'\n && 'KEQ_REQUEST' in route\n && typeof (route as Record<string, unknown>).KEQ_REQUEST === 'symbol'\n )\n }\n\n private isNestMiddlewareClass(m: Type<KeqNestMiddleware> | KeqMiddleware): m is Type<KeqNestMiddleware> {\n return typeof m === 'function' && m.prototype !== undefined && 'use' in m.prototype\n }\n\n private findMiddlewareInstance(\n middlewareClass: Type<KeqNestMiddleware>,\n modulesContainer: ModulesContainer,\n ): KeqNestMiddleware | undefined {\n for (const [, moduleWrapper] of modulesContainer) {\n const provider = moduleWrapper.providers.get(middlewareClass)\n if (provider?.instance) {\n return provider.instance as KeqNestMiddleware\n }\n }\n return undefined\n }\n\n private resolveMiddleware(\n m: Type<KeqNestMiddleware> | KeqMiddleware,\n modulesContainer: ModulesContainer,\n ): KeqMiddleware {\n if (this.isNestMiddlewareClass(m)) {\n const instance = this.findMiddlewareInstance(m, modulesContainer)\n if (!instance) {\n throw new Error(\n `[KeqModule] Nest middleware class '${m.name}' is not registered as a provider in any module. `\n + 'Ensure the middleware class is added to the providers array of a @Module() decorator.',\n )\n }\n return async (ctx, next) => {\n await instance.use(ctx, next)\n }\n }\n return m\n }\n\n /**\n * 从 ModulesContainer 中查找目标 KeqModule 的 KeqRequest 实例\n *\n * 通过 metatype 精确匹配模块,从模块 providers 中直接获取 KeqRequest。\n * 不依赖 ModuleRef 的作用域机制,直接从 NestJS 全局模块容器中查找。\n *\n * @param routeTarget - 目标 KeqModule 类,必须具有 KEQ_REQUEST symbol 属性\n * @param modulesContainer - NestJS 全局模块容器\n * @returns KeqRequest 实例,未找到时返回 undefined\n */\n private findInModulesContainer(\n routeTarget: KeqModuleClass,\n modulesContainer: ModulesContainer,\n ): KeqRequest | undefined {\n const token = routeTarget.KEQ_REQUEST\n for (const [, moduleWrapper] of modulesContainer) {\n if (moduleWrapper.metatype !== routeTarget) continue\n\n const provider = moduleWrapper.providers.get(token)\n if (!provider) continue\n\n if (provider.instance) {\n return provider.instance as KeqRequest\n }\n }\n return undefined\n }\n\n private buildRoute(info: KeqRouteInfo): KeqRoute {\n const routes: KeqRoute[] = []\n if (info.host) {\n const host = info.host\n routes.push((ctx) => ctx.request.url.host === host)\n }\n if (info.method) {\n const method = info.method.toLowerCase()\n routes.push((ctx) => ctx.request.method.toLowerCase() === method)\n }\n if (info.pathname) {\n const isMatch: (test: string) => boolean = picomatch(info.pathname)\n routes.push((ctx) => isMatch(ctx.request.url.pathname))\n }\n if (routes.length === 1) return routes[0]\n return composeRoute(routes)\n }\n\n private wrapWithExclusion(middleware: KeqMiddleware, excludedRoutes: KeqRoute[]): KeqMiddleware {\n const excluded: KeqMiddleware = async (ctx, next) => {\n if (await this.checkExcluded(ctx, excludedRoutes)) {\n return next()\n }\n return middleware(ctx, next)\n }\n return excluded\n }\n\n private async checkExcluded(ctx: KeqExecutionContext, excludedRoutes: KeqRoute[]): Promise<boolean> {\n for (const route of excludedRoutes) {\n if (await route(ctx)) return true\n }\n return false\n }\n}\n","import type { KeqMiddlewareModule } from '../interfaces/keq-middleware-module.interface.js'\n\n\nexport function hasConfigureKeqMiddleware(instance: unknown): instance is KeqMiddlewareModule {\n return (\n instance !== null\n && instance !== undefined\n && typeof (instance as Record<string, unknown>).configureKeqMiddleware === 'function'\n )\n}\n","import { Global, Module, OnModuleInit, Type } from '@nestjs/common'\nimport { ModulesContainer } from '@nestjs/core'\nimport { KeqRequest } from 'keq'\nimport { KeqMiddlewareConsumerImpl } from './keq-middleware-consumer.js'\nimport { hasConfigureKeqMiddleware } from './utils/has-configure-keq-middleware.js'\n\n/**\n * 全局标记,用于跨 KeqModule 实例去重\n *\n * 当 pnpm 等包管理器因为 peerDependencies 版本差异生成多个\n * `@keq-request/nestjs` 的 `.pnpm` 目录时,Node.js 会将它们视为不同模块,\n * 产生多个 `KeqModule` 类引用,导致 NestJS 创建多个 `KeqModule` 实例。\n *\n * 每个实例的 `onModuleInit()` 都会遍历 `ModulesContainer` 并调用所有\n * 实现了 `configureKeqMiddleware` 的模块。通过 `Symbol.for()`(全局 Symbol\n * 注册表)在 `moduleWrapper.metatype` 上做标记,确保同一个模块类\n * 只被处理一次,无论有多少个 `KeqModule` 实例。\n */\nconst CONFIGURED_SYMBOL = Symbol.for('__keq_middleware_configured__')\n\n/**\n * 在 NestJS 模块初始化早期阶段校验 `configureKeqMiddleware` 的实现模块\n * 是否显式导入了 `KeqModule`。\n *\n * NestJS 初始化分为两个严格串行的阶段:\n * 1. 全量扫描 — 所有模块 wrapper 加入 ModulesContainer,metatype 已设置,\n * DynamicModule 的 imports 已被解析并写入 moduleWrapper.imports Set\n * 2. 实例创建 — 按依赖顺序调用构造函数 → onModuleInit\n *\n * 此校验在阶段 2 开头(构造函数中)执行,早于所有 onModuleInit,\n * 确保中间件缺失问题在启动期就被捕获,而非在业务模块 onModuleInit 发请求时静默失效。\n *\n * 使用运行时 imports Set 而非 Reflect.getMetadata('imports', metatype) 的原因:\n * - DynamicModule 通过 register() 返回的 imports 不在静态装饰器元数据中\n * - 从 ModulesContainer 获取 KeqModule metatype 可避免 pnpm 重复包实例导致的 === 比对失败\n */\nexport function validateConfigureKeqMiddlewareImports(modulesContainer: ModulesContainer): void {\n // 从 ModulesContainer 中查找 KeqModule 的 metatype,\n // 解决 pnpm 下 @keq-request/nestjs 存在多个包实例导致 === 引用比对失败的问题\n let keqModuleMetatype: Type<any> | undefined\n for (const [, moduleWrapper] of modulesContainer) {\n if (moduleWrapper.metatype?.name === 'KeqModule') {\n keqModuleMetatype = moduleWrapper.metatype\n break\n }\n }\n\n for (const [, moduleWrapper] of modulesContainer) {\n const metatype = moduleWrapper.metatype\n if (typeof metatype?.prototype?.configureKeqMiddleware !== 'function') continue\n\n // 使用运行时 imports Set(NestJS 扫描阶段已解析 DynamicModule 的 imports),\n // 替代 Reflect.getMetadata('imports', metatype),解决:\n // 1. DynamicModule 的 imports 无法通过静态元数据检测\n // 2. 重复包实例导致的类引用不匹配\n const hasKeqModule = [...moduleWrapper.imports].some(\n (importedModule) => importedModule.metatype === keqModuleMetatype,\n )\n\n if (!hasKeqModule) {\n throw new Error(\n `[KeqModule] '${metatype.name}' implements configureKeqMiddleware() `\n + 'but does not explicitly import KeqModule. '\n + 'Add \\'imports: [KeqModule]\\' to its @Module() decorator to ensure '\n + 'middleware is registered before the module\\'s onModuleInit() runs.',\n )\n }\n }\n}\n\n@Global()\n@Module({\n providers: [{ provide: KeqRequest, useFactory: () => new KeqRequest() }],\n exports: [KeqRequest],\n})\nexport class KeqModule implements OnModuleInit {\n constructor(\n private readonly modulesContainer: ModulesContainer,\n private readonly keqRequest: KeqRequest,\n ) {\n validateConfigureKeqMiddlewareImports(modulesContainer)\n }\n\n onModuleInit(): void {\n for (const [, moduleWrapper] of this.modulesContainer) {\n const instance = moduleWrapper.instance\n if (!hasConfigureKeqMiddleware(instance)) continue\n\n // 跨 KeqModule 实例去重:\n // moduleWrapper.metatype 是同一个类引用,Symbol.for 是全局共享 key\n if (moduleWrapper.metatype[CONFIGURED_SYMBOL]) continue\n moduleWrapper.metatype[CONFIGURED_SYMBOL] = true\n\n const consumer = new KeqMiddlewareConsumerImpl()\n instance.configureKeqMiddleware(consumer)\n consumer.applyTo(this.keqRequest, this.modulesContainer)\n }\n }\n}\n"],"mappings":"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"}
package/dist/index.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  import { Global, Module } from "@nestjs/common";
2
- import { ModuleRef, ModulesContainer } from "@nestjs/core";
2
+ import { ModulesContainer } from "@nestjs/core";
3
3
  import { KeqRequest, composeRoute } from "keq";
4
4
  import picomatch from "picomatch";
5
5
  //#region src/constants.ts
@@ -28,9 +28,9 @@ var KeqMiddlewareConsumerImpl = class {
28
28
  };
29
29
  return proxy;
30
30
  }
31
- applyTo(keqRequest, moduleRef) {
31
+ applyTo(keqRequest, modulesContainer) {
32
32
  for (const entry of this.entries) {
33
- let resolved = entry.middlewares.map((m) => this.resolveMiddleware(m, moduleRef));
33
+ let resolved = entry.middlewares.map((m) => this.resolveMiddleware(m, modulesContainer));
34
34
  if (entry.excludedRouteInfos && entry.excludedRouteInfos.length > 0) {
35
35
  const excludedRoutes = entry.excludedRouteInfos.map((info) => this.buildRoute(info));
36
36
  resolved = resolved.map((mw) => this.wrapWithExclusion(mw, excludedRoutes));
@@ -39,7 +39,7 @@ var KeqMiddlewareConsumerImpl = class {
39
39
  else for (const routeTarget of entry.routes) {
40
40
  if (routeTarget === KEQ_ROUTES.ALL) continue;
41
41
  if (this.isKeqModuleClass(routeTarget)) {
42
- const targetRequest = moduleRef.get(routeTarget.KEQ_REQUEST, { strict: false });
42
+ const targetRequest = this.findInModulesContainer(routeTarget, modulesContainer);
43
43
  if (targetRequest) for (const mw of resolved) targetRequest.use(mw);
44
44
  } else {
45
45
  const route = this.buildRoute(routeTarget);
@@ -54,15 +54,41 @@ var KeqMiddlewareConsumerImpl = class {
54
54
  isNestMiddlewareClass(m) {
55
55
  return typeof m === "function" && m.prototype !== void 0 && "use" in m.prototype;
56
56
  }
57
- resolveMiddleware(m, moduleRef) {
57
+ findMiddlewareInstance(middlewareClass, modulesContainer) {
58
+ for (const [, moduleWrapper] of modulesContainer) {
59
+ const provider = moduleWrapper.providers.get(middlewareClass);
60
+ if (provider?.instance) return provider.instance;
61
+ }
62
+ }
63
+ resolveMiddleware(m, modulesContainer) {
58
64
  if (this.isNestMiddlewareClass(m)) {
59
- const instance = moduleRef.get(m, { strict: false });
65
+ const instance = this.findMiddlewareInstance(m, modulesContainer);
66
+ if (!instance) throw new Error(`[KeqModule] Nest middleware class '${m.name}' is not registered as a provider in any module. Ensure the middleware class is added to the providers array of a @Module() decorator.`);
60
67
  return async (ctx, next) => {
61
68
  await instance.use(ctx, next);
62
69
  };
63
70
  }
64
71
  return m;
65
72
  }
73
+ /**
74
+ * 从 ModulesContainer 中查找目标 KeqModule 的 KeqRequest 实例
75
+ *
76
+ * 通过 metatype 精确匹配模块,从模块 providers 中直接获取 KeqRequest。
77
+ * 不依赖 ModuleRef 的作用域机制,直接从 NestJS 全局模块容器中查找。
78
+ *
79
+ * @param routeTarget - 目标 KeqModule 类,必须具有 KEQ_REQUEST symbol 属性
80
+ * @param modulesContainer - NestJS 全局模块容器
81
+ * @returns KeqRequest 实例,未找到时返回 undefined
82
+ */
83
+ findInModulesContainer(routeTarget, modulesContainer) {
84
+ const token = routeTarget.KEQ_REQUEST;
85
+ for (const [, moduleWrapper] of modulesContainer) {
86
+ if (moduleWrapper.metatype !== routeTarget) continue;
87
+ const provider = moduleWrapper.providers.get(token);
88
+ if (!provider) continue;
89
+ if (provider.instance) return provider.instance;
90
+ }
91
+ }
66
92
  buildRoute(info) {
67
93
  const routes = [];
68
94
  if (info.host) {
@@ -112,21 +138,63 @@ function __decorate(decorators, target, key, desc) {
112
138
  }
113
139
  //#endregion
114
140
  //#region src/keq.module.ts
115
- var _ref, _ref2, _ref3;
141
+ var _ref, _ref2;
142
+ /**
143
+ * 全局标记,用于跨 KeqModule 实例去重
144
+ *
145
+ * 当 pnpm 等包管理器因为 peerDependencies 版本差异生成多个
146
+ * `@keq-request/nestjs` 的 `.pnpm` 目录时,Node.js 会将它们视为不同模块,
147
+ * 产生多个 `KeqModule` 类引用,导致 NestJS 创建多个 `KeqModule` 实例。
148
+ *
149
+ * 每个实例的 `onModuleInit()` 都会遍历 `ModulesContainer` 并调用所有
150
+ * 实现了 `configureKeqMiddleware` 的模块。通过 `Symbol.for()`(全局 Symbol
151
+ * 注册表)在 `moduleWrapper.metatype` 上做标记,确保同一个模块类
152
+ * 只被处理一次,无论有多少个 `KeqModule` 实例。
153
+ */
154
+ const CONFIGURED_SYMBOL = Symbol.for("__keq_middleware_configured__");
155
+ /**
156
+ * 在 NestJS 模块初始化早期阶段校验 `configureKeqMiddleware` 的实现模块
157
+ * 是否显式导入了 `KeqModule`。
158
+ *
159
+ * NestJS 初始化分为两个严格串行的阶段:
160
+ * 1. 全量扫描 — 所有模块 wrapper 加入 ModulesContainer,metatype 已设置,
161
+ * DynamicModule 的 imports 已被解析并写入 moduleWrapper.imports Set
162
+ * 2. 实例创建 — 按依赖顺序调用构造函数 → onModuleInit
163
+ *
164
+ * 此校验在阶段 2 开头(构造函数中)执行,早于所有 onModuleInit,
165
+ * 确保中间件缺失问题在启动期就被捕获,而非在业务模块 onModuleInit 发请求时静默失效。
166
+ *
167
+ * 使用运行时 imports Set 而非 Reflect.getMetadata('imports', metatype) 的原因:
168
+ * - DynamicModule 通过 register() 返回的 imports 不在静态装饰器元数据中
169
+ * - 从 ModulesContainer 获取 KeqModule metatype 可避免 pnpm 重复包实例导致的 === 比对失败
170
+ */
171
+ function validateConfigureKeqMiddlewareImports(modulesContainer) {
172
+ let keqModuleMetatype;
173
+ for (const [, moduleWrapper] of modulesContainer) if (moduleWrapper.metatype?.name === "KeqModule") {
174
+ keqModuleMetatype = moduleWrapper.metatype;
175
+ break;
176
+ }
177
+ for (const [, moduleWrapper] of modulesContainer) {
178
+ const metatype = moduleWrapper.metatype;
179
+ if (typeof metatype?.prototype?.configureKeqMiddleware !== "function") continue;
180
+ if (![...moduleWrapper.imports].some((importedModule) => importedModule.metatype === keqModuleMetatype)) throw new Error(`[KeqModule] '${metatype.name}' implements configureKeqMiddleware() but does not explicitly import KeqModule. Add 'imports: [KeqModule]' to its @Module() decorator to ensure middleware is registered before the module's onModuleInit() runs.`);
181
+ }
182
+ }
116
183
  let KeqModule = class KeqModule {
117
- constructor(modulesContainer, moduleRef, keqRequest) {
184
+ constructor(modulesContainer, keqRequest) {
118
185
  this.modulesContainer = modulesContainer;
119
- this.moduleRef = moduleRef;
120
186
  this.keqRequest = keqRequest;
187
+ validateConfigureKeqMiddlewareImports(modulesContainer);
121
188
  }
122
189
  onModuleInit() {
123
190
  for (const [, moduleWrapper] of this.modulesContainer) {
124
191
  const instance = moduleWrapper.instance;
125
- if (hasConfigureKeqMiddleware(instance)) {
126
- const consumer = new KeqMiddlewareConsumerImpl();
127
- instance.configureKeqMiddleware(consumer);
128
- consumer.applyTo(this.keqRequest, this.moduleRef);
129
- }
192
+ if (!hasConfigureKeqMiddleware(instance)) continue;
193
+ if (moduleWrapper.metatype[CONFIGURED_SYMBOL]) continue;
194
+ moduleWrapper.metatype[CONFIGURED_SYMBOL] = true;
195
+ const consumer = new KeqMiddlewareConsumerImpl();
196
+ instance.configureKeqMiddleware(consumer);
197
+ consumer.applyTo(this.keqRequest, this.modulesContainer);
130
198
  }
131
199
  }
132
200
  };
@@ -139,13 +207,9 @@ KeqModule = __decorate([
139
207
  }],
140
208
  exports: [KeqRequest]
141
209
  }),
142
- __decorateMetadata("design:paramtypes", [
143
- typeof (_ref = typeof ModulesContainer !== "undefined" && ModulesContainer) === "function" ? _ref : Object,
144
- typeof (_ref2 = typeof ModuleRef !== "undefined" && ModuleRef) === "function" ? _ref2 : Object,
145
- typeof (_ref3 = typeof KeqRequest !== "undefined" && KeqRequest) === "function" ? _ref3 : Object
146
- ])
210
+ __decorateMetadata("design:paramtypes", [typeof (_ref = typeof ModulesContainer !== "undefined" && ModulesContainer) === "function" ? _ref : Object, typeof (_ref2 = typeof KeqRequest !== "undefined" && KeqRequest) === "function" ? _ref2 : Object])
147
211
  ], KeqModule);
148
212
  //#endregion
149
- export { KEQ_ROUTES, KeqModule };
213
+ export { KEQ_ROUTES, KeqModule, validateConfigureKeqMiddlewareImports };
150
214
 
151
215
  //# sourceMappingURL=index.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","names":[],"sources":["../src/constants.ts","../src/keq-middleware-consumer.ts","../src/utils/has-configure-keq-middleware.ts","../src/keq.module.ts"],"sourcesContent":["export const KEQ_ROUTES = {\n ALL: Symbol('KEQ_ROUTES.ALL'),\n} as const\n","import type { Type } from '@nestjs/common'\nimport type { ModuleRef } from '@nestjs/core'\nimport type { KeqExecutionContext, KeqMiddleware, KeqRoute } from 'keq'\nimport { composeRoute, KeqRequest } from 'keq'\nimport picomatch from 'picomatch'\nimport { KEQ_ROUTES } from './constants.js'\nimport type { KeqMiddlewareConfigProxy, KeqMiddlewareConsumer, KeqModuleClass, KeqRouteInfo } from './interfaces/keq-middleware-consumer.interface.js'\nimport type { KeqNestMiddleware } from './interfaces/keq-nest-middleware.interface.js'\n\n\ninterface MiddlewareEntry {\n middlewares: Array<Type<KeqNestMiddleware> | KeqMiddleware>\n routes: Array<typeof KEQ_ROUTES.ALL | KeqRouteInfo | KeqModuleClass>\n excludedRouteInfos?: KeqRouteInfo[]\n}\n\nexport class KeqMiddlewareConsumerImpl implements KeqMiddlewareConsumer {\n private entries: MiddlewareEntry[] = []\n\n apply(...middlewares: Array<Type<KeqNestMiddleware> | KeqMiddleware>): KeqMiddlewareConfigProxy {\n const entry: MiddlewareEntry = { middlewares, routes: [] }\n const excludedRouteInfos: KeqRouteInfo[] = []\n this.entries.push(entry)\n\n const proxy: KeqMiddlewareConfigProxy = {\n exclude: (...routes) => {\n excludedRouteInfos.push(...routes)\n return proxy\n },\n forRoutes: (...routes) => {\n entry.routes = routes\n if (excludedRouteInfos.length > 0) {\n entry.excludedRouteInfos = excludedRouteInfos\n }\n return this\n },\n }\n return proxy\n }\n\n applyTo(keqRequest: KeqRequest, moduleRef: ModuleRef): void {\n for (const entry of this.entries) {\n let resolved = entry.middlewares.map((m) => this.resolveMiddleware(m, moduleRef))\n\n if (entry.excludedRouteInfos && entry.excludedRouteInfos.length > 0) {\n const excludedRoutes = entry.excludedRouteInfos.map((info) => this.buildRoute(info))\n resolved = resolved.map((mw) => this.wrapWithExclusion(mw, excludedRoutes))\n }\n\n const isGlobal = entry.routes.length === 0\n || entry.routes.some((r) => r === KEQ_ROUTES.ALL)\n\n if (isGlobal) {\n for (const mw of resolved) keqRequest.use(mw)\n } else {\n for (const routeTarget of entry.routes) {\n if (routeTarget === KEQ_ROUTES.ALL) continue\n\n if (this.isKeqModuleClass(routeTarget)) {\n const targetRequest = moduleRef.get<KeqRequest>(routeTarget.KEQ_REQUEST, { strict: false })\n if (targetRequest) {\n for (const mw of resolved) targetRequest.use(mw)\n }\n } else {\n const route = this.buildRoute(routeTarget as KeqRouteInfo)\n keqRequest.useRouter().route(route, resolved)\n }\n }\n }\n }\n }\n\n private isKeqModuleClass(route: unknown): route is KeqModuleClass {\n return (\n typeof route === 'function'\n && 'KEQ_REQUEST' in route\n && typeof (route as Record<string, unknown>).KEQ_REQUEST === 'symbol'\n )\n }\n\n private isNestMiddlewareClass(m: Type<KeqNestMiddleware> | KeqMiddleware): m is Type<KeqNestMiddleware> {\n return typeof m === 'function' && m.prototype !== undefined && 'use' in m.prototype\n }\n\n private resolveMiddleware(m: Type<KeqNestMiddleware> | KeqMiddleware, moduleRef: ModuleRef): KeqMiddleware {\n if (this.isNestMiddlewareClass(m)) {\n const instance = moduleRef.get<KeqNestMiddleware>(m, { strict: false })\n return async (ctx, next) => {\n await instance.use(ctx, next)\n }\n }\n return m\n }\n\n private buildRoute(info: KeqRouteInfo): KeqRoute {\n const routes: KeqRoute[] = []\n if (info.host) {\n const host = info.host\n routes.push((ctx) => ctx.request.url.host === host)\n }\n if (info.method) {\n const method = info.method.toLowerCase()\n routes.push((ctx) => ctx.request.method.toLowerCase() === method)\n }\n if (info.pathname) {\n const isMatch: (test: string) => boolean = picomatch(info.pathname)\n routes.push((ctx) => isMatch(ctx.request.url.pathname))\n }\n if (routes.length === 1) return routes[0]\n return composeRoute(routes)\n }\n\n private wrapWithExclusion(middleware: KeqMiddleware, excludedRoutes: KeqRoute[]): KeqMiddleware {\n const excluded: KeqMiddleware = async (ctx, next) => {\n if (await this.checkExcluded(ctx, excludedRoutes)) {\n return next()\n }\n return middleware(ctx, next)\n }\n return excluded\n }\n\n private async checkExcluded(ctx: KeqExecutionContext, excludedRoutes: KeqRoute[]): Promise<boolean> {\n for (const route of excludedRoutes) {\n if (await route(ctx)) return true\n }\n return false\n }\n}\n","import type { KeqMiddlewareModule } from '../interfaces/keq-middleware-module.interface.js'\n\n\nexport function hasConfigureKeqMiddleware(instance: unknown): instance is KeqMiddlewareModule {\n return (\n instance !== null\n && instance !== undefined\n && typeof (instance as Record<string, unknown>).configureKeqMiddleware === 'function'\n )\n}\n","import { Global, Module, OnModuleInit } from '@nestjs/common'\nimport { ModulesContainer, ModuleRef } from '@nestjs/core'\nimport { KeqRequest } from 'keq'\nimport { KeqMiddlewareConsumerImpl } from './keq-middleware-consumer.js'\nimport { hasConfigureKeqMiddleware } from './utils/has-configure-keq-middleware.js'\n\n\n@Global()\n@Module({\n providers: [{ provide: KeqRequest, useFactory: () => new KeqRequest() }],\n exports: [KeqRequest],\n})\nexport class KeqModule implements OnModuleInit {\n constructor(\n private readonly modulesContainer: ModulesContainer,\n private readonly moduleRef: ModuleRef,\n private readonly keqRequest: KeqRequest,\n ) {}\n\n onModuleInit(): void {\n for (const [, moduleWrapper] of this.modulesContainer) {\n const instance = moduleWrapper.instance\n if (hasConfigureKeqMiddleware(instance)) {\n const consumer = new KeqMiddlewareConsumerImpl()\n instance.configureKeqMiddleware(consumer)\n consumer.applyTo(this.keqRequest, this.moduleRef)\n }\n }\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../src/constants.ts","../src/keq-middleware-consumer.ts","../src/utils/has-configure-keq-middleware.ts","../src/keq.module.ts"],"sourcesContent":["export const KEQ_ROUTES = {\n ALL: Symbol('KEQ_ROUTES.ALL'),\n} as const\n","import type { Type } from '@nestjs/common'\nimport type { ModulesContainer } from '@nestjs/core'\nimport type { KeqExecutionContext, KeqMiddleware, KeqRoute } from 'keq'\nimport { composeRoute, KeqRequest } from 'keq'\nimport picomatch from 'picomatch'\nimport { KEQ_ROUTES } from './constants.js'\nimport type { KeqMiddlewareConfigProxy, KeqMiddlewareConsumer, KeqModuleClass, KeqRouteInfo } from './interfaces/keq-middleware-consumer.interface.js'\nimport type { KeqNestMiddleware } from './interfaces/keq-nest-middleware.interface.js'\n\n\ninterface MiddlewareEntry {\n middlewares: Array<Type<KeqNestMiddleware> | KeqMiddleware>\n routes: Array<typeof KEQ_ROUTES.ALL | KeqRouteInfo | KeqModuleClass>\n excludedRouteInfos?: KeqRouteInfo[]\n}\n\nexport class KeqMiddlewareConsumerImpl implements KeqMiddlewareConsumer {\n private entries: MiddlewareEntry[] = []\n\n apply(...middlewares: Array<Type<KeqNestMiddleware> | KeqMiddleware>): KeqMiddlewareConfigProxy {\n const entry: MiddlewareEntry = { middlewares, routes: [] }\n const excludedRouteInfos: KeqRouteInfo[] = []\n this.entries.push(entry)\n\n const proxy: KeqMiddlewareConfigProxy = {\n exclude: (...routes) => {\n excludedRouteInfos.push(...routes)\n return proxy\n },\n forRoutes: (...routes) => {\n entry.routes = routes\n if (excludedRouteInfos.length > 0) {\n entry.excludedRouteInfos = excludedRouteInfos\n }\n return this\n },\n }\n return proxy\n }\n\n applyTo(keqRequest: KeqRequest, modulesContainer: ModulesContainer): void {\n for (const entry of this.entries) {\n let resolved = entry.middlewares.map((m) => this.resolveMiddleware(m, modulesContainer))\n\n if (entry.excludedRouteInfos && entry.excludedRouteInfos.length > 0) {\n const excludedRoutes = entry.excludedRouteInfos.map((info) => this.buildRoute(info))\n resolved = resolved.map((mw) => this.wrapWithExclusion(mw, excludedRoutes))\n }\n\n const isGlobal = entry.routes.length === 0\n || entry.routes.some((r) => r === KEQ_ROUTES.ALL)\n\n if (isGlobal) {\n for (const mw of resolved) keqRequest.use(mw)\n } else {\n for (const routeTarget of entry.routes) {\n if (routeTarget === KEQ_ROUTES.ALL) continue\n\n if (this.isKeqModuleClass(routeTarget)) {\n const targetRequest = this.findInModulesContainer(routeTarget, modulesContainer)\n if (targetRequest) {\n for (const mw of resolved) targetRequest.use(mw)\n }\n } else {\n const route = this.buildRoute(routeTarget as KeqRouteInfo)\n keqRequest.useRouter().route(route, resolved)\n }\n }\n }\n }\n }\n\n private isKeqModuleClass(route: unknown): route is KeqModuleClass {\n return (\n typeof route === 'function'\n && 'KEQ_REQUEST' in route\n && typeof (route as Record<string, unknown>).KEQ_REQUEST === 'symbol'\n )\n }\n\n private isNestMiddlewareClass(m: Type<KeqNestMiddleware> | KeqMiddleware): m is Type<KeqNestMiddleware> {\n return typeof m === 'function' && m.prototype !== undefined && 'use' in m.prototype\n }\n\n private findMiddlewareInstance(\n middlewareClass: Type<KeqNestMiddleware>,\n modulesContainer: ModulesContainer,\n ): KeqNestMiddleware | undefined {\n for (const [, moduleWrapper] of modulesContainer) {\n const provider = moduleWrapper.providers.get(middlewareClass)\n if (provider?.instance) {\n return provider.instance as KeqNestMiddleware\n }\n }\n return undefined\n }\n\n private resolveMiddleware(\n m: Type<KeqNestMiddleware> | KeqMiddleware,\n modulesContainer: ModulesContainer,\n ): KeqMiddleware {\n if (this.isNestMiddlewareClass(m)) {\n const instance = this.findMiddlewareInstance(m, modulesContainer)\n if (!instance) {\n throw new Error(\n `[KeqModule] Nest middleware class '${m.name}' is not registered as a provider in any module. `\n + 'Ensure the middleware class is added to the providers array of a @Module() decorator.',\n )\n }\n return async (ctx, next) => {\n await instance.use(ctx, next)\n }\n }\n return m\n }\n\n /**\n * 从 ModulesContainer 中查找目标 KeqModule 的 KeqRequest 实例\n *\n * 通过 metatype 精确匹配模块,从模块 providers 中直接获取 KeqRequest。\n * 不依赖 ModuleRef 的作用域机制,直接从 NestJS 全局模块容器中查找。\n *\n * @param routeTarget - 目标 KeqModule 类,必须具有 KEQ_REQUEST symbol 属性\n * @param modulesContainer - NestJS 全局模块容器\n * @returns KeqRequest 实例,未找到时返回 undefined\n */\n private findInModulesContainer(\n routeTarget: KeqModuleClass,\n modulesContainer: ModulesContainer,\n ): KeqRequest | undefined {\n const token = routeTarget.KEQ_REQUEST\n for (const [, moduleWrapper] of modulesContainer) {\n if (moduleWrapper.metatype !== routeTarget) continue\n\n const provider = moduleWrapper.providers.get(token)\n if (!provider) continue\n\n if (provider.instance) {\n return provider.instance as KeqRequest\n }\n }\n return undefined\n }\n\n private buildRoute(info: KeqRouteInfo): KeqRoute {\n const routes: KeqRoute[] = []\n if (info.host) {\n const host = info.host\n routes.push((ctx) => ctx.request.url.host === host)\n }\n if (info.method) {\n const method = info.method.toLowerCase()\n routes.push((ctx) => ctx.request.method.toLowerCase() === method)\n }\n if (info.pathname) {\n const isMatch: (test: string) => boolean = picomatch(info.pathname)\n routes.push((ctx) => isMatch(ctx.request.url.pathname))\n }\n if (routes.length === 1) return routes[0]\n return composeRoute(routes)\n }\n\n private wrapWithExclusion(middleware: KeqMiddleware, excludedRoutes: KeqRoute[]): KeqMiddleware {\n const excluded: KeqMiddleware = async (ctx, next) => {\n if (await this.checkExcluded(ctx, excludedRoutes)) {\n return next()\n }\n return middleware(ctx, next)\n }\n return excluded\n }\n\n private async checkExcluded(ctx: KeqExecutionContext, excludedRoutes: KeqRoute[]): Promise<boolean> {\n for (const route of excludedRoutes) {\n if (await route(ctx)) return true\n }\n return false\n }\n}\n","import type { KeqMiddlewareModule } from '../interfaces/keq-middleware-module.interface.js'\n\n\nexport function hasConfigureKeqMiddleware(instance: unknown): instance is KeqMiddlewareModule {\n return (\n instance !== null\n && instance !== undefined\n && typeof (instance as Record<string, unknown>).configureKeqMiddleware === 'function'\n )\n}\n","import { Global, Module, OnModuleInit, Type } from '@nestjs/common'\nimport { ModulesContainer } from '@nestjs/core'\nimport { KeqRequest } from 'keq'\nimport { KeqMiddlewareConsumerImpl } from './keq-middleware-consumer.js'\nimport { hasConfigureKeqMiddleware } from './utils/has-configure-keq-middleware.js'\n\n/**\n * 全局标记,用于跨 KeqModule 实例去重\n *\n * 当 pnpm 等包管理器因为 peerDependencies 版本差异生成多个\n * `@keq-request/nestjs` 的 `.pnpm` 目录时,Node.js 会将它们视为不同模块,\n * 产生多个 `KeqModule` 类引用,导致 NestJS 创建多个 `KeqModule` 实例。\n *\n * 每个实例的 `onModuleInit()` 都会遍历 `ModulesContainer` 并调用所有\n * 实现了 `configureKeqMiddleware` 的模块。通过 `Symbol.for()`(全局 Symbol\n * 注册表)在 `moduleWrapper.metatype` 上做标记,确保同一个模块类\n * 只被处理一次,无论有多少个 `KeqModule` 实例。\n */\nconst CONFIGURED_SYMBOL = Symbol.for('__keq_middleware_configured__')\n\n/**\n * 在 NestJS 模块初始化早期阶段校验 `configureKeqMiddleware` 的实现模块\n * 是否显式导入了 `KeqModule`。\n *\n * NestJS 初始化分为两个严格串行的阶段:\n * 1. 全量扫描 — 所有模块 wrapper 加入 ModulesContainer,metatype 已设置,\n * DynamicModule 的 imports 已被解析并写入 moduleWrapper.imports Set\n * 2. 实例创建 — 按依赖顺序调用构造函数 → onModuleInit\n *\n * 此校验在阶段 2 开头(构造函数中)执行,早于所有 onModuleInit,\n * 确保中间件缺失问题在启动期就被捕获,而非在业务模块 onModuleInit 发请求时静默失效。\n *\n * 使用运行时 imports Set 而非 Reflect.getMetadata('imports', metatype) 的原因:\n * - DynamicModule 通过 register() 返回的 imports 不在静态装饰器元数据中\n * - 从 ModulesContainer 获取 KeqModule metatype 可避免 pnpm 重复包实例导致的 === 比对失败\n */\nexport function validateConfigureKeqMiddlewareImports(modulesContainer: ModulesContainer): void {\n // 从 ModulesContainer 中查找 KeqModule 的 metatype,\n // 解决 pnpm 下 @keq-request/nestjs 存在多个包实例导致 === 引用比对失败的问题\n let keqModuleMetatype: Type<any> | undefined\n for (const [, moduleWrapper] of modulesContainer) {\n if (moduleWrapper.metatype?.name === 'KeqModule') {\n keqModuleMetatype = moduleWrapper.metatype\n break\n }\n }\n\n for (const [, moduleWrapper] of modulesContainer) {\n const metatype = moduleWrapper.metatype\n if (typeof metatype?.prototype?.configureKeqMiddleware !== 'function') continue\n\n // 使用运行时 imports Set(NestJS 扫描阶段已解析 DynamicModule 的 imports),\n // 替代 Reflect.getMetadata('imports', metatype),解决:\n // 1. DynamicModule 的 imports 无法通过静态元数据检测\n // 2. 重复包实例导致的类引用不匹配\n const hasKeqModule = [...moduleWrapper.imports].some(\n (importedModule) => importedModule.metatype === keqModuleMetatype,\n )\n\n if (!hasKeqModule) {\n throw new Error(\n `[KeqModule] '${metatype.name}' implements configureKeqMiddleware() `\n + 'but does not explicitly import KeqModule. '\n + 'Add \\'imports: [KeqModule]\\' to its @Module() decorator to ensure '\n + 'middleware is registered before the module\\'s onModuleInit() runs.',\n )\n }\n }\n}\n\n@Global()\n@Module({\n providers: [{ provide: KeqRequest, useFactory: () => new KeqRequest() }],\n exports: [KeqRequest],\n})\nexport class KeqModule implements OnModuleInit {\n constructor(\n private readonly modulesContainer: ModulesContainer,\n private readonly keqRequest: KeqRequest,\n ) {\n validateConfigureKeqMiddlewareImports(modulesContainer)\n }\n\n onModuleInit(): void {\n for (const [, moduleWrapper] of this.modulesContainer) {\n const instance = moduleWrapper.instance\n if (!hasConfigureKeqMiddleware(instance)) continue\n\n // 跨 KeqModule 实例去重:\n // moduleWrapper.metatype 是同一个类引用,Symbol.for 是全局共享 key\n if (moduleWrapper.metatype[CONFIGURED_SYMBOL]) continue\n moduleWrapper.metatype[CONFIGURED_SYMBOL] = true\n\n const consumer = new KeqMiddlewareConsumerImpl()\n instance.configureKeqMiddleware(consumer)\n consumer.applyTo(this.keqRequest, this.modulesContainer)\n }\n }\n}\n"],"mappings":"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"}
package/jest.config.cts CHANGED
@@ -5,6 +5,16 @@ import { compilerOptions } from './tsconfig.json'
5
5
 
6
6
  // eslint-disable-next-line @typescript-eslint/require-await
7
7
  export default async (): Promise<Config> => ({
8
- preset: 'ts-jest',
9
- moduleNameMapper: pathsToModuleNameMapper(compilerOptions.paths, { prefix: '<rootDir>/src', useESM: true }),
8
+ transform: {
9
+ '^.+\\.tsx?$': ['ts-jest', {
10
+ tsconfig: {
11
+ noEmitOnError: false,
12
+ },
13
+ }],
14
+ },
15
+ testMatch: [
16
+ '<rootDir>/__tests__/**/*.spec.ts',
17
+ '<rootDir>/src/**/*.spec.ts',
18
+ ],
19
+ moduleNameMapper: pathsToModuleNameMapper(compilerOptions.paths, { prefix: '<rootDir>', useESM: true }),
10
20
  })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@keq-request/nestjs",
3
- "version": "5.0.0-beta.12",
3
+ "version": "5.0.0-beta.13",
4
4
  "description": "Request API write by Typescript for flexibility, readability, and a low learning curve.",
5
5
  "keywords": [
6
6
  "request",
@@ -39,14 +39,16 @@
39
39
  "devDependencies": {
40
40
  "@nestjs/common": "^11.1.19",
41
41
  "@nestjs/core": "^11.1.19",
42
+ "@nestjs/testing": "^11.1.27",
42
43
  "@types/node": "^20.19.39",
43
44
  "@types/picomatch": "^4.0.0",
44
- "keq": "5.0.0-beta.12"
45
+ "reflect-metadata": "^0.2.2",
46
+ "keq": "5.0.0-beta.13"
45
47
  },
46
48
  "peerDependencies": {
47
49
  "@nestjs/common": "^11.0.0",
48
50
  "@nestjs/core": "^11.0.0",
49
- "keq": "^5.0.0-beta.12"
51
+ "keq": "^5.0.0-beta.13"
50
52
  },
51
53
  "engines": {
52
54
  "node": ">=20.0.0"
@@ -10,7 +10,7 @@ export interface KeqRouteInfo {
10
10
  pathname?: string
11
11
  }
12
12
 
13
- export interface KeqModuleClass {
13
+ export interface KeqModuleClass extends Type<any> {
14
14
  readonly KEQ_REQUEST: symbol
15
15
  }
16
16
 
@@ -1,5 +1,5 @@
1
1
  import type { Type } from '@nestjs/common'
2
- import type { ModuleRef } from '@nestjs/core'
2
+ import type { ModulesContainer } from '@nestjs/core'
3
3
  import type { KeqExecutionContext, KeqMiddleware, KeqRoute } from 'keq'
4
4
  import { composeRoute, KeqRequest } from 'keq'
5
5
  import picomatch from 'picomatch'
@@ -38,9 +38,9 @@ export class KeqMiddlewareConsumerImpl implements KeqMiddlewareConsumer {
38
38
  return proxy
39
39
  }
40
40
 
41
- applyTo(keqRequest: KeqRequest, moduleRef: ModuleRef): void {
41
+ applyTo(keqRequest: KeqRequest, modulesContainer: ModulesContainer): void {
42
42
  for (const entry of this.entries) {
43
- let resolved = entry.middlewares.map((m) => this.resolveMiddleware(m, moduleRef))
43
+ let resolved = entry.middlewares.map((m) => this.resolveMiddleware(m, modulesContainer))
44
44
 
45
45
  if (entry.excludedRouteInfos && entry.excludedRouteInfos.length > 0) {
46
46
  const excludedRoutes = entry.excludedRouteInfos.map((info) => this.buildRoute(info))
@@ -57,7 +57,7 @@ export class KeqMiddlewareConsumerImpl implements KeqMiddlewareConsumer {
57
57
  if (routeTarget === KEQ_ROUTES.ALL) continue
58
58
 
59
59
  if (this.isKeqModuleClass(routeTarget)) {
60
- const targetRequest = moduleRef.get<KeqRequest>(routeTarget.KEQ_REQUEST, { strict: false })
60
+ const targetRequest = this.findInModulesContainer(routeTarget, modulesContainer)
61
61
  if (targetRequest) {
62
62
  for (const mw of resolved) targetRequest.use(mw)
63
63
  }
@@ -82,9 +82,31 @@ export class KeqMiddlewareConsumerImpl implements KeqMiddlewareConsumer {
82
82
  return typeof m === 'function' && m.prototype !== undefined && 'use' in m.prototype
83
83
  }
84
84
 
85
- private resolveMiddleware(m: Type<KeqNestMiddleware> | KeqMiddleware, moduleRef: ModuleRef): KeqMiddleware {
85
+ private findMiddlewareInstance(
86
+ middlewareClass: Type<KeqNestMiddleware>,
87
+ modulesContainer: ModulesContainer,
88
+ ): KeqNestMiddleware | undefined {
89
+ for (const [, moduleWrapper] of modulesContainer) {
90
+ const provider = moduleWrapper.providers.get(middlewareClass)
91
+ if (provider?.instance) {
92
+ return provider.instance as KeqNestMiddleware
93
+ }
94
+ }
95
+ return undefined
96
+ }
97
+
98
+ private resolveMiddleware(
99
+ m: Type<KeqNestMiddleware> | KeqMiddleware,
100
+ modulesContainer: ModulesContainer,
101
+ ): KeqMiddleware {
86
102
  if (this.isNestMiddlewareClass(m)) {
87
- const instance = moduleRef.get<KeqNestMiddleware>(m, { strict: false })
103
+ const instance = this.findMiddlewareInstance(m, modulesContainer)
104
+ if (!instance) {
105
+ throw new Error(
106
+ `[KeqModule] Nest middleware class '${m.name}' is not registered as a provider in any module. `
107
+ + 'Ensure the middleware class is added to the providers array of a @Module() decorator.',
108
+ )
109
+ }
88
110
  return async (ctx, next) => {
89
111
  await instance.use(ctx, next)
90
112
  }
@@ -92,6 +114,34 @@ export class KeqMiddlewareConsumerImpl implements KeqMiddlewareConsumer {
92
114
  return m
93
115
  }
94
116
 
117
+ /**
118
+ * 从 ModulesContainer 中查找目标 KeqModule 的 KeqRequest 实例
119
+ *
120
+ * 通过 metatype 精确匹配模块,从模块 providers 中直接获取 KeqRequest。
121
+ * 不依赖 ModuleRef 的作用域机制,直接从 NestJS 全局模块容器中查找。
122
+ *
123
+ * @param routeTarget - 目标 KeqModule 类,必须具有 KEQ_REQUEST symbol 属性
124
+ * @param modulesContainer - NestJS 全局模块容器
125
+ * @returns KeqRequest 实例,未找到时返回 undefined
126
+ */
127
+ private findInModulesContainer(
128
+ routeTarget: KeqModuleClass,
129
+ modulesContainer: ModulesContainer,
130
+ ): KeqRequest | undefined {
131
+ const token = routeTarget.KEQ_REQUEST
132
+ for (const [, moduleWrapper] of modulesContainer) {
133
+ if (moduleWrapper.metatype !== routeTarget) continue
134
+
135
+ const provider = moduleWrapper.providers.get(token)
136
+ if (!provider) continue
137
+
138
+ if (provider.instance) {
139
+ return provider.instance as KeqRequest
140
+ }
141
+ }
142
+ return undefined
143
+ }
144
+
95
145
  private buildRoute(info: KeqRouteInfo): KeqRoute {
96
146
  const routes: KeqRoute[] = []
97
147
  if (info.host) {
package/src/keq.module.ts CHANGED
@@ -1,9 +1,72 @@
1
- import { Global, Module, OnModuleInit } from '@nestjs/common'
2
- import { ModulesContainer, ModuleRef } from '@nestjs/core'
1
+ import { Global, Module, OnModuleInit, Type } from '@nestjs/common'
2
+ import { ModulesContainer } from '@nestjs/core'
3
3
  import { KeqRequest } from 'keq'
4
4
  import { KeqMiddlewareConsumerImpl } from './keq-middleware-consumer.js'
5
5
  import { hasConfigureKeqMiddleware } from './utils/has-configure-keq-middleware.js'
6
6
 
7
+ /**
8
+ * 全局标记,用于跨 KeqModule 实例去重
9
+ *
10
+ * 当 pnpm 等包管理器因为 peerDependencies 版本差异生成多个
11
+ * `@keq-request/nestjs` 的 `.pnpm` 目录时,Node.js 会将它们视为不同模块,
12
+ * 产生多个 `KeqModule` 类引用,导致 NestJS 创建多个 `KeqModule` 实例。
13
+ *
14
+ * 每个实例的 `onModuleInit()` 都会遍历 `ModulesContainer` 并调用所有
15
+ * 实现了 `configureKeqMiddleware` 的模块。通过 `Symbol.for()`(全局 Symbol
16
+ * 注册表)在 `moduleWrapper.metatype` 上做标记,确保同一个模块类
17
+ * 只被处理一次,无论有多少个 `KeqModule` 实例。
18
+ */
19
+ const CONFIGURED_SYMBOL = Symbol.for('__keq_middleware_configured__')
20
+
21
+ /**
22
+ * 在 NestJS 模块初始化早期阶段校验 `configureKeqMiddleware` 的实现模块
23
+ * 是否显式导入了 `KeqModule`。
24
+ *
25
+ * NestJS 初始化分为两个严格串行的阶段:
26
+ * 1. 全量扫描 — 所有模块 wrapper 加入 ModulesContainer,metatype 已设置,
27
+ * DynamicModule 的 imports 已被解析并写入 moduleWrapper.imports Set
28
+ * 2. 实例创建 — 按依赖顺序调用构造函数 → onModuleInit
29
+ *
30
+ * 此校验在阶段 2 开头(构造函数中)执行,早于所有 onModuleInit,
31
+ * 确保中间件缺失问题在启动期就被捕获,而非在业务模块 onModuleInit 发请求时静默失效。
32
+ *
33
+ * 使用运行时 imports Set 而非 Reflect.getMetadata('imports', metatype) 的原因:
34
+ * - DynamicModule 通过 register() 返回的 imports 不在静态装饰器元数据中
35
+ * - 从 ModulesContainer 获取 KeqModule metatype 可避免 pnpm 重复包实例导致的 === 比对失败
36
+ */
37
+ export function validateConfigureKeqMiddlewareImports(modulesContainer: ModulesContainer): void {
38
+ // 从 ModulesContainer 中查找 KeqModule 的 metatype,
39
+ // 解决 pnpm 下 @keq-request/nestjs 存在多个包实例导致 === 引用比对失败的问题
40
+ let keqModuleMetatype: Type<any> | undefined
41
+ for (const [, moduleWrapper] of modulesContainer) {
42
+ if (moduleWrapper.metatype?.name === 'KeqModule') {
43
+ keqModuleMetatype = moduleWrapper.metatype
44
+ break
45
+ }
46
+ }
47
+
48
+ for (const [, moduleWrapper] of modulesContainer) {
49
+ const metatype = moduleWrapper.metatype
50
+ if (typeof metatype?.prototype?.configureKeqMiddleware !== 'function') continue
51
+
52
+ // 使用运行时 imports Set(NestJS 扫描阶段已解析 DynamicModule 的 imports),
53
+ // 替代 Reflect.getMetadata('imports', metatype),解决:
54
+ // 1. DynamicModule 的 imports 无法通过静态元数据检测
55
+ // 2. 重复包实例导致的类引用不匹配
56
+ const hasKeqModule = [...moduleWrapper.imports].some(
57
+ (importedModule) => importedModule.metatype === keqModuleMetatype,
58
+ )
59
+
60
+ if (!hasKeqModule) {
61
+ throw new Error(
62
+ `[KeqModule] '${metatype.name}' implements configureKeqMiddleware() `
63
+ + 'but does not explicitly import KeqModule. '
64
+ + 'Add \'imports: [KeqModule]\' to its @Module() decorator to ensure '
65
+ + 'middleware is registered before the module\'s onModuleInit() runs.',
66
+ )
67
+ }
68
+ }
69
+ }
7
70
 
8
71
  @Global()
9
72
  @Module({
@@ -13,18 +76,24 @@ import { hasConfigureKeqMiddleware } from './utils/has-configure-keq-middleware.
13
76
  export class KeqModule implements OnModuleInit {
14
77
  constructor(
15
78
  private readonly modulesContainer: ModulesContainer,
16
- private readonly moduleRef: ModuleRef,
17
79
  private readonly keqRequest: KeqRequest,
18
- ) {}
80
+ ) {
81
+ validateConfigureKeqMiddlewareImports(modulesContainer)
82
+ }
19
83
 
20
84
  onModuleInit(): void {
21
85
  for (const [, moduleWrapper] of this.modulesContainer) {
22
86
  const instance = moduleWrapper.instance
23
- if (hasConfigureKeqMiddleware(instance)) {
24
- const consumer = new KeqMiddlewareConsumerImpl()
25
- instance.configureKeqMiddleware(consumer)
26
- consumer.applyTo(this.keqRequest, this.moduleRef)
27
- }
87
+ if (!hasConfigureKeqMiddleware(instance)) continue
88
+
89
+ // 跨 KeqModule 实例去重:
90
+ // moduleWrapper.metatype 是同一个类引用,Symbol.for 是全局共享 key
91
+ if (moduleWrapper.metatype[CONFIGURED_SYMBOL]) continue
92
+ moduleWrapper.metatype[CONFIGURED_SYMBOL] = true
93
+
94
+ const consumer = new KeqMiddlewareConsumerImpl()
95
+ instance.configureKeqMiddleware(consumer)
96
+ consumer.applyTo(this.keqRequest, this.modulesContainer)
28
97
  }
29
98
  }
30
99
  }