@fluojs/testing 1.0.2 → 1.0.3

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
@@ -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 클래스가 아니라 사용자가 작성한 애플리케이션 모듈 클래스에 계속 연결됩니다.
@@ -164,11 +166,11 @@ fluo는 테스트가 명시적인 `rootModule`을 이름으로 지정해야 한
164
166
  ## 공개 API
165
167
 
166
168
  - **루트 패키지**: `createTestingModule(...)`, `createTestApp(...)`, 모듈 introspection 헬퍼, 공용 테스트 타입
167
- - **서브패스**: `@fluojs/testing/app`, `@fluojs/testing/module`, `@fluojs/testing/http`, `@fluojs/testing/mock`, `@fluojs/testing/types`, `@fluojs/testing/vitest`
169
+ - **서브패스**: `@fluojs/testing/app`, `@fluojs/testing/module`, `@fluojs/testing/http`, `@fluojs/testing/mock`, `@fluojs/testing/types`, `@fluojs/testing/vitest`, `@fluojs/testing/vitest/tooling`
168
170
  - **Mock 서브패스**: `@fluojs/testing/mock`
169
171
  - **HTTP 헬퍼**: `@fluojs/testing/http`
170
172
  - **하니스 서브패스**: `platform-conformance`, `http-adapter-portability`, `web-runtime-adapter-portability`, `fetch-style-websocket-conformance`
171
- - **도구 지원**: `@fluojs/testing/vitest`와 `fluoBabelDecoratorsPlugin()` (`vitest`와 `@babel/core`를 함께 요구)
173
+ - **도구 지원**: `@fluojs/testing/vitest`의 `fluoBabelDecoratorsPlugin()` 및 `@fluojs/testing/vitest/tooling`의 Vitest workspace config helper (`vitest`와 `@babel/core`를 함께 요구)
172
174
 
173
175
  ## 관련 패키지
174
176
 
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.
@@ -162,11 +164,11 @@ fluo differs from NestJS by requiring tests to name an explicit `rootModule`. Th
162
164
  ## Public API
163
165
 
