@keq-request/nestjs 5.0.0-beta.11 → 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.11 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:2746) [MODULE_TYPELESS_PACKAGE_JSON] Warning: Module type of file:///home/runner/work/keq/keq/packages/nestjs/tsdown.config.ts?no-cache=0efd33bf-1c41-465a-8588-1641d5aab98c 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 5.83 kB │ gzip: 2.01 kB
16
- ℹ [CJS] dist/index.js.map 7.28 kB │ gzip: 2.38 kB
17
- ℹ [CJS] 2 files, total: 13.11 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 0.99 kB │ gzip: 0.43 kB
21
- ℹ [CJS] dist/index.d.ts 1.86 kB │ gzip: 0.65 kB
22
- ℹ [CJS] 2 files, total: 2.85 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
23
31
  [PLUGIN_TIMINGS] Warning: Your build spent significant time in plugins. Here is a breakdown:
24
32
  - rolldown-plugin-dts:generate (48%)
25
- - tsdown:deps (46%)
33
+ - tsdown:deps (43%)
26
34
  See https://rolldown.rs/options/checks#plugintimings for more details.
27
35
 
28
- ✔ Build complete in 5198ms
29
- [PLUGIN_TIMINGS] Warning: Your build spent significant time in plugin `rolldown-plugin-dts:generate`. See https://rolldown.rs/options/checks#plugintimings for more details.
30
- ℹ [ESM] dist/index.mjs 4.54 kB │ gzip: 1.57 kB
31
-
32
- ℹ [ESM] dist/index.mjs.map 7.21 kB │ gzip: 2.36 kB
33
- ℹ [ESM] dist/index.d.mts.map 0.99 kB │ gzip: 0.43 kB
34
- ℹ [ESM] dist/index.d.mts 1.86 kB │ gzip: 0.66 kB
35
- ℹ [ESM] 4 files, total: 14.60 kB
36
- ✔ Build complete in 5201ms
36
+ ✔ Build complete in 5614ms
package/CHANGELOG.md CHANGED
@@ -1,5 +1,27 @@
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
+
15
+ ## 5.0.0-beta.12
16
+
17
+ ### Minor Changes
18
+
19
+ - fbc851f: **Feat:** Add `exclude()` method to `KeqMiddlewareConfigProxy`, allowing middleware to skip specific routes. Supports `KeqRouteInfo` with `host`, `method`, and `pathname` patterns, following the NestJS `MiddlewareConfigProxy.exclude()` convention.
20
+
21
+ ### Patch Changes
22
+
23
+ - keq@5.0.0-beta.12
24
+
3
25
  ## 5.0.0-beta.11
4
26
 
5
27
  ### Patch 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,10 +43,11 @@ 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 {
50
+ exclude(...routes: KeqRouteInfo[]): KeqMiddlewareConfigProxy;
34
51
  forRoutes(...routes: Array<typeof KEQ_ROUTES.ALL | KeqRouteInfo | KeqModuleClass>): KeqMiddlewareConsumer;
35
52
  }
36
53
  interface KeqMiddlewareConsumer {
@@ -48,5 +65,5 @@ interface KeqModuleOptions {
48
65
  isolate?: boolean;
49
66
  }
50
67
  //#endregion
51
- 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 };
52
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,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;;;UClBxD,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,10 +43,11 @@ 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 {
50
+ exclude(...routes: KeqRouteInfo[]): KeqMiddlewareConfigProxy;
34
51
  forRoutes(...routes: Array<typeof KEQ_ROUTES.ALL | KeqRouteInfo | KeqModuleClass>): KeqMiddlewareConsumer;
35
52
  }
36
53
  interface KeqMiddlewareConsumer {
@@ -48,5 +65,5 @@ interface KeqModuleOptions {
48
65
  isolate?: boolean;
49
66
  }
50
67
  //#endregion
51
- 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 };
52
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,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;;;UClBxD,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
@@ -37,20 +37,33 @@ var KeqMiddlewareConsumerImpl = class {
37
37
  middlewares,
38
38
  routes: []
39
39
  };
