@fluojs/testing 1.0.5 → 1.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.ko.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  <p><a href="./README.md"><kbd>English</kbd></a> <strong><kbd>한국어</kbd></strong></p>
4
4
 
5
- fluo 애플리케이션을 위한 기본 request-level 테스트 헬퍼, 모듈 구성, 프로바이더 오버라이드 유틸리티입니다.
5
+ Node.js 20+ fluo 애플리케이션을 위한 기본 request-level 테스트 헬퍼, 모듈 구성, 프로바이더 오버라이드 유틸리티입니다.
6
6
 
7
7
  `@fluojs/testing`은 fluo 애플리케이션 테스트를 위한 공식적인 기준(Baseline)을 제공합니다. 격리된 테스트 환경을 구축하고, 의존성을 가짜(Fake)나 목(Mock)으로 교체하며, 모듈 그래프에서 직접 컴포넌트를 resolve하거나 `createTestApp(...).request(...).send()`로 가상 HTTP 요청을 실행하여 e2e 스타일 테스트를 수행할 수 있게 합니다.
8
8
 
@@ -45,17 +45,19 @@ import { createTestApp } from '@fluojs/testing';
45
45
 
46
46
  const app = await createTestApp({ rootModule: AppModule });
47
47
 
48
- const response = await app
49
- .request('POST', '/users/')
50
- .header('x-request-id', 'test-request-1')
51
- .query('include', 'profile')
52
- .principal({ subject: 'user-1', roles: ['admin'] })
53
- .body({ name: 'Ada' })
54
- .send();
55
-
56
- expect(response.status).toBe(201);
57
-
58
- await app.close();
48
+ try {
49
+ const response = await app
50
+ .request('POST', '/users/')
51
+ .header('x-request-id', 'test-request-1')
52
+ .query('include', 'profile')
53
+ .principal({ subject: 'user-1', roles: ['admin'] })
54
+ .body({ name: 'Ada' })
55
+ .send();
56
+
57
+ expect(response.status).toBe(201);
58
+ } finally {
59
+ await app.close();
60
+ }
59
61
  ```
60
62
 
61
63
  애플리케이션 route, guard, interceptor, DTO validation, request body, query parameter, header, synthetic principal, serialized response를 검증하는 기본 HTTP/e2e 스타일 경로로는 `createTestApp({ rootModule })`을 사용하세요. 하나의 slice 안에서 module wiring, provider visibility, provider/guard/interceptor override가 계약일 때는 `createTestingModule(...)`을 사용합니다.
@@ -77,7 +79,7 @@ const module = await createTestingModule({ rootModule: AppModule })
77
79
  const service = await module.resolve(UserService);
78
80
  ```
79
81
 
80
- Testing builder는 route-pipeline 테스트에서 cross-cutting behavior를 교체할 수 있도록 `overrideGuard(...)`, `overrideInterceptor(...)`, `overrideFilter(...)`도 지원합니다.
82
+ Testing builder는 route-pipeline 테스트에서 cross-cutting behavior를 교체할 수 있도록 `overrideProviders([[token, value], ...])`, `overrideGuard(...)`, `overrideInterceptor(...)`, `overrideFilter(...)`도 지원합니다. Guard와 interceptor override는 route가 같은 token을 `@UseGuards(...)` 또는 `@UseInterceptors(...)`로 참조할 때 request path에서도 안전하게 검증할 수 있습니다. Filter override는 컴파일된 module graph의 token을 교체하므로, 해당 filter가 runtime app 표면에 등록되는 경우 request-level coverage와 함께 사용하세요.
81
83
 
82
84
  `compile()`은 lifecycle hook이 있는 singleton provider에 대해 production module bootstrap과 같은 의미를 따릅니다. effective provider graph를 해석하고, testing module을 반환하기 전에 provider 순서대로 각 instance의 `onModuleInit()`을 실행한 뒤 `onApplicationBootstrap()`을 실행합니다. `get()`은 synchronous singleton 및 multi-provider 경로에서도 DI ownership 의미를 보존하므로, 반복 sync read는 같은 singleton contribution을 재사용하고 `module.container.dispose()`가 해당 instance를 계속 정리할 수 있습니다.
83
85
 
@@ -101,20 +103,22 @@ import { createTestApp } from '@fluojs/testing';
101
103
 
102
104
  const app = await createTestApp({ rootModule: AppModule });
103
105
 
104
- const response = await app
105
- .request('POST', '/users/')
106
- .header('authorization', 'Bearer test-token')
107
- .query('include', ['profile', 'settings'])
108
- .principal({ subject: 'user-1', roles: ['member'] })
109
- .body({ name: 'Ada' })
110
- .send();
111
-
112
- expect(response.status).toBe(201);
113
-
114
- await app.close();
106
+ try {
107
+ const response = await app
108
+ .request('POST', '/users/')
109
+ .header('authorization', 'Bearer test-token')
110
+ .query('include', ['profile', 'settings'])
111
+ .principal({ subject: 'user-1', roles: ['member'] })
112
+ .body({ name: 'Ada' })
113
+ .send();
114
+
115
+ expect(response.status).toBe(201);
116
+ } finally {
117
+ await app.close();
118
+ }
115
119
  ```
116
120
 
117
- `app.request(...).send()`는 수동 `FrameworkRequest`/`FrameworkResponse` stub 없이 HTTP 의미에 가까운 테스트를 작성하게 해 주므로 애플리케이션 개발자의 기본 경로입니다. `app.dispatch(...)`, `makeRequest(...)`, raw `FluoFactory.create(...)` 테스트는 adapter/runtime contract, framework internal, 또는 low-level dispatch boundary 자체를 증명해야 하는 compatibility case에 남겨 둡니다.
121
+ `app.request(...).send()`는 수동 `FrameworkRequest`/`FrameworkResponse` stub 없이 HTTP 의미에 가까운 테스트를 작성하게 해 주므로 애플리케이션 개발자의 기본 경로입니다. Assertion 실패가 runtime resource 누수로 이어지지 않도록 반환된 app은 `finally` 블록에서 닫으세요. `app.dispatch(...)`, `makeRequest(...)`, raw `FluoFactory.create(...)` 테스트는 adapter/runtime contract, framework internal, 또는 low-level dispatch boundary 자체를 증명해야 하는 compatibility case에 남겨 둡니다.
118
122
 
119
123
  `createTestApp(...)`은 runtime HTTP bootstrap과 같은 application bootstrap option을 받습니다. 여기에는 `providers`, `filters`, `converters`, `interceptors`, `middleware`, `observers`, `versioning`, diagnostics option이 포함됩니다. 테스트 헬퍼는 request-context middleware를 앞에 추가하되, 호출자가 넘긴 middleware를 같은 app middleware chain 안에 보존합니다.
120
124
 
@@ -128,7 +132,7 @@ const repo = createMock<UserRepository>({ findById: vi.fn() });
128
132
  const mailer = createDeepMock(MailService);
129
133
  ```
130
134
 
131
- `asMock(value)`는 기존 값을 mock-friendly 타입으로 좁히고, `mockToken(token, value)`는 token 기반 dependency를 위한 provider override tuple을 만듭니다. `createMock(..., { strict: true })`는 지정하지 않은 member 접근을 거부합니다.
135
+ `asMock(value)`는 기존 값을 mock-friendly 타입으로 좁히고, `mockToken(token, value)`는 token 기반 dependency를 위한 provider override tuple을 만듭니다. `createMock(..., { strict: true })`는 지정하지 않은 member 접근을 거부합니다. `DeepMocked<T>`는 호환성을 위해 공용 testing type에서 노출되며 Vitest mock type boundary를 의도적으로 반영합니다. Vitest를 사용하지 않는 소비자는 `@fluojs/testing/app`, `@fluojs/testing/module`, 또는 harness subpath에서 non-mock helper만 import하세요.
132
136
 
133
137
  배포된 런타임 import가 안정적으로 해석되도록, mock 헬퍼를 사용할 워크스페이스에는 `vitest`를 함께 설치해야 합니다.
134
138
 
@@ -136,7 +140,7 @@ const mailer = createDeepMock(MailService);
136
140
 
137
141
  프레임워크 지향 플랫폼 패키지를 작성할 때는 `@fluojs/testing/platform-conformance`, `@fluojs/testing/http-adapter-portability`, `@fluojs/testing/web-runtime-adapter-portability` 같은 서브패스를 사용해 적합성 및 이식성 검증을 수행합니다.
138
142
 
139
- 이식성 하니스의 cleanup도 계약에 포함됩니다. 앱이 bootstrap된 뒤 setup, `listen()`, assertion이 실패하면 하니스는 해당 partial app을 닫습니다. `app.close()`가 실패하면 하니스는 cleanup 실패를 보고하며, setup 또는 assertion이 이미 실패한 경우에는 원래 실패와 cleanup 실패를 모두 보존하는 aggregate error를 발생시킵니다.
143
+ 이식성 하니스의 cleanup도 계약에 포함됩니다. 앱이 bootstrap된 뒤 setup, `listen()`, partial app을 노출한 run callback, assertion이 실패하면 하니스는 해당 partial app을 닫습니다. `app.close()`가 실패하면 하니스는 cleanup 실패를 보고하며, setup 또는 assertion이 이미 실패한 경우에는 원래 실패와 cleanup 실패를 모두 보존하는 aggregate error를 발생시킵니다.
140
144
 
