@fluojs/testing 1.0.4 → 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,EAId,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;AAqyBD;;;;;;;;;;;;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
  /**
@@ -90,20 +90,8 @@ function normalizeOverride(token, value) {
90
90
  useValue: value
91
91
  };
92
92
  }
93
- function isContainerIntrospection(value) {
94
- if (typeof value !== 'object' || value === null) {
95
- return false;
96
- }
97
- const candidate = value;
98
- const parentValid = candidate.parent === undefined || isContainerIntrospection(candidate.parent);
99
- const requestScopeValid = candidate.requestScopeEnabled === undefined || typeof candidate.requestScopeEnabled === 'boolean';
100
- return candidate.registrations instanceof Map && candidate.multiRegistrations instanceof Map && candidate.multiSingletonCache instanceof Map && candidate.singletonCache instanceof Map && parentValid && requestScopeValid;
101
- }
102
93
  function toContainerIntrospection(container) {
103
- if (!isContainerIntrospection(container)) {
104
- throw new Error('Testing container introspection is unavailable for the current container implementation.');
105
- }
106
- return container;
94
+ return container.inspectResolutionState();
107
95
  }
108
96
  function isPromiseLike(value) {
109
97
  return (typeof value === 'object' || typeof value === 'function') && value !== null && typeof value.then === 'function';
@@ -481,7 +469,6 @@ class DefaultOverrideProviderBuilder {
481
469
  class DefaultTestingModuleBuilder {
482
470
  overrides = [];
483
471
  moduleReplacements = new Map();
484
- originalModuleDefinitions = new Map();
485
472
  constructor(options) {
486
473
  this.options = options;
487
474
  }
@@ -543,21 +530,14 @@ class DefaultTestingModuleBuilder {
543
530
  return this.createTestingModuleRef(bootstrapped, syncResolver);
544
531
  }
545
532
  bootstrapTestingModule() {
546
- const bootstrapped = this.bootstrapWithPatchedModuleImports();
533
+ const bootstrapped = this.bootstrapWithModuleReplacements();
547
534
  if (this.overrides.length > 0) {
548
535
  bootstrapped.container.override(...this.overrides);
549
536
  }
550
537
  return bootstrapped;
551
538
  }
552
- bootstrapWithPatchedModuleImports() {
553
- try {
554
- const rootModule = this._applyModuleReplacements(this.options.rootModule);
555
- return bootstrapModule(rootModule, {
556
- providers: this.options.providers
557
- });
558
- } finally {
559
- this.restorePatchedModuleImports();
560
- }
539
+ bootstrapWithModuleReplacements() {
540
+ return bootstrapModule(this.options.rootModule, this.createBootstrapModuleOptions());
561
541
  }
562
542
  createTestingModuleRef(bootstrapped, syncResolver) {
563
543
  const dispatcher = createTestingDispatcher(bootstrapped);
@@ -600,43 +580,26 @@ class DefaultTestingModuleBuilder {
600
580
  }
601
581
  };
602
582
  }
603
- _applyModuleReplacements(module) {
604
- if (this.moduleReplacements.size === 0) {
605
- return module;
606
- }
607
- const replacement = this.moduleReplacements.get(module);
608
- if (replacement) {
609
- return replacement;
610
- }
611
- const metadata = getModuleMetadata(module);
612
- if (!metadata?.imports || metadata.imports.length === 0) {
613
- return module;
614
- }
615
- const rewrittenImports = this.rewriteModuleImports(metadata.imports);
616
- const hasChange = rewrittenImports.some((imp, i) => imp !== metadata.imports[i]);
617
- if (!hasChange) {
618
- return module;
619
- }
620
- this.patchModuleImports(module, metadata, rewrittenImports);
621
- return module;
622
- }
623
- rewriteModuleImports(imports) {
624
- 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
+ };
625
593
  }
626
- patchModuleImports(module, metadata, imports) {
627
- if (!this.originalModuleDefinitions.has(module)) {
628
- this.originalModuleDefinitions.set(module, metadata);
594
+ createModuleReplacements() {
595
+ if (!this.options.moduleReplacements && this.moduleReplacements.size === 0) {
596
+ return undefined;
629
597
  }
630
- defineModule(module, {
631
- ...metadata,
632
- imports
633
- });
634
- }
635
- restorePatchedModuleImports() {
636
- for (const [module, metadata] of this.originalModuleDefinitions) {
637
- defineModule(module, metadata);
598
+ const moduleReplacements = new Map(this.options.moduleReplacements);
599
+ for (const [moduleType, replacement] of this.moduleReplacements) {
600
+ moduleReplacements.set(moduleType, replacement);
638
601
  }
639
- this.originalModuleDefinitions.clear();
602
+ return moduleReplacements;
640
603
  }
641
604
  }
642
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;AAgHD;;;;;;;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;IAoDzD,4BAA4B,CAAC,KAAK,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IAoDjF,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)) {
@@ -50,6 +57,17 @@ async function requestHttps(url) {
50
57
  request.end();
51
58
  });
52
59
  }
60
+ function createLogCaptureLogger() {
61
+ const messages = [];
62
+ const capture = (...args) => messages.push(args.map(arg => String(arg)).join(' '));
63
+ return {
64
+ debug: capture,
65
+ error: capture,
66
+ log: capture,
67
+ messages,
68
+ warn: capture
69
+ };
70
+ }
53
71
  async function runWithCleanup(app, adapterName, assertion) {
54
72
  let hasAssertionError = false;
55
73
  let assertionError;
@@ -89,6 +107,21 @@ async function prepareAndListenWithCleanup(app, adapterName, prepare) {
89
107
  throw setupError;
90
108
  }
91
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
+ }
92
125
 
93
126
  /**
94
127
  * A portability harness for testing HTTP adapters to ensure they behave
@@ -522,17 +555,7 @@ export class HttpAdapterPortabilityHarness {
522
555
  }
523
556
  async assertReportsConfiguredHostInStartupLogs() {
524
557
  let _initProto8, _initClass8;
525
- const loggerEvents = [];
526
- const logger = {
527
- debug() {},
528
- error(message, error, context) {
529
- loggerEvents.push(`error:${context}:${message}:${error instanceof Error ? error.message : 'none'}`);
530
- },
531
- log(message, context) {
532
- loggerEvents.push(`log:${context}:${message}`);
533
- },
534
- warn() {}
535
- };
558
+ const logger = createLogCaptureLogger();
536
559
  let _HealthController;
537
560
  class HealthController {
538
561
  static {
@@ -557,12 +580,12 @@ export class HttpAdapterPortabilityHarness {
557
580
  defineModule(AppModule, {
558
581
  controllers: [_HealthController]
559
582
  });
560
- const app = await this.options.run(AppModule, {
583
+ const app = await runApplicationWithRejectedAppCleanup(() => this.options.run(AppModule, {
561
584
  cors: false,
562
585
  host: '127.0.0.1',
563
586
  logger,
564
587
  port: 0
565
- });
588
+ }), this.options.name);
566
589
  await runWithListeningUrlCleanup(app, this.options.name, async baseUrl => {
567
590
  const response = await fetch(`${baseUrl}/health`);
568
591
  if (response.status !== 200) {
@@ -574,25 +597,14 @@ export class HttpAdapterPortabilityHarness {
574
597
  })) {
575
598
  throw new Error(`${this.options.name} adapter changed host-bound response payload.`);
576
599
  }
577
- const expectedLog = `log:FluoFactory:Listening on ${baseUrl}`;
578
- if (!loggerEvents.includes(expectedLog)) {
600
+ if (!logger.messages.some(message => message.includes(`Listening on ${baseUrl}`))) {
579
601
  throw new Error(`${this.options.name} adapter changed startup host logging.`);
580
602
  }
581
603
  });
582
604
  }
583
605
  async assertReportsHttpsStartupUrl(https) {
584
606
  let _initProto9, _initClass9;
585
- const loggerEvents = [];
586
- const logger = {
587
- debug() {},
588
- error(message, error, context) {
589
- loggerEvents.push(`error:${context}:${message}:${error instanceof Error ? error.message : 'none'}`);
590
- },
591
- log(message, context) {
592
- loggerEvents.push(`log:${context}:${message}`);
593
- },
594
- warn() {}
595
- };
607
+ const logger = createLogCaptureLogger();
596
608
  let _HealthController2;
597
609
  class HealthController {
598
610
  static {
@@ -617,13 +629,13 @@ export class HttpAdapterPortabilityHarness {
617
629
  defineModule(AppModule, {
618
630
  controllers: [_HealthController2]
619
631
  });
620
- const app = await this.options.run(AppModule, {
632
+ const app = await runApplicationWithRejectedAppCleanup(() => this.options.run(AppModule, {
621
633
  cors: false,
622
634
  host: '127.0.0.1',
623
635
  https,
624
636
  logger,
625
637
  port: 0
626
- });
638
+ }), this.options.name);
627
639
  await runWithListeningUrlCleanup(app, this.options.name, async baseUrl => {
628
640
  const response = await requestHttps(`${baseUrl}/health`);
629
641
  if (response.statusCode !== 200) {
@@ -634,8 +646,7 @@ export class HttpAdapterPortabilityHarness {
634
646
  })) {
635
647
  throw new Error(`${this.options.name} adapter changed HTTPS response payload semantics.`);
636
648
  }
637
- const expectedLog = `log:FluoFactory:Listening on ${baseUrl}`;
638
- if (!loggerEvents.includes(expectedLog)) {
649
+ if (!logger.messages.some(message => message.includes(`Listening on ${baseUrl}`))) {
639
650
  throw new Error(`${this.options.name} adapter changed HTTPS startup logging.`);
640
651
  }
641
652
  });
@@ -674,12 +685,12 @@ export class HttpAdapterPortabilityHarness {
674
685
  });
675
686
  const signal = 'SIGTERM';
676
687
  const listenersBefore = new Set(process.listeners(signal));
677
- const app = await this.options.run(AppModule, {
688
+ const app = await runApplicationWithRejectedAppCleanup(() => this.options.run(AppModule, {
678
689
  cors: false,
679
690
  logger,
680
691
  port: 0,
681
692
  shutdownSignals: [signal]
682
- });
693
+ }), this.options.name);
683
694
  const registeredListeners = process.listeners(signal).filter(listener => !listenersBefore.has(listener));
684
695
  await runWithCleanup(app, this.options.name, async () => {
685
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":"AA4EA;;;;;GAKG;AACH,wBAAgB,uBAAuB,CAAC,WAAW,EAAE,MAAM,GAAG,GAAG,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAEzF;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,64 +1,131 @@
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
- function collectSourceEntries(sourceRoot) {
7
- const entries = [];
8
- for (const directoryEntry of readdirSync(sourceRoot, {
9
- withFileTypes: true
10
- })) {
11
- const entryPath = join(sourceRoot, directoryEntry.name);
12
- if (directoryEntry.isDirectory()) {
13
- entries.push(...collectSourceEntries(entryPath));
6
+ const workspaceAliasCache = new Map();
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)) {
14
51
  continue;
15
52
  }
16
- if (directoryEntry.isFile()) {
17
- entries.push(entryPath);
53
+ const resolvedNestedTarget = resolveRuntimeExportTarget(nestedTarget);
54
+ if (resolvedNestedTarget) {
55
+ return resolvedNestedTarget;
18
56
  }
19
57
  }
20
- 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)}`;
21
81
  }
22
82
  function collectWorkspaceAliasesFromRoot(repoRoot) {
23
83
  const packagesRoot = join(repoRoot, 'packages');
24
84
  const aliases = {};
25
85
  for (const packageDirectoryName of readdirSync(packagesRoot)) {
26
86
  const packageRoot = join(packagesRoot, packageDirectoryName);
27
- const sourceRoot = join(packageRoot, 'src');
28
87
  const manifestPath = join(packageRoot, 'package.json');
29
- if (!existsSync(sourceRoot) || !existsSync(manifestPath)) {
88
+ if (!existsSync(manifestPath)) {
30
89
  continue;
31
90
  }
32
91
  const manifest = JSON.parse(readFileSync(manifestPath, 'utf8'));
33
- const scopeName = manifest.name ?? `@fluojs/${packageDirectoryName}`;
34
- for (const sourceEntryPath of collectSourceEntries(sourceRoot)) {
35
- const relativeSourceEntry = relative(sourceRoot, sourceEntryPath);
36
- if (extname(sourceEntryPath) !== '.ts' || relativeSourceEntry.endsWith('.test.ts') || relativeSourceEntry === 'index.ts') {
37
- continue;
38
- }
39
- const subpath = relativeSourceEntry.slice(0, -3).split(sep).join('/');
40
- aliases[`${scopeName}/${subpath}`] = sourceEntryPath;
41
- }
42
- const indexPath = join(sourceRoot, 'index.ts');
43
- if (existsSync(indexPath)) {
44
- 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;
45
105
  }
46
106
  }
47
- return {
48
- '@fluojs/runtime/internal/http-adapter': join(packagesRoot, 'runtime', 'src', 'internal-http-adapter.ts'),
49
- '@fluojs/runtime/internal/request-response-factory': join(packagesRoot, 'runtime', 'src', 'internal-request-response-factory.ts'),
50
- ...aliases
51
- };
107
+ return aliases;
52
108
  }
53
109
 
54
110
  /**
55
- * Collects source-file aliases for a fluo monorepo checkout.
111
+ * Collects package-export aliases for a fluo monorepo checkout.
56
112
  *
57
113
  * @param repoRootUrl - Repository root as a file URL or absolute path URL string.
58
- * @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.
59
115
  */
