@fluojs/platform-fastify 1.0.0-beta.1

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 fluo contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.ko.md ADDED
@@ -0,0 +1,160 @@
1
+ # @fluojs/platform-fastify
2
+
3
+ <p><a href="./README.md"><kbd>English</kbd></a> <strong><kbd>한국어</kbd></strong></p>
4
+
5
+ fluo 런타임을 위한 Fastify 기반 HTTP 어댑터 패키지입니다.
6
+
7
+ ## 목차
8
+
9
+ - [설치](#설치)
10
+ - [사용 시점](#사용-시점)
11
+ - [빠른 시작](#빠른-시작)
12
+ - [주요 패턴](#주요-패턴)
13
+ - [성능](#성능)
14
+ - [공개 API 개요](#공개-api-개요)
15
+ - [트러블슈팅](#트러블슈팅)
16
+ - [관련 패키지](#관련-패키지)
17
+ - [예제 소스](#예제-소스)
18
+
19
+ ## 설치
20
+
21
+ ```bash
22
+ npm install @fluojs/platform-fastify fastify
23
+ ```
24
+
25
+ ## 사용 시점
26
+
27
+ fluo 애플리케이션을 위한 고성능 HTTP 어댑터가 필요한 경우 이 패키지를 사용합니다. Fastify는 낮은 오버헤드와 효율적인 요청 처리로 잘 알려져 있으며, 높은 처리량과 동시성이 요구되는 프로덕션 fluo 애플리케이션에 권장되는 선택입니다.
28
+
29
+ ## 빠른 시작
30
+
31
+ ```typescript
32
+ import { createFastifyAdapter } from '@fluojs/platform-fastify';
33
+ import { fluoFactory } from '@fluojs/runtime';
34
+ import { AppModule } from './app.module';
35
+
36
+ const app = await fluoFactory.create(AppModule, {
37
+ adapter: createFastifyAdapter({ port: 3000 }),
38
+ });
39
+
40
+ await app.listen();
41
+ ```
42
+
43
+ ## 주요 패턴
44
+
45
+ ### 멀티파트 및 Raw Body
46
+ Fastify 어댑터는 내부 Fastify 플러그인을 통해 멀티파트 form-data 및 raw body 파싱을 기본적으로 지원하며, 이는 표준 fluo 요청 인터페이스를 통해 노출됩니다. 어댑터를 직접 생성할 때는 멀티파트 제한을 두 번째 인자로 전달하고, `bootstrapFastifyApplication(...)` 및 `runFastifyApplication(...)`에서는 같은 설정을 `options.multipart` 아래에 전달하면 됩니다.
47
+
48
+ ```typescript
49
+ const adapter = createFastifyAdapter(
50
+ {
51
+ port: 3000,
52
+ rawBody: true,
53
+ },
54
+ {
55
+ maxTotalSize: 10 * 1024 * 1024,
56
+ },
57
+ );
58
+ ```
59
+
60
+ ### 서버 기반 실시간 통신 (Real-Time)
61
+ Fastify는 `@fluojs/websockets`가 기본 Node.js HTTP 서버에 직접 연결될 수 있도록 `server-backed` 기능을 제공합니다.
62
+
63
+ ```typescript
64
+ @WebSocketGateway({ path: '/ws' })
65
+ export class MyGateway {}
66
+ ```
67
+
68
+ ### CORS 설정
69
+ CORS는 부트스트랩 옵션을 통해 처리됩니다. fluo는 별도의 Fastify 플러그인에 의존하지 않고 내부 CORS 로직을 관리합니다.
70
+
71
+ ```typescript
72
+ // 단순 origin 문자열 설정
73
+ await bootstrapFastifyApplication(AppModule, {
74
+ cors: 'https://my-frontend.com',
75
+ port: 3000,
76
+ });
77
+
78
+ // 세부 설정
79
+ await bootstrapFastifyApplication(AppModule, {
80
+ cors: {
81
+ origin: ['https://a.com', 'https://b.com'],
82
+ methods: ['GET', 'POST', 'PUT', 'DELETE'],
83
+ },
84
+ port: 3000,
85
+ });
86
+
87
+ // 명시적으로 비활성화
88
+ await bootstrapFastifyApplication(AppModule, {
89
+ cors: false,
90
+ port: 3000,
91
+ });
92
+ ```
93
+
94
+ ### 글로벌 접두사 (Global Prefix)
95
+ 라우팅 접두사를 전역으로 설정하고, 헬스 체크와 같은 특정 경로는 제외할 수 있습니다.
96
+
97
+ ```typescript
98
+ await bootstrapFastifyApplication(AppModule, {
99
+ globalPrefix: '/api',
100
+ globalPrefixExclude: ['/health'],
101
+ port: 3000,
102
+ });
103
+ ```
104
+
105
+ ### 로깅 (Logging)
106
+ fluo는 자체 로깅 시스템을 사용합니다. 어댑터는 Fastify 인스턴스를 생성할 때 네이티브 로거를 비활성화하며, 부트스트랩 옵션에 제공된 fluo 로거를 통해 로그를 기록합니다.
107
+
108
+ ```typescript
109
+ await runFastifyApplication(AppModule, {
110
+ logger: myLogger,
111
+ port: 3000,
112
+ });
113
+ ```
114
+
115
+ ### 미들웨어 (Middleware)
116
+ 요청이 핸들러에 도달하기 전에 실행되는 런타임 레벨의 미들웨어를 등록할 수 있습니다. 이는 Fastify 전용 플러그인이 아닌 표준 `MiddlewareLike` 함수라는 점에 유의하세요.
117
+
118
+ ```typescript
119
+ await bootstrapFastifyApplication(AppModule, {
120
+ middleware: [myCustomMiddleware],
121
+ port: 3000,
122
+ });
123
+ ```
124
+
125
+ ## 성능
126
+
127
+ fluo의 Fastify 어댑터는 높은 동시성 시나리오에서 raw Node.js 어댑터보다 훨씬 뛰어난 성능을 발휘합니다.
128
+
129
+ | 어댑터 | 초당 요청 수 (Req/sec) | 평균 지연 시간 (Avg Latency) |
130
+ | --- | ---: | ---: |
131
+ | Raw Node.js 어댑터 | ~31,000 | 4.0ms |
132
+ | Fastify 어댑터 | **~58,000** | **2.1ms** |
133
+
134
+ *표준 `/health` 엔드포인트에서 `wrk`를 사용하여 측정되었습니다.*
135
+
136
+ ## 공개 API 개요
137
+
138
+ - `createFastifyAdapter(options)`: Fastify 어댑터를 위한 권장 팩토리입니다.
139
+ - `bootstrapFastifyApplication(module, options)`: 암시적 리스닝 없이 수행하는 고급 부트스트랩입니다.
140
+ - `runFastifyApplication(module, options)`: 생명주기 관리를 포함한 빠른 시작 헬퍼입니다. timeout/실패 시에는 해당 상태를 로그와 `process.exitCode`로 보고하고, 최종 프로세스 종료는 주변 호스트에 맡깁니다.
141
+ - `FastifyHttpApplicationAdapter`: 핵심 어댑터 구현 클래스입니다.
142
+
143
+ ## 트러블슈팅
144
+
145
+ - **CORS 오류**: `cors` 부트스트랩 옵션을 사용 중인지 확인하세요. Fastify의 네이티브 CORS 플러그인을 사용하지 않으므로 오직 fluo가 관리하는 CORS 로직만 적용됩니다.
146
+ - **미들웨어 문제**: `middleware` 옵션은 런타임 레벨의 `MiddlewareLike[]` 함수 배열을 받습니다. 이는 Fastify 플러그인이 아니며 다른 fluo 어댑터들과 공통으로 사용되는 표준 인터페이스를 따릅니다.
147
+ - **로깅 (Logging)**: 로그 스트림 중복을 방지하기 위해 Fastify의 네이티브 로거가 비활성화됩니다. 모든 로깅 설정은 `runFastifyApplication` 또는 `bootstrapFastifyApplication`의 `logger` 옵션을 통해 이루어져야 합니다.
148
+ - **글로벌 접두사 (Global Prefix)**: 내부 경로 또는 헬스 체크 엔드포인트에 접두사가 붙지 않도록 `globalPrefixExclude`를 적절히 설정하세요.
149
+
150
+ ## 관련 패키지
151
+
152
+ - `@fluojs/runtime`: 핵심 런타임입니다.
153
+ - `@fluojs/platform-express`: 대안 Express 기반 어댑터입니다.
154
+ - `@fluojs/websockets`: 실시간 게이트웨이 지원을 제공합니다.
155
+
156
+ ## 예제 소스
157
+
158
+ - `packages/platform-fastify/src/adapter.test.ts`
159
+ - `examples/minimal/src/main.ts`
160
+ - `examples/realworld-api/src/main.ts`
package/README.md ADDED
@@ -0,0 +1,160 @@
1
+ # @fluojs/platform-fastify
2
+
3
+ <p><strong><kbd>English</kbd></strong> <a href="./README.ko.md"><kbd>한국어</kbd></a></p>
4
+
5
+ Fastify-backed HTTP adapter for the fluo runtime.
6
+
7
+ ## Table of Contents
8
+
9
+ - [Installation](#installation)
10
+ - [When to Use](#when-to-use)
11
+ - [Quick Start](#quick-start)
12
+ - [Common Patterns](#common-patterns)
13
+ - [Performance](#performance)
14
+ - [Public API Overview](#public-api-overview)
15
+ - [Troubleshooting](#troubleshooting)
16
+ - [Related Packages](#related-packages)
17
+ - [Example Sources](#example-sources)
18
+
19
+ ## Installation
20
+
21
+ ```bash
22
+ npm install @fluojs/platform-fastify fastify
23
+ ```
24
+
25
+ ## When to Use
26
+
27
+ Use this package when you need a high-performance HTTP adapter for your fluo application. Fastify is known for its low overhead and efficient request handling, making it the recommended choice for production fluo applications requiring high throughput and concurrency.
28
+
29
+ ## Quick Start
30
+
31
+ ```typescript
32
+ import { createFastifyAdapter } from '@fluojs/platform-fastify';
33
+ import { fluoFactory } from '@fluojs/runtime';
34
+ import { AppModule } from './app.module';
35
+
36
+ const app = await fluoFactory.create(AppModule, {
37
+ adapter: createFastifyAdapter({ port: 3000 }),
38
+ });
39
+
40
+ await app.listen();
41
+ ```
42
+
43
+ ## Common Patterns
44
+
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`.
47
+
48
+ ```typescript
49
+ const adapter = createFastifyAdapter(
50
+ {
51
+ port: 3000,
52
+ rawBody: true,
53
+ },
54
+ {
55
+ maxTotalSize: 10 * 1024 * 1024,
56
+ },
57
+ );
58
+ ```
59
+
60
+ ### Server-Backed Real-Time
61
+ Fastify provides a `server-backed` capability that allows `@fluojs/websockets` to attach directly to the underlying Node.js HTTP server.
62
+
63
+ ```typescript
64
+ @WebSocketGateway({ path: '/ws' })
65
+ export class MyGateway {}
66
+ ```
67
+
68
+ ### CORS Configuration
69
+ CORS is handled via bootstrap options. fluo manages the underlying CORS logic rather than relying on a separate Fastify plugin.
70
+
71
+ ```typescript
72
+ // Simple origin string
73
+ await bootstrapFastifyApplication(AppModule, {
74
+ cors: 'https://my-frontend.com',
75
+ port: 3000,
76
+ });
77
+
78
+ // Fine-grained control
79
+ await bootstrapFastifyApplication(AppModule, {
80
+ cors: {
81
+ origin: ['https://a.com', 'https://b.com'],
82
+ methods: ['GET', 'POST', 'PUT', 'DELETE'],
83
+ },
84
+ port: 3000,
85
+ });
86
+
87
+ // Explicitly disabled
88
+ await bootstrapFastifyApplication(AppModule, {
89
+ cors: false,
90
+ port: 3000,
91
+ });
92
+ ```
93
+
94
+ ### Global Prefix
95
+ Configure a global routing prefix and exclude specific paths like health checks.
96
+
97
+ ```typescript
98
+ await bootstrapFastifyApplication(AppModule, {
99
+ globalPrefix: '/api',
100
+ globalPrefixExclude: ['/health'],
101
+ port: 3000,
102
+ });
103
+ ```
104
+
105
+ ### Logging
106
+ fluo uses its own logging system. The adapter creates the Fastify instance with its native logger disabled and pipes through the fluo logger provided in the bootstrap options.
107
+
108
+ ```typescript
109
+ await runFastifyApplication(AppModule, {
110
+ logger: myLogger,
111
+ port: 3000,
112
+ });
113
+ ```
114
+
115
+ ### Middleware
116
+ You can register runtime-level middleware that runs before the request reaches the handlers. Note that these are standard `MiddlewareLike` functions, not Fastify-specific plugins.
117
+
118
+ ```typescript
119
+ await bootstrapFastifyApplication(AppModule, {
120
+ middleware: [myCustomMiddleware],
121
+ port: 3000,
122
+ });
123
+ ```
124
+
125
+ ## Performance
126
+
127
+ fluo's Fastify adapter significantly outperforms the raw Node.js adapter in high-concurrency scenarios.
128
+
129
+ | Adapter | Requests/sec | Avg Latency |
130
+ | --- | ---: | ---: |
131
+ | Raw Node.js Adapter | ~31,000 | 4.0ms |
132
+ | Fastify Adapter | **~58,000** | **2.1ms** |
133
+
134
+ *Measured using `wrk` on a standard `/health` endpoint.*
135
+
136
+ ## Public API Overview
137
+
138
+ - `createFastifyAdapter(options)`: Recommended factory for the Fastify adapter.
139
+ - `bootstrapFastifyApplication(module, options)`: advanced bootstrap without implicit listening.
140
+ - `runFastifyApplication(module, options)`: Quick-start helper with lifecycle management. On timeout/failure it reports the condition through logging and `process.exitCode`, while leaving final process termination to the surrounding host.
141
+ - `FastifyHttpApplicationAdapter`: The core adapter implementation.
142
+
143
+ ## Troubleshooting
144
+
145
+ - **CORS Errors**: Ensure you're using the `cors` bootstrap option. Since Fastify's native CORS plugin is not registered, only the fluo-managed CORS logic applies.
146
+ - **Middleware Issues**: The `middleware` option accepts runtime-level `MiddlewareLike[]` functions. These are not Fastify plugins and follow the standard middleware interface used across fluo adapters.
147
+ - **Logging**: The native Fastify logger is disabled to prevent duplicate log streams. All logging should be configured via the fluo `logger` option in `runFastifyApplication` or `bootstrapFastifyApplication`.
148
+ - **Global Prefix**: Use `globalPrefixExclude` to prevent the prefix from being applied to internal routes or health check endpoints.
149
+
150
+ ## Related Packages
151
+
152
+ - `@fluojs/runtime`: Core framework runtime.
153
+ - `@fluojs/platform-express`: Alternative Express-based adapter.
154
+ - `@fluojs/websockets`: Real-time gateway support.
155
+
156
+ ## Example Sources
157
+
158
+ - `packages/platform-fastify/src/adapter.test.ts`
159
+ - `examples/minimal/src/main.ts`
160
+ - `examples/realworld-api/src/main.ts`
@@ -0,0 +1,132 @@
1
+ import type { ServerOptions as HttpsServerOptions } from 'node:https';
2
+ import { type CorsOptions, type Dispatcher, type HttpApplicationAdapter, type MiddlewareLike, type SecurityHeadersOptions } from '@fluojs/http';
3
+ import { type Application, type ApplicationLogger, type CreateApplicationOptions, type ModuleType, type MultipartOptions, type UploadedFile } from '@fluojs/runtime';
4
+ declare module '@fluojs/http' {
5
+ interface FrameworkRequest {
6
+ files?: UploadedFile[];
7
+ rawBody?: Uint8Array;
8
+ }
9
+ }
10
+ /**
11
+ * Transport-level knobs for the standalone Fastify HTTP adapter factory.
12
+ */
13
+ export interface FastifyAdapterOptions {
14
+ host?: string;
15
+ https?: HttpsServerOptions;
16
+ maxBodySize?: number;
17
+ port?: number;
18
+ rawBody?: boolean;
19
+ retryDelayMs?: number;
20
+ retryLimit?: number;
21
+ shutdownTimeoutMs?: number;
22
+ }
23
+ /** Node.js shutdown signals supported by `runFastifyApplication(...)`. */
24
+ export type FastifyApplicationSignal = 'SIGINT' | 'SIGTERM';
25
+ /** CORS shorthand accepted by the Fastify runtime bootstrap helpers. */
26
+ export type CorsInput = false | string | string[] | CorsOptions;
27
+ /**
28
+ * Bootstrap options for creating a Fastify-backed application without
29
+ * implicitly registering process shutdown listeners.
30
+ */
31
+ export interface BootstrapFastifyApplicationOptions extends Omit<CreateApplicationOptions, 'adapter' | 'logger' | 'middleware'> {
32
+ cors?: CorsInput;
33
+ globalPrefix?: string;
34
+ globalPrefixExclude?: readonly string[];
35
+ host?: string;
36
+ https?: HttpsServerOptions;
37
+ logger?: ApplicationLogger;
38
+ maxBodySize?: number;
39
+ middleware?: MiddlewareLike[];
40
+ multipart?: MultipartOptions;
41
+ port?: number;
42
+ rawBody?: boolean;
43
+ retryDelayMs?: number;
44
+ retryLimit?: number;
45
+ securityHeaders?: false | SecurityHeadersOptions;
46
+ shutdownTimeoutMs?: number;
47
+ }
48
+ /**
49
+ * Bootstrap options for `runFastifyApplication(...)`, including shutdown hooks.
50
+ */
51
+ export interface RunFastifyApplicationOptions extends BootstrapFastifyApplicationOptions {
52
+ forceExitTimeoutMs?: number;
53
+ shutdownSignals?: false | readonly FastifyApplicationSignal[];
54
+ }
55
+ interface FastifyListenTarget {
56
+ bindTarget: string;
57
+ url: string;
58
+ }
59
+ /**
60
+ * Fastify-backed `HttpApplicationAdapter` implementation used by the runtime.
61
+ *
62
+ * It preserves the shared Fluo dispatcher contract while exposing Fastify's
63
+ * server-backed realtime capability and multipart/raw-body integrations.
64
+ */
65
+ export declare class FastifyHttpApplicationAdapter implements HttpApplicationAdapter {
66
+ private readonly port;
67
+ private readonly host;
68
+ private readonly retryDelayMs;
69
+ private readonly retryLimit;
70
+ private readonly httpsOptions;
71
+ private readonly multipartOptions?;
72
+ private readonly maxBodySize;
73
+ private readonly preserveRawBody;
74
+ private readonly shutdownTimeoutMs;
75
+ private closeInFlight?;
76
+ private dispatcher?;
77
+ private pluginsReady;
78
+ private readonly app;
79
+ private readonly requestResponseFactory;
80
+ constructor(port: number, host: string | undefined, retryDelayMs: number | undefined, retryLimit: number | undefined, httpsOptions: HttpsServerOptions | undefined, multipartOptions?: MultipartOptions | undefined, maxBodySize?: number, preserveRawBody?: boolean, shutdownTimeoutMs?: number);
81
+ getServer(): unknown;
82
+ getRealtimeCapability(): import("@fluojs/http").ServerBackedHttpAdapterRealtimeCapability;
83
+ getListenTarget(): FastifyListenTarget;
84
+ listen(dispatcher: Dispatcher): Promise<void>;
85
+ close(): Promise<void>;
86
+ private registerPluginsAndRoute;
87
+ private listenWithRetry;
88
+ private handleRequest;
89
+ }
90
+ /**
91
+ * Create the recommended Fastify adapter for `FluoFactory.create(...)`.
92
+ *
93
+ * @example
94
+ * ```ts
95
+ * const app = await FluoFactory.create(AppModule, {
96
+ * adapter: createFastifyAdapter({ port: 3000 }),
97
+ * });
98
+ * ```
99
+ *
100
+ * @param options Transport-level Fastify settings such as host, port, retries, and raw-body preservation.
101
+ * @param multipartOptions Optional multipart parsing limits exposed through `FrameworkRequest.files`.
102
+ * @returns A runtime `HttpApplicationAdapter` backed by Fastify.
103
+ */
104
+ export declare function createFastifyAdapter(options?: FastifyAdapterOptions, multipartOptions?: MultipartOptions): HttpApplicationAdapter;
105
+ /**
106
+ * Bootstrap a Fastify-backed application without implicitly calling `listen()`.
107
+ *
108
+ * @param rootModule Root application module compiled by the Fluo runtime.
109
+ * @param options Runtime, middleware, and Fastify adapter settings.
110
+ * @returns An initialized application shell that can be listened to later.
111
+ */
112
+ export declare function bootstrapFastifyApplication(rootModule: ModuleType, options: BootstrapFastifyApplicationOptions): Promise<Application>;
113
+ /**
114
+ * Bootstrap and prepare a Fastify-backed application with shutdown registration.
115
+ *
116
+ * This helper mirrors the README quick-start path: create the adapter, wire the
117
+ * runtime, and attach signal handling so callers only need to invoke `listen()`.
118
+ *
119
+ * @param rootModule Root application module compiled by the Fluo runtime.
120
+ * @param options Runtime, adapter, and shutdown registration settings.
121
+ * @returns A bootstrapped application shell ready to listen.
122
+ */
123
+ export declare function runFastifyApplication(rootModule: ModuleType, options: RunFastifyApplicationOptions): Promise<Application>;
124
+ /**
125
+ * Detect whether a thrown Fastify multipart error maps to payload-too-large semantics.
126
+ *
127
+ * @param error Unknown Fastify or plugin error thrown while parsing multipart input.
128
+ * @returns `true` when the error should surface as `PayloadTooLargeException`.
129
+ */
130
+ export declare function isFastifyMultipartTooLargeError(error: unknown): boolean;
131
+ export {};
132
+ //# sourceMappingURL=adapter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"adapter.d.ts","sourceRoot":"","sources":["../src/adapter.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,610 @@
1
+ import multipart from '@fastify/multipart';
2
+ import fastify from 'fastify';
3
+ import fastifyRawBody from 'fastify-raw-body';
4
+ import { createServerBackedHttpAdapterRealtimeCapability, createErrorResponse, HttpException, InternalServerErrorException, PayloadTooLargeException } from '@fluojs/http';
5
+ import { createNodeShutdownSignalRegistration, defaultNodeShutdownSignals } from '@fluojs/runtime/node';
6
+ import { bootstrapHttpAdapterApplication, runHttpAdapterApplication } from '@fluojs/runtime/internal/http-adapter';
7
+ import { dispatchWithRequestResponseFactory } from '@fluojs/runtime/internal/request-response-factory';
8
+
9
+ /**
10
+ * Transport-level knobs for the standalone Fastify HTTP adapter factory.
11
+ */
12
+
13
+ /** Node.js shutdown signals supported by `runFastifyApplication(...)`. */
14
+
15
+ /** CORS shorthand accepted by the Fastify runtime bootstrap helpers. */
16
+
17
+ const DEFAULT_MAX_BODY_SIZE = 1 * 1024 * 1024;
18
+ const DEFAULT_SHUTDOWN_TIMEOUT_MS = 10_000;
19
+
20
+ /**
21
+ * Bootstrap options for creating a Fastify-backed application without
22
+ * implicitly registering process shutdown listeners.
23
+ */
24
+
25
+ /**
26
+ * Bootstrap options for `runFastifyApplication(...)`, including shutdown hooks.
27
+ */
28
+
29
+ /**
30
+ * Fastify-backed `HttpApplicationAdapter` implementation used by the runtime.
31
+ *
32
+ * It preserves the shared Fluo dispatcher contract while exposing Fastify's
33
+ * server-backed realtime capability and multipart/raw-body integrations.
34
+ */
35
+ export class FastifyHttpApplicationAdapter {
36
+ closeInFlight;
37
+ dispatcher;
38
+ pluginsReady = false;
39
+ app;
40
+ requestResponseFactory;
41
+ constructor(port, host, retryDelayMs = 150, retryLimit = 20, httpsOptions, multipartOptions, maxBodySize = DEFAULT_MAX_BODY_SIZE, preserveRawBody = false, shutdownTimeoutMs = DEFAULT_SHUTDOWN_TIMEOUT_MS) {
42
+ this.port = port;
43
+ this.host = host;
44
+ this.retryDelayMs = retryDelayMs;
45
+ this.retryLimit = retryLimit;
46
+ this.httpsOptions = httpsOptions;
47
+ this.multipartOptions = multipartOptions;
48
+ this.maxBodySize = maxBodySize;
49
+ this.preserveRawBody = preserveRawBody;
50
+ this.shutdownTimeoutMs = shutdownTimeoutMs;
51
+ this.app = createFastifyApp(this.httpsOptions, this.maxBodySize);
52
+ this.requestResponseFactory = createFastifyRequestResponseFactory(this.multipartOptions, this.maxBodySize, this.preserveRawBody);
53
+ }
54
+ getServer() {
55
+ return this.app.server;
56
+ }
57
+ getRealtimeCapability() {
58
+ return createServerBackedHttpAdapterRealtimeCapability(this.app.server);
59
+ }
60
+ getListenTarget() {
61
+ return resolveListenTarget(this.app.server.address() ?? null, this.port, this.host, this.httpsOptions !== undefined);
62
+ }
63
+ async listen(dispatcher) {
64
+ this.dispatcher = dispatcher;
65
+ await this.registerPluginsAndRoute();
66
+ await this.listenWithRetry();
67
+ }
68
+ async close() {
69
+ if (!this.app.server.listening) {
70
+ this.dispatcher = undefined;
71
+ return;
72
+ }
73
+ if (!this.closeInFlight) {
74
+ const closePromise = this.app.close();
75
+ const closeInFlight = closePromise.finally(() => {
76
+ this.closeInFlight = undefined;
77
+ this.dispatcher = undefined;
78
+ });
79
+ this.closeInFlight = closeInFlight;
80
+ void closeInFlight.catch(() => {});
81
+ }
82
+ const closeInFlight = this.closeInFlight;
83
+ if (!closeInFlight) {
84
+ return;
85
+ }
86
+ await waitForCloseWithTimeout(closeInFlight, this.shutdownTimeoutMs);
87
+ }
88
+ async registerPluginsAndRoute() {
89
+ if (this.pluginsReady) {
90
+ return;
91
+ }
92
+ await this.app.register(multipart);
93
+ if (this.preserveRawBody) {
94
+ await this.app.register(fastifyRawBody, {
95
+ encoding: 'utf8',
96
+ field: 'rawBody',
97
+ global: true,
98
+ runFirst: true
99
+ });
100
+ }
101
+ this.app.all('*', async (request, reply) => {
102
+ await this.handleRequest(request, reply);
103
+ });
104
+ this.pluginsReady = true;
105
+ }
106
+ async listenWithRetry() {
107
+ for (let attempt = 0;; attempt++) {
108
+ try {
109
+ await this.app.listen({
110
+ host: this.host,
111
+ port: this.port
112
+ });
113
+ return;
114
+ } catch (error) {
115
+ if (!isAddressInUseError(error) || attempt >= this.retryLimit) {
116
+ throw error;
117
+ }
118
+ await delay(this.retryDelayMs);
119
+ }
120
+ }
121
+ }
122
+ async handleRequest(request, reply) {
123
+ await dispatchWithRequestResponseFactory({
124
+ dispatcher: this.dispatcher,
125
+ dispatcherNotReadyMessage: 'Fastify adapter received a request before dispatcher binding completed.',
126
+ factory: this.requestResponseFactory,
127
+ rawRequest: request,
128
+ rawResponse: reply
129
+ });
130
+ }
131
+ }
132
+ function createFastifyRequestResponseFactory(multipartOptions, maxBodySize = DEFAULT_MAX_BODY_SIZE, preserveRawBody = false) {
133
+ return {
134
+ async createRequest(request, signal) {
135
+ return createFrameworkRequest(request, signal, multipartOptions, maxBodySize, preserveRawBody);
136
+ },
137
+ createRequestSignal(reply) {
138
+ return createRequestSignal(reply.raw);
139
+ },
140
+ createResponse(reply) {
141
+ return createFrameworkResponse(reply);
142
+ },
143
+ resolveRequestId(request) {
144
+ return resolveRequestIdFromHeaders(request.raw.headers);
145
+ },
146
+ async writeErrorResponse(error, response, requestId) {
147
+ const httpError = toHttpException(error);
148
+ response.setStatus(httpError.status);
149
+ await response.send(createErrorResponse(httpError, requestId));
150
+ }
151
+ };
152
+ }
153
+
154
+ /**
155
+ * Create the recommended Fastify adapter for `FluoFactory.create(...)`.
156
+ *
157
+ * @example
158
+ * ```ts
159
+ * const app = await FluoFactory.create(AppModule, {
160
+ * adapter: createFastifyAdapter({ port: 3000 }),
161
+ * });
162
+ * ```
163
+ *
164
+ * @param options Transport-level Fastify settings such as host, port, retries, and raw-body preservation.
165
+ * @param multipartOptions Optional multipart parsing limits exposed through `FrameworkRequest.files`.
166
+ * @returns A runtime `HttpApplicationAdapter` backed by Fastify.
167
+ */
168
+ export function createFastifyAdapter(options = {}, multipartOptions) {
169
+ return new FastifyHttpApplicationAdapter(resolvePort(options.port), options.host, options.retryDelayMs, options.retryLimit, options.https, multipartOptions, options.maxBodySize, options.rawBody, options.shutdownTimeoutMs);
170
+ }
171
+
172
+ /**
173
+ * Bootstrap a Fastify-backed application without implicitly calling `listen()`.
174
+ *
175
+ * @param rootModule Root application module compiled by the Fluo runtime.
176
+ * @param options Runtime, middleware, and Fastify adapter settings.
177
+ * @returns An initialized application shell that can be listened to later.
178
+ */
179
+ export async function bootstrapFastifyApplication(rootModule, options) {
180
+ return bootstrapHttpAdapterApplication(rootModule, options, createFastifyAdapter(options, options.multipart));
181
+ }
182
+
183
+ /**
184
+ * Bootstrap and prepare a Fastify-backed application with shutdown registration.
185
+ *
186
+ * This helper mirrors the README quick-start path: create the adapter, wire the
187
+ * runtime, and attach signal handling so callers only need to invoke `listen()`.
188
+ *
189
+ * @param rootModule Root application module compiled by the Fluo runtime.
190
+ * @param options Runtime, adapter, and shutdown registration settings.
191
+ * @returns A bootstrapped application shell ready to listen.
192
+ */
193
+ export async function runFastifyApplication(rootModule, options) {
194
+ const adapter = createFastifyAdapter(options, options.multipart);
195
+ return runHttpAdapterApplication(rootModule, {
196
+ ...options,
197
+ shutdownRegistration: createNodeShutdownSignalRegistration(options.shutdownSignals ?? defaultNodeShutdownSignals())
198
+ }, adapter);
199
+ }
200
+ function createFrameworkResponse(reply) {
201
+ return {
202
+ committed: reply.sent,
203
+ headers: {},
204
+ raw: reply,
205
+ stream: createFrameworkResponseStream(reply),
206
+ redirect(status, location) {
207
+ this.setStatus(status);
208
+ this.setHeader('Location', location);
209
+ this.committed = true;
210
+ reply.redirect(location, status);
211
+ },
212
+ async send(body) {
213
+ if (reply.sent) {
214
+ this.committed = true;
215
+ return;
216
+ }
217
+ const existingContentType = reply.getHeader('content-type');
218
+ const serialized = serializeResponseBody(body, typeof existingContentType === 'string' ? existingContentType : undefined);
219
+ if (!reply.hasHeader('content-type') && serialized.defaultContentType) {
220
+ reply.header('content-type', serialized.defaultContentType);
221
+ }
222
+ this.committed = true;
223
+ await reply.send(serialized.payload);
224
+ },
225
+ setHeader(name, value) {
226
+ const lowerName = name.toLowerCase();
227
+ if (lowerName === 'set-cookie') {
228
+ const merged = mergeSetCookieHeader(reply.getHeader(name), value);
229
+ reply.header(name, merged);
230
+ this.headers[name] = merged;
231
+ return;
232
+ }
233
+ reply.header(name, value);
234
+ this.headers[name] = value;
235
+ },
236
+ setStatus(code) {
237
+ reply.status(code);
238
+ this.statusCode = code;
239
+ this.statusSet = true;
240
+ },
241
+ statusCode: undefined,
242
+ statusSet: false
243
+ };
244
+ }
245
+ function createFrameworkResponseStream(reply) {
246
+ let hijacked = false;
247
+ const ensureHijacked = () => {
248
+ if (!hijacked && !reply.sent) {
249
+ reply.raw.statusCode = reply.statusCode;
250
+ for (const [name, value] of Object.entries(reply.getHeaders())) {
251
+ if (value !== undefined) {
252
+ reply.raw.setHeader(name, value);
253
+ }
254
+ }
255
+ reply.hijack();
256
+ hijacked = true;
257
+ }
258
+ };
259
+ return {
260
+ close() {
261
+ ensureHijacked();
262
+ if (!reply.raw.writableEnded) {
263
+ reply.raw.end();
264
+ }
265
+ },
266
+ get closed() {
267
+ return reply.raw.writableEnded;
268
+ },
269
+ flush() {
270
+ ensureHijacked();
271
+ reply.raw.flushHeaders?.();
272
+ },
273
+ onClose(listener) {
274
+ reply.raw.on('close', listener);
275
+ return () => {
276
+ reply.raw.removeListener('close', listener);
277
+ };
278
+ },
279
+ waitForDrain() {
280
+ ensureHijacked();
281
+ if (reply.raw.writableEnded) {
282
+ return Promise.resolve();
283
+ }
284
+ return new Promise(resolve => {
285
+ reply.raw.once('drain', () => resolve());
286
+ });
287
+ },
288
+ write(chunk) {
289
+ ensureHijacked();
290
+ return reply.raw.write(chunk);
291
+ }
292
+ };
293
+ }
294
+ async function createFrameworkRequest(request, signal, multipartOptions, maxBodySize = DEFAULT_MAX_BODY_SIZE, preserveRawBody = false) {
295
+ const rawUrl = request.raw.url ?? '/';
296
+ const url = new URL(rawUrl, 'http://localhost');
297
+ const headers = normalizeHeaders(request.headers);
298
+ const contentType = headers['content-type'];
299
+ const isMultipart = typeof contentType === 'string' && contentType.includes('multipart/form-data');
300
+ let body = request.body;
301
+ let files;
302
+ if (isMultipart) {
303
+ const parsed = await parseMultipartRequest(request, {
304
+ ...multipartOptions,
305
+ maxTotalSize: multipartOptions?.maxTotalSize ?? maxBodySize
306
+ });
307
+ body = parsed.fields;
308
+ files = parsed.files;
309
+ }
310
+ const frameworkRequest = {
311
+ body,
312
+ cookies: parseCookieHeader(Array.isArray(headers.cookie) ? headers.cookie[0] : headers.cookie),
313
+ headers,
314
+ method: request.method,
315
+ params: {},
316
+ path: url.pathname,
317
+ query: parseQueryParams(url.searchParams),
318
+ raw: request.raw,
319
+ signal,
320
+ url: url.pathname + url.search
321
+ };
322
+ if (files) {
323
+ frameworkRequest.files = files;
324
+ }
325
+ if (preserveRawBody && !isMultipart) {
326
+ const rawBodyValue = request.rawBody;
327
+ if (rawBodyValue !== undefined) {
328
+ frameworkRequest.rawBody = typeof rawBodyValue === 'string' ? Buffer.from(rawBodyValue, 'utf8') : rawBodyValue;
329
+ }
330
+ }
331
+ return frameworkRequest;
332
+ }
333
+ async function parseMultipartRequest(request, options = {}) {
334
+ const fields = {};
335
+ const files = [];
336
+ const maxFileSize = options.maxFileSize ?? 10 * 1024 * 1024;
337
+ const maxFiles = options.maxFiles ?? 10;
338
+ const maxTotalSize = options.maxTotalSize ?? 10 * 1024 * 1024;
339
+ const contentLength = Number(request.headers['content-length']);
340
+ let totalSize = 0;
341
+ if (Number.isFinite(contentLength) && contentLength > maxTotalSize) {
342
+ throw new PayloadTooLargeException('Request body exceeds the configured multipart limits.');
343
+ }
344
+ try {
345
+ for await (const part of request.parts({
346
+ limits: {
347
+ fileSize: maxFileSize,
348
+ files: maxFiles
349
+ }
350
+ })) {
351
+ if (part.type === 'file') {
352
+ if (files.length >= maxFiles) {
353
+ throw new PayloadTooLargeException(`Exceeded maximum file count of ${String(maxFiles)}.`);
354
+ }
355
+ const buffer = await part.toBuffer();
356
+ totalSize += buffer.byteLength;
357
+ if (totalSize > maxTotalSize) {
358
+ throw new PayloadTooLargeException('Request body exceeds the configured multipart limits.');
359
+ }
360
+ files.push({
361
+ buffer,
362
+ fieldname: part.fieldname,
363
+ mimetype: part.mimetype,
364
+ originalname: part.filename,
365
+ size: buffer.byteLength
366
+ });
367
+ continue;
368
+ }
369
+ const value = String(part.value ?? '');
370
+ totalSize += Buffer.byteLength(value, 'utf8');
371
+ if (totalSize > maxTotalSize) {
372
+ throw new PayloadTooLargeException('Request body exceeds the configured multipart limits.');
373
+ }
374
+ setMultiValue(fields, part.fieldname, value);
375
+ }
376
+ } catch (error) {
377
+ if (isFastifyMultipartTooLargeError(error)) {
378
+ throw new PayloadTooLargeException('Request body exceeds the configured multipart limits.');
379
+ }
380
+ throw error;
381
+ }
382
+ return {
383
+ fields,
384
+ files
385
+ };
386
+ }
387
+
388
+ /**
389
+ * Detect whether a thrown Fastify multipart error maps to payload-too-large semantics.
390
+ *
391
+ * @param error Unknown Fastify or plugin error thrown while parsing multipart input.
392
+ * @returns `true` when the error should surface as `PayloadTooLargeException`.
393
+ */
394
+ export function isFastifyMultipartTooLargeError(error) {
395
+ if (!(error instanceof Error)) {
396
+ return false;
397
+ }
398
+ const candidate = error;
399
+ if (candidate.statusCode === 413) {
400
+ return true;
401
+ }
402
+ if (typeof candidate.code === 'string' && /FILE_TOO_LARGE|LIMIT/i.test(candidate.code)) {
403
+ return true;
404
+ }
405
+ return error.message.includes('toobig') || error.message.includes('File too large');
406
+ }
407
+ function normalizeHeaders(headers) {
408
+ const normalized = {};
409
+ for (const [name, value] of Object.entries(headers)) {
410
+ if (Array.isArray(value)) {
411
+ normalized[name] = value;
412
+ continue;
413
+ }
414
+ if (typeof value === 'number') {
415
+ normalized[name] = String(value);
416
+ continue;
417
+ }
418
+ if (typeof value === 'string' || value === undefined) {
419
+ normalized[name] = value;
420
+ continue;
421
+ }
422
+ normalized[name] = String(value);
423
+ }
424
+ return normalized;
425
+ }
426
+ function parseQueryParams(searchParams) {
427
+ const query = {};
428
+ for (const [key, value] of searchParams.entries()) {
429
+ const current = query[key];
430
+ if (current === undefined) {
431
+ query[key] = value;
432
+ continue;
433
+ }
434
+ if (Array.isArray(current)) {
435
+ current.push(value);
436
+ continue;
437
+ }
438
+ query[key] = [current, value];
439
+ }
440
+ return query;
441
+ }
442
+ function parseCookieHeader(cookieHeader) {
443
+ if (!cookieHeader) {
444
+ return {};
445
+ }
446
+ return Object.fromEntries(cookieHeader.split(';').map(pair => pair.trim()).filter(Boolean).map(pair => {
447
+ const index = pair.indexOf('=');
448
+ if (index === -1) {
449
+ return [pair.trim(), ''];
450
+ }
451
+ const rawValue = pair.slice(index + 1).trim();
452
+ try {
453
+ return [pair.slice(0, index).trim(), decodeURIComponent(rawValue)];
454
+ } catch {
455
+ return [pair.slice(0, index).trim(), rawValue];
456
+ }
457
+ }));
458
+ }
459
+ function setMultiValue(target, key, value) {
460
+ const existing = target[key];
461
+ if (existing === undefined) {
462
+ target[key] = value;
463
+ return;
464
+ }
465
+ if (Array.isArray(existing)) {
466
+ existing.push(value);
467
+ return;
468
+ }
469
+ target[key] = [existing, value];
470
+ }
471
+ function createRequestSignal(response) {
472
+ const controller = new AbortController();
473
+ const abort = reason => {
474
+ if (!controller.signal.aborted) {
475
+ controller.abort(new Error(reason));
476
+ }
477
+ };
478
+ response.once('close', () => {
479
+ if (!response.writableEnded) {
480
+ abort('Response closed before response commit.');
481
+ }
482
+ });
483
+ return controller.signal;
484
+ }
485
+ function resolveRequestIdFromHeaders(headers) {
486
+ const requestId = headers['x-request-id'] ?? headers['x-correlation-id'];
487
+ return Array.isArray(requestId) ? requestId[0] : requestId;
488
+ }
489
+ function createFastifyApp(httpsOptions, maxBodySize) {
490
+ if (httpsOptions) {
491
+ return fastify({
492
+ bodyLimit: maxBodySize,
493
+ https: httpsOptions,
494
+ logger: false
495
+ });
496
+ }
497
+ return fastify({
498
+ bodyLimit: maxBodySize,
499
+ logger: false
500
+ });
501
+ }
502
+ function resolveListenTarget(address, port, host, useHttps) {
503
+ const protocol = useHttps ? 'https' : 'http';
504
+ const resolvedPort = typeof address === 'object' && address !== null ? address.port : port;
505
+ const bindHost = typeof address === 'object' && address !== null ? address.address : host ?? '0.0.0.0';
506
+ const publicHost = resolvePublicHost(host ?? bindHost);
507
+ const bindTarget = `${formatHostForAuthority(bindHost)}:${String(resolvedPort)}`;
508
+ const url = `${protocol}://${formatHostForAuthority(publicHost)}:${String(resolvedPort)}`;
509
+ return {
510
+ bindTarget,
511
+ url
512
+ };
513
+ }
514
+ function resolvePublicHost(host) {
515
+ return isWildcardHost(host) ? 'localhost' : host;
516
+ }
517
+ function isWildcardHost(host) {
518
+ return host === '0.0.0.0' || host === '::' || host === '[::]';
519
+ }
520
+ function formatHostForAuthority(host) {
521
+ return host.includes(':') && !host.startsWith('[') ? `[${host}]` : host;
522
+ }
523
+ function resolvePort(value) {
524
+ const port = value ?? 3000;
525
+ if (!Number.isInteger(port) || port < 0 || port > 65535) {
526
+ throw new Error(`Invalid PORT value: ${String(value ?? 3000)}.`);
527
+ }
528
+ return port;
529
+ }
530
+ function toHttpException(error) {
531
+ if (error instanceof HttpException) {
532
+ return error;
533
+ }
534
+ return new InternalServerErrorException('Internal server error.', {
535
+ cause: error
536
+ });
537
+ }
538
+ function isAddressInUseError(error) {
539
+ if (!(error instanceof Error)) {
540
+ return false;
541
+ }
542
+ return error.code === 'EADDRINUSE';
543
+ }
544
+ function delay(ms) {
545
+ return new Promise(resolve => {
546
+ setTimeout(resolve, ms);
547
+ });
548
+ }
549
+ function waitForCloseWithTimeout(closePromise, timeoutMs) {
550
+ return new Promise((resolve, reject) => {
551
+ const timeout = setTimeout(() => {
552
+ reject(new Error(`Fastify adapter shutdown timeout exceeded ${String(timeoutMs)}ms.`));
553
+ }, timeoutMs);
554
+ void closePromise.then(() => {
555
+ clearTimeout(timeout);
556
+ resolve();
557
+ }, error => {
558
+ clearTimeout(timeout);
559
+ reject(error);
560
+ });
561
+ });
562
+ }
563
+ function mergeSetCookieHeader(current, incoming) {
564
+ const nextValues = Array.isArray(incoming) ? incoming : [incoming];
565
+ if (current === undefined || typeof current === 'number') {
566
+ return nextValues.length === 1 ? nextValues[0] : [...nextValues];
567
+ }
568
+ const currentValues = Array.isArray(current) ? current : [current];
569
+ const merged = [...currentValues, ...nextValues];
570
+ return merged.length === 1 ? merged[0] : merged;
571
+ }
572
+ function serializeResponseBody(body, contentType) {
573
+ if (body === undefined) {
574
+ return {
575
+ payload: ''
576
+ };
577
+ }
578
+ if (Buffer.isBuffer(body)) {
579
+ return {
580
+ defaultContentType: 'application/octet-stream',
581
+ payload: body
582
+ };
583
+ }
584
+ if (body instanceof Uint8Array) {
585
+ return {
586
+ defaultContentType: 'application/octet-stream',
587
+ payload: Buffer.from(body)
588
+ };
589
+ }
590
+ if (body instanceof ArrayBuffer) {
591
+ return {
592
+ defaultContentType: 'application/octet-stream',
593
+ payload: Buffer.from(body)
594
+ };
595
+ }
596
+ if (typeof body === 'string') {
597
+ const isJson = isJsonContentType(contentType);
598
+ return {
599
+ defaultContentType: isJson ? undefined : 'text/plain; charset=utf-8',
600
+ payload: isJson ? JSON.stringify(body) : body
601
+ };
602
+ }
603
+ return {
604
+ defaultContentType: 'application/json; charset=utf-8',
605
+ payload: JSON.stringify(body)
606
+ };
607
+ }
608
+ function isJsonContentType(contentType) {
609
+ return typeof contentType === 'string' && contentType.toLowerCase().includes('application/json');
610
+ }
@@ -0,0 +1,2 @@
1
+ export * from './adapter.js';
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,cAAc,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1 @@
1
+ export * from './adapter.js';
package/package.json ADDED
@@ -0,0 +1,54 @@
1
+ {
2
+ "name": "@fluojs/platform-fastify",
3
+ "description": "Fastify-based HTTP adapter for the Fluo runtime.",
4
+ "keywords": [
5
+ "fluo",
6
+ "fastify",
7
+ "http-adapter",
8
+ "platform",
9
+ "server"
10
+ ],
11
+ "version": "1.0.0-beta.1",
12
+ "private": false,
13
+ "license": "MIT",
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "https://github.com/fluojs/fluo.git",
17
+ "directory": "packages/platform-fastify"
18
+ },
19
+ "engines": {
20
+ "node": ">=20.0.0"
21
+ },
22
+ "publishConfig": {
23
+ "access": "public"
24
+ },
25
+ "type": "module",
26
+ "exports": {
27
+ ".": {
28
+ "types": "./dist/index.d.ts",
29
+ "import": "./dist/index.js"
30
+ }
31
+ },
32
+ "main": "./dist/index.js",
33
+ "types": "./dist/index.d.ts",
34
+ "files": [
35
+ "dist"
36
+ ],
37
+ "dependencies": {
38
+ "@fastify/multipart": "^9.2.1",
39
+ "fastify": "^5.6.1",
40
+ "fastify-raw-body": "^5.0.0",
41
+ "@fluojs/http": "^1.0.0-beta.1",
42
+ "@fluojs/runtime": "^1.0.0-beta.1"
43
+ },
44
+ "devDependencies": {
45
+ "vitest": "^3.2.4"
46
+ },
47
+ "scripts": {
48
+ "prebuild": "node ../../tooling/scripts/clean-dist.mjs",
49
+ "build": "pnpm exec babel src --extensions .ts --ignore 'src/**/*.test.ts' --out-dir dist --config-file ../../tooling/babel/babel.config.cjs && pnpm exec tsc -p tsconfig.build.json",
50
+ "typecheck": "pnpm exec tsc -p tsconfig.json --noEmit",
51
+ "test": "pnpm exec vitest run -c vitest.config.ts",
52
+ "test:watch": "pnpm exec vitest -c vitest.config.ts"
53
+ }
54
+ }