141
145
  `HttpAdapterPortabilityHarness`와 web-runtime portability harness 메서드는 공개 어댑터 계약 체크입니다. 직접 같은 검증을 다시 만들기보다 `assertPreservesMalformedCookieValues()`, `assertSupportsSseStreaming()`, `assertPreservesRawBodyForJsonAndText()`, `assertPreservesExactRawBodyBytesForByteSensitivePayloads()`, `assertExcludesRawBodyForMultipart()`, `assertDefaultsMultipartTotalLimitToMaxBodySize()`, `assertSettlesStreamDrainWaitOnClose()`, `assertReportsConfiguredHostInStartupLogs()`, `assertReportsHttpsStartupUrl(...)`, `assertRemovesShutdownSignalListenersAfterClose()`처럼 초점이 분명한 assertion을 사용하세요.
142
146
 
@@ -170,6 +174,10 @@ fluo는 테스트가 명시적인 `rootModule`을 이름으로 지정해야 한
170
174
  - **하니스 서브패스**: `platform-conformance`, `http-adapter-portability`, `web-runtime-adapter-portability`, `fetch-style-websocket-conformance`
171
175
  - **도구 지원**: `@fluojs/testing/vitest`의 `fluoBabelDecoratorsPlugin()` 및 `@fluojs/testing/vitest/tooling`의 Vitest workspace config helper (`vitest`와 `@babel/core`를 함께 요구)
172
176
 
177
+ Package manifest는 `engines.node >=20.0.0`을 선언합니다. 문서화된 경우 non-Node runtime 애플리케이션 테스트에서 runtime-native 도구를 사용할 수 있지만, 배포된 `@fluojs/testing` 패키지 자체는 이 Node.js engine floor를 따릅니다.
178
+
179
+ `@fluojs/testing/vitest/tooling`은 각 package의 공개 `exports`에 선언된 entrypoint만 workspace alias로 매핑합니다. Private source file, internal helper, export되지 않은 source entrypoint는 의도적으로 제외하므로 테스트가 published package 소비자에게 제공되는 import boundary와 같은 경계를 검증합니다.
180
+
173
181
  ## 관련 패키지
174
182
 
175
183
  - `@fluojs/di`: 테스트 컨테이너가 사용하는 기반 DI 시스템입니다.
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  <p><strong><kbd>English</kbd></strong> <a href="./README.ko.md"><kbd>한국어</kbd></a></p>
4
4
 
5
- Default request-level testing helpers, testing module construction, and provider overrides for fluo applications.
5
+ Node.js 20+ request-level testing helpers, testing module construction, and provider overrides for fluo applications.
6
6
 
7
7
  ## Table of Contents
8
8
 
@@ -18,7 +18,7 @@ Default request-level testing helpers, testing module construction, and provider
18
18
  ## Installation
19
19
 
20
20
  ```bash
21
- npm install --save-dev @fluojs/testing vitest
21
+ pnpm add -D @fluojs/testing vitest
22
22
  ```
23
23
 
24
24
  `vitest` is a required peer dependency for the mock helpers and the `@fluojs/testing/vitest` entrypoint. `@babel/core` is declared as a peer because the Vitest decorators plugin loads Babel from the consuming workspace; package managers may surface that peer even when you only use the non-Vitest harness subpaths.
@@ -26,7 +26,7 @@ npm install --save-dev @fluojs/testing vitest
26
26
  If you use `@fluojs/testing/vitest`, install `@babel/core` in the consuming workspace as well because `fluoBabelDecoratorsPlugin()` invokes Babel at runtime. The Vitest plugin transforms `.ts`, `.tsx`, `.mts`, and `.cts` source ids after removing Vite query/hash suffixes, skips `node_modules`, and resolves the nearest root Babel config named `babel.config.cjs`, `babel.config.mjs`, `babel.config.js`, or `babel.config.json`:
27
27
 
28
28
  ```bash
29
- npm install --save-dev @babel/core
29
+ pnpm add -D @babel/core
30
30
  ```
31
31
 
32
32
  ## When to Use
@@ -43,17 +43,19 @@ import { createTestApp } from '@fluojs/testing';
43
43
 
44
44
  const app = await createTestApp({ rootModule: AppModule });
45
45
 
46
- const response = await app
47
- .request('POST', '/users/')
48
- .header('x-request-id', 'test-request-1')
49
- .query('include', 'profile')
50
- .principal({ subject: 'user-1', roles: ['admin'] })
51
- .body({ name: 'Ada' })
52
- .send();
53
-
54
- expect(response.status).toBe(201);
55
-
56
- await app.close();
46
+ try {
47
+ const response = await app
48
+ .request('POST', '/users/')
49
+ .header('x-request-id', 'test-request-1')
50
+ .query('include', 'profile')
51
+ .principal({ subject: 'user-1', roles: ['admin'] })
52
+ .body({ name: 'Ada' })
53
+ .send();
54
+
55
+ expect(response.status).toBe(201);
56
+ } finally {
57
+ await app.close();
58
+ }
57
59
  ```
58
60
 
59
61
  Use `createTestApp({ rootModule })` as the default HTTP/e2e-style path for application routes, guards, interceptors, DTO validation, request bodies, query parameters, headers, synthetic principals, and serialized responses. Reach for `createTestingModule(...)` when the contract is module wiring, provider visibility, or provider/guard/interceptor overrides inside one slice.
@@ -75,7 +77,7 @@ const module = await createTestingModule({ rootModule: AppModule })
75
77
  const service = await module.resolve(UserService);
76
78
  ```
77
79
 
78
- The testing builder also supports `overrideGuard(...)`, `overrideInterceptor(...)`, and `overrideFilter(...)` for route-pipeline tests that need to replace cross-cutting behavior.
80
+ The testing builder also supports `overrideProviders([[token, value], ...])`, `overrideGuard(...)`, `overrideInterceptor(...)`, and `overrideFilter(...)` for route-pipeline tests that need to replace cross-cutting behavior. Guard and interceptor overrides are request-path safe when the route references the same token via `@UseGuards(...)` or `@UseInterceptors(...)`; filter overrides replace the token in the compiled module graph and should be paired with request-level coverage where that filter is registered in the runtime app surface.
79
81
 
80
82
  `compile()` follows production module-bootstrap semantics for lifecycle-bearing singleton providers: it resolves the effective provider graph, runs `onModuleInit()` for each resolved instance, then runs `onApplicationBootstrap()` in the same provider order before the testing module is returned. `get()` keeps DI ownership semantics for synchronous singleton and multi-provider paths, so repeated sync reads reuse the same singleton contributions and `module.container.dispose()` can still clean them up.
81
83
 
@@ -99,20 +101,22 @@ import { createTestApp } from '@fluojs/testing';
99
101
 
100
102
  const app = await createTestApp({ rootModule: AppModule });
101
103
 
102
- const response = await app
103
- .request('POST', '/users/')
104
- .header('authorization', 'Bearer test-token')
105
- .query('include', ['profile', 'settings'])
106
- .principal({ subject: 'user-1', roles: ['member'] })
107
- .body({ name: 'Ada' })
108
- .send();
109
-
110
- expect(response.status).toBe(201);
111
-
112
- await app.close();
104
+ try {
105
+ const response = await app
106
+ .request('POST', '/users/')
107
+ .header('authorization', 'Bearer test-token')
108
+ .query('include', ['profile', 'settings'])
109
+ .principal({ subject: 'user-1', roles: ['member'] })
110
+ .body({ name: 'Ada' })
111
+ .send();
112
+
113
+ expect(response.status).toBe(201);
114
+ } finally {
115
+ await app.close();
116
+ }
113
117
  ```
114
118
 
115
- `app.request(...).send()` is the preferred app-developer path because it keeps tests close to HTTP semantics without manual `FrameworkRequest`/`FrameworkResponse` stubs. Keep `app.dispatch(...)`, `makeRequest(...)`, and raw `FluoFactory.create(...)` tests for adapter/runtime contracts, framework internals, or compatibility cases where the low-level dispatch boundary itself is what the test must prove.
119
+ `app.request(...).send()` is the preferred app-developer path because it keeps tests close to HTTP semantics without manual `FrameworkRequest`/`FrameworkResponse` stubs. Close the returned app from a `finally` block so assertion failures do not leak runtime resources. Keep `app.dispatch(...)`, `makeRequest(...)`, and raw `FluoFactory.create(...)` tests for adapter/runtime contracts, framework internals, or compatibility cases where the low-level dispatch boundary itself is what the test must prove.
116
120
 
117
121
  `createTestApp(...)` accepts the same application bootstrap options as the runtime HTTP bootstrap, including `providers`, `filters`, `converters`, `interceptors`, `middleware`, `observers`, `versioning`, and diagnostics options. The testing helper prepends its request-context middleware while preserving caller-provided middleware in the same app middleware chain.
118
122
 
@@ -126,7 +130,7 @@ const repo = createMock<UserRepository>({ findById: vi.fn() });
126
130
  const mailer = createDeepMock(MailService);
127
131
  ```