60
116
  export function collectWorkspaceAliases(repoRootUrl) {
61
- return collectWorkspaceAliasesFromRoot(fileURLToPath(repoRootUrl));
117
+ const repoRoot = fileURLToPath(repoRootUrl);
118
+ const cachedAliases = workspaceAliasCache.get(repoRoot);
119
+ if (cachedAliases) {
120
+ return {
121
+ ...cachedAliases
122
+ };
123
+ }
124
+ const aliases = collectWorkspaceAliasesFromRoot(repoRoot);
125
+ workspaceAliasCache.set(repoRoot, aliases);
126
+ return {
127
+ ...aliases
128
+ };
62
129
  }
63
130
 
64
131
  /**
package/package.json CHANGED
@@ -9,7 +9,7 @@
9
9
  "override",
10
10
  "module-builder"
11
11
  ],
12
- "version": "1.0.4",
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",
83
+ "@fluojs/config": "^1.0.3",
84
84
  "@fluojs/core": "^1.0.3",
85
- "@fluojs/http": "^1.1.0",
86
- "@fluojs/di": "^1.0.3",
87
- "@fluojs/runtime": "^1.1.2"
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.3",
96
- "@fluojs/platform-cloudflare-workers": "^1.0.3",
97
- "@fluojs/platform-deno": "^1.0.4",
98
- "@fluojs/platform-nodejs": "^1.0.3",
99
- "@fluojs/platform-express": "^1.0.3",
100
- "@fluojs/platform-fastify": "^1.0.4"
95
+ "@fluojs/platform-bun": "^1.0.7",
96
+ "@fluojs/platform-cloudflare-workers": "^1.0.4",
97
+ "@fluojs/platform-deno": "^1.0.8",
98
+ "@fluojs/platform-nodejs": "^1.0.5",
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",