40
+ const excludedRouteInfos = [];
40
41
  this.entries.push(entry);
41
- return { forRoutes: (...routes) => {
42
- entry.routes = routes;
43
- return this;
44
- } };
42
+ const proxy = {
43
+ exclude: (...routes) => {
44
+ excludedRouteInfos.push(...routes);
45
+ return proxy;
46
+ },
47
+ forRoutes: (...routes) => {
48
+ entry.routes = routes;
49
+ if (excludedRouteInfos.length > 0) entry.excludedRouteInfos = excludedRouteInfos;
50
+ return this;
51
+ }
52
+ };
53
+ return proxy;
45
54
  }
46
- applyTo(keqRequest, moduleRef) {
55
+ applyTo(keqRequest, modulesContainer) {
47
56
  for (const entry of this.entries) {
48
- const resolved = entry.middlewares.map((m) => this.resolveMiddleware(m, moduleRef));
57
+ let resolved = entry.middlewares.map((m) => this.resolveMiddleware(m, modulesContainer));
58
+ if (entry.excludedRouteInfos && entry.excludedRouteInfos.length > 0) {
59
+ const excludedRoutes = entry.excludedRouteInfos.map((info) => this.buildRoute(info));
60
+ resolved = resolved.map((mw) => this.wrapWithExclusion(mw, excludedRoutes));
61
+ }
49
62
  if (entry.routes.length === 0 || entry.routes.some((r) => r === KEQ_ROUTES.ALL)) for (const mw of resolved) keqRequest.use(mw);
50
63
  else for (const routeTarget of entry.routes) {
51
64
  if (routeTarget === KEQ_ROUTES.ALL) continue;
52
65
  if (this.isKeqModuleClass(routeTarget)) {
53
- const targetRequest = moduleRef.get(routeTarget.KEQ_REQUEST, { strict: false });
66
+ const targetRequest = this.findInModulesContainer(routeTarget, modulesContainer);
54
67
  if (targetRequest) for (const mw of resolved) targetRequest.use(mw);
55
68
  } else {
56
69
  const route = this.buildRoute(routeTarget);
@@ -65,15 +78,41 @@ var KeqMiddlewareConsumerImpl = class {
65
78
  isNestMiddlewareClass(m) {
66
79
  return typeof m === "function" && m.prototype !== void 0 && "use" in m.prototype;
67
80
  }
68
- 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) {
69
88
  if (this.isNestMiddlewareClass(m)) {
70
- 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.`);
71
91
  return async (ctx, next) => {
72
92
  await instance.use(ctx, next);
73
93
  };
74
94
  }
75
95
  return m;
76
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
+ }
77
116
  buildRoute(info) {
78
117
  const routes = [];
79
118
  if (info.host) {
@@ -91,6 +130,17 @@ var KeqMiddlewareConsumerImpl = class {
91
130
  if (routes.length === 1) return routes[0];
92
131
  return (0, keq.composeRoute)(routes);
93
132
  }
133
+ wrapWithExclusion(middleware, excludedRoutes) {
134
+ const excluded = async (ctx, next) => {
135
+ if (await this.checkExcluded(ctx, excludedRoutes)) return next();
136
+ return middleware(ctx, next);
137
+ };
138
+ return excluded;
139
+ }
140
+ async checkExcluded(ctx, excludedRoutes) {
141
+ for (const route of excludedRoutes) if (await route(ctx)) return true;
142
+ return false;
143
+ }
94
144
  };
95
145
  //#endregion
96
146
  //#region src/utils/has-configure-keq-middleware.ts
@@ -112,21 +162,63 @@ function __decorate(decorators, target, key, desc) {
112
162
  }
113
163
  //#endregion
114
164
  //#region src/keq.module.ts
115
- 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
+ }
116
207
  let KeqModule = class KeqModule {
117
- constructor(modulesContainer, moduleRef, keqRequest) {
208
+ constructor(modulesContainer, keqRequest) {
118
209
  this.modulesContainer = modulesContainer;
119
- this.moduleRef = moduleRef;
120
210
  this.keqRequest = keqRequest;
211
+ validateConfigureKeqMiddlewareImports(modulesContainer);
121
212
  }
122
213
  onModuleInit() {
123
214
  for (const [, moduleWrapper] of this.modulesContainer) {
124
215
  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
- }
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);
130
222
  }
131
223
  }
132
224
  };
@@ -139,11 +231,7 @@ KeqModule = __decorate([
139
231
  }],
140
232
  exports: [keq.KeqRequest]
141
233
  }),
142
- __decorateMetadata("design:paramtypes", [
143
- typeof (_ref = typeof _nestjs_core.ModulesContainer !== "undefined" && _nestjs_core.ModulesContainer) === "function" ? _ref : Object,
144
- typeof (_ref2 = typeof _nestjs_core.ModuleRef !== "undefined" && _nestjs_core.ModuleRef) === "function" ? _ref2 : Object,
145
- typeof (_ref3 = typeof keq.KeqRequest !== "undefined" && keq.KeqRequest) === "function" ? _ref3 : Object
146
- ])
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])
147
235
  ], KeqModule);
148
236
  //#endregion
149
237
  exports.KEQ_ROUTES = KEQ_ROUTES;
@@ -153,5 +241,6 @@ Object.defineProperty(exports, "KeqModule", {
153
241
  return KeqModule;
154
242
  }
155
243
  });
244
+ exports.validateConfigureKeqMiddlewareImports = validateConfigureKeqMiddlewareImports;
156
245
 
157
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 { 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}\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 this.entries.push(entry)\n return {\n forRoutes: (...routes) => {\n entry.routes = routes\n return this\n },\n }\n }\n\n applyTo(keqRequest: KeqRequest, moduleRef: ModuleRef): void {\n for (const entry of this.entries) {\n const resolved = entry.middlewares.map((m) => this.resolveMiddleware(m, moduleRef))\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","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
@@ -13,20 +13,33 @@ var KeqMiddlewareConsumerImpl = class {
13
13
  middlewares,
14
14
  routes: []
15
15
  };
16
+ const excludedRouteInfos = [];
16
17
  this.entries.push(entry);
17
- return { forRoutes: (...routes) => {
18
- entry.routes = routes;
19
- return this;
20
- } };
18
+ const proxy = {
19
+ exclude: (...routes) => {
20
+ excludedRouteInfos.push(...routes);
21
+ return proxy;
22
+ },
23
+ forRoutes: (...routes) => {
24
+ entry.routes = routes;
25
+ if (excludedRouteInfos.length > 0) entry.excludedRouteInfos = excludedRouteInfos;
26
+ return this;
27
+ }
28
+ };
29
+ return proxy;
21
30
  }
22
- applyTo(keqRequest, moduleRef) {
31
+ applyTo(keqRequest, modulesContainer) {
23
32
  for (const entry of this.entries) {
24
- const resolved = entry.middlewares.map((m) => this.resolveMiddleware(m, moduleRef));
33
+ let resolved = entry.middlewares.map((m) => this.resolveMiddleware(m, modulesContainer));
34
+ if (entry.excludedRouteInfos && entry.excludedRouteInfos.length > 0) {
35
+ const excludedRoutes = entry.excludedRouteInfos.map((info) => this.buildRoute(info));
36
+ resolved = resolved.map((mw) => this.wrapWithExclusion(mw, excludedRoutes));
37
+ }
25
38
  if (entry.routes.length === 0 || entry.routes.some((r) => r === KEQ_ROUTES.ALL)) for (const mw of resolved) keqRequest.use(mw);
26
39
  else for (const routeTarget of entry.routes) {
27
40
  if (routeTarget === KEQ_ROUTES.ALL) continue;
28
41
  if (this.isKeqModuleClass(routeTarget)) {
29
- const targetRequest = moduleRef.get(routeTarget.KEQ_REQUEST, { strict: false });
42
+ const targetRequest = this.findInModulesContainer(routeTarget, modulesContainer);
30
43
  if (targetRequest) for (const mw of resolved) targetRequest.use(mw);
31
44
  } else {
32
45
  const route = this.buildRoute(routeTarget);
@@ -41,15 +54,41 @@ var KeqMiddlewareConsumerImpl = class {
41
54
  isNestMiddlewareClass(m) {
42
55
  return typeof m === "function" && m.prototype !== void 0 && "use" in m.prototype;
43
56
  }
44
- 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) {
45
64
  if (this.isNestMiddlewareClass(m)) {
46
- 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.`);
47
67
  return async (ctx, next) => {
48
68
  await instance.use(ctx, next);
49
69
  };
50
70
  }
51
71
  return m;
52
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
+ }
53
92
  buildRoute(info) {
54
93
  const routes = [];
55
94
  if (info.host) {
@@ -67,6 +106,17 @@ var KeqMiddlewareConsumerImpl = class {
67
106
  if (routes.length === 1) return routes[0];
68
107
  return composeRoute(routes);
69
108
  }
109
+ wrapWithExclusion(middleware, excludedRoutes) {
110
+ const excluded = async (ctx, next) => {
111
+ if (await this.checkExcluded(ctx, excludedRoutes)) return next();
112
+ return middleware(ctx, next);
113
+ };
114
+ return excluded;
115
+ }
116
+ async checkExcluded(ctx, excludedRoutes) {
117
+ for (const route of excludedRoutes) if (await route(ctx)) return true;
118
+ return false;
119
+ }
70
120
  };
71
121
  //#endregion
72
122
  //#region src/utils/has-configure-keq-middleware.ts
@@ -88,21 +138,63 @@ function __decorate(decorators, target, key, desc) {
88
138
  }
89
139
  //#endregion
90
140
  //#region src/keq.module.ts
91
- 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
+ }
92
183
  let KeqModule = class KeqModule {
93
- constructor(modulesContainer, moduleRef, keqRequest) {
184
+ constructor(modulesContainer, keqRequest) {
94
185
  this.modulesContainer = modulesContainer;
95
- this.moduleRef = moduleRef;
96
186
  this.keqRequest = keqRequest;
187
+ validateConfigureKeqMiddlewareImports(modulesContainer);
97
188
  }
98
189
  onModuleInit() {
99
190
  for (const [, moduleWrapper] of this.modulesContainer) {
100
191
  const instance = moduleWrapper.instance;
101
- if (hasConfigureKeqMiddleware(instance)) {
102
- const consumer = new KeqMiddlewareConsumerImpl();
103
- instance.configureKeqMiddleware(consumer);
104
- consumer.applyTo(this.keqRequest, this.moduleRef);
105
- }
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);
106
198
  }
107
199
  }
108
200
  };
@@ -115,13 +207,9 @@ KeqModule = __decorate([
115
207
  }],
116
208
  exports: [KeqRequest]
117
209
  }),
118
- __decorateMetadata("design:paramtypes", [
119
- typeof (_ref = typeof ModulesContainer !== "undefined" && ModulesContainer) === "function" ? _ref : Object,
120
- typeof (_ref2 = typeof ModuleRef !== "undefined" && ModuleRef) === "function" ? _ref2 : Object,
121
- typeof (_ref3 = typeof KeqRequest !== "undefined" && KeqRequest) === "function" ? _ref3 : Object
122
- ])
210
+ __decorateMetadata("design:paramtypes", [typeof (_ref = typeof ModulesContainer !== "undefined" && ModulesContainer) === "function" ? _ref : Object, typeof (_ref2 = typeof KeqRequest !== "undefined" && KeqRequest) === "function" ? _ref2 : Object])
123
211
  ], KeqModule);