128
132
 
129
- `asMock(value)` narrows an existing value to a mock-friendly type, and `mockToken(token, value)` creates a provider override tuple for token-based dependencies. `createMock(..., { strict: true })` rejects access to unspecified members.
133
+ `asMock(value)` narrows an existing value to a mock-friendly type, and `mockToken(token, value)` creates a provider override tuple for token-based dependencies. `createMock(..., { strict: true })` rejects access to unspecified members. `DeepMocked<T>` is exposed from the shared testing types for compatibility and intentionally reflects the Vitest mock type boundary; consumers that do not use Vitest should import only non-mock helpers from `@fluojs/testing/app`, `@fluojs/testing/module`, or the harness subpaths.
130
134
 
131
135
  Install `vitest` in the consuming workspace before using the mock helpers so the published runtime import resolves consistently.
132
136
 
@@ -134,7 +138,7 @@ Install `vitest` in the consuming workspace before using the mock helpers so the
134
138
 
135
139
  Use subpaths like `@fluojs/testing/platform-conformance`, `@fluojs/testing/http-adapter-portability`, and `@fluojs/testing/web-runtime-adapter-portability` when authoring framework-facing platform packages.
136
140
 
137
- Portability harness cleanup is part of the contract: if setup, `listen()`, or an assertion fails after an app has been bootstrapped, the harness closes that partial app. If `app.close()` fails, the harness reports that cleanup failure, and when setup or an assertion already failed it raises an aggregate error that preserves both the original failure and the cleanup failure.
141
+ Portability harness cleanup is part of the contract: if setup, `listen()`, a run callback that surfaces a partial app, or an assertion fails after an app has been bootstrapped, the harness closes that partial app. If `app.close()` fails, the harness reports that cleanup failure, and when setup or an assertion already failed it raises an aggregate error that preserves both the original failure and the cleanup failure.
138
142
 
139
143
  `HttpAdapterPortabilityHarness` and web-runtime portability harness methods are the public adapter contract checks. Prefer focused assertions such as `assertPreservesMalformedCookieValues()`, `assertSupportsSseStreaming()`, `assertPreservesRawBodyForJsonAndText()`, `assertPreservesExactRawBodyBytesForByteSensitivePayloads()`, `assertExcludesRawBodyForMultipart()`, `assertDefaultsMultipartTotalLimitToMaxBodySize()`, `assertSettlesStreamDrainWaitOnClose()`, `assertReportsConfiguredHostInStartupLogs()`, `assertReportsHttpsStartupUrl(...)`, and `assertRemovesShutdownSignalListenersAfterClose()` instead of hand-rolled equivalents.
140
144
 
@@ -168,6 +172,10 @@ fluo differs from NestJS by requiring tests to name an explicit `rootModule`. Th
168
172
  - **Harness subpaths**: `platform-conformance`, `http-adapter-portability`, `web-runtime-adapter-portability`, `fetch-style-websocket-conformance`
169
173
  - **Tooling**: `@fluojs/testing/vitest` with `fluoBabelDecoratorsPlugin()` and `@fluojs/testing/vitest/tooling` with Vitest workspace config helpers (requires `vitest` and `@babel/core` in the consuming workspace)
170
174
 
175
+ The package manifest declares `engines.node >=20.0.0`. Non-Node runtime application tests can still use runtime-native tools where documented, but the published `@fluojs/testing` package itself is governed by that Node.js engine floor.
176
+
177
+ `@fluojs/testing/vitest/tooling` maps workspace aliases only for each package's declared public `exports`. Private source files, internal helpers, and unexported source entrypoints are intentionally excluded so tests exercise the same import boundaries that consumers receive from published packages.
178
+
171
179
  ## Related Packages
172
180
 
173
181
  - `@fluojs/di`: powers provider resolution in compiled test containers
package/dist/app.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"app.d.ts","sourceRoot":"","sources":["../src/app.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EACV,OAAO,EACP,yBAAyB,EAE1B,MAAM,YAAY,CAAC;AA6BpB;;;;;;;;;;;;GAYG;AACH,wBAAsB,aAAa,CAAC,OAAO,EAAE,yBAAyB,GAAG,OAAO,CAAC,OAAO,CAAC,CAyBxF"}
1
+ {"version":3,"file":"app.d.ts","sourceRoot":"","sources":["../src/app.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EACV,OAAO,EACP,yBAAyB,EAE1B,MAAM,YAAY,CAAC;AA6BpB;;;;;;;;;;;;GAYG;AACH,wBAAsB,aAAa,CAAC,OAAO,EAAE,yBAAyB,GAAG,OAAO,CAAC,OAAO,CAAC,CA2BxF"}
package/dist/app.js CHANGED
@@ -38,6 +38,7 @@ export async function createTestApp(options) {
38
38
  ...options,
39
39
  middleware: [createTestRequestContextMiddleware(), ...(options.middleware ?? [])]
40
40
  });
41
+ let closePromise;
41
42
  const request = (methodOrRequest, pathOrOptions, options) => {
42
43
  return createRequestBuilder(app.dispatcher, normalizeRequestInput(methodOrRequest, pathOrOptions, options));
43
44
  };
@@ -48,7 +49,8 @@ export async function createTestApp(options) {
48
49
  request,
49
50
  dispatch,
50
51
  close: async () => {
51
- await app.close();
52
+ closePromise ??= app.close();
53
+ await closePromise;
52
54
  }
53
55
  };
54
56
  }
