@fluojs/platform-fastify 1.0.0-beta.3 → 1.0.0-beta.5
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 +8 -1
- package/README.md +8 -1
- package/dist/adapter.d.ts +3 -1
- package/dist/adapter.d.ts.map +1 -1
- package/dist/adapter.js +164 -13
- package/package.json +5 -4
package/README.ko.md
CHANGED
|
@@ -43,7 +43,7 @@ await app.listen();
|
|
|
43
43
|
## 주요 패턴
|
|
44
44
|
|
|
45
45
|
### 멀티파트 및 Raw Body
|
|
46
|
-
Fastify 어댑터는 내부 Fastify 플러그인을 통해 멀티파트 form-data 및 raw body 파싱을 기본적으로 지원하며, 이는 표준 fluo 요청 인터페이스를 통해 노출됩니다. 어댑터를 직접 생성할 때는 멀티파트 제한을 두 번째 인자로 전달하고, `bootstrapFastifyApplication(...)` 및 `runFastifyApplication(...)`에서는 같은 설정을 `options.multipart` 아래에 전달하면 됩니다.
|
|
46
|
+
Fastify 어댑터는 내부 Fastify 플러그인을 통해 멀티파트 form-data 및 raw body 파싱을 기본적으로 지원하며, 이는 표준 fluo 요청 인터페이스를 통해 노출됩니다. `rawBody: true`를 활성화하면 멀티파트가 아닌 요청에서 `FrameworkRequest.rawBody`가 원본 요청 바이트를 그대로 보존하므로 webhook 서명 검증이나 기타 바이트 민감한 흐름에서 정확한 payload를 다시 사용할 수 있습니다. 어댑터를 직접 생성할 때는 멀티파트 제한을 두 번째 인자로 전달하고, `bootstrapFastifyApplication(...)` 및 `runFastifyApplication(...)`에서는 같은 설정을 `options.multipart` 아래에 전달하면 됩니다.
|
|
47
47
|
|
|
48
48
|
```typescript
|
|
49
49
|
const adapter = createFastifyAdapter(
|
|
@@ -125,6 +125,13 @@ await bootstrapFastifyApplication(AppModule, {
|
|
|
125
125
|
});
|
|
126
126
|
```
|
|
127
127
|
|
|
128
|
+
### 네이티브 라우트 등록과 안전한 폴백
|
|
129
|
+
fluo 라우트 메타데이터를 Fastify 경로로 그대로 옮길 수 있는 경우, 어댑터는 모든 요청을 단일 와일드카드 라우트로 보내는 대신 Fastify 네이티브 per-route 핸들러를 등록합니다. 의미 보존이 가능한 unversioned route에서는 Fastify가 미리 고른 descriptor와 params를 공유 fluo dispatcher에 전달하므로 duplicate route matching을 건너뛰면서도 middleware, guards, interceptors, observers, SSE, multipart, raw body, streaming, error handling 의미론은 그대로 유지됩니다.
|
|
130
|
+
|
|
131
|
+
여러 라우트가 같은 method와 정규화된 param shape를 공유하는 경우(예: `/:id` 와 `/:slug`), `@All(...)`을 사용하는 경우, non-URI versioning에 의존하는 경우, 또는 duplicate slash / trailing slash 변형으로 들어온 경우에는 어댑터가 해당 요청을 의도적으로 와일드카드 fallback 경로에 남겨 둡니다. 이렇게 해서 Fastify 등록 단계에서 부팅 실패가 나거나 fluo의 등록 순서 기반 매칭 의미론이 좁아지지 않도록 보장합니다. app middleware가 native handoff 이후 framework request의 method 또는 path를 rewrite하면 dispatcher는 stale handoff를 무시하고 rewrite된 요청을 다시 매칭합니다.
|
|
132
|
+
|
|
133
|
+
어댑터는 매칭되지 않은 경로와 이식성에 민감한 경우를 위해 와일드카드 fallback 라우트를 계속 유지하며, Fastify의 trailing slash / duplicate slash 정규화를 켜서 네이티브 선택 경로도 fluo의 문서화된 route path 계약과 맞추어 동작하도록 합니다. CORS 처리는 Fastify 플러그인이 아니라 fluo의 공유 middleware 경로가 계속 소유하고, `OPTIONS` 같은 미지원 메서드는 fluo route가 명시적으로 소유하지 않는 한 fallback dispatcher 경로로 흐릅니다.
|
|
134
|
+
|
|
128
135
|
## 성능
|
|
129
136
|
|
|
130
137
|
fluo의 Fastify 어댑터는 높은 동시성 시나리오에서 raw Node.js 어댑터보다 훨씬 뛰어난 성능을 발휘합니다.
|
package/README.md
CHANGED
|
@@ -43,7 +43,7 @@ await app.listen();
|
|
|
43
43
|
## Common Patterns
|
|
44
44
|
|
|
45
45
|
### Multipart and Raw Body
|
|
46
|
-
The Fastify adapter includes built-in support for multipart form-data and raw body parsing via internal Fastify plugins, exposed through the standard fluo request interface. When you construct the adapter directly, pass multipart limits as the second argument. `bootstrapFastifyApplication(...)` and `runFastifyApplication(...)` accept the same multipart settings under `options.multipart`.
|
|
46
|
+
The Fastify adapter includes built-in support for multipart form-data and raw body parsing via internal Fastify plugins, exposed through the standard fluo request interface. When `rawBody: true` is enabled, `FrameworkRequest.rawBody` preserves the original request bytes for non-multipart requests so webhook signature verification and other byte-sensitive flows can replay the exact payload. When you construct the adapter directly, pass multipart limits as the second argument. `bootstrapFastifyApplication(...)` and `runFastifyApplication(...)` accept the same multipart settings under `options.multipart`.
|
|
47
47
|
|
|
48
48
|
```typescript
|
|
49
49
|
const adapter = createFastifyAdapter(
|
|
@@ -125,6 +125,13 @@ await bootstrapFastifyApplication(AppModule, {
|
|
|
125
125
|
});
|
|
126
126
|
```
|
|
127
127
|
|
|
128
|
+
### Native Route Registration with Safe Fallback
|
|
129
|
+
When fluo route metadata can be translated directly, the adapter registers Fastify-native per-route handlers instead of sending every request through a single wildcard route. For semantically safe unversioned routes, those native handlers hand a pre-matched descriptor and params to the shared fluo dispatcher so duplicate route matching is skipped without changing framework-owned guards, interceptors, observers, SSE, multipart, raw body, streaming, or error handling.
|
|
130
|
+
|
|
131
|
+
When multiple routes share the same method and normalized param shape (for example `/:id` and `/:slug`), use `@All(...)`, depend on non-URI versioning, or arrive through duplicate-slash / trailing-slash variants, the adapter intentionally leaves those requests on the wildcard fallback path so Fastify registration cannot boot-fail or narrow fluo's matching semantics. If app middleware rewrites the framework request method or path after a native handoff was attached, the dispatcher ignores that stale handoff and rematches the rewritten request.
|
|
132
|
+
|
|
133
|
+
The adapter keeps a wildcard fallback route for unmatched paths and portability-sensitive cases, and enables Fastify trailing-slash / duplicate-slash normalization so native selection stays aligned with fluo's documented route path contract. CORS handling remains owned by fluo's shared middleware path rather than Fastify plugins, and unsupported methods such as `OPTIONS` continue through the fallback dispatcher path unless a fluo route explicitly owns them.
|
|
134
|
+
|
|
128
135
|
## Performance
|
|
129
136
|
|
|
130
137
|
fluo's Fastify adapter significantly outperforms the raw Node.js adapter in high-concurrency scenarios.
|
package/dist/adapter.d.ts
CHANGED
|
@@ -83,7 +83,9 @@ export declare class FastifyHttpApplicationAdapter implements HttpApplicationAda
|
|
|
83
83
|
getListenTarget(): FastifyListenTarget;
|
|
84
84
|
listen(dispatcher: Dispatcher): Promise<void>;
|
|
85
85
|
close(): Promise<void>;
|
|
86
|
-
private
|
|
86
|
+
private registerPluginsAndRoutes;
|
|
87
|
+
private registerNativeRoutes;
|
|
88
|
+
private registerWildcardFallbackRoute;
|
|
87
89
|
private listenWithRetry;
|
|
88
90
|
private handleRequest;
|
|
89
91
|
}
|
package/dist/adapter.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"adapter.d.ts","sourceRoot":"","sources":["../src/adapter.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,IAAI,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAOtE,OAAO,
|
|
1
|
+
{"version":3,"file":"adapter.d.ts","sourceRoot":"","sources":["../src/adapter.ts"],"names":[],"mappings":"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"}
|
package/dist/adapter.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
+
import { Transform } from 'node:stream';
|
|
1
2
|
import multipart from '@fastify/multipart';
|
|
2
3
|
import fastify from 'fastify';
|
|
3
|
-
import fastifyRawBody from 'fastify-raw-body';
|
|
4
4
|
import { createServerBackedHttpAdapterRealtimeCapability, createErrorResponse, HttpException, InternalServerErrorException, PayloadTooLargeException } from '@fluojs/http';
|
|
5
|
+
import { attachFrameworkRequestNativeRouteHandoff, bindRawRequestNativeRouteHandoff, consumeRawRequestNativeRouteHandoff, isRoutePathNormalizationSensitive } from '@fluojs/http/internal';
|
|
5
6
|
import { createNodeShutdownSignalRegistration, defaultNodeShutdownSignals } from '@fluojs/runtime/node';
|
|
6
7
|
import { bootstrapHttpAdapterApplication, runHttpAdapterApplication } from '@fluojs/runtime/internal/http-adapter';
|
|
7
8
|
import { dispatchWithRequestResponseFactory } from '@fluojs/runtime/internal/request-response-factory';
|
|
@@ -16,6 +17,7 @@ import { dispatchWithRequestResponseFactory } from '@fluojs/runtime/internal/req
|
|
|
16
17
|
|
|
17
18
|
const DEFAULT_MAX_BODY_SIZE = 1 * 1024 * 1024;
|
|
18
19
|
const DEFAULT_SHUTDOWN_TIMEOUT_MS = 10_000;
|
|
20
|
+
const FASTIFY_NATIVE_ROUTE_METHODS = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD'];
|
|
19
21
|
|
|
20
22
|
/**
|
|
21
23
|
* Bootstrap options for creating a Fastify-backed application without
|
|
@@ -62,7 +64,7 @@ export class FastifyHttpApplicationAdapter {
|
|
|
62
64
|
}
|
|
63
65
|
async listen(dispatcher) {
|
|
64
66
|
this.dispatcher = dispatcher;
|
|
65
|
-
await this.
|
|
67
|
+
await this.registerPluginsAndRoutes(dispatcher);
|
|
66
68
|
await this.listenWithRetry();
|
|
67
69
|
}
|
|
68
70
|
async close() {
|
|
@@ -85,23 +87,41 @@ export class FastifyHttpApplicationAdapter {
|
|
|
85
87
|
}
|
|
86
88
|
await waitForCloseWithTimeout(closeInFlight, this.shutdownTimeoutMs);
|
|
87
89
|
}
|
|
88
|
-
async
|
|
90
|
+
async registerPluginsAndRoutes(dispatcher) {
|
|
89
91
|
if (this.pluginsReady) {
|
|
90
92
|
return;
|
|
91
93
|
}
|
|
92
94
|
await this.app.register(multipart);
|
|
93
95
|
if (this.preserveRawBody) {
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
96
|
+
this.app.addHook('preParsing', captureRawBodyPreParsingHook);
|
|
97
|
+
}
|
|
98
|
+
this.registerNativeRoutes(resolveDispatcherRouteDescriptors(dispatcher));
|
|
99
|
+
this.registerWildcardFallbackRoute();
|
|
100
|
+
this.pluginsReady = true;
|
|
101
|
+
}
|
|
102
|
+
registerNativeRoutes(descriptors) {
|
|
103
|
+
for (const route of createFastifyNativeRoutes(descriptors)) {
|
|
104
|
+
this.app.route({
|
|
105
|
+
handler: async (request, reply) => {
|
|
106
|
+
const requestPath = readRequestPathFromRawUrl(request.raw.url);
|
|
107
|
+
const params = normalizeNativeRouteParams(request.params);
|
|
108
|
+
if (!isRoutePathNormalizationSensitive(requestPath) && !hasNativeRouteParamSeparators(params)) {
|
|
109
|
+
bindRawRequestNativeRouteHandoff(request.raw, {
|
|
110
|
+
descriptor: route.descriptor,
|
|
111
|
+
params
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
await this.handleRequest(request, reply);
|
|
115
|
+
},
|
|
116
|
+
method: route.method,
|
|
117
|
+
url: route.path
|
|
99
118
|
});
|
|
100
119
|
}
|
|
120
|
+
}
|
|
121
|
+
registerWildcardFallbackRoute() {
|
|
101
122
|
this.app.all('*', async (request, reply) => {
|
|
102
123
|
await this.handleRequest(request, reply);
|
|
103
124
|
});
|
|
104
|
-
this.pluginsReady = true;
|
|
105
125
|
}
|
|
106
126
|
async listenWithRetry() {
|
|
107
127
|
for (let attempt = 0;; attempt++) {
|
|
@@ -150,6 +170,56 @@ function createFastifyRequestResponseFactory(multipartOptions, maxBodySize = DEF
|
|
|
150
170
|
}
|
|
151
171
|
};
|
|
152
172
|
}
|
|
173
|
+
function resolveDispatcherRouteDescriptors(dispatcher) {
|
|
174
|
+
return dispatcher.describeRoutes?.() ?? [];
|
|
175
|
+
}
|
|
176
|
+
function createFastifyNativeRoutes(descriptors) {
|
|
177
|
+
const candidates = new Map();
|
|
178
|
+
const shapePaths = new Map();
|
|
179
|
+
const versionSensitiveRouteKeys = collectVersionSensitiveRouteKeys(descriptors);
|
|
180
|
+
for (const descriptor of descriptors) {
|
|
181
|
+
if (!isFastifyNativeRouteDescriptor(descriptor) || versionSensitiveRouteKeys.has(`${descriptor.route.method}:${descriptor.route.path}`)) {
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
registerFastifyNativeRouteCandidate(candidates, shapePaths, descriptor);
|
|
185
|
+
}
|
|
186
|
+
return [...candidates.values()].filter(candidate => shapePaths.get(candidate.shapeKey)?.size === 1).map(({
|
|
187
|
+
descriptor,
|
|
188
|
+
method,
|
|
189
|
+
path
|
|
190
|
+
}) => ({
|
|
191
|
+
descriptor,
|
|
192
|
+
method,
|
|
193
|
+
path
|
|
194
|
+
}));
|
|
195
|
+
}
|
|
196
|
+
function isFastifyNativeRouteDescriptor(descriptor) {
|
|
197
|
+
return descriptor.route.method !== 'ALL' && FASTIFY_NATIVE_ROUTE_METHODS.includes(descriptor.route.method) && descriptor.route.version === undefined;
|
|
198
|
+
}
|
|
199
|
+
function registerFastifyNativeRouteCandidate(candidates, shapePaths, descriptor) {
|
|
200
|
+
const method = descriptor.route.method;
|
|
201
|
+
const path = descriptor.route.path;
|
|
202
|
+
const routeKey = `${method}:${path}`;
|
|
203
|
+
const shapeKey = `${method}:${canonicalizeFastifyRouteShape(path)}`;
|
|
204
|
+
if (!candidates.has(routeKey)) {
|
|
205
|
+
candidates.set(routeKey, {
|
|
206
|
+
descriptor,
|
|
207
|
+
method,
|
|
208
|
+
path,
|
|
209
|
+
shapeKey
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
let paths = shapePaths.get(shapeKey);
|
|
213
|
+
if (!paths) {
|
|
214
|
+
paths = new Set();
|
|
215
|
+
shapePaths.set(shapeKey, paths);
|
|
216
|
+
}
|
|
217
|
+
paths.add(path);
|
|
218
|
+
}
|
|
219
|
+
function canonicalizeFastifyRouteShape(path) {
|
|
220
|
+
const segments = path.split('/').filter(Boolean).map(segment => segment.startsWith(':') ? ':' : segment);
|
|
221
|
+
return segments.length === 0 ? '/' : `/${segments.join('/')}`;
|
|
222
|
+
}
|
|
153
223
|
|
|
154
224
|
/**
|
|
155
225
|
* Create the recommended Fastify adapter for `FluoFactory.create(...)`.
|
|
@@ -222,6 +292,18 @@ function createFrameworkResponse(reply) {
|
|
|
222
292
|
this.committed = true;
|
|
223
293
|
await reply.send(serialized.payload);
|
|
224
294
|
},
|
|
295
|
+
async sendSimpleJson(body) {
|
|
296
|
+
if (reply.sent) {
|
|
297
|
+
this.committed = true;
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
const serialized = serializeResponseBody(body);
|
|
301
|
+
if (!reply.hasHeader('content-type') && serialized.defaultContentType) {
|
|
302
|
+
reply.header('content-type', serialized.defaultContentType);
|
|
303
|
+
}
|
|
304
|
+
this.committed = true;
|
|
305
|
+
await reply.send(serialized.payload);
|
|
306
|
+
},
|
|
225
307
|
setHeader(name, value) {
|
|
226
308
|
const lowerName = name.toLowerCase();
|
|
227
309
|
if (lowerName === 'set-cookie') {
|
|
@@ -333,10 +415,40 @@ async function createFrameworkRequest(request, signal, multipartOptions, maxBody
|
|
|
333
415
|
if (preserveRawBody && !isMultipart) {
|
|
334
416
|
const rawBodyValue = request.rawBody;
|
|
335
417
|
if (rawBodyValue !== undefined) {
|
|
336
|
-
frameworkRequest.rawBody =
|
|
418
|
+
frameworkRequest.rawBody = rawBodyValue;
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
const nativeRouteHandoff = consumeRawRequestNativeRouteHandoff(request.raw);
|
|
422
|
+
return nativeRouteHandoff ? attachFrameworkRequestNativeRouteHandoff(frameworkRequest, nativeRouteHandoff) : frameworkRequest;
|
|
423
|
+
}
|
|
424
|
+
function normalizeNativeRouteParams(params) {
|
|
425
|
+
if (typeof params !== 'object' || params === null) {
|
|
426
|
+
return {};
|
|
427
|
+
}
|
|
428
|
+
return Object.fromEntries(Object.entries(params).flatMap(([key, value]) => typeof value === 'string' ? [[key, value]] : value === undefined ? [] : [[key, String(value)]]));
|
|
429
|
+
}
|
|
430
|
+
function hasNativeRouteParamSeparators(params) {
|
|
431
|
+
return Object.values(params).some(value => value.includes('/'));
|
|
432
|
+
}
|
|
433
|
+
function collectVersionSensitiveRouteKeys(descriptors) {
|
|
434
|
+
const grouped = new Map();
|
|
435
|
+
for (const descriptor of descriptors) {
|
|
436
|
+
if (!FASTIFY_NATIVE_ROUTE_METHODS.includes(descriptor.route.method)) {
|
|
437
|
+
continue;
|
|
337
438
|
}
|
|
439
|
+
const routeKey = `${descriptor.route.method}:${descriptor.route.path}`;
|
|
440
|
+
const current = grouped.get(routeKey) ?? {
|
|
441
|
+
count: 0,
|
|
442
|
+
hasVersioned: false
|
|
443
|
+
};
|
|
444
|
+
current.count += 1;
|
|
445
|
+
current.hasVersioned ||= descriptor.route.version !== undefined;
|
|
446
|
+
grouped.set(routeKey, current);
|
|
338
447
|
}
|
|
339
|
-
return
|
|
448
|
+
return new Set([...grouped.entries()].filter(([, current]) => current.count > 1 || current.hasVersioned).map(([routeKey]) => routeKey));
|
|
449
|
+
}
|
|
450
|
+
function readRequestPathFromRawUrl(rawUrl) {
|
|
451
|
+
return new URL(rawUrl ?? '/', 'http://localhost').pathname;
|
|
340
452
|
}
|
|
341
453
|
async function parseMultipartRequest(request, options = {}) {
|
|
342
454
|
const fields = {};
|
|
@@ -498,15 +610,54 @@ function createFastifyApp(httpsOptions, maxBodySize) {
|
|
|
498
610
|
if (httpsOptions) {
|
|
499
611
|
return fastify({
|
|
500
612
|
bodyLimit: maxBodySize,
|
|
613
|
+
exposeHeadRoutes: false,
|
|
501
614
|
https: httpsOptions,
|
|
502
|
-
logger: false
|
|
615
|
+
logger: false,
|
|
616
|
+
routerOptions: {
|
|
617
|
+
ignoreDuplicateSlashes: true,
|
|
618
|
+
ignoreTrailingSlash: true
|
|
619
|
+
}
|
|
503
620
|
});
|
|
504
621
|
}
|
|
505
622
|
return fastify({
|
|
506
623
|
bodyLimit: maxBodySize,
|
|
507
|
-
|
|
624
|
+
exposeHeadRoutes: false,
|
|
625
|
+
logger: false,
|
|
626
|
+
routerOptions: {
|
|
627
|
+
ignoreDuplicateSlashes: true,
|
|
628
|
+
ignoreTrailingSlash: true
|
|
629
|
+
}
|
|
508
630
|
});
|
|
509
631
|
}
|
|
632
|
+
function captureRawBodyPreParsingHook(request, _reply, payload, done) {
|
|
633
|
+
if (isMultipartRequestContentType(request.headers['content-type'])) {
|
|
634
|
+
done(null, payload);
|
|
635
|
+
return;
|
|
636
|
+
}
|
|
637
|
+
const chunks = [];
|
|
638
|
+
const capture = new Transform({
|
|
639
|
+
transform(chunk, _encoding, callback) {
|
|
640
|
+
const bufferChunk = Buffer.isBuffer(chunk) ? chunk : chunk instanceof Uint8Array ? Buffer.from(chunk) : Buffer.from(String(chunk), 'utf8');
|
|
641
|
+
chunks.push(bufferChunk);
|
|
642
|
+
callback(null, chunk);
|
|
643
|
+
},
|
|
644
|
+
flush(callback) {
|
|
645
|
+
if (chunks.length > 0) {
|
|
646
|
+
request.rawBody = Buffer.concat(chunks);
|
|
647
|
+
}
|
|
648
|
+
callback();
|
|
649
|
+
}
|
|
650
|
+
});
|
|
651
|
+
payload.on('error', error => {
|
|
652
|
+
capture.destroy(error);
|
|
653
|
+
});
|
|
654
|
+
payload.pipe(capture);
|
|
655
|
+
done(null, capture);
|
|
656
|
+
}
|
|
657
|
+
function isMultipartRequestContentType(contentType) {
|
|
658
|
+
const primaryValue = Array.isArray(contentType) ? contentType[0] : contentType;
|
|
659
|
+
return typeof primaryValue === 'string' && primaryValue.includes('multipart/form-data');
|
|
660
|
+
}
|
|
510
661
|
function resolveListenTarget(address, port, host, useHttps) {
|
|
511
662
|
const protocol = useHttps ? 'https' : 'http';
|
|
512
663
|
const resolvedPort = typeof address === 'object' && address !== null ? address.port : port;
|
package/package.json
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
"platform",
|
|
9
9
|
"server"
|
|
10
10
|
],
|
|
11
|
-
"version": "1.0.0-beta.
|
|
11
|
+
"version": "1.0.0-beta.5",
|
|
12
12
|
"private": false,
|
|
13
13
|
"license": "MIT",
|
|
14
14
|
"repository": {
|
|
@@ -38,11 +38,12 @@
|
|
|
38
38
|
"@fastify/multipart": "^9.2.1",
|
|
39
39
|
"fastify": "^5.8.5",
|
|
40
40
|
"fastify-raw-body": "^5.0.0",
|
|
41
|
-
"@fluojs/http": "^1.0.0-beta.
|
|
42
|
-
"@fluojs/runtime": "^1.0.0-beta.
|
|
41
|
+
"@fluojs/http": "^1.0.0-beta.4",
|
|
42
|
+
"@fluojs/runtime": "^1.0.0-beta.5"
|
|
43
43
|
},
|
|
44
44
|
"devDependencies": {
|
|
45
|
-
"vitest": "^3.2.4"
|
|
45
|
+
"vitest": "^3.2.4",
|
|
46
|
+
"@fluojs/di": "^1.0.0-beta.5"
|
|
46
47
|
},
|
|
47
48
|
"scripts": {
|
|
48
49
|
"prebuild": "node ../../tooling/scripts/clean-dist.mjs",
|