164
166
  - **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`
167
+ - **Subpaths**: `@fluojs/testing/app`, `@fluojs/testing/module`, `@fluojs/testing/http`, `@fluojs/testing/mock`, `@fluojs/testing/types`, `@fluojs/testing/vitest`, `@fluojs/testing/vitest/tooling`
166
168
  - **Mock subpath**: `@fluojs/testing/mock`
167
169
  - **HTTP helpers**: `@fluojs/testing/http`
168
170
  - **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)
171
+ - **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
172
 
171
173
  ## Related Packages
172
174
 
@@ -1 +1 @@
1
- {"version":3,"file":"module.d.ts","sourceRoot":"","sources":["../src/module.ts"],"names":[],"mappings":"AACA,OAAO,EAGL,KAAK,SAAS,EAId,KAAK,QAAQ,EACd,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;AAkkBD;;;;;;;;;;;;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,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;AAyuBD;;;;;;;;;;;;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,77 @@ 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.multi === true) {
130
+ return false;
131
+ }
132
+ if (provider.scope !== Scope.DEFAULT) {
133
+ return false;
134
+ }
135
+ return provider.type === 'class' || provider.type === 'value' && hasAnyBootstrapLifecycleHook(provider.useValue);
136
+ }
137
+ async function resolveTestingLifecycleInstances(bootstrapped, overrides = []) {
138
+ const lifecycleProviders = [...bootstrapped.effectiveProviders.runtimeProviders, ...bootstrapped.effectiveProviders.moduleProviders, ...overrides];
139
+ const instances = [];
140
+ const seenProviders = new Set();
141
+ const introspection = toContainerIntrospection(bootstrapped.container);
142
+ for (const provider of lifecycleProviders) {
143
+ const token = providerToken(provider);
144
+ const effectiveProviders = effectiveProvidersForToken(introspection, token);
145
+ for (const effectiveProvider of effectiveProviders) {
146
+ if (seenProviders.has(effectiveProvider)) {
147
+ continue;
148
+ }
149
+ seenProviders.add(effectiveProvider);
150
+ if (!isSingletonLifecycleProvider(effectiveProvider)) {
151
+ continue;
152
+ }
153
+ if (effectiveProvider.type === 'value') {
154
+ instances.push(effectiveProvider.useValue);
155
+ continue;
156
+ }
157
+ try {
158
+ instances.push(await bootstrapped.container.resolve(token));
159
+ } catch (error) {
160
+ if (error instanceof Error && error.message.includes('Request-scoped provider')) {
161
+ continue;
162
+ }
163
+ throw error;
164
+ }
165
+ }
166
+ }
167
+ return instances;
168
+ }
169
+ async function runTestingBootstrapLifecycle(bootstrapped, overrides = []) {
170
+ const instances = await resolveTestingLifecycleInstances(bootstrapped, overrides);
171
+ for (const instance of instances) {
172
+ if (hasLifecycleHook(instance, 'onModuleInit')) {
173
+ await instance.onModuleInit();
174
+ }
175
+ }
176
+ for (const instance of instances) {
177
+ if (hasLifecycleHook(instance, 'onApplicationBootstrap')) {
178
+ await instance.onApplicationBootstrap();
179
+ }
180
+ }
181
+ }
111
182
  function rootContainerIntrospection(target) {
112
183
  return target.parent ? rootContainerIntrospection(target.parent) : target;
113
184
  }
@@ -190,6 +261,20 @@ function canPromoteCachedSingleton(state, token) {
190
261
  const provider = lookupProvider(state.introspection, token);
191
262
  return provider !== undefined && provider.scope !== 'request' && providerGraphIsSyncResolvable(state, token);
192
263
  }
264
+ function providerGraphForProviderIsSyncResolvable(state, provider, visited = new Set()) {
265
+ if (provider.type === 'factory') {
266
+ return state.factoryResolutionKinds.get(provider) === 'sync';
267
+ }
268
+ if (provider.type === 'existing') {
269
+ return provider.useExisting !== undefined && providerGraphIsSyncResolvable(state, provider.useExisting, visited);
270
+ }
271
+ return provider.inject.every(entry => {
272
+ if (isOptionalToken(entry) && !hasToken(state, entry.token)) {
273
+ return true;
274
+ }
275
+ return providerGraphIsSyncResolvable(state, dependencyToken(entry), visited);
276
+ });
277
+ }
193
278
  function resolveSyncDependency(entry, state) {
194
279
  if (isOptionalToken(entry)) {
195
280
  if (!hasToken(state, entry.token)) {
@@ -259,6 +344,24 @@ function resolveSyncProvider(provider, state) {
259
344
  state.singletonCache.set(provider.provide, Promise.resolve(instance));
260
345
  return instance;
261
346
  }
347
+ function resolveSyncMultiProvider(provider, state) {
348
+ if (provider.scope === 'request' && !state.introspection.requestScopeEnabled) {
349
+ throw new Error(`Request-scoped provider ${String(provider.provide)} cannot be resolved outside request scope.`);
350
+ }
351
+ if (provider.scope === 'transient') {
352
+ return instantiateSyncProvider(provider, state);
353
+ }
354
+ if (state.syncMultiSingletonValues.has(provider)) {
355
+ return state.syncMultiSingletonValues.get(provider);
356
+ }
357
+ if (state.multiSingletonCache.has(provider)) {
358
+ throw new Error(`Token ${String(provider.provide)} was already resolved asynchronously. Use resolve() instead of get() for this provider.`);
359
+ }
360
+ const instance = instantiateSyncProvider(provider, state);
361
+ state.syncMultiSingletonValues.set(provider, instance);
362
+ state.multiSingletonCache.set(provider, Promise.resolve(instance));
363
+ return instance;
364
+ }
262
365
  function resolveSyncToken(token, state) {
263
366
  if (state.resolutionChain.has(token)) {
264
367
  throw new Error(`Circular dependency detected while resolving token ${String(token)} via get().`);
@@ -267,7 +370,7 @@ function resolveSyncToken(token, state) {
267
370
  try {
268
371
  const multiProviders = collectMultiProviders(state.introspection, token);
269
372
  if (multiProviders.length > 0) {
270
- return multiProviders.map(provider => instantiateSyncProvider(provider, state));
373
+ return multiProviders.map(provider => resolveSyncMultiProvider(provider, state));
271
374
  }
272
375
  const provider = lookupProvider(state.introspection, token);
273
376
  if (!provider) {
@@ -285,8 +388,10 @@ function createSyncResolver(container) {
285
388
  const state = {
286
389
  factoryResolutionKinds,
287
390
  introspection,
391
+ multiSingletonCache: rootContainerIntrospection(introspection).multiSingletonCache,
288
392
  resolutionChain: new Set(),
289
393
  singletonCache: rootContainerIntrospection(introspection).singletonCache,
394
+ syncMultiSingletonValues: new Map(),
290
395
  syncSingletonValues: new Map()
291
396
  };
292
397
  return {
@@ -298,6 +403,12 @@ function createSyncResolver(container) {
298
403
  }
299
404
  state.syncSingletonValues.set(token, await promise);
300
405
  }
406
+ for (const [provider, promise] of state.multiSingletonCache) {
407
+ if (!providerGraphForProviderIsSyncResolvable(state, provider)) {
408
+ continue;
409
+ }
410
+ state.syncMultiSingletonValues.set(provider, await promise);
411
+ }
301
412
  }
302
413
  };
303
414
  }
@@ -392,7 +503,10 @@ class DefaultTestingModuleBuilder {
392
503
  }
393
504
  async compile() {
394
505
  const bootstrapped = this.bootstrapTestingModule();
395
- return this.createTestingModuleRef(bootstrapped);
506
+ const syncResolver = createSyncResolver(bootstrapped.container);
507
+ await runTestingBootstrapLifecycle(bootstrapped, this.overrides);
508
+ await syncResolver.syncFromContainer();
509
+ return this.createTestingModuleRef(bootstrapped, syncResolver);
396
510
  }
397
511
  bootstrapTestingModule() {
398
512
  const bootstrapped = this.bootstrapWithPatchedModuleImports();
@@ -411,9 +525,8 @@ class DefaultTestingModuleBuilder {
411
525
  this.restorePatchedModuleImports();
412
526
  }
413
527
  }
414
- createTestingModuleRef(bootstrapped) {
528
+ createTestingModuleRef(bootstrapped, syncResolver) {
415
529
  const dispatcher = createTestingDispatcher(bootstrapped);
416
- const syncResolver = createSyncResolver(bootstrapped.container);
417
530
  return {
418
531
  ...bootstrapped,
419
532
  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":"AAIA,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;AAEF;;;;;;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;AAuGD;;;;;;;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;IAkDrD,oCAAoC,IAAI,OAAO,CAAC,IAAI,CAAC;IAsErD,wDAAwD,IAAI,OAAO,CAAC,IAAI,CAAC;IA+CzE,iCAAiC,IAAI,OAAO,CAAC,IAAI,CAAC;IAuDlD,8CAA8C,IAAI,OAAO,CAAC,IAAI,CAAC;IAsD/D,0BAA0B,IAAI,OAAO,CAAC,IAAI,CAAC;IAmDjD;;;OAGG;IACG,mCAAmC,IAAI,OAAO,CAAC,IAAI,CAAC;IA0DpD,wCAAwC,IAAI,OAAO,CAAC,IAAI,CAAC;IAqDzD,4BAA4B,CAAC,KAAK,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IAqDjF,8CAA8C,IAAI,OAAO,CAAC,IAAI,CAAC;CA4CtE;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;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
- async function findAvailablePort() {
20
- return await new Promise((resolve, reject) => {
21
- const server = createServer();
22
- server.once('error', reject);
23
- server.listen(0, () => {
24
- const address = server.address();
25
- if (!address || typeof address === 'string') {
26
- reject(new Error('Failed to resolve an available port.'));
27
- return;
28
- }
29
- server.close(error => {
30
- if (error) {
31
- reject(error);
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 runWithCleanup(app, this.options.name, async () => {
148
- const response = await fetch(`http://127.0.0.1:${String(port)}/cookies`, {
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 runWithCleanup(app, this.options.name, async () => {
203
- const [jsonResponse, textResponse] = await Promise.all([fetch(`http://127.0.0.1:${String(port)}/webhooks/json`, {
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(`http://127.0.0.1:${String(port)}/webhooks/text`, {
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 runWithCleanup(app, this.options.name, async () => {
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(`http://127.0.0.1:${String(port)}/webhooks/bytes`, {
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 runWithCleanup(app, this.options.name, async () => {
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(`http://127.0.0.1:${String(port)}/uploads`, {
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 runWithCleanup(app, this.options.name, async () => {
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(`http://127.0.0.1:${String(port)}/uploads`, {
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 runWithCleanup(app, this.options.name, async () => {
452
- const response = await fetch(`http://127.0.0.1:${String(port)}/events`, {
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 runWithCleanup(app, this.options.name, async () => {
520
- const response = await fetch(`http://127.0.0.1:${String(port)}/events`, {
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 runWithCleanup(app, this.options.name, async () => {
577
- const response = await fetch(`http://127.0.0.1:${String(port)}/health`);
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 http://127.0.0.1:${String(port)}`;
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 runWithCleanup(app, this.options.name, async () => {
639
- const response = await requestHttps(`https://127.0.0.1:${String(port)}/health`);
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 https://127.0.0.1:${String(port)}`;
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.2",
12
+ "version": "1.0.3",
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",
@@ -76,11 +80,11 @@
76
80
  "dist"
77
81
  ],
78
82
  "dependencies": {
83
+ "@fluojs/core": "^1.0.3",
79
84
  "@fluojs/config": "^1.0.2",
80
- "@fluojs/core": "^1.0.2",
81
- "@fluojs/http": "^1.0.0",
82
- "@fluojs/di": "^1.0.2",
83
- "@fluojs/runtime": "^1.1.0"
85
+ "@fluojs/http": "^1.1.0",
86
+ "@fluojs/di": "^1.0.3",
87
+ "@fluojs/runtime": "^1.1.1"
84
88
  },
85
89
  "peerDependencies": {
86
90
  "@babel/core": ">=7.0.0",
@@ -89,11 +93,11 @@
89
93
  "devDependencies": {
90
94
  "vitest": "^3.2.4",
91
95
  "@fluojs/platform-bun": "^1.0.2",
92
- "@fluojs/platform-cloudflare-workers": "^1.0.2",
93
- "@fluojs/platform-deno": "^1.0.2",
96
+ "@fluojs/platform-deno": "^1.0.3",
94
97
  "@fluojs/platform-nodejs": "^1.0.2",
95
98
  "@fluojs/platform-express": "^1.0.2",
96
- "@fluojs/platform-fastify": "^1.0.2"
99
+ "@fluojs/platform-fastify": "^1.0.3",
100
+ "@fluojs/platform-cloudflare-workers": "^1.0.2"
97
101
  },
98
102
  "scripts": {
99
103
  "prebuild": "node ../../tooling/scripts/clean-dist.mjs",