@@ -1 +1 @@
1
- {"version":3,"file":"platform-conformance.d.ts","sourceRoot":"","sources":["../../src/conformance/platform-conformance.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AACjD,OAAO,KAAK,EACV,iBAAiB,EACjB,uBAAuB,EACvB,gBAAgB,EAChB,aAAa,EACb,wBAAwB,EACzB,MAAM,iBAAiB,CAAC;AAEzB;;GAEG;AACH,MAAM,WAAW,2BAA2B;IAC1C,eAAe,EAAE,MAAM,iBAAiB,CAAC;IACzC,UAAU,EAAE,CAAC,SAAS,EAAE,iBAAiB,KAAK,YAAY,CAAC,IAAI,CAAC,CAAC;IACjE,IAAI,EAAE,MAAM,CAAC;IACb,aAAa,CAAC,EAAE,aAAa,CAAC;CAC/B;AAED;;GAEG;AACH,MAAM,WAAW,qCAAqC;IACpD,OAAO,CAAC,EAAE,CACR,SAAS,EAAE,iBAAiB,EAC5B,UAAU,EAAE,wBAAwB,KACjC,YAAY,CAAC,SAAS,uBAAuB,EAAE,CAAC,CAAC;IACtD,aAAa,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAClC,2BAA2B,CAAC,EAAE,aAAa,CAAC,uBAAuB,CAAC,UAAU,CAAC,CAAC,CAAC;CAClF;AAED;;GAEG;AACH,MAAM,WAAW,kCAAkC;IACjD,gBAAgB,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACrC,OAAO,CAAC,EAAE,CAAC,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,OAAO,KAAK,OAAO,CAAC;IACrD,oBAAoB,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACzC,QAAQ,CAAC,EAAE,CAAC,QAAQ,EAAE,gBAAgB,KAAK,gBAAgB,CAAC;CAC7D;AAED;;GAEG;AACH,MAAM,WAAW,iCAAiC;IAChD,4BAA4B,CAAC,EAAE,CAAC,SAAS,EAAE,iBAAiB,KAAK,YAAY,CAAC,OAAO,CAAC,CAAC;IACvF,eAAe,EAAE,MAAM,iBAAiB,CAAC;IACzC,WAAW,CAAC,EAAE,qCAAqC,CAAC;IACpD,SAAS,CAAC,EAAE;QACV,QAAQ,EAAE,2BAA2B,CAAC;QACtC,MAAM,EAAE,2BAA2B,CAAC;KACrC,CAAC;IACF,QAAQ,CAAC,EAAE,kCAAkC,CAAC;CAC/C;AAmGD;;GAEG;AACH,qBAAa,0BAA0B;IACzB,OAAO,CAAC,QAAQ,CAAC,OAAO;gBAAP,OAAO,EAAE,iCAAiC;IAEjE,yCAAyC,IAAI,OAAO,CAAC,IAAI,CAAC;IA4B1D,0BAA0B,IAAI,OAAO,CAAC,IAAI,CAAC;IA6B3C,sBAAsB,IAAI,OAAO,CAAC,IAAI,CAAC;IAkCvC,2CAA2C,IAAI,OAAO,CAAC,IAAI,CAAC;IAoC5D,uBAAuB,IAAI,OAAO,CAAC,IAAI,CAAC;IA4CxC,uBAAuB,IAAI,OAAO,CAAC,IAAI,CAAC;IAgBxC,SAAS,IAAI,OAAO,CAAC,IAAI,CAAC;CAQjC;AAED;;;;;GAKG;AACH,wBAAgB,gCAAgC,CAC9C,OAAO,EAAE,iCAAiC,GACzC,0BAA0B,CAE5B"}
1
+ {"version":3,"file":"platform-conformance.d.ts","sourceRoot":"","sources":["../../src/conformance/platform-conformance.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AACjD,OAAO,KAAK,EACV,iBAAiB,EACjB,uBAAuB,EACvB,gBAAgB,EAChB,aAAa,EACb,wBAAwB,EACzB,MAAM,iBAAiB,CAAC;AAEzB;;GAEG;AACH,MAAM,WAAW,2BAA2B;IAC1C,eAAe,EAAE,MAAM,iBAAiB,CAAC;IACzC,UAAU,EAAE,CAAC,SAAS,EAAE,iBAAiB,KAAK,YAAY,CAAC,IAAI,CAAC,CAAC;IACjE,IAAI,EAAE,MAAM,CAAC;IACb,aAAa,CAAC,EAAE,aAAa,CAAC;CAC/B;AAED;;GAEG;AACH,MAAM,WAAW,qCAAqC;IACpD,OAAO,CAAC,EAAE,CACR,SAAS,EAAE,iBAAiB,EAC5B,UAAU,EAAE,wBAAwB,KACjC,YAAY,CAAC,SAAS,uBAAuB,EAAE,CAAC,CAAC;IACtD,aAAa,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAClC,2BAA2B,CAAC,EAAE,aAAa,CAAC,uBAAuB,CAAC,UAAU,CAAC,CAAC,CAAC;CAClF;AAED;;GAEG;AACH,MAAM,WAAW,kCAAkC;IACjD,gBAAgB,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACrC,OAAO,CAAC,EAAE,CAAC,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,OAAO,KAAK,OAAO,CAAC;IACrD,oBAAoB,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACzC,QAAQ,CAAC,EAAE,CAAC,QAAQ,EAAE,gBAAgB,KAAK,gBAAgB,CAAC;CAC7D;AAED;;GAEG;AACH,MAAM,WAAW,iCAAiC;IAChD,4BAA4B,CAAC,EAAE,CAAC,SAAS,EAAE,iBAAiB,KAAK,YAAY,CAAC,OAAO,CAAC,CAAC;IACvF,eAAe,EAAE,MAAM,iBAAiB,CAAC;IACzC,WAAW,CAAC,EAAE,qCAAqC,CAAC;IACpD,SAAS,CAAC,EAAE;QACV,QAAQ,EAAE,2BAA2B,CAAC;QACtC,MAAM,EAAE,2BAA2B,CAAC;KACrC,CAAC;IACF,QAAQ,CAAC,EAAE,kCAAkC,CAAC;CAC/C;AAmGD;;GAEG;AACH,qBAAa,0BAA0B;IACzB,OAAO,CAAC,QAAQ,CAAC,OAAO;gBAAP,OAAO,EAAE,iCAAiC;IAEjE,yCAAyC,IAAI,OAAO,CAAC,IAAI,CAAC;IAwC1D,0BAA0B,IAAI,OAAO,CAAC,IAAI,CAAC;IA6B3C,sBAAsB,IAAI,OAAO,CAAC,IAAI,CAAC;IAyCvC,2CAA2C,IAAI,OAAO,CAAC,IAAI,CAAC;IAoC5D,uBAAuB,IAAI,OAAO,CAAC,IAAI,CAAC;IAoDxC,uBAAuB,IAAI,OAAO,CAAC,IAAI,CAAC;IAwBxC,SAAS,IAAI,OAAO,CAAC,IAAI,CAAC;CAQjC;AAED;;;;;GAKG;AACH,wBAAgB,gCAAgC,CAC9C,OAAO,EAAE,iCAAiC,GACzC,0BAA0B,CAE5B"}
@@ -91,20 +91,27 @@ export class PlatformConformanceHarness {
91
91
  }
92
92
  async assertValidationHasNoLongLivedSideEffects() {
93
93
  const component = this.options.createComponent();
94
- const beforeState = component.state();
95
- const beforeEffects = this.options.captureValidationSideEffects ? await this.options.captureValidationSideEffects(component) : undefined;
96
- await component.validate();
97
- const afterState = component.state();
98
- if (beforeState !== afterState) {
99
- throw new Error(`validate() must not transition component state. Expected "${beforeState}" but received "${afterState}".`);
100
- }
101
- if (!this.options.captureValidationSideEffects) {
102
- return;
103
- }
104
- const compare = this.options.snapshot?.compare ?? defaultCompare;
105
- const afterEffects = await this.options.captureValidationSideEffects(component);
106
- if (!compare(beforeEffects, afterEffects)) {
107
- throw new Error('validate() introduced long-lived side effects.');
94
+ let assertionError;
95
+ try {
96
+ const beforeState = component.state();
97
+ const beforeEffects = this.options.captureValidationSideEffects ? await this.options.captureValidationSideEffects(component) : undefined;
98
+ await component.validate();
99
+ const afterState = component.state();
100
+ if (beforeState !== afterState) {
101
+ throw new Error(`validate() must not transition component state. Expected "${beforeState}" but received "${afterState}".`);
102
+ }
103
+ if (!this.options.captureValidationSideEffects) {
104
+ return;
105
+ }
106
+ const compare = this.options.snapshot?.compare ?? defaultCompare;
107
+ const afterEffects = await this.options.captureValidationSideEffects(component);
108
+ if (!compare(beforeEffects, afterEffects)) {
109
+ throw new Error('validate() introduced long-lived side effects.');
110
+ }
111
+ } catch (error) {
112
+ assertionError = error;
113
+ } finally {
114
+ await runCleanupWithAssertionContext(() => component.stop(), 'stop() after validation side-effect check', assertionError);
108
115
  }
109
116
  }
110
117
  async assertStartIsDeterministic() {
@@ -133,27 +140,34 @@ export class PlatformConformanceHarness {
133
140
  async assertStopIsIdempotent() {
134
141
  const component = this.options.createComponent();
135
142
  const compare = this.options.snapshot?.compare ?? defaultCompare;
136
- const startOutcome = await captureOutcome(() => component.start());
137
- if (!startOutcome.ok) {
138
- throw new Error(`stop() idempotency check requires a startable component: ${startOutcome.message}`);
139
- }
140
- const firstStop = await captureOutcome(() => component.stop());
141
- if (!firstStop.ok) {
142
- throw new Error(`first stop() call failed: ${firstStop.message}`);
143
- }
144
- const firstState = component.state();
145
- const firstSnapshot = component.snapshot();
146
- const secondStop = await captureOutcome(() => component.stop());
147
- if (!secondStop.ok) {
148
- throw new Error(`stop() is not idempotent: second call failed with "${secondStop.message}".`);
149
- }
150
- const secondState = component.state();
151
- const secondSnapshot = component.snapshot();
152
- if (firstState !== secondState) {
153
- throw new Error(`stop() changed state across duplicate calls (${firstState} -> ${secondState}).`);
154
- }
155
- if (!compare(firstSnapshot, secondSnapshot)) {
156
- throw new Error('stop() is not idempotent: duplicate calls changed component snapshot output.');
143
+ let assertionError;
144
+ try {
145
+ const startOutcome = await captureOutcome(() => component.start());
146
+ if (!startOutcome.ok) {
147
+ throw new Error(`stop() idempotency check requires a startable component: ${startOutcome.message}`);
148
+ }
149
+ const firstStop = await captureOutcome(() => component.stop());
150
+ if (!firstStop.ok) {
151
+ throw new Error(`first stop() call failed: ${firstStop.message}`);
152
+ }
153
+ const firstState = component.state();
154
+ const firstSnapshot = component.snapshot();
155
+ const secondStop = await captureOutcome(() => component.stop());
156
+ if (!secondStop.ok) {
157
+ throw new Error(`stop() is not idempotent: second call failed with "${secondStop.message}".`);
158
+ }
159
+ const secondState = component.state();
160
+ const secondSnapshot = component.snapshot();
161
+ if (firstState !== secondState) {
162
+ throw new Error(`stop() changed state across duplicate calls (${firstState} -> ${secondState}).`);
163
+ }
164
+ if (!compare(firstSnapshot, secondSnapshot)) {
165
+ throw new Error('stop() is not idempotent: duplicate calls changed component snapshot output.');
166
+ }
167
+ } catch (error) {
168
+ assertionError = error;
169
+ } finally {
170
+ await runCleanupWithAssertionContext(() => component.stop(), 'final stop() after stop() idempotency check', assertionError);
157
171
  }
158
172
  }
159
173
  async assertSnapshotSafeInDegradedAndFailedStates() {
@@ -178,44 +192,58 @@ export class PlatformConformanceHarness {
178
192
  }
179
193
  async assertStableDiagnostics() {
180
194
  const component = this.options.createComponent();
181
- const validation = await component.validate();
182
- const diagnostics = [...validation.issues, ...(validation.warnings ?? [])];
183
- if (this.options.diagnostics?.collect) {
184
- const extra = await this.options.diagnostics.collect(component, validation);
185
- diagnostics.push(...extra);
186
- }
187
- const requiredFixHintSeverities = this.options.diagnostics?.requireFixHintForSeverities ?? DEFAULT_REQUIRED_FIX_HINT_SEVERITIES;
188
- for (const issue of diagnostics) {
189
- if (issue.code.trim().length === 0) {
190
- throw new Error(`Diagnostics must provide a stable non-empty code. Received message: ${issue.message}`);
195
+ let assertionError;
196
+ try {
197
+ const validation = await component.validate();
198
+ const diagnostics = [...validation.issues, ...(validation.warnings ?? [])];
199
+ if (this.options.diagnostics?.collect) {
200
+ const extra = await this.options.diagnostics.collect(component, validation);
201
+ diagnostics.push(...extra);
191
202
  }
192
- if (requiredFixHintSeverities.includes(issue.severity) && (!issue.fixHint || issue.fixHint.trim().length === 0)) {
193
- throw new Error(`Diagnostic ${issue.code} (${issue.severity}) must provide a fixHint. Message: ${issue.message}`);
203
+ const requiredFixHintSeverities = this.options.diagnostics?.requireFixHintForSeverities ?? DEFAULT_REQUIRED_FIX_HINT_SEVERITIES;
204
+ for (const issue of diagnostics) {
205
+ if (issue.code.trim().length === 0) {
206
+ throw new Error(`Diagnostics must provide a stable non-empty code. Received message: ${issue.message}`);
207
+ }
208
+ if (requiredFixHintSeverities.includes(issue.severity) && (!issue.fixHint || issue.fixHint.trim().length === 0)) {
209
+ throw new Error(`Diagnostic ${issue.code} (${issue.severity}) must provide a fixHint. Message: ${issue.message}`);
210
+ }
194
211
  }
195
- }
196
- const expectedCodes = this.options.diagnostics?.expectedCodes;
197
- if (!expectedCodes) {
198
- return;
199
- }
200
- const normalizeCodes = codes => [...new Set(codes)].sort();
201
- const actualCodes = normalizeCodes(diagnostics.map(diagnostic => diagnostic.code));
202
- const normalizedExpectedCodes = normalizeCodes(expectedCodes);
203
- if (!defaultCompare(actualCodes, normalizedExpectedCodes)) {
204
- const actualDetails = diagnostics.map(diagnostic => `${diagnostic.code}(${diagnostic.severity}): ${diagnostic.message}`).join('; ');
205
- throw new Error(`Diagnostic code set changed. Expected [${normalizedExpectedCodes.join(', ')}] but received [${actualCodes.join(', ')}]. Actual diagnostics: ${actualDetails}`);
212
+ const expectedCodes = this.options.diagnostics?.expectedCodes;
213
+ if (!expectedCodes) {
214
+ return;
215
+ }
216
+ const normalizeCodes = codes => [...new Set(codes)].sort();
217
+ const actualCodes = normalizeCodes(diagnostics.map(diagnostic => diagnostic.code));
218
+ const normalizedExpectedCodes = normalizeCodes(expectedCodes);
219
+ if (!defaultCompare(actualCodes, normalizedExpectedCodes)) {
220
+ const actualDetails = diagnostics.map(diagnostic => `${diagnostic.code}(${diagnostic.severity}): ${diagnostic.message}`).join('; ');
221
+ throw new Error(`Diagnostic code set changed. Expected [${normalizedExpectedCodes.join(', ')}] but received [${actualCodes.join(', ')}]. Actual diagnostics: ${actualDetails}`);
222
+ }
223
+ } catch (error) {
224
+ assertionError = error;
225
+ } finally {
226
+ await runCleanupWithAssertionContext(() => component.stop(), 'stop() after stable diagnostics check', assertionError);
206
227
  }
207
228
  }
208
229
  async assertSnapshotSanitized() {
209
230
  const component = this.options.createComponent();
210
- const snapshot = component.snapshot();
211
- const sanitize = this.options.snapshot?.sanitize;
212
- const candidate = sanitize ? sanitize(snapshot) : snapshot;
213
- const forbiddenPatterns = this.options.snapshot?.forbiddenKeyPatterns ?? DEFAULT_FORBIDDEN_KEY_PATTERNS;
214
- const allowPatterns = this.options.snapshot?.allowKeyPatterns ?? [];
215
- const violations = [];
216
- collectForbiddenKeyPaths(candidate, forbiddenPatterns, allowPatterns, '', violations);
217
- if (violations.length > 0) {
218
- throw new Error(`snapshot() contains unsanitized keys: ${violations.join(', ')}`);
231
+ let assertionError;
232
+ try {
233
+ const snapshot = component.snapshot();
234
+ const sanitize = this.options.snapshot?.sanitize;
235
+ const candidate = sanitize ? sanitize(snapshot) : snapshot;
236
+ const forbiddenPatterns = this.options.snapshot?.forbiddenKeyPatterns ?? DEFAULT_FORBIDDEN_KEY_PATTERNS;
237
+ const allowPatterns = this.options.snapshot?.allowKeyPatterns ?? [];
238
+ const violations = [];
239
+ collectForbiddenKeyPaths(candidate, forbiddenPatterns, allowPatterns, '', violations);
240
+ if (violations.length > 0) {
241
+ throw new Error(`snapshot() contains unsanitized keys: ${violations.join(', ')}`);
242
+ }
243
+ } catch (error) {
244
+ assertionError = error;
245
+ } finally {
246
+ await runCleanupWithAssertionContext(() => component.stop(), 'stop() after snapshot sanitization check', assertionError);
219
247
  }
220
248
  }
221
249
  async assertAll() {
@@ -1 +1 @@
1
- {"version":3,"file":"module.d.ts","sourceRoot":"","sources":["../src/module.ts"],"names":[],"mappings":"AACA,OAAO,EAGL,KAAK,SAAS,EAKd,KAAK,QAAQ,EAEd,MAAM,YAAY,CAAC;AACpB,OAAO,KAAK,EAAqC,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAMrF,OAAO,KAAK,EAA2B,oBAAoB,EAAE,oBAAoB,EAAoB,MAAM,YAAY,CAAC;AAExH;;;;;GAKG;AACH,wBAAgB,sBAAsB,CAAC,UAAU,EAAE,UAAU,GAAG,QAAQ,EAAE,CAQzE;AAED;;;;;GAKG;AACH,wBAAgB,wBAAwB,CAAC,UAAU,EAAE,UAAU,GAAG,SAAS,EAAE,CAQ5E;AAED;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAAC,UAAU,EAAE,UAAU,GAAG,UAAU,EAAE,CAQzE;AA+vBD;;;;;;;;;;;;GAYG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,oBAAoB,GAAG,oBAAoB,CAEvF;AAED;;GAEG;AACH,eAAO,MAAM,IAAI;;CAEhB,CAAC"}
1
+ {"version":3,"file":"module.d.ts","sourceRoot":"","sources":["../src/module.ts"],"names":[],"mappings":"AACA,OAAO,EAGL,KAAK,SAAS,EAKd,KAAK,QAAQ,EAEd,MAAM,YAAY,CAAC;AACpB,OAAO,KAAK,EAGV,UAAU,EACX,MAAM,iBAAiB,CAAC;AAMzB,OAAO,KAAK,EAA2B,oBAAoB,EAAE,oBAAoB,EAAoB,MAAM,YAAY,CAAC;AAYxH;;;;;GAKG;AACH,wBAAgB,sBAAsB,CAAC,UAAU,EAAE,UAAU,GAAG,QAAQ,EAAE,CAQzE;AAED;;;;;GAKG;AACH,wBAAgB,wBAAwB,CAAC,UAAU,EAAE,UAAU,GAAG,SAAS,EAAE,CAQ5E;AAED;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAAC,UAAU,EAAE,UAAU,GAAG,UAAU,EAAE,CAQzE;AA6tBD;;;;;;;;;;;;GAYG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,oBAAoB,GAAG,oBAAoB,CAEvF;AAED;;GAEG;AACH,eAAO,MAAM,IAAI;;CAEhB,CAAC"}
package/dist/module.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { getModuleMetadata } from '@fluojs/core';
2
2
  import { isForwardRef, isOptionalToken, Scope } from '@fluojs/di';
3
- import { bootstrapModule, defineModule } from '@fluojs/runtime';
3
+ import { bootstrapModule } from '@fluojs/runtime';
4
4
  import { createDispatcher, createHandlerMapping } from '@fluojs/http';
5
5
  import { createTestRequestContextMiddleware, makeRequest } from './http.js';
6
6
  /**
@@ -469,7 +469,6 @@ class DefaultOverrideProviderBuilder {
469
469
  class DefaultTestingModuleBuilder {
470
470
  overrides = [];
471
471
  moduleReplacements = new Map();
472
- originalModuleDefinitions = new Map();
473
472
  constructor(options) {
474
473
  this.options = options;
475
474
  }
@@ -531,21 +530,14 @@ class DefaultTestingModuleBuilder {
531
530
  return this.createTestingModuleRef(bootstrapped, syncResolver);
532
531
  }
533
532
  bootstrapTestingModule() {
534
- const bootstrapped = this.bootstrapWithPatchedModuleImports();
533
+ const bootstrapped = this.bootstrapWithModuleReplacements();
535
534
  if (this.overrides.length > 0) {
536
535
  bootstrapped.container.override(...this.overrides);
537
536
  }
538
537
  return bootstrapped;
539
538
  }
540
- bootstrapWithPatchedModuleImports() {
541
- try {
542
- const rootModule = this._applyModuleReplacements(this.options.rootModule);
543
- return bootstrapModule(rootModule, {
544
- providers: this.options.providers
545
- });
546
- } finally {
547
- this.restorePatchedModuleImports();
548
- }
539
+ bootstrapWithModuleReplacements() {
540
+ return bootstrapModule(this.options.rootModule, this.createBootstrapModuleOptions());
549
541
  }
550
542
  createTestingModuleRef(bootstrapped, syncResolver) {
551
543
  const dispatcher = createTestingDispatcher(bootstrapped);
@@ -588,43 +580,26 @@ class DefaultTestingModuleBuilder {
588
580
  }
589
581
  };
590
582
  }
591
- _applyModuleReplacements(module) {
592
- if (this.moduleReplacements.size === 0) {
593
- return module;
594
- }
595
- const replacement = this.moduleReplacements.get(module);
596
- if (replacement) {
597
- return replacement;
598
- }
599
- const metadata = getModuleMetadata(module);
600
- if (!metadata?.imports || metadata.imports.length === 0) {
601
- return module;
602
- }
603
- const rewrittenImports = this.rewriteModuleImports(metadata.imports);
604
- const hasChange = rewrittenImports.some((imp, i) => imp !== metadata.imports[i]);
605
- if (!hasChange) {
606
- return module;
607
- }
608
- this.patchModuleImports(module, metadata, rewrittenImports);
609
- return module;
610
- }
611
- rewriteModuleImports(imports) {
612
- return imports.map(moduleImport => this._applyModuleReplacements(moduleImport));
583
+ createBootstrapModuleOptions() {
584
+ const moduleReplacements = this.createModuleReplacements();
585
+ return {
586
+ duplicateProviderPolicy: this.options.duplicateProviderPolicy,
587
+ logger: this.options.logger,
588
+ moduleGraphCache: this.options.moduleGraphCache,
589
+ moduleReplacements,
590
+ providers: this.options.providers,
591
+ validationTokens: this.options.validationTokens
592
+ };
613
593
  }
614
- patchModuleImports(module, metadata, imports) {
615
- if (!this.originalModuleDefinitions.has(module)) {
616
- this.originalModuleDefinitions.set(module, metadata);
594
+ createModuleReplacements() {
595
+ if (!this.options.moduleReplacements && this.moduleReplacements.size === 0) {
596
+ return undefined;
617
597
  }
618
- defineModule(module, {
619
- ...metadata,
620
- imports
621
- });
622
- }
623
- restorePatchedModuleImports() {
624
- for (const [module, metadata] of this.originalModuleDefinitions) {
625
- defineModule(module, metadata);
598
+ const moduleReplacements = new Map(this.options.moduleReplacements);
599
+ for (const [moduleType, replacement] of this.moduleReplacements) {
600
+ moduleReplacements.set(moduleType, replacement);
626
601
  }
627
- this.originalModuleDefinitions.clear();
602
+ return moduleReplacements;
628
603
  }
629
604
  }
630
605
 
@@ -1 +1 @@
1
- {"version":3,"file":"http-adapter-portability.d.ts","sourceRoot":"","sources":["../../src/portability/http-adapter-portability.ts"],"names":[],"mappings":"AAGA,OAAO,EAAwC,KAAK,UAAU,EAAE,KAAK,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAE3G,OAAO,QAAQ,cAAc,CAAC;IAC5B,UAAU,gBAAgB;QACxB,KAAK,CAAC,EAAE,YAAY,EAAE,CAAC;QACvB,OAAO,CAAC,EAAE,UAAU,CAAC;KACtB;CACF;AAED,KAAK,OAAO,GAAG;IACb,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACvB,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACzB,CAAC;AAUF;;;;;;GAMG;AACH,MAAM,WAAW,oCAAoC,CACnD,iBAAiB,SAAS,MAAM,EAChC,WAAW,SAAS,MAAM,EAC1B,IAAI,SAAS,OAAO,GAAG,OAAO;IAE9B;;;;;;OAMG;IACH,SAAS,EAAE,CAAC,UAAU,EAAE,UAAU,EAAE,OAAO,EAAE,iBAAiB,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAEjF;;OAEG;IACH,2BAA2B,CAAC,EAAE,MAAM,CAAC;IAErC;;OAEG;IACH,2BAA2B,CAAC,EAAE,CAAC,GAAG,EAAE,IAAI,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAElE;;OAEG;IACH,IAAI,EAAE,MAAM,CAAC;IAEb;;;;;;OAMG;IACH,GAAG,EAAE,CAAC,UAAU,EAAE,UAAU,EAAE,OAAO,EAAE,WAAW,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CACtE;AA6HD;;;;;;;GAOG;AACH,qBAAa,6BAA6B,CACxC,iBAAiB,SAAS,MAAM,EAChC,WAAW,SAAS,MAAM,EAC1B,IAAI,SAAS,OAAO,GAAG,OAAO;IAOlB,OAAO,CAAC,QAAQ,CAAC,OAAO;IALpC;;;;OAIG;gBAC0B,OAAO,EAAE,oCAAoC,CAAC,iBAAiB,EAAE,WAAW,EAAE,IAAI,CAAC;IAEhH;;;OAGG;IACG,oCAAoC,IAAI,OAAO,CAAC,IAAI,CAAC;IAiDrD,oCAAoC,IAAI,OAAO,CAAC,IAAI,CAAC;IAqErD,wDAAwD,IAAI,OAAO,CAAC,IAAI,CAAC;IA8CzE,iCAAiC,IAAI,OAAO,CAAC,IAAI,CAAC;IAsDlD,8CAA8C,IAAI,OAAO,CAAC,IAAI,CAAC;IAqD/D,0BAA0B,IAAI,OAAO,CAAC,IAAI,CAAC;IAkDjD;;;OAGG;IACG,mCAAmC,IAAI,OAAO,CAAC,IAAI,CAAC;IAyDpD,wCAAwC,IAAI,OAAO,CAAC,IAAI,CAAC;IAyCzD,4BAA4B,CAAC,KAAK,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IAyCjF,8CAA8C,IAAI,OAAO,CAAC,IAAI,CAAC;CA2CtE;AAED;;;;;;;;GAQG;AACH,wBAAgB,mCAAmC,CACjD,iBAAiB,SAAS,MAAM,EAChC,WAAW,SAAS,MAAM,EAC1B,IAAI,SAAS,OAAO,GAAG,OAAO,EAE9B,OAAO,EAAE,oCAAoC,CAAC,iBAAiB,EAAE,WAAW,EAAE,IAAI,CAAC,GAClF,6BAA6B,CAAC,iBAAiB,EAAE,WAAW,EAAE,IAAI,CAAC,CAErE"}
1
+ {"version":3,"file":"http-adapter-portability.d.ts","sourceRoot":"","sources":["../../src/portability/http-adapter-portability.ts"],"names":[],"mappings":"AAGA,OAAO,EAAwC,KAAK,UAAU,EAAE,KAAK,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAE3G,OAAO,QAAQ,cAAc,CAAC;IAC5B,UAAU,gBAAgB;QACxB,KAAK,CAAC,EAAE,YAAY,EAAE,CAAC;QACvB,OAAO,CAAC,EAAE,UAAU,CAAC;KACtB;CACF;AAED,KAAK,OAAO,GAAG;IACb,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACvB,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACzB,CAAC;AAcF;;;;;;GAMG;AACH,MAAM,WAAW,oCAAoC,CACnD,iBAAiB,SAAS,MAAM,EAChC,WAAW,SAAS,MAAM,EAC1B,IAAI,SAAS,OAAO,GAAG,OAAO;IAE9B;;;;;;OAMG;IACH,SAAS,EAAE,CAAC,UAAU,EAAE,UAAU,EAAE,OAAO,EAAE,iBAAiB,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAEjF;;OAEG;IACH,2BAA2B,CAAC,EAAE,MAAM,CAAC;IAErC;;OAEG;IACH,2BAA2B,CAAC,EAAE,CAAC,GAAG,EAAE,IAAI,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAElE;;OAEG;IACH,IAAI,EAAE,MAAM,CAAC;IAEb;;;;;;OAMG;IACH,GAAG,EAAE,CAAC,UAAU,EAAE,UAAU,EAAE,OAAO,EAAE,WAAW,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CACtE;AAqKD;;;;;;;GAOG;AACH,qBAAa,6BAA6B,CACxC,iBAAiB,SAAS,MAAM,EAChC,WAAW,SAAS,MAAM,EAC1B,IAAI,SAAS,OAAO,GAAG,OAAO;IAOlB,OAAO,CAAC,QAAQ,CAAC,OAAO;IALpC;;;;OAIG;gBAC0B,OAAO,EAAE,oCAAoC,CAAC,iBAAiB,EAAE,WAAW,EAAE,IAAI,CAAC;IAEhH;;;OAGG;IACG,oCAAoC,IAAI,OAAO,CAAC,IAAI,CAAC;IAiDrD,oCAAoC,IAAI,OAAO,CAAC,IAAI,CAAC;IAqErD,wDAAwD,IAAI,OAAO,CAAC,IAAI,CAAC;IA8CzE,iCAAiC,IAAI,OAAO,CAAC,IAAI,CAAC;IAsDlD,8CAA8C,IAAI,OAAO,CAAC,IAAI,CAAC;IAqD/D,0BAA0B,IAAI,OAAO,CAAC,IAAI,CAAC;IAkDjD;;;OAGG;IACG,mCAAmC,IAAI,OAAO,CAAC,IAAI,CAAC;IAyDpD,wCAAwC,IAAI,OAAO,CAAC,IAAI,CAAC;IA6CzD,4BAA4B,CAAC,KAAK,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IA6CjF,8CAA8C,IAAI,OAAO,CAAC,IAAI,CAAC;CA+CtE;AAED;;;;;;;;GAQG;AACH,wBAAgB,mCAAmC,CACjD,iBAAiB,SAAS,MAAM,EAChC,WAAW,SAAS,MAAM,EAC1B,IAAI,SAAS,OAAO,GAAG,OAAO,EAE9B,OAAO,EAAE,oCAAoC,CAAC,iBAAiB,EAAE,WAAW,EAAE,IAAI,CAAC,GAClF,6BAA6B,CAAC,iBAAiB,EAAE,WAAW,EAAE,IAAI,CAAC,CAErE"}
@@ -18,6 +18,13 @@ import { defineModule } from '@fluojs/runtime';
18
18
  function hasListenTarget(value) {
19
19
  return typeof value === 'object' && value !== null && 'getListenTarget' in value && typeof value.getListenTarget === 'function';
20
20
  }
21
+ function hasRejectedApp(value) {
22
+ if (typeof value !== 'object' || value === null || !('app' in value)) {
23
+ return false;
24
+ }
25
+ const app = Reflect.get(value, 'app');
26
+ return typeof app === 'object' && app !== null && 'close' in app && typeof Reflect.get(app, 'close') === 'function' && 'listen' in app && typeof Reflect.get(app, 'listen') === 'function';
27
+ }
21
28
  function resolveListeningUrl(app, adapterName) {
22
29
  const adapter = Reflect.get(app, 'adapter');
23
30
  if (!hasListenTarget(adapter)) {
@@ -100,6 +107,21 @@ async function prepareAndListenWithCleanup(app, adapterName, prepare) {
100
107
  throw setupError;
101
108
  }
102
109
  }
110
+ async function runApplicationWithRejectedAppCleanup(run, adapterName) {
111
+ try {
112
+ return await run();
113
+ } catch (runError) {
114
+ if (!hasRejectedApp(runError)) {
115
+ throw runError;
116
+ }
117
+ try {
118
+ await runError.app.close();
119
+ } catch (cleanupError) {
120
+ throw new AggregateError([runError, cleanupError], `${adapterName} adapter run() rejected and app.close() also failed during portability harness cleanup.`);
121
+ }
122
+ throw runError;
123
+ }
124
+ }
103
125
 
104
126
  /**
105
127
  * A portability harness for testing HTTP adapters to ensure they behave
@@ -558,12 +580,12 @@ export class HttpAdapterPortabilityHarness {
558
580
  defineModule(AppModule, {
559
581
  controllers: [_HealthController]
560
582
  });
561
- const app = await this.options.run(AppModule, {
583
+ const app = await runApplicationWithRejectedAppCleanup(() => this.options.run(AppModule, {
562
584
  cors: false,
563
585
  host: '127.0.0.1',
564
586
  logger,
565
587
  port: 0
566
- });
588
+ }), this.options.name);
567
589
  await runWithListeningUrlCleanup(app, this.options.name, async baseUrl => {
568
590
  const response = await fetch(`${baseUrl}/health`);
569
591
  if (response.status !== 200) {
@@ -607,13 +629,13 @@ export class HttpAdapterPortabilityHarness {
607
629
  defineModule(AppModule, {
608
630
  controllers: [_HealthController2]
609
631
  });
610
- const app = await this.options.run(AppModule, {
632
+ const app = await runApplicationWithRejectedAppCleanup(() => this.options.run(AppModule, {
611
633
  cors: false,
612
634
  host: '127.0.0.1',
613
635
  https,
614
636
  logger,
615
637
  port: 0
616
- });
638
+ }), this.options.name);
617
639
  await runWithListeningUrlCleanup(app, this.options.name, async baseUrl => {
618
640
  const response = await requestHttps(`${baseUrl}/health`);
619
641
  if (response.statusCode !== 200) {
@@ -663,12 +685,12 @@ export class HttpAdapterPortabilityHarness {
663
685
  });
664
686
  const signal = 'SIGTERM';
665
687
  const listenersBefore = new Set(process.listeners(signal));
666
- const app = await this.options.run(AppModule, {
688
+ const app = await runApplicationWithRejectedAppCleanup(() => this.options.run(AppModule, {
667
689
  cors: false,
668
690
  logger,
669
691
  port: 0,
670
692
  shutdownSignals: [signal]
671
- });
693
+ }), this.options.name);
672
694
  const registeredListeners = process.listeners(signal).filter(listener => !listenersBefore.has(listener));
673
695
  await runWithCleanup(app, this.options.name, async () => {
674
696
  if (registeredListeners.length === 0) {
@@ -1,8 +1,8 @@
1
1
  /**
2
- * Collects source-file aliases for a fluo monorepo checkout.
2
+ * Collects package-export aliases for a fluo monorepo checkout.
3
3
  *
4
4
  * @param repoRootUrl - Repository root as a file URL or absolute path URL string.
5
- * @returns A Vite/Vitest alias map that points public package imports at workspace source files.
5
+ * @returns A Vite/Vitest alias map that points exported package imports at workspace source files.
6
6
  */
7
7
  export declare function collectWorkspaceAliases(repoRootUrl: string | URL): Record<string, string>;
8
8
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"tooling.d.ts","sourceRoot":"","sources":["../../src/vitest/tooling.ts"],"names":[],"mappings":"AA8EA;;;;;GAKG;AACH,wBAAgB,uBAAuB,CAAC,WAAW,EAAE,MAAM,GAAG,GAAG,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAYzF;AAED;;;;;;GAMG;AACH,wBAAgB,+BAA+B,CAAC,WAAW,EAAE,MAAM,GAAG,GAAG,EAAE,SAAS,KAAK,uBAaxF;AAED;;;;GAIG;AACH,wBAAgB,sBAAsB,wBAErC"}
1
+ {"version":3,"file":"tooling.d.ts","sourceRoot":"","sources":["../../src/vitest/tooling.ts"],"names":[],"mappings":"AAmJA;;;;;GAKG;AACH,wBAAgB,uBAAuB,CAAC,WAAW,EAAE,MAAM,GAAG,GAAG,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAYzF;AAED;;;;;;GAMG;AACH,wBAAgB,+BAA+B,CAAC,WAAW,EAAE,MAAM,GAAG,GAAG,EAAE,SAAS,KAAK,uBAaxF;AAED;;;;GAIG;AACH,wBAAgB,sBAAsB,wBAErC"}
@@ -1,62 +1,117 @@
1
- import { existsSync, readFileSync, readdirSync } from 'node:fs';
2
- import { extname, join, relative, sep } from 'node:path';
1
+ import { existsSync, readdirSync, readFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
3
  import { fileURLToPath } from 'node:url';
4
4
  import { defineConfig, mergeConfig } from 'vitest/config';
5
5
  import { fluoBabelDecoratorsPlugin } from '../vitest.js';
6
6
  const workspaceAliasCache = new Map();
7
- function collectSourceEntries(sourceRoot) {
8
- const entries = [];
9
- for (const directoryEntry of readdirSync(sourceRoot, {
10
- withFileTypes: true
11
- })) {
12
- const entryPath = join(sourceRoot, directoryEntry.name);
13
- if (directoryEntry.isDirectory()) {
14
- entries.push(...collectSourceEntries(entryPath));
7
+ const runtimeExportConditions = ['import', 'default', 'node', 'browser', 'worker'];
8
+ const typeOnlyExportConditions = new Set(['types', 'typings']);
9
+ const javascriptExtensions = ['.js', '.mjs', '.cjs'];
10
+ function isRecord(value) {
11
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
12
+ }
13
+ function collectExportEntries(exportsField) {
14
+ if (typeof exportsField === 'string' || Array.isArray(exportsField)) {
15
+ return [['.', exportsField]];
16
+ }
17
+ if (!isRecord(exportsField)) {
18
+ return [];
19
+ }
20
+ const entries = Object.entries(exportsField);
21
+ const hasSubpathKeys = entries.some(([subpath]) => subpath === '.' || subpath.startsWith('./'));
22
+ if (!hasSubpathKeys) {
23
+ return [['.', exportsField]];
24
+ }
25
+ return entries.filter(([subpath]) => subpath === '.' || subpath.startsWith('./'));
26
+ }
27
+ function resolveRuntimeExportTarget(exportTarget) {
28
+ if (typeof exportTarget === 'string') {
29
+ return exportTarget;
30
+ }
31
+ if (Array.isArray(exportTarget)) {
32
+ for (const candidate of exportTarget) {
33
+ const resolvedCandidate = resolveRuntimeExportTarget(candidate);
34
+ if (resolvedCandidate) {
35
+ return resolvedCandidate;
36
+ }
37
+ }
38
+ return undefined;
39
+ }
40
+ if (!isRecord(exportTarget)) {
41
+ return undefined;
42
+ }
43
+ for (const condition of runtimeExportConditions) {
44
+ const resolvedCondition = resolveRuntimeExportTarget(exportTarget[condition]);
45
+ if (resolvedCondition) {
46
+ return resolvedCondition;
47
+ }
48
+ }
49
+ for (const [condition, nestedTarget] of Object.entries(exportTarget)) {
50
+ if (typeOnlyExportConditions.has(condition)) {
15
51
  continue;
16
52
  }
17
- if (directoryEntry.isFile()) {
18
- entries.push(entryPath);
53
+ const resolvedNestedTarget = resolveRuntimeExportTarget(nestedTarget);
54
+ if (resolvedNestedTarget) {
55
+ return resolvedNestedTarget;
19
56
  }
20
57
  }
21
- return entries;
58
+ return undefined;
59
+ }
60
+ function resolveSourcePathFromExportTarget(packageRoot, exportTarget) {
61
+ const runtimeTarget = resolveRuntimeExportTarget(exportTarget);
62
+ if (!runtimeTarget?.startsWith('./dist/')) {
63
+ return undefined;
64
+ }
65
+ for (const extension of javascriptExtensions) {
66
+ if (!runtimeTarget.endsWith(extension)) {
67
+ continue;
68
+ }
69
+ const sourcePath = join(packageRoot, 'src', `${runtimeTarget.slice('./dist/'.length, -extension.length)}.ts`);
70
+ if (existsSync(sourcePath)) {
71
+ return sourcePath;
72
+ }
73
+ }
74
+ return undefined;
75
+ }
76
+ function toAliasName(packageName, exportSubpath) {
77
+ if (exportSubpath === '.') {
78
+ return packageName;
79
+ }
80
+ return `${packageName}/${exportSubpath.slice('./'.length)}`;
22
81
  }
23
82
  function collectWorkspaceAliasesFromRoot(repoRoot) {
24
83
  const packagesRoot = join(repoRoot, 'packages');
25
84
  const aliases = {};
26
85
  for (const packageDirectoryName of readdirSync(packagesRoot)) {
27
86
  const packageRoot = join(packagesRoot, packageDirectoryName);
28
- const sourceRoot = join(packageRoot, 'src');
29
87
  const manifestPath = join(packageRoot, 'package.json');
30
- if (!existsSync(sourceRoot) || !existsSync(manifestPath)) {
88
+ if (!existsSync(manifestPath)) {
31
89
  continue;
32
90
  }
33
91
  const manifest = JSON.parse(readFileSync(manifestPath, 'utf8'));
34
- const scopeName = manifest.name ?? `@fluojs/${packageDirectoryName}`;
35
- for (const sourceEntryPath of collectSourceEntries(sourceRoot)) {
36
- const relativeSourceEntry = relative(sourceRoot, sourceEntryPath);
37
- if (extname(sourceEntryPath) !== '.ts' || relativeSourceEntry.endsWith('.test.ts') || relativeSourceEntry === 'index.ts') {
38
- continue;
39
- }
40
- const subpath = relativeSourceEntry.slice(0, -3).split(sep).join('/');
41
- aliases[`${scopeName}/${subpath}`] = sourceEntryPath;
42
- }
43
- const indexPath = join(sourceRoot, 'index.ts');
44
- if (existsSync(indexPath)) {
45
- aliases[scopeName] = indexPath;
92
+ const packageName = manifest.name ?? `@fluojs/${packageDirectoryName}`;
93
+ const exportAliases = collectExportEntries(manifest.exports).map(([exportSubpath, exportTarget]) => {
94
+ const sourcePath = resolveSourcePathFromExportTarget(packageRoot, exportTarget);
95
+ return sourcePath ? {
96
+ aliasName: toAliasName(packageName, exportSubpath),
97
+ sourcePath
98
+ } : undefined;
99
+ }).filter(entry => entry !== undefined).sort((left, right) => right.aliasName.length - left.aliasName.length);
100
+ for (const {
101
+ aliasName,
102
+ sourcePath
103
+ } of exportAliases) {
104
+ aliases[aliasName] = sourcePath;
46
105
  }
47
106
  }
48
- return {
49
- '@fluojs/runtime/internal/http-adapter': join(packagesRoot, 'runtime', 'src', 'internal-http-adapter.ts'),
50
- '@fluojs/runtime/internal/request-response-factory': join(packagesRoot, 'runtime', 'src', 'internal-request-response-factory.ts'),
51
- ...aliases
52
- };
107
+ return aliases;
53
108
  }
54
109
 
55
110
  /**
56
- * Collects source-file aliases for a fluo monorepo checkout.
111
+ * Collects package-export aliases for a fluo monorepo checkout.
57
112
  *
58
113
  * @param repoRootUrl - Repository root as a file URL or absolute path URL string.
59
- * @returns A Vite/Vitest alias map that points public package imports at workspace source files.
114
+ * @returns A Vite/Vitest alias map that points exported package imports at workspace source files.
60
115
  */
61
116
  export function collectWorkspaceAliases(repoRootUrl) {
62
117
  const repoRoot = fileURLToPath(repoRootUrl);
package/package.json CHANGED
@@ -9,7 +9,7 @@
9
9
  "override",
10
10
  "module-builder"
11
11
  ],
12
- "version": "1.0.5",
12
+ "version": "1.0.6",
13
13
  "private": false,
14
14
  "license": "MIT",
15
15
  "repository": {
@@ -80,11 +80,11 @@
80
80
  "dist"
81
81
  ],
82
82
  "dependencies": {
83
- "@fluojs/config": "^1.0.2",
84
- "@fluojs/http": "^1.1.0",
85
- "@fluojs/di": "^1.1.0",
83
+ "@fluojs/config": "^1.0.3",
86
84
  "@fluojs/core": "^1.0.3",
87
- "@fluojs/runtime": "^1.1.6"
85
+ "@fluojs/http": "^1.1.2",
86
+ "@fluojs/di": "^1.1.0",
87
+ "@fluojs/runtime": "^1.1.8"
88
88
  },
89
89
  "peerDependencies": {
90
90
  "@babel/core": ">=7.0.0",
@@ -92,12 +92,12 @@
92
92
  },
93
93
  "devDependencies": {
94
94
  "vitest": "^3.2.4",
95
- "@fluojs/platform-bun": "^1.0.5",
96
- "@fluojs/platform-deno": "^1.0.6",
95
+ "@fluojs/platform-bun": "^1.0.7",
96
+ "@fluojs/platform-cloudflare-workers": "^1.0.4",
97
+ "@fluojs/platform-deno": "^1.0.8",
97
98
  "@fluojs/platform-nodejs": "^1.0.5",
98
- "@fluojs/platform-cloudflare-workers": "^1.0.3",
99
- "@fluojs/platform-express": "^1.0.5",
100
- "@fluojs/platform-fastify": "^1.0.6"
99
+ "@fluojs/platform-express": "^1.0.6",
100
+ "@fluojs/platform-fastify": "^1.0.8"
101
101
  },
102
102
  "scripts": {
103
103
  "prebuild": "node ../../tooling/scripts/clean-dist.mjs",