@fluojs/testing 1.0.2 → 1.0.4
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 +5 -5
- package/README.md +5 -5
- package/dist/module.d.ts.map +1 -1
- package/dist/module.js +154 -7
- package/dist/portability/http-adapter-portability.d.ts.map +1 -1
- package/dist/portability/http-adapter-portability.js +49 -61
- package/dist/vitest/tooling.d.ts +22 -0
- package/dist/vitest/tooling.d.ts.map +1 -0
- package/dist/vitest/tooling.js +90 -0
- package/package.json +15 -11
package/README.ko.md
CHANGED
|
@@ -79,6 +79,8 @@ const service = await module.resolve(UserService);
|
|
|
79
79
|
|
|
80
80
|
Testing builder는 route-pipeline 테스트에서 cross-cutting behavior를 교체할 수 있도록 `overrideGuard(...)`, `overrideInterceptor(...)`, `overrideFilter(...)`도 지원합니다.
|
|
81
81
|
|
|
82
|
+
`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
|
+
|
|
82
84
|
### `overrideModule()` 사용 시 모듈 identity 보존
|
|
83
85
|
|
|
84
86
|
`createTestingModule({ rootModule })`에는 명시적인 루트 모듈이 필요합니다. 그래야 테스트가 프로덕션 bootstrap과 같은 모듈 그래프 형태를 컴파일합니다. `overrideModule(source, replacement)`로 import된 모듈을 교체해도, 컴파일된 testing module은 provider 해석에 replacement import를 사용하면서 원래 `rootModule`과 컴파일된 `modules[].type` identity를 보존합니다. 따라서 diagnostics, graph assertion, module introspection 헬퍼는 테스트 전용 synthetic wrapper 클래스가 아니라 사용자가 작성한 애플리케이션 모듈 클래스에 계속 연결됩니다.
|
|
@@ -163,12 +165,10 @@ fluo는 테스트가 명시적인 `rootModule`을 이름으로 지정해야 한
|
|
|
163
165
|
|
|
164
166
|
## 공개 API
|
|
165
167
|
|
|
166
|
-
- **루트 패키지**: `createTestingModule(...)`, `createTestApp(...)`, 모듈 introspection 헬퍼, 공용 테스트 타입
|
|
167
|
-
- **서브패스**: `@fluojs/testing/app`, `@fluojs/testing/module`, `@fluojs/testing/http`, `@fluojs/testing/mock`, `@fluojs/testing/types`, `@fluojs/testing/vitest`
|
|
168
|
-
- **Mock 서브패스**: `@fluojs/testing/mock`
|
|
169
|
-
- **HTTP 헬퍼**: `@fluojs/testing/http`
|
|
168
|
+
- **루트 패키지**: `createTestingModule(...)`, `Test.createTestingModule(...)`, `createTestApp(...)`, 모듈 introspection 헬퍼, 공용 테스트 타입
|
|
169
|
+
- **서브패스**: `@fluojs/testing/app`, `@fluojs/testing/module`, `@fluojs/testing/http`, `@fluojs/testing/mock`, `@fluojs/testing/types`, `@fluojs/testing/vitest`, `@fluojs/testing/vitest/tooling`
|
|
170
170
|
- **하니스 서브패스**: `platform-conformance`, `http-adapter-portability`, `web-runtime-adapter-portability`, `fetch-style-websocket-conformance`
|
|
171
|
-
- **도구 지원**: `@fluojs/testing/vitest
|
|
171
|
+
- **도구 지원**: `@fluojs/testing/vitest`의 `fluoBabelDecoratorsPlugin()` 및 `@fluojs/testing/vitest/tooling`의 Vitest workspace config helper (`vitest`와 `@babel/core`를 함께 요구)
|
|
172
172
|
|
|
173
173
|
## 관련 패키지
|
|
174
174
|
|
package/README.md
CHANGED
|
@@ -77,6 +77,8 @@ const service = await module.resolve(UserService);
|
|
|
77
77
|
|
|
78
78
|
The testing builder also supports `overrideGuard(...)`, `overrideInterceptor(...)`, and `overrideFilter(...)` for route-pipeline tests that need to replace cross-cutting behavior.
|
|
79
79
|
|
|
80
|
+
`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
|
+
|
|
80
82
|
### Preserve module identity with `overrideModule()`
|
|
81
83
|
|
|
82
84
|
`createTestingModule({ rootModule })` requires an explicit root module so tests compile the same module graph shape that production bootstrap uses. When `overrideModule(source, replacement)` swaps imported modules, the compiled testing module preserves the original `rootModule` and compiled `modules[].type` identities while using the replacement imports for provider resolution. This keeps diagnostics, graph assertions, and module-introspection helpers tied to the application module classes you authored instead of synthetic test-only wrapper classes.
|
|
@@ -161,12 +163,10 @@ fluo differs from NestJS by requiring tests to name an explicit `rootModule`. Th
|
|
|
161
163
|
|
|
162
164
|
## Public API
|
|
163
165
|
|
|
164
|
-
- **Root package**: `createTestingModule(...)`, `createTestApp(...)`, module introspection helpers, shared testing types
|
|
165
|
-
- **Subpaths**: `@fluojs/testing/app`, `@fluojs/testing/module`, `@fluojs/testing/http`, `@fluojs/testing/mock`, `@fluojs/testing/types`, `@fluojs/testing/vitest`
|
|
166
|
-
- **Mock subpath**: `@fluojs/testing/mock`
|
|
167
|
-
- **HTTP helpers**: `@fluojs/testing/http`
|
|
166
|
+
- **Root package**: `createTestingModule(...)`, `Test.createTestingModule(...)`, `createTestApp(...)`, module introspection helpers, shared testing types
|
|
167
|
+
- **Subpaths**: `@fluojs/testing/app`, `@fluojs/testing/module`, `@fluojs/testing/http`, `@fluojs/testing/mock`, `@fluojs/testing/types`, `@fluojs/testing/vitest`, `@fluojs/testing/vitest/tooling`
|
|
168
168
|
- **Harness subpaths**: `platform-conformance`, `http-adapter-portability`, `web-runtime-adapter-portability`, `fetch-style-websocket-conformance`
|
|
169
|
-
- **Tooling**: `@fluojs/testing/vitest` with `fluoBabelDecoratorsPlugin()` (requires `vitest` and `@babel/core` in the consuming workspace)
|
|
169
|
+
- **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
170
|
|
|
171
171
|
## Related Packages
|
|
172
172
|
|
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,EAId,KAAK,QAAQ,
|
|
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"}
|
package/dist/module.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { getModuleMetadata } from '@fluojs/core';
|
|
2
|
-
import { isForwardRef, isOptionalToken } from '@fluojs/di';
|
|
2
|
+
import { isForwardRef, isOptionalToken, Scope } from '@fluojs/di';
|
|
3
3
|
import { bootstrapModule, defineModule } from '@fluojs/runtime';
|
|
4
4
|
import { createDispatcher, createHandlerMapping } from '@fluojs/http';
|
|
5
5
|
import { createTestRequestContextMiddleware, makeRequest } from './http.js';
|
|
@@ -97,7 +97,7 @@ function isContainerIntrospection(value) {
|
|
|
97
97
|
const candidate = value;
|
|
98
98
|
const parentValid = candidate.parent === undefined || isContainerIntrospection(candidate.parent);
|
|
99
99
|
const requestScopeValid = candidate.requestScopeEnabled === undefined || typeof candidate.requestScopeEnabled === 'boolean';
|
|
100
|
-
return candidate.registrations instanceof Map && candidate.multiRegistrations instanceof Map && candidate.singletonCache instanceof Map && parentValid && requestScopeValid;
|
|
100
|
+
return candidate.registrations instanceof Map && candidate.multiRegistrations instanceof Map && candidate.multiSingletonCache instanceof Map && candidate.singletonCache instanceof Map && parentValid && requestScopeValid;
|
|
101
101
|
}
|
|
102
102
|
function toContainerIntrospection(container) {
|
|
103
103
|
if (!isContainerIntrospection(container)) {
|
|
@@ -108,6 +108,108 @@ function toContainerIntrospection(container) {
|
|
|
108
108
|
function isPromiseLike(value) {
|
|
109
109
|
return (typeof value === 'object' || typeof value === 'function') && value !== null && typeof value.then === 'function';
|
|
110
110
|
}
|
|
111
|
+
function hasLifecycleHook(value, hookName) {
|
|
112
|
+
return (typeof value === 'object' || typeof value === 'function') && value !== null && typeof value[hookName] === 'function';
|
|
113
|
+
}
|
|
114
|
+
function hasAnyBootstrapLifecycleHook(value) {
|
|
115
|
+
return hasLifecycleHook(value, 'onModuleInit') || hasLifecycleHook(value, 'onApplicationBootstrap');
|
|
116
|
+
}
|
|
117
|
+
function providerToken(provider) {
|
|
118
|
+
return isProviderDescriptor(provider) ? provider.provide : provider;
|
|
119
|
+
}
|
|
120
|
+
function effectiveProvidersForToken(introspection, token) {
|
|
121
|
+
const multiProviders = collectMultiProviders(introspection, token);
|
|
122
|
+
if (multiProviders.length > 0) {
|
|
123
|
+
return multiProviders;
|
|
124
|
+
}
|
|
125
|
+
const provider = lookupProvider(introspection, token);
|
|
126
|
+
return provider ? [provider] : [];
|
|
127
|
+
}
|
|
128
|
+
function isSingletonLifecycleProvider(provider) {
|
|
129
|
+
if (provider.scope !== Scope.DEFAULT) {
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
return provider.type === 'class' || provider.type === 'value' && hasAnyBootstrapLifecycleHook(provider.useValue);
|
|
133
|
+
}
|
|
134
|
+
async function resolveLifecycleDependency(entry, bootstrapped, introspection) {
|
|
135
|
+
if (isOptionalToken(entry) && !hasTokenInContainer(introspection, entry.token)) {
|
|
136
|
+
return undefined;
|
|
137
|
+
}
|
|
138
|
+
return bootstrapped.container.resolve(dependencyToken(entry));
|
|
139
|
+
}
|
|
140
|
+
async function instantiateLifecycleProvider(provider, bootstrapped, introspection) {
|
|
141
|
+
if (provider.type !== 'class' || !provider.useClass) {
|
|
142
|
+
throw new Error(`Lifecycle provider ${String(provider.provide)} must use a class provider.`);
|
|
143
|
+
}
|
|
144
|
+
const dependencies = await Promise.all(provider.inject.map(entry => resolveLifecycleDependency(entry, bootstrapped, introspection)));
|
|
145
|
+
return new provider.useClass(...dependencies);
|
|
146
|
+
}
|
|
147
|
+
async function resolveMultiLifecycleProvider(provider, bootstrapped, introspection) {
|
|
148
|
+
const cache = rootContainerIntrospection(introspection).multiSingletonCache;
|
|
149
|
+
const cached = cache.get(provider);
|
|
150
|
+
if (cached) {
|
|
151
|
+
return cached;
|
|
152
|
+
}
|
|
153
|
+
const instance = instantiateLifecycleProvider(provider, bootstrapped, introspection).catch(error => {
|
|
154
|
+
cache.delete(provider);
|
|
155
|
+
throw error;
|
|
156
|
+
});
|
|
157
|
+
cache.set(provider, instance);
|
|
158
|
+
return instance;
|
|
159
|
+
}
|
|
160
|
+
async function resolveTestingLifecycleInstances(bootstrapped, overrides = []) {
|
|
161
|
+
const lifecycleProviders = [...bootstrapped.effectiveProviders.runtimeProviders, ...bootstrapped.effectiveProviders.moduleProviders, ...overrides];
|
|
162
|
+
const instances = [];
|
|
163
|
+
const seenProviders = new Set();
|
|
164
|
+
const introspection = toContainerIntrospection(bootstrapped.container);
|
|
165
|
+
for (const provider of lifecycleProviders) {
|
|
166
|
+
const token = providerToken(provider);
|
|
167
|
+
const effectiveProviders = effectiveProvidersForToken(introspection, token);
|
|
168
|
+
for (let index = 0; index < effectiveProviders.length; index += 1) {
|
|
169
|
+
const effectiveProvider = effectiveProviders[index];
|
|
170
|
+
if (!effectiveProvider) {
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
if (seenProviders.has(effectiveProvider)) {
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
seenProviders.add(effectiveProvider);
|
|
177
|
+
if (!isSingletonLifecycleProvider(effectiveProvider)) {
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
if (effectiveProvider.type === 'value') {
|
|
181
|
+
instances.push(effectiveProvider.useValue);
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
try {
|
|
185
|
+
if (effectiveProvider.multi === true) {
|
|
186
|
+
instances.push(await resolveMultiLifecycleProvider(effectiveProvider, bootstrapped, introspection));
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
instances.push(await bootstrapped.container.resolve(token));
|
|
190
|
+
} catch (error) {
|
|
191
|
+
if (error instanceof Error && error.message.includes('Request-scoped provider')) {
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
throw error;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
return instances;
|
|
199
|
+
}
|
|
200
|
+
async function runTestingBootstrapLifecycle(bootstrapped, overrides = []) {
|
|
201
|
+
const instances = await resolveTestingLifecycleInstances(bootstrapped, overrides);
|
|
202
|
+
for (const instance of instances) {
|
|
203
|
+
if (hasLifecycleHook(instance, 'onModuleInit')) {
|
|
204
|
+
await instance.onModuleInit();
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
for (const instance of instances) {
|
|
208
|
+
if (hasLifecycleHook(instance, 'onApplicationBootstrap')) {
|
|
209
|
+
await instance.onApplicationBootstrap();
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
111
213
|
function rootContainerIntrospection(target) {
|
|
112
214
|
return target.parent ? rootContainerIntrospection(target.parent) : target;
|
|
113
215
|
}
|
|
@@ -124,7 +226,10 @@ function lookupProvider(target, token) {
|
|
|
124
226
|
return target.parent ? lookupProvider(target.parent, token) : undefined;
|
|
125
227
|
}
|
|
126
228
|
function hasToken(state, token) {
|
|
127
|
-
return
|
|
229
|
+
return hasTokenInContainer(state.introspection, token);
|
|
230
|
+
}
|
|
231
|
+
function hasTokenInContainer(introspection, token) {
|
|
232
|
+
return lookupProvider(introspection, token) !== undefined || collectMultiProviders(introspection, token).length > 0;
|
|
128
233
|
}
|
|
129
234
|
function dependencyToken(entry) {
|
|
130
235
|
if (isOptionalToken(entry)) {
|
|
@@ -190,6 +295,20 @@ function canPromoteCachedSingleton(state, token) {
|
|
|
190
295
|
const provider = lookupProvider(state.introspection, token);
|
|
191
296
|
return provider !== undefined && provider.scope !== 'request' && providerGraphIsSyncResolvable(state, token);
|
|
192
297
|
}
|
|
298
|
+
function providerGraphForProviderIsSyncResolvable(state, provider, visited = new Set()) {
|
|
299
|
+
if (provider.type === 'factory') {
|
|
300
|
+
return state.factoryResolutionKinds.get(provider) === 'sync';
|
|
301
|
+
}
|
|
302
|
+
if (provider.type === 'existing') {
|
|
303
|
+
return provider.useExisting !== undefined && providerGraphIsSyncResolvable(state, provider.useExisting, visited);
|
|
304
|
+
}
|
|
305
|
+
return provider.inject.every(entry => {
|
|
306
|
+
if (isOptionalToken(entry) && !hasToken(state, entry.token)) {
|
|
307
|
+
return true;
|
|
308
|
+
}
|
|
309
|
+
return providerGraphIsSyncResolvable(state, dependencyToken(entry), visited);
|
|
310
|
+
});
|
|
311
|
+
}
|
|
193
312
|
function resolveSyncDependency(entry, state) {
|
|
194
313
|
if (isOptionalToken(entry)) {
|
|
195
314
|
if (!hasToken(state, entry.token)) {
|
|
@@ -259,6 +378,24 @@ function resolveSyncProvider(provider, state) {
|
|
|
259
378
|
state.singletonCache.set(provider.provide, Promise.resolve(instance));
|
|
260
379
|
return instance;
|
|
261
380
|
}
|
|
381
|
+
function resolveSyncMultiProvider(provider, state) {
|
|
382
|
+
if (provider.scope === 'request' && !state.introspection.requestScopeEnabled) {
|
|
383
|
+
throw new Error(`Request-scoped provider ${String(provider.provide)} cannot be resolved outside request scope.`);
|
|
384
|
+
}
|
|
385
|
+
if (provider.scope === 'transient') {
|
|
386
|
+
return instantiateSyncProvider(provider, state);
|
|
387
|
+
}
|
|
388
|
+
if (state.syncMultiSingletonValues.has(provider)) {
|
|
389
|
+
return state.syncMultiSingletonValues.get(provider);
|
|
390
|
+
}
|
|
391
|
+
if (state.multiSingletonCache.has(provider)) {
|
|
392
|
+
throw new Error(`Token ${String(provider.provide)} was already resolved asynchronously. Use resolve() instead of get() for this provider.`);
|
|
393
|
+
}
|
|
394
|
+
const instance = instantiateSyncProvider(provider, state);
|
|
395
|
+
state.syncMultiSingletonValues.set(provider, instance);
|
|
396
|
+
state.multiSingletonCache.set(provider, Promise.resolve(instance));
|
|
397
|
+
return instance;
|
|
398
|
+
}
|
|
262
399
|
function resolveSyncToken(token, state) {
|
|
263
400
|
if (state.resolutionChain.has(token)) {
|
|
264
401
|
throw new Error(`Circular dependency detected while resolving token ${String(token)} via get().`);
|
|
@@ -267,7 +404,7 @@ function resolveSyncToken(token, state) {
|
|
|
267
404
|
try {
|
|
268
405
|
const multiProviders = collectMultiProviders(state.introspection, token);
|
|
269
406
|
if (multiProviders.length > 0) {
|
|
270
|
-
return multiProviders.map(provider =>
|
|
407
|
+
return multiProviders.map(provider => resolveSyncMultiProvider(provider, state));
|
|
271
408
|
}
|
|
272
409
|
const provider = lookupProvider(state.introspection, token);
|
|
273
410
|
if (!provider) {
|
|
@@ -285,8 +422,10 @@ function createSyncResolver(container) {
|
|
|
285
422
|
const state = {
|
|
286
423
|
factoryResolutionKinds,
|
|
287
424
|
introspection,
|
|
425
|
+
multiSingletonCache: rootContainerIntrospection(introspection).multiSingletonCache,
|
|
288
426
|
resolutionChain: new Set(),
|
|
289
427
|
singletonCache: rootContainerIntrospection(introspection).singletonCache,
|
|
428
|
+
syncMultiSingletonValues: new Map(),
|
|
290
429
|
syncSingletonValues: new Map()
|
|
291
430
|
};
|
|
292
431
|
return {
|
|
@@ -298,6 +437,12 @@ function createSyncResolver(container) {
|
|
|
298
437
|
}
|
|
299
438
|
state.syncSingletonValues.set(token, await promise);
|
|
300
439
|
}
|
|
440
|
+
for (const [provider, promise] of state.multiSingletonCache) {
|
|
441
|
+
if (!providerGraphForProviderIsSyncResolvable(state, provider)) {
|
|
442
|
+
continue;
|
|
443
|
+
}
|
|
444
|
+
state.syncMultiSingletonValues.set(provider, await promise);
|
|
445
|
+
}
|
|
301
446
|
}
|
|
302
447
|
};
|
|
303
448
|
}
|
|
@@ -392,7 +537,10 @@ class DefaultTestingModuleBuilder {
|
|
|
392
537
|
}
|
|
393
538
|
async compile() {
|
|
394
539
|
const bootstrapped = this.bootstrapTestingModule();
|
|
395
|
-
|
|
540
|
+
const syncResolver = createSyncResolver(bootstrapped.container);
|
|
541
|
+
await runTestingBootstrapLifecycle(bootstrapped, this.overrides);
|
|
542
|
+
await syncResolver.syncFromContainer();
|
|
543
|
+
return this.createTestingModuleRef(bootstrapped, syncResolver);
|
|
396
544
|
}
|
|
397
545
|
bootstrapTestingModule() {
|
|
398
546
|
const bootstrapped = this.bootstrapWithPatchedModuleImports();
|
|
@@ -411,9 +559,8 @@ class DefaultTestingModuleBuilder {
|
|
|
411
559
|
this.restorePatchedModuleImports();
|
|
412
560
|
}
|
|
413
561
|
}
|
|
414
|
-
createTestingModuleRef(bootstrapped) {
|
|
562
|
+
createTestingModuleRef(bootstrapped, syncResolver) {
|
|
415
563
|
const dispatcher = createTestingDispatcher(bootstrapped);
|
|
416
|
-
const syncResolver = createSyncResolver(bootstrapped.container);
|
|
417
564
|
return {
|
|
418
565
|
...bootstrapped,
|
|
419
566
|
has: token => bootstrapped.container.has(token),
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"http-adapter-portability.d.ts","sourceRoot":"","sources":["../../src/portability/http-adapter-portability.ts"],"names":[],"mappings":"
|
|
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"}
|
|
@@ -3,7 +3,6 @@ function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol"
|
|
|
3
3
|
function _toPrimitive(t, r) { if ("object" != typeof t || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r || "default"); if ("object" != typeof i) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); }
|
|
4
4
|
function _setFunctionName(e, t, n) { "symbol" == typeof t && (t = (t = t.description) ? "[" + t + "]" : ""); try { Object.defineProperty(e, "name", { configurable: !0, value: n ? n + " " + t : t }); } catch (e) {} return e; }
|
|
5
5
|
function _checkInRHS(e) { if (Object(e) !== e) throw TypeError("right-hand side of 'in' should be an object, got " + (null !== e ? typeof e : "null")); return e; }
|
|
6
|
-
import { createServer } from 'node:net';
|
|
7
6
|
import { request as httpsRequest } from 'node:https';
|
|
8
7
|
import { Controller, Get, Post, SseResponse } from '@fluojs/http';
|
|
9
8
|
import { defineModule } from '@fluojs/runtime';
|
|
@@ -16,25 +15,19 @@ import { defineModule } from '@fluojs/runtime';
|
|
|
16
15
|
* @template TApp - Type for the application instance.
|
|
17
16
|
*/
|
|
18
17
|
|
|
19
|
-
|
|
20
|
-
return
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
return;
|
|
33
|
-
}
|
|
34
|
-
resolve(address.port);
|
|
35
|
-
});
|
|
36
|
-
});
|
|
37
|
-
});
|
|
18
|
+
function hasListenTarget(value) {
|
|
19
|
+
return typeof value === 'object' && value !== null && 'getListenTarget' in value && typeof value.getListenTarget === 'function';
|
|
20
|
+
}
|
|
21
|
+
function resolveListeningUrl(app, adapterName) {
|
|
22
|
+
const adapter = Reflect.get(app, 'adapter');
|
|
23
|
+
if (!hasListenTarget(adapter)) {
|
|
24
|
+
throw new Error(`${adapterName} adapter portability harness cannot resolve the listener URL after binding port 0.`);
|
|
25
|
+
}
|
|
26
|
+
const target = adapter.getListenTarget();
|
|
27
|
+
if (typeof target.url !== 'string' || target.url.length === 0) {
|
|
28
|
+
throw new Error(`${adapterName} adapter portability harness resolved an empty listener URL after binding port 0.`);
|
|
29
|
+
}
|
|
30
|
+
return target.url;
|
|
38
31
|
}
|
|
39
32
|
async function requestHttps(url) {
|
|
40
33
|
return await new Promise((resolve, reject) => {
|
|
@@ -78,6 +71,11 @@ async function runWithCleanup(app, adapterName, assertion) {
|
|
|
78
71
|
throw assertionError;
|
|
79
72
|
}
|
|
80
73
|
}
|
|
74
|
+
async function runWithListeningUrlCleanup(app, adapterName, assertion) {
|
|
75
|
+
await runWithCleanup(app, adapterName, async () => {
|
|
76
|
+
await assertion(resolveListeningUrl(app, adapterName));
|
|
77
|
+
});
|
|
78
|
+
}
|
|
81
79
|
async function prepareAndListenWithCleanup(app, adapterName, prepare) {
|
|
82
80
|
try {
|
|
83
81
|
await prepare?.();
|
|
@@ -138,14 +136,13 @@ export class HttpAdapterPortabilityHarness {
|
|
|
138
136
|
defineModule(AppModule, {
|
|
139
137
|
controllers: [_CookieController]
|
|
140
138
|
});
|
|
141
|
-
const port = await findAvailablePort();
|
|
142
139
|
const app = await this.options.bootstrap(AppModule, {
|
|
143
140
|
cors: false,
|
|
144
|
-
port
|
|
141
|
+
port: 0
|
|
145
142
|
});
|
|
146
143
|
await prepareAndListenWithCleanup(app, this.options.name);
|
|
147
|
-
await
|
|
148
|
-
const response = await fetch(
|
|
144
|
+
await runWithListeningUrlCleanup(app, this.options.name, async baseUrl => {
|
|
145
|
+
const response = await fetch(`${baseUrl}/cookies`, {
|
|
149
146
|
headers: {
|
|
150
147
|
cookie: 'good=hello%20world; bad=%E0%A4%A'
|
|
151
148
|
}
|
|
@@ -192,15 +189,14 @@ export class HttpAdapterPortabilityHarness {
|
|
|
192
189
|
defineModule(AppModule, {
|
|
193
190
|
controllers: [_WebhookController]
|
|
194
191
|
});
|
|
195
|
-
const port = await findAvailablePort();
|
|
196
192
|
const app = await this.options.bootstrap(AppModule, {
|
|
197
193
|
cors: false,
|
|
198
|
-
port,
|
|
194
|
+
port: 0,
|
|
199
195
|
rawBody: true
|
|
200
196
|
});
|
|
201
197
|
await prepareAndListenWithCleanup(app, this.options.name);
|
|
202
|
-
await
|
|
203
|
-
const [jsonResponse, textResponse] = await Promise.all([fetch(
|
|
198
|
+
await runWithListeningUrlCleanup(app, this.options.name, async baseUrl => {
|
|
199
|
+
const [jsonResponse, textResponse] = await Promise.all([fetch(`${baseUrl}/webhooks/json`, {
|
|
204
200
|
body: JSON.stringify({
|
|
205
201
|
provider: 'stripe'
|
|
206
202
|
}),
|
|
@@ -208,7 +204,7 @@ export class HttpAdapterPortabilityHarness {
|
|
|
208
204
|
'content-type': 'application/json'
|
|
209
205
|
},
|
|
210
206
|
method: 'POST'
|
|
211
|
-
}), fetch(
|
|
207
|
+
}), fetch(`${baseUrl}/webhooks/text`, {
|
|
212
208
|
body: 'ping=1',
|
|
213
209
|
headers: {
|
|
214
210
|
'content-type': 'text/plain; charset=utf-8'
|
|
@@ -261,19 +257,18 @@ export class HttpAdapterPortabilityHarness {
|
|
|
261
257
|
defineModule(AppModule, {
|
|
262
258
|
controllers: [_WebhookController2]
|
|
263
259
|
});
|
|
264
|
-
const port = await findAvailablePort();
|
|
265
260
|
const app = await this.options.bootstrap(AppModule, {
|
|
266
261
|
cors: false,
|
|
267
|
-
port,
|
|
262
|
+
port: 0,
|
|
268
263
|
rawBody: true
|
|
269
264
|
});
|
|
270
265
|
await prepareAndListenWithCleanup(app, this.options.name, async () => {
|
|
271
266
|
await this.options.prepareExactRawBodyByteTest?.(app);
|
|
272
267
|
});
|
|
273
|
-
await
|
|
268
|
+
await runWithListeningUrlCleanup(app, this.options.name, async baseUrl => {
|
|
274
269
|
const payload = Uint8Array.from([0xe9, 0x41]);
|
|
275
270
|
const contentType = this.options.exactRawBodyByteContentType ?? 'text/plain; charset=latin1';
|
|
276
|
-
const response = await fetch(
|
|
271
|
+
const response = await fetch(`${baseUrl}/webhooks/bytes`, {
|
|
277
272
|
body: payload,
|
|
278
273
|
headers: {
|
|
279
274
|
'content-type': contentType
|
|
@@ -319,20 +314,19 @@ export class HttpAdapterPortabilityHarness {
|
|
|
319
314
|
defineModule(AppModule, {
|
|
320
315
|
controllers: [_UploadController]
|
|
321
316
|
});
|
|
322
|
-
const port = await findAvailablePort();
|
|
323
317
|
const app = await this.options.bootstrap(AppModule, {
|
|
324
318
|
cors: false,
|
|
325
|
-
port,
|
|
319
|
+
port: 0,
|
|
326
320
|
rawBody: true
|
|
327
321
|
});
|
|
328
322
|
await prepareAndListenWithCleanup(app, this.options.name);
|
|
329
|
-
await
|
|
323
|
+
await runWithListeningUrlCleanup(app, this.options.name, async baseUrl => {
|
|
330
324
|
const form = new FormData();
|
|
331
325
|
form.set('name', 'Ada');
|
|
332
326
|
form.set('payload', new Blob(['hello'], {
|
|
333
327
|
type: 'text/plain'
|
|
334
328
|
}), 'payload.txt');
|
|
335
|
-
const response = await fetch(
|
|
329
|
+
const response = await fetch(`${baseUrl}/uploads`, {
|
|
336
330
|
body: form,
|
|
337
331
|
method: 'POST'
|
|
338
332
|
});
|
|
@@ -378,23 +372,22 @@ export class HttpAdapterPortabilityHarness {
|
|
|
378
372
|
defineModule(AppModule, {
|
|
379
373
|
controllers: [_UploadController2]
|
|
380
374
|
});
|
|
381
|
-
const port = await findAvailablePort();
|
|
382
375
|
const app = await this.options.bootstrap(AppModule, {
|
|
383
376
|
cors: false,
|
|
384
377
|
maxBodySize: 8,
|
|
385
378
|
multipart: {
|
|
386
379
|
maxFileSize: 1024
|
|
387
380
|
},
|
|
388
|
-
port
|
|
381
|
+
port: 0
|
|
389
382
|
});
|
|
390
383
|
await prepareAndListenWithCleanup(app, this.options.name);
|
|
391
|
-
await
|
|
384
|
+
await runWithListeningUrlCleanup(app, this.options.name, async baseUrl => {
|
|
392
385
|
const form = new FormData();
|
|
393
386
|
form.set('name', 'Ada');
|
|
394
387
|
form.set('payload', new Blob(['12345678'], {
|
|
395
388
|
type: 'text/plain'
|
|
396
389
|
}), 'payload.txt');
|
|
397
|
-
const response = await fetch(
|
|
390
|
+
const response = await fetch(`${baseUrl}/uploads`, {
|
|
398
391
|
body: form,
|
|
399
392
|
method: 'POST'
|
|
400
393
|
});
|
|
@@ -442,14 +435,13 @@ export class HttpAdapterPortabilityHarness {
|
|
|
442
435
|
defineModule(AppModule, {
|
|
443
436
|
controllers: [_EventsController]
|
|
444
437
|
});
|
|
445
|
-
const port = await findAvailablePort();
|
|
446
438
|
const app = await this.options.bootstrap(AppModule, {
|
|
447
439
|
cors: false,
|
|
448
|
-
port
|
|
440
|
+
port: 0
|
|
449
441
|
});
|
|
450
442
|
await prepareAndListenWithCleanup(app, this.options.name);
|
|
451
|
-
await
|
|
452
|
-
const response = await fetch(
|
|
443
|
+
await runWithListeningUrlCleanup(app, this.options.name, async baseUrl => {
|
|
444
|
+
const response = await fetch(`${baseUrl}/events`, {
|
|
453
445
|
headers: {
|
|
454
446
|
accept: 'text/event-stream'
|
|
455
447
|
}
|
|
@@ -510,14 +502,13 @@ export class HttpAdapterPortabilityHarness {
|
|
|
510
502
|
defineModule(AppModule, {
|
|
511
503
|
controllers: [_EventsController2]
|
|
512
504
|
});
|
|
513
|
-
const port = await findAvailablePort();
|
|
514
505
|
const app = await this.options.bootstrap(AppModule, {
|
|
515
506
|
cors: false,
|
|
516
|
-
port
|
|
507
|
+
port: 0
|
|
517
508
|
});
|
|
518
509
|
await prepareAndListenWithCleanup(app, this.options.name);
|
|
519
|
-
await
|
|
520
|
-
const response = await fetch(
|
|
510
|
+
await runWithListeningUrlCleanup(app, this.options.name, async baseUrl => {
|
|
511
|
+
const response = await fetch(`${baseUrl}/events`, {
|
|
521
512
|
headers: {
|
|
522
513
|
accept: 'text/event-stream'
|
|
523
514
|
}
|
|
@@ -566,15 +557,14 @@ export class HttpAdapterPortabilityHarness {
|
|
|
566
557
|
defineModule(AppModule, {
|
|
567
558
|
controllers: [_HealthController]
|
|
568
559
|
});
|
|
569
|
-
const port = await findAvailablePort();
|
|
570
560
|
const app = await this.options.run(AppModule, {
|
|
571
561
|
cors: false,
|
|
572
562
|
host: '127.0.0.1',
|
|
573
563
|
logger,
|
|
574
|
-
port
|
|
564
|
+
port: 0
|
|
575
565
|
});
|
|
576
|
-
await
|
|
577
|
-
const response = await fetch(
|
|
566
|
+
await runWithListeningUrlCleanup(app, this.options.name, async baseUrl => {
|
|
567
|
+
const response = await fetch(`${baseUrl}/health`);
|
|
578
568
|
if (response.status !== 200) {
|
|
579
569
|
throw new Error(`${this.options.name} adapter changed host-bound health response semantics.`);
|
|
580
570
|
}
|
|
@@ -584,7 +574,7 @@ export class HttpAdapterPortabilityHarness {
|
|
|
584
574
|
})) {
|
|
585
575
|
throw new Error(`${this.options.name} adapter changed host-bound response payload.`);
|
|
586
576
|
}
|
|
587
|
-
const expectedLog = `log:FluoFactory:Listening on
|
|
577
|
+
const expectedLog = `log:FluoFactory:Listening on ${baseUrl}`;
|
|
588
578
|
if (!loggerEvents.includes(expectedLog)) {
|
|
589
579
|
throw new Error(`${this.options.name} adapter changed startup host logging.`);
|
|
590
580
|
}
|
|
@@ -627,16 +617,15 @@ export class HttpAdapterPortabilityHarness {
|
|
|
627
617
|
defineModule(AppModule, {
|
|
628
618
|
controllers: [_HealthController2]
|
|
629
619
|
});
|
|
630
|
-
const port = await findAvailablePort();
|
|
631
620
|
const app = await this.options.run(AppModule, {
|
|
632
621
|
cors: false,
|
|
633
622
|
host: '127.0.0.1',
|
|
634
623
|
https,
|
|
635
624
|
logger,
|
|
636
|
-
port
|
|
625
|
+
port: 0
|
|
637
626
|
});
|
|
638
|
-
await
|
|
639
|
-
const response = await requestHttps(
|
|
627
|
+
await runWithListeningUrlCleanup(app, this.options.name, async baseUrl => {
|
|
628
|
+
const response = await requestHttps(`${baseUrl}/health`);
|
|
640
629
|
if (response.statusCode !== 200) {
|
|
641
630
|
throw new Error(`${this.options.name} adapter changed HTTPS response status semantics.`);
|
|
642
631
|
}
|
|
@@ -645,7 +634,7 @@ export class HttpAdapterPortabilityHarness {
|
|
|
645
634
|
})) {
|
|
646
635
|
throw new Error(`${this.options.name} adapter changed HTTPS response payload semantics.`);
|
|
647
636
|
}
|
|
648
|
-
const expectedLog = `log:FluoFactory:Listening on
|
|
637
|
+
const expectedLog = `log:FluoFactory:Listening on ${baseUrl}`;
|
|
649
638
|
if (!loggerEvents.includes(expectedLog)) {
|
|
650
639
|
throw new Error(`${this.options.name} adapter changed HTTPS startup logging.`);
|
|
651
640
|
}
|
|
@@ -685,11 +674,10 @@ export class HttpAdapterPortabilityHarness {
|
|
|
685
674
|
});
|
|
686
675
|
const signal = 'SIGTERM';
|
|
687
676
|
const listenersBefore = new Set(process.listeners(signal));
|
|
688
|
-
const port = await findAvailablePort();
|
|
689
677
|
const app = await this.options.run(AppModule, {
|
|
690
678
|
cors: false,
|
|
691
679
|
logger,
|
|
692
|
-
port,
|
|
680
|
+
port: 0,
|
|
693
681
|
shutdownSignals: [signal]
|
|
694
682
|
});
|
|
695
683
|
const registeredListeners = process.listeners(signal).filter(listener => !listenersBefore.has(listener));
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Collects source-file aliases for a fluo monorepo checkout.
|
|
3
|
+
*
|
|
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.
|
|
6
|
+
*/
|
|
7
|
+
export declare function collectWorkspaceAliases(repoRootUrl: string | URL): Record<string, string>;
|
|
8
|
+
/**
|
|
9
|
+
* Creates the shared Vitest configuration used by fluo workspace packages.
|
|
10
|
+
*
|
|
11
|
+
* @param repoRootUrl - Repository root as a file URL or absolute path URL string.
|
|
12
|
+
* @param overrides - Optional Vitest config overrides merged after the fluo defaults.
|
|
13
|
+
* @returns A Vitest configuration with fluo decorator transforms and workspace aliases.
|
|
14
|
+
*/
|
|
15
|
+
export declare function createFluoVitestWorkspaceConfig(repoRootUrl: string | URL, overrides?: {}): Record<string, any>;
|
|
16
|
+
/**
|
|
17
|
+
* Defines a Vitest config rooted at the current fluo repository checkout.
|
|
18
|
+
*
|
|
19
|
+
* @returns A Vitest configuration for repository-local package tests.
|
|
20
|
+
*/
|
|
21
|
+
export declare function defineFluoVitestConfig(): Record<string, any>;
|
|
22
|
+
//# sourceMappingURL=tooling.d.ts.map
|
|
@@ -0,0 +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"}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { existsSync, readFileSync, readdirSync } from 'node:fs';
|
|
2
|
+
import { extname, join, relative, sep } from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { defineConfig, mergeConfig } from 'vitest/config';
|
|
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));
|
|
14
|
+
continue;
|
|
15
|
+
}
|
|
16
|
+
if (directoryEntry.isFile()) {
|
|
17
|
+
entries.push(entryPath);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return entries;
|
|
21
|
+
}
|
|
22
|
+
function collectWorkspaceAliasesFromRoot(repoRoot) {
|
|
23
|
+
const packagesRoot = join(repoRoot, 'packages');
|
|
24
|
+
const aliases = {};
|
|
25
|
+
for (const packageDirectoryName of readdirSync(packagesRoot)) {
|
|
26
|
+
const packageRoot = join(packagesRoot, packageDirectoryName);
|
|
27
|
+
const sourceRoot = join(packageRoot, 'src');
|
|
28
|
+
const manifestPath = join(packageRoot, 'package.json');
|
|
29
|
+
if (!existsSync(sourceRoot) || !existsSync(manifestPath)) {
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
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;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
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
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Collects source-file aliases for a fluo monorepo checkout.
|
|
56
|
+
*
|
|
57
|
+
* @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.
|
|
59
|
+
*/
|
|
60
|
+
export function collectWorkspaceAliases(repoRootUrl) {
|
|
61
|
+
return collectWorkspaceAliasesFromRoot(fileURLToPath(repoRootUrl));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Creates the shared Vitest configuration used by fluo workspace packages.
|
|
66
|
+
*
|
|
67
|
+
* @param repoRootUrl - Repository root as a file URL or absolute path URL string.
|
|
68
|
+
* @param overrides - Optional Vitest config overrides merged after the fluo defaults.
|
|
69
|
+
* @returns A Vitest configuration with fluo decorator transforms and workspace aliases.
|
|
70
|
+
*/
|
|
71
|
+
export function createFluoVitestWorkspaceConfig(repoRootUrl, overrides = {}) {
|
|
72
|
+
return mergeConfig(defineConfig({
|
|
73
|
+
plugins: [fluoBabelDecoratorsPlugin()],
|
|
74
|
+
resolve: {
|
|
75
|
+
alias: collectWorkspaceAliases(repoRootUrl)
|
|
76
|
+
},
|
|
77
|
+
test: {
|
|
78
|
+
environment: 'node'
|
|
79
|
+
}
|
|
80
|
+
}), defineConfig(overrides));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Defines a Vitest config rooted at the current fluo repository checkout.
|
|
85
|
+
*
|
|
86
|
+
* @returns A Vitest configuration for repository-local package tests.
|
|
87
|
+
*/
|
|
88
|
+
export function defineFluoVitestConfig() {
|
|
89
|
+
return createFluoVitestWorkspaceConfig(new URL('../../../../', import.meta.url));
|
|
90
|
+
}
|
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.4",
|
|
13
13
|
"private": false,
|
|
14
14
|
"license": "MIT",
|
|
15
15
|
"repository": {
|
|
@@ -68,6 +68,10 @@
|
|
|
68
68
|
"./vitest": {
|
|
69
69
|
"types": "./dist/vitest.d.ts",
|
|
70
70
|
"import": "./dist/vitest.js"
|
|
71
|
+
},
|
|
72
|
+
"./vitest/tooling": {
|
|
73
|
+
"types": "./dist/vitest/tooling.d.ts",
|
|
74
|
+
"import": "./dist/vitest/tooling.js"
|
|
71
75
|
}
|
|
72
76
|
},
|
|
73
77
|
"main": "./dist/index.js",
|
|
@@ -77,10 +81,10 @@
|
|
|
77
81
|
],
|
|
78
82
|
"dependencies": {
|
|
79
83
|
"@fluojs/config": "^1.0.2",
|
|
80
|
-
"@fluojs/core": "^1.0.
|
|
81
|
-
"@fluojs/http": "^1.
|
|
82
|
-
"@fluojs/di": "^1.0.
|
|
83
|
-
"@fluojs/runtime": "^1.1.
|
|
84
|
+
"@fluojs/core": "^1.0.3",
|
|
85
|
+
"@fluojs/http": "^1.1.0",
|
|
86
|
+
"@fluojs/di": "^1.0.3",
|
|
87
|
+
"@fluojs/runtime": "^1.1.2"
|
|
84
88
|
},
|
|
85
89
|
"peerDependencies": {
|
|
86
90
|
"@babel/core": ">=7.0.0",
|
|
@@ -88,12 +92,12 @@
|
|
|
88
92
|
},
|
|
89
93
|
"devDependencies": {
|
|
90
94
|
"vitest": "^3.2.4",
|
|
91
|
-
"@fluojs/platform-bun": "^1.0.
|
|
92
|
-
"@fluojs/platform-cloudflare-workers": "^1.0.
|
|
93
|
-
"@fluojs/platform-deno": "^1.0.
|
|
94
|
-
"@fluojs/platform-nodejs": "^1.0.
|
|
95
|
-
"@fluojs/platform-express": "^1.0.
|
|
96
|
-
"@fluojs/platform-fastify": "^1.0.
|
|
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"
|
|
97
101
|
},
|
|
98
102
|
"scripts": {
|
|
99
103
|
"prebuild": "node ../../tooling/scripts/clean-dist.mjs",
|