124
212
  //#endregion
125
- export { KEQ_ROUTES, KeqModule };
213
+ export { KEQ_ROUTES, KeqModule, validateConfigureKeqMiddlewareImports };
126
214
 
127
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 { 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}\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 this.entries.push(entry)\n return {\n forRoutes: (...routes) => {\n entry.routes = routes\n return this\n },\n }\n }\n\n applyTo(keqRequest: KeqRequest, moduleRef: ModuleRef): void {\n for (const entry of this.entries) {\n const resolved = entry.middlewares.map((m) => this.resolveMiddleware(m, moduleRef))\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","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":";;;;;AAAA,MAAa,aAAa,EACxB,KAAK,OAAO,iBAAiB,EAC9B;;;ACaD,IAAa,4BAAb,MAAwE;CACtE,UAAqC,EAAE;CAEvC,MAAM,GAAG,aAAuF;EAC9F,MAAM,QAAyB;GAAE;GAAa,QAAQ,EAAE;GAAE;AAC1D,OAAK,QAAQ,KAAK,MAAM;AACxB,SAAO,EACL,YAAY,GAAG,WAAW;AACxB,SAAM,SAAS;AACf,UAAO;KAEV;;CAGH,QAAQ,YAAwB,WAA4B;AAC1D,OAAK,MAAM,SAAS,KAAK,SAAS;GAChC,MAAM,WAAW,MAAM,YAAY,KAAK,MAAM,KAAK,kBAAkB,GAAG,UAAU,CAAC;AAInF,OAHiB,MAAM,OAAO,WAAW,KACpC,MAAM,OAAO,MAAM,MAAM,MAAM,WAAW,IAAI,CAGjD,MAAK,MAAM,MAAM,SAAU,YAAW,IAAI,GAAG;OAE7C,MAAK,MAAM,eAAe,MAAM,QAAQ;AACtC,QAAI,gBAAgB,WAAW,IAAK;AAEpC,QAAI,KAAK,iBAAiB,YAAY,EAAE;KACtC,MAAM,gBAAgB,UAAU,IAAgB,YAAY,aAAa,EAAE,QAAQ,OAAO,CAAC;AAC3F,SAAI,cACF,MAAK,MAAM,MAAM,SAAU,eAAc,IAAI,GAAG;WAE7C;KACL,MAAM,QAAQ,KAAK,WAAW,YAA4B;AAC1D,gBAAW,WAAW,CAAC,MAAM,OAAO,SAAS;;;;;CAOvD,iBAAyB,OAAyC;AAChE,SACE,OAAO,UAAU,cACd,iBAAiB,SACjB,OAAQ,MAAkC,gBAAgB;;CAIjE,sBAA8B,GAA0E;AACtG,SAAO,OAAO,MAAM,cAAc,EAAE,cAAc,KAAA,KAAa,SAAS,EAAE;;CAG5E,kBAA0B,GAA4C,WAAqC;AACzG,MAAI,KAAK,sBAAsB,EAAE,EAAE;GACjC,MAAM,WAAW,UAAU,IAAuB,GAAG,EAAE,QAAQ,OAAO,CAAC;AACvE,UAAO,OAAO,KAAK,SAAS;AAC1B,UAAM,SAAS,IAAI,KAAK,KAAK;;;AAGjC,SAAO;;CAGT,WAAmB,MAA8B;EAC/C,MAAM,SAAqB,EAAE;AAC7B,MAAI,KAAK,MAAM;GACb,MAAM,OAAO,KAAK;AAClB,UAAO,MAAM,QAAQ,IAAI,QAAQ,IAAI,SAAS,KAAK;;AAErD,MAAI,KAAK,QAAQ;GACf,MAAM,SAAS,KAAK,OAAO,aAAa;AACxC,UAAO,MAAM,QAAQ,IAAI,QAAQ,OAAO,aAAa,KAAK,OAAO;;AAEnE,MAAI,KAAK,UAAU;GACjB,MAAM,UAAqC,UAAU,KAAK,SAAS;AACnE,UAAO,MAAM,QAAQ,QAAQ,IAAI,QAAQ,IAAI,SAAS,CAAC;;AAEzD,MAAI,OAAO,WAAW,EAAG,QAAO,OAAO;AACvC,SAAO,aAAa,OAAO;;;;;ACzF/B,SAAgB,0BAA0B,UAAoD;AAC5F,QACE,aAAa,QACV,aAAa,KAAA,KACb,OAAQ,SAAqC,2BAA2B;;;;;;;;;;;;;;;;;;ACKxE,IAAA,YAAA,MAAM,UAAkC;CAC7C,YACE,kBACA,WACA,YACA;AAHiB,OAAA,mBAAA;AACA,OAAA,YAAA;AACA,OAAA,aAAA;;CAGnB,eAAqB;AACnB,OAAK,MAAM,GAAG,kBAAkB,KAAK,kBAAkB;GACrD,MAAM,WAAW,cAAc;AAC/B,OAAI,0BAA0B,SAAS,EAAE;IACvC,MAAM,WAAW,IAAI,2BAA2B;AAChD,aAAS,uBAAuB,SAAS;AACzC,aAAS,QAAQ,KAAK,YAAY,KAAK,UAAU;;;;;;CAlBxD,QAAQ;CACR,OAAO;EACN,WAAW,CAAC;GAAE,SAAS;GAAY,kBAAkB,IAAI,YAAY;GAAE,CAAC;EACxE,SAAS,CAAC,WAAW;EACtB,CAAC"}
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.11",
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.11"
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.11"
51
+ "keq": "^5.0.0-beta.13"
50
52
  },
51
53
  "engines": {
52
54
  "node": ">=20.0.0"
@@ -10,11 +10,12 @@ 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
 
17
17
  export interface KeqMiddlewareConfigProxy {
18
+ exclude(...routes: KeqRouteInfo[]): KeqMiddlewareConfigProxy
18
19
  forRoutes(...routes: Array<typeof KEQ_ROUTES.ALL | KeqRouteInfo | KeqModuleClass>): KeqMiddlewareConsumer
19
20
  }
20
21
 
@@ -1,6 +1,6 @@
1
1
  import type { Type } from '@nestjs/common'
2
- import type { ModuleRef } from '@nestjs/core'
3
- import type { KeqMiddleware, KeqRoute } from 'keq'
2
+ import type { ModulesContainer } from '@nestjs/core'
3
+ import type { KeqExecutionContext, KeqMiddleware, KeqRoute } from 'keq'
4
4
  import { composeRoute, KeqRequest } from 'keq'
5
5
  import picomatch from 'picomatch'
6
6
  import { KEQ_ROUTES } from './constants.js'
@@ -11,6 +11,7 @@ import type { KeqNestMiddleware } from './interfaces/keq-nest-middleware.interfa
11
11
  interface MiddlewareEntry {
12
12
  middlewares: Array<Type<KeqNestMiddleware> | KeqMiddleware>
13
13
  routes: Array<typeof KEQ_ROUTES.ALL | KeqRouteInfo | KeqModuleClass>
14
+ excludedRouteInfos?: KeqRouteInfo[]
14
15
  }
15
16
 
16
17
  export class KeqMiddlewareConsumerImpl implements KeqMiddlewareConsumer {
@@ -18,18 +19,34 @@ export class KeqMiddlewareConsumerImpl implements KeqMiddlewareConsumer {
18
19
 
19
20
  apply(...middlewares: Array<Type<KeqNestMiddleware> | KeqMiddleware>): KeqMiddlewareConfigProxy {
20
21
  const entry: MiddlewareEntry = { middlewares, routes: [] }
22
+ const excludedRouteInfos: KeqRouteInfo[] = []
21
23
  this.entries.push(entry)
22
- return {
24
+
25
+ const proxy: KeqMiddlewareConfigProxy = {
26
+ exclude: (...routes) => {
27
+ excludedRouteInfos.push(...routes)
28
+ return proxy
29
+ },
23
30
  forRoutes: (...routes) => {
24
31
  entry.routes = routes
32
+ if (excludedRouteInfos.length > 0) {
33
+ entry.excludedRouteInfos = excludedRouteInfos
34
+ }
25
35
  return this
26
36
  },
27
37
  }
38
+ return proxy
28
39
  }
29
40
 
30
- applyTo(keqRequest: KeqRequest, moduleRef: ModuleRef): void {
41
+ applyTo(keqRequest: KeqRequest, modulesContainer: ModulesContainer): void {
31
42
  for (const entry of this.entries) {
32
- const resolved = entry.middlewares.map((m) => this.resolveMiddleware(m, moduleRef))
43
+ let resolved = entry.middlewares.map((m) => this.resolveMiddleware(m, modulesContainer))
44
+
45
+ if (entry.excludedRouteInfos && entry.excludedRouteInfos.length > 0) {
46
+ const excludedRoutes = entry.excludedRouteInfos.map((info) => this.buildRoute(info))
47
+ resolved = resolved.map((mw) => this.wrapWithExclusion(mw, excludedRoutes))
48
+ }
49
+
33
50
  const isGlobal = entry.routes.length === 0
34
51
  || entry.routes.some((r) => r === KEQ_ROUTES.ALL)
35
52
 
@@ -40,7 +57,7 @@ export class KeqMiddlewareConsumerImpl implements KeqMiddlewareConsumer {
40
57
  if (routeTarget === KEQ_ROUTES.ALL) continue
41
58
 
42
59
  if (this.isKeqModuleClass(routeTarget)) {
43
- const targetRequest = moduleRef.get<KeqRequest>(routeTarget.KEQ_REQUEST, { strict: false })
60
+ const targetRequest = this.findInModulesContainer(routeTarget, modulesContainer)
44
61
  if (targetRequest) {
45
62
  for (const mw of resolved) targetRequest.use(mw)
46
63
  }
@@ -65,9 +82,31 @@ export class KeqMiddlewareConsumerImpl implements KeqMiddlewareConsumer {
65
82
  return typeof m === 'function' && m.prototype !== undefined && 'use' in m.prototype
66
83
  }
67
84
 
68
- 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 {
69
102
  if (this.isNestMiddlewareClass(m)) {
70
- 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
+ }
71
110
  return async (ctx, next) => {
72
111
  await instance.use(ctx, next)
73
112
  }
@@ -75,6 +114,34 @@ export class KeqMiddlewareConsumerImpl implements KeqMiddlewareConsumer {
75
114
  return m
76
115
  }
77
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
+
78
145
  private buildRoute(info: KeqRouteInfo): KeqRoute {
79
146
  const routes: KeqRoute[] = []
80
147
  if (info.host) {
@@ -92,4 +159,21 @@ export class KeqMiddlewareConsumerImpl implements KeqMiddlewareConsumer {
92
159
  if (routes.length === 1) return routes[0]
93
160
  return composeRoute(routes)
94
161
  }
162
+
163
+ private wrapWithExclusion(middleware: KeqMiddleware, excludedRoutes: KeqRoute[]): KeqMiddleware {
164
+ const excluded: KeqMiddleware = async (ctx, next) => {
165
+ if (await this.checkExcluded(ctx, excludedRoutes)) {
166
+ return next()
167
+ }
168
+ return middleware(ctx, next)
169
+ }
170
+ return excluded
171
+ }
172
+
173
+ private async checkExcluded(ctx: KeqExecutionContext, excludedRoutes: KeqRoute[]): Promise<boolean> {
174
+ for (const route of excludedRoutes) {
175
+ if (await route(ctx)) return true
176
+ }
177
+ return false
178
+ }
95
179
  }
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
  }