@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 +35 -27
- package/README.md +37 -29
- package/dist/app.d.ts.map +1 -1
- package/dist/app.js +3 -1
- package/dist/conformance/platform-conformance.d.ts.map +1 -1
- package/dist/conformance/platform-conformance.js +95 -67
- package/dist/module.d.ts.map +1 -1
- package/dist/module.js +21 -46
- package/dist/portability/http-adapter-portability.d.ts.map +1 -1
- package/dist/portability/http-adapter-portability.js +28 -6
- package/dist/vitest/tooling.d.ts +2 -2
- package/dist/vitest/tooling.d.ts.map +1 -1
- package/dist/vitest/tooling.js +89 -34
- package/package.json +10 -10
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
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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,
|
|
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
|
-
|
|
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;
|
|
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
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
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
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
const
|
|
185
|
-
diagnostics
|
|
186
|
-
|
|
187
|
-
|
|
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
|
-
|
|
193
|
-
|
|
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
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
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
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
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() {
|
package/dist/module.d.ts.map
CHANGED
|
@@ -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,
|
|
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
|
|
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.
|
|
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
|
-
|
|
541
|
-
|
|
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
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
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
|
-
|
|
615
|
-
if (!this.
|
|
616
|
-
|
|
594
|
+
createModuleReplacements() {
|
|
595
|
+
if (!this.options.moduleReplacements && this.moduleReplacements.size === 0) {
|
|
596
|
+
return undefined;
|
|
617
597
|
}
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
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
|
-
|
|
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;
|
|
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) {
|
package/dist/vitest/tooling.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Collects
|
|
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
|
|
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":"
|
|
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"}
|
package/dist/vitest/tooling.js
CHANGED
|
@@ -1,62 +1,117 @@
|
|
|
1
|
-
import { existsSync,
|
|
2
|
-
import {
|
|
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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
-
|
|
18
|
-
|
|
53
|
+
const resolvedNestedTarget = resolveRuntimeExportTarget(nestedTarget);
|
|
54
|
+
if (resolvedNestedTarget) {
|
|
55
|
+
return resolvedNestedTarget;
|
|
19
56
|
}
|
|
20
57
|
}
|
|
21
|
-
return
|
|
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(
|
|
88
|
+
if (!existsSync(manifestPath)) {
|
|
31
89
|
continue;
|
|
32
90
|
}
|
|
33
91
|
const manifest = JSON.parse(readFileSync(manifestPath, 'utf8'));
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
|
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
|
|
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.
|
|
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.
|
|
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/
|
|
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.
|
|
96
|
-
"@fluojs/platform-
|
|
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-
|
|
99
|
-
"@fluojs/platform-
|
|